diff --git a/.HA_VERSION b/.HA_VERSION index 6784e1e..3f77693 100644 --- a/.HA_VERSION +++ b/.HA_VERSION @@ -1 +1 @@ -2026.6.3 \ No newline at end of file +2026.7.1 \ No newline at end of file diff --git a/.cache/brands/integrations/asusrouter/dark_logo.png b/.cache/brands/integrations/asusrouter/dark_logo.png new file mode 100644 index 0000000..5ba9dae Binary files /dev/null and b/.cache/brands/integrations/asusrouter/dark_logo.png differ diff --git a/.cache/brands/integrations/blink/dark_logo.png b/.cache/brands/integrations/blink/dark_logo.png new file mode 100644 index 0000000..be8fded Binary files /dev/null and b/.cache/brands/integrations/blink/dark_logo.png differ diff --git a/.cache/brands/integrations/blink/logo.png b/.cache/brands/integrations/blink/logo.png new file mode 100644 index 0000000..be8fded Binary files /dev/null and b/.cache/brands/integrations/blink/logo.png differ diff --git a/.cache/brands/integrations/climate_scheduler/logo.png b/.cache/brands/integrations/climate_scheduler/logo.png new file mode 100644 index 0000000..4451c38 Binary files /dev/null and b/.cache/brands/integrations/climate_scheduler/logo.png differ diff --git a/.cache/brands/integrations/generic/logo.png b/.cache/brands/integrations/generic/logo.png new file mode 100644 index 0000000..69a2a57 Binary files /dev/null and b/.cache/brands/integrations/generic/logo.png differ diff --git a/.cache/brands/integrations/google_generative_ai_conversation/logo.png b/.cache/brands/integrations/google_generative_ai_conversation/logo.png new file mode 100644 index 0000000..ceac1e8 Binary files /dev/null and b/.cache/brands/integrations/google_generative_ai_conversation/logo.png differ diff --git a/.cache/brands/integrations/google_travel_time/logo.png b/.cache/brands/integrations/google_travel_time/logo.png new file mode 100644 index 0000000..f2db6aa Binary files /dev/null and b/.cache/brands/integrations/google_travel_time/logo.png differ diff --git a/.cache/brands/integrations/hassio/logo.png b/.cache/brands/integrations/hassio/logo.png new file mode 100644 index 0000000..7045fc7 Binary files /dev/null and b/.cache/brands/integrations/hassio/logo.png differ diff --git a/.cache/brands/integrations/intelligent_heating_pilot/dark_logo.png b/.cache/brands/integrations/intelligent_heating_pilot/dark_logo.png new file mode 100644 index 0000000..afda4df Binary files /dev/null and b/.cache/brands/integrations/intelligent_heating_pilot/dark_logo.png differ diff --git a/.cache/brands/integrations/intelligent_heating_pilot/logo.png b/.cache/brands/integrations/intelligent_heating_pilot/logo.png new file mode 100644 index 0000000..098cccf Binary files /dev/null and b/.cache/brands/integrations/intelligent_heating_pilot/logo.png differ diff --git a/.cache/brands/integrations/maintenance_supporter/logo.png b/.cache/brands/integrations/maintenance_supporter/logo.png new file mode 100644 index 0000000..c4e6dbf Binary files /dev/null and b/.cache/brands/integrations/maintenance_supporter/logo.png differ diff --git a/.cache/brands/integrations/met/logo.png b/.cache/brands/integrations/met/logo.png new file mode 100644 index 0000000..94d8158 Binary files /dev/null and b/.cache/brands/integrations/met/logo.png differ diff --git a/.cache/brands/integrations/midea_ac_lan/dark_logo.png b/.cache/brands/integrations/midea_ac_lan/dark_logo.png new file mode 100644 index 0000000..33adb36 Binary files /dev/null and b/.cache/brands/integrations/midea_ac_lan/dark_logo.png differ diff --git a/.cache/brands/integrations/nest_protect/dark_logo.png b/.cache/brands/integrations/nest_protect/dark_logo.png new file mode 100644 index 0000000..20507c6 Binary files /dev/null and b/.cache/brands/integrations/nest_protect/dark_logo.png differ diff --git a/.cache/brands/integrations/scheduler/dark_logo.png b/.cache/brands/integrations/scheduler/dark_logo.png new file mode 100644 index 0000000..862df27 Binary files /dev/null and b/.cache/brands/integrations/scheduler/dark_logo.png differ diff --git a/.cache/brands/integrations/scheduler/logo.png b/.cache/brands/integrations/scheduler/logo.png new file mode 100644 index 0000000..de9b2f6 Binary files /dev/null and b/.cache/brands/integrations/scheduler/logo.png differ diff --git a/.cache/brands/integrations/waze_travel_time/logo.png b/.cache/brands/integrations/waze_travel_time/logo.png new file mode 100644 index 0000000..89dbb23 Binary files /dev/null and b/.cache/brands/integrations/waze_travel_time/logo.png differ diff --git a/.ha_run.lock b/.ha_run.lock index 54d43ec..ce2b266 100644 --- a/.ha_run.lock +++ b/.ha_run.lock @@ -1 +1 @@ -{"pid": 71, "version": 1, "ha_version": "2026.6.3", "start_ts": 1781553089.670326} \ No newline at end of file +{"pid": 71, "version": 1, "ha_version": "2026.7.1", "start_ts": 1783446559.6159952} \ No newline at end of file diff --git a/automations.yaml b/automations.yaml index df79d91..58f9590 100644 --- a/automations.yaml +++ b/automations.yaml @@ -189,7 +189,7 @@ hours: 0 minutes: 45 seconds: 0 - below: 1 + below: 0.2 id: freezer_down - trigger: numeric_state entity_id: @@ -198,7 +198,7 @@ hours: 0 minutes: 20 seconds: 0 - below: 1 + below: 0.2 id: fridge_down conditions: [] actions: @@ -303,14 +303,12 @@ mode: single - id: '1781034724187' alias: 'Office: Turn Off Lights on Exit' - description: Turns off office lights 2 minutes after the door closes, but only if living - room motion was detected after the door shut. + description: Turns off office lights 2 minutes after the door closes, but only if + living room motion was detected after the door shut. triggers: - trigger: state entity_id: binary_sensor.office_door_contact to: 'off' - for: - minutes: 2 conditions: - condition: state entity_id: light.playroom_light @@ -319,6 +317,8 @@ value_template: '{{ states.binary_sensor.basement.last_changed > states.binary_sensor.office_door_contact.last_changed }}' actions: + - delay: + minutes: 2 - action: light.turn_off target: entity_id: light.playroom_light @@ -708,44 +708,471 @@ entity_id: sensor.coffee_maker_power above: 1000 id: coffee_started + conditions: + - condition: state + entity_id: input_boolean.coffee_maker_running + state: 'off' + actions: + - action: input_boolean.turn_on + target: + entity_id: input_boolean.coffee_maker_running + - action: notify.notify + data: + title: ☕ Coffee Time + message: Someone is making coffee. + - action: notify.persistent_notification + data: + title: ☕ Coffee Time + message: Someone is making coffee. + - delay: + hours: 0 + minutes: 4 + seconds: 0 + - wait_template: '{{ states(''sensor.coffee_maker_power'') | float(0) < 350 }}' + timeout: + hours: 0 + minutes: 5 + seconds: 0 + - action: input_boolean.turn_off + target: + entity_id: input_boolean.coffee_maker_running + - action: notify.notify + data: + title: ☕ Coffee Ready + message: Your coffee is ready to enjoy! + mode: single +- id: '1781793117883' + alias: Basement Temperature Change + description: '' + triggers: + - type: temperature + device_id: 3e67afd88a4b4d17552c8f370aaf40b5 + entity_id: 64689573a8b11a83829efcae0022d7c1 + domain: sensor + trigger: device + above: 15 + below: 10 conditions: [] actions: - - choose: - - conditions: - - condition: trigger - id: coffee_started - - condition: state - entity_id: input_boolean.coffee_maker_running - state: 'off' - sequence: - - action: input_boolean.turn_on - target: - entity_id: input_boolean.coffee_maker_running - - action: notify.notify - data: - title: ☕ Coffee Time - message: Someone is making coffee. - - action: notify.persistent_notification - data: - title: ☕ Coffee Time - message: Someone is making coffee. - - delay: - hours: 0 - minutes: 4 - seconds: 0 - - wait_for_trigger: - - trigger: numeric_state - entity_id: sensor.coffee_maker_power - below: 350 - timeout: - hours: 0 - minutes: 3 - seconds: 0 - - action: input_boolean.turn_off - target: - entity_id: input_boolean.coffee_maker_running - - action: notify.notify - data: - title: ☕ Coffee Ready - message: Your coffee is ready to enjoy! + - action: notify.notify + metadata: {} + data: + title: Basement Temperature Change + message: Basement Temperature Changed! mode: single +- id: '1782161891986' + alias: 'Notification: Pool Sensor Low Battery' + description: Alert when the pool temperature sensor battery drops below 15% + triggers: + - entity_id: sensor.pool_temperature_battery + below: 15 + trigger: numeric_state + conditions: [] + actions: + - data: + title: Pool Sensor Battery Low + message: The pool temperature sensor battery is currently at {{ states('sensor.pool_temperature_battery') + }}%. Please change it soon. + action: notify.persistent_notification + - action: notify.notify + metadata: {} + data: + title: Pool Sensor Battery Low + message: The pool temperature sensor battery is currently at {{ states('sensor.pool_temperature_battery') + }}%. Please change it soon. + mode: single +- &id001 + id: commute_morning_traffic_alert_map + alias: 'Commute: Morning Traffic Alert with Map' + description: Checks commute times at 7:15 AM and attaches a map if traffic is bad. + triggers: + - at: 07:15:00 + trigger: time + conditions: + - condition: or + conditions: + - condition: template + value_template: '{{ states(''sensor.google_travel_time_work_work'') | float(0) + > 35 }}' + - condition: template + value_template: '{{ states(''sensor.waze_travel_time'') | float(0) > 35 }}' + actions: + - action: camera.snapshot + target: + entity_id: camera.camera_commute_map + data: + filename: /config/www/commute_snapshot.jpg + - action: notify.notify + data: + title: "\U0001F6A8 Commute Delay Alert!" + message: "Traffic is heavier than usual this morning. \nGoogle Maps: {{ states('sensor.google_travel_time_work_work') + }} mins. Waze: {{ states('sensor.waze_travel_time') }} mins." + data: + image: /local/commute_snapshot.jpg + - action: notify.persistent_notification + data: + title: "\U0001F6A8 Commute Delay Alert!" + message: "Traffic is heavier than usual this morning. \nGoogle Maps: {{ states('sensor.google_travel_time_work_work') + }} mins. Waze: {{ states('sensor.waze_travel_time') }} mins.\n![Commute Map](/local/commute_snapshot.jpg)" + mode: single +- *id001 +- *id001 +- id: '1782999027626' + alias: 'Commute: Morning Traffic Alert' + description: Pushes an interactive, live traffic map to my phone before the commute. + triggers: + - at: 07:15:00 + trigger: time + actions: + - action: notify.notify + data: + title: Morning Commute Traffic + message: Swipe down to view live traffic along your route. + data: + push: + category: map + action_data: + latitude: '45.474668' + longitude: '-73.737158' + shows_traffic: true + shows_scale: true +- id: '1783000087983' + alias: Morning Commute Alert + description: Notify when commute is longer than 35 minutes. + triggers: + - trigger: time + at: 07:15:00 + conditions: + - condition: time + weekday: + - mon + - tue + - wed + - thu + - fri + - condition: or + conditions: + - condition: numeric_state + entity_id: sensor.google_travel_time_work_work + above: 35 + - condition: numeric_state + entity_id: sensor.waze_travel_time + above: 35 + actions: + - action: notify.notify + data: + title: "\U0001F697 Morning Commute" + message: "Google: {{ states('sensor.google_travel_time_work_work') }} min\nWaze: + {{ states('sensor.waze_travel_time') }} min\n{% set g = states('sensor.google_travel_time_work_work') + | float(0) %} {% if g > 50 %} \U0001F6A8 Major traffic delays. {% elif g > + 40 %} ⚠️ Heavy traffic. {% else %} \U0001F7E1 Moderate traffic. {% endif %}" + mode: single +- id: '1783049020714' + alias: Aqara-Magic-Cube-T1-Pro-(CTP-R01) - Franco + description: '' + use_blueprint: + path: SirGoodenough/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.yaml + input: + topic: zigbee2mqtt/Ziggy + action_slide_face_1: + - action: light.toggle + metadata: {} + target: + device_id: 2f8c5ab2ca0d4be31919fba6df365813 + data: + brightness_pct: 25 + action_doubletap_face_1: [] + action_rotate_cw_face_1: + - action: script.cube_dimmer_control + metadata: {} + data: + light: light.playroom_light + angle: -20 + action_rotate_ccw_face_1: [] + action_flipped90_face_1: [] +- id: '1783162325964' + alias: Backyard Shed Camera Motion AI Description + description: Takes a snapshot when the Blink camera detects motion, analyzes it + with AI, and sends a mobile and persistent notification. + triggers: + - trigger: state + entity_id: binary_sensor.backyard_shed_motion + to: 'on' + actions: + - action: blink.trigger_camera + target: + entity_id: camera.backyard_shed + - delay: 00:00:02 + - action: camera.snapshot + target: + entity_id: camera.backyard_shed + data: + filename: /media/blink.jpg + - action: camera.snapshot + target: + entity_id: camera.backyard_shed + data: + filename: /config/www/blink.jpg + - delay: 00:00:02 + - action: ai_task.generate_data + continue_on_error: true + data: + task_name: Backyard Shed Camera Analysis + entity_id: ai_task.google_ai_task + instructions: Describe what you see in this image in brief. Focus on any people, + objects, animals or activities. Text needs to be max 240 characters. + attachments: + - media_content_id: media-source://media_source/local/blink.jpg + media_content_type: image/jpeg + response_variable: ai_profile + - action: notify.notify + continue_on_error: true + data: + title: Motion Detected - Backyard Shed + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }}' + - action: notify.persistent_notification + data: + title: Motion Detected - Backyard Shed + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }} + + ![Snapshot](/local/blink.jpg)' + data: + notification_id: backyard_shed_motion + mode: queued + max: 10 +- id: '1783293479398' + alias: Blink Camera Fast Update Cycle + description: Polls the Blink cloud every 30 seconds to bypass the default 5-minute + delay and force instant motion detection state updates. + triggers: + - trigger: time_pattern + seconds: /30 + actions: + - action: homeassistant.update_entity + target: + entity_id: + - binary_sensor.backyard_shed_motion + - binary_sensor.front_door_motion + mode: single +- id: '1783304677475' + alias: Motion Detection AI Tree + description: Takes a snapshot when the Blink camera detects motion, analyzes it + with AI, and sends a mobile and persistent notification. + triggers: + - trigger: state + entity_id: binary_sensor.backyard_tree_motion + to: 'on' + conditions: [] + actions: + - action: blink.trigger_camera + target: + entity_id: camera.backyard_tree + - delay: 00:00:02 + - action: camera.snapshot + target: + entity_id: camera.backyard_tree + data: + filename: /media/blinkTree.jpg + - action: camera.snapshot + target: + entity_id: camera.backyard_tree + data: + filename: /config/www/blinkTree.jpg + - delay: 00:00:02 + - action: ai_task.generate_data + continue_on_error: true + data: + task_name: Backyard Shed Camera Analysis + entity_id: ai_task.google_ai_task + instructions: Describe what you see in this image in brief. Focus on any people, + objects, animals or activities. Text needs to be max 240 characters. + attachments: + - media_content_id: media-source://media_source/local/blinkTree.jpg + media_content_type: image/jpeg + response_variable: ai_profile + - action: notify.notify + data: + title: Motion Detected - Backyard Tree + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }}' + - action: persistent_notification.create + data: + title: Motion Detected - Backyard Tree + notification_id: backyard_tree_motion + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }} + + ![Snapshot](/local/blinkTree.jpg)' + mode: queued + max: 10 +- id: '1783387792679' + description: Takes a snapshot when the Blink camera detects motion, analyzes it + with AI, and sends a mobile and persistent notification. + triggers: + - trigger: state + entity_id: binary_sensor.backyard_shed_motion + to: 'on' + conditions: [] + actions: + - action: blink.trigger_camera + target: + entity_id: camera.living_room_living_room_camera + - delay: 00:00:02 + - action: camera.snapshot + target: + entity_id: camera.living_room_living_room_camera + data: + filename: /media/blink.jpg + - action: camera.snapshot + target: + entity_id: camera.living_room_living_room_camera + data: + filename: /config/www/nest.jpg + - delay: 00:00:02 + - action: ai_task.generate_data + continue_on_error: true + data: + task_name: Backyard Shed Camera Analysis + entity_id: ai_task.google_ai_task + instructions: Describe what you see in this image in brief. Focus on any people, + objects, or activities. Text needs to be max 225 characters. + attachments: + - media_content_id: media-source://media_source/local/nest.jpg + media_content_type: image/jpeg + response_variable: ai_profile + - action: notify.notify + data: + title: Motion Detected - Backyard Shed + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }}' + - action: persistent_notification.create + data: + title: Motion Detected - Backyard Shed + notification_id: backyard_shed_motion2 + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }} + + ![Snapshot](/local/nest.jpg)' + mode: queued + max: 10 +- id: '1783452320698' + alias: Nest Test Motion + description: Takes a snapshot when the Blink camera detects motion, analyzes it + with AI, and sends a mobile and persistent notification. + triggers: + - trigger: state + entity_id: binary_sensor.backyard_tree_motion + to: 'on' + conditions: [] + actions: + - delay: 00:00:02 + - action: camera.snapshot + target: + entity_id: + - camera.front + - camera.backyard_shed + data: + filename: /media/nest_Living2.jpg + - action: camera.snapshot + target: + entity_id: camera.backyard_tree + data: + filename: /config/www/blinkTree.jpg + - delay: 00:00:02 + - action: ai_task.generate_data + continue_on_error: true + data: + task_name: Backyard Shed Camera Analysis + entity_id: ai_task.google_ai_task + instructions: Describe what you see in this image in brief. Focus on any people, + objects, or activities. Text needs to be max 225 characters. + attachments: + - media_content_id: media-source://media_source/local/blinkTree.jpg + media_content_type: image/jpeg + response_variable: ai_profile + - action: notify.notify + data: + title: Motion Detected - Backyard Tree + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }}' + - action: persistent_notification.create + data: + title: Motion Detected - Backyard Tree + notification_id: backyard_tree_motion + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }} + + ![Snapshot](/local/blinkTree.jpg)' + mode: queued + max: 10 +- id: '1783469453396' + alias: Motion Front Door - AI Description + description: Takes a snapshot when the Nest camera detects motion, a person, an + animal, or a doorbell press, analyzes it with AI, and sends a mobile and persistent + notification. + triggers: + - trigger: event + event_type: state_changed + event_data: + entity_id: event.front_door_front_door_motion + conditions: + - condition: template + value_template: '{{ trigger.event.data.new_state.attributes.event_type in [''motion'', + ''person'', ''animal'', ''chime''] }}' + actions: + - action: blink.trigger_camera + target: + entity_id: camera.front + data: {} + - delay: 00:00:02 + - action: camera.snapshot + target: + entity_id: camera.front + data: + filename: /media/blink_Front.jpg + - action: camera.snapshot + target: + entity_id: camera.front + data: + filename: /config/www/blink_Front.jpg + - delay: 00:00:02 + - action: ai_task.generate_data + continue_on_error: true + data: + task_name: Nest Front Camera Analysis + entity_id: ai_task.google_ai_task + instructions: Describe what you see in this image in brief. Focus on any people, + objects, animals or activities. Text needs to be max 240 characters. + attachments: + - media_content_id: media-source://media_source/local/blink_Front.jpg + media_content_type: image/jpeg + response_variable: ai_profile + - action: notify.notify + continue_on_error: true + data: + title: Motion Detected - Backyard Shed + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }}' + - action: notify.persistent_notification + data: + title: Motion Detected - Backyard Shed + message: '{{ ai_profile.data if ai_profile is defined and ''data'' in ai_profile + else ''Motion detected, but the AI Task integration failed to return a response.'' + }} + + ![Snapshot](/local/blink_Front.jpg)' + data: + notification_id: nest_front_motion + mode: queued + max: 10 diff --git a/blueprints/automation/SirGoodenough/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.yaml b/blueprints/automation/SirGoodenough/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.yaml new file mode 100644 index 0000000..e534c45 --- /dev/null +++ b/blueprints/automation/SirGoodenough/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.yaml @@ -0,0 +1,1882 @@ +blueprint: + name: Aqara-Magic-Cube-T1-Pro-(CTP-R01) - 2024-06-04 + author: SirGoodenough + description: "This Blueprint uses a Zigbee2MQTT built sensor to sort out the 58+ + unique commands (118+ total) available from the Xiaomi Magic Cube Remote. (Some + unique commands listed as + are not counted & available thru templating only. + See the related document.)\n**NOTE:** This blueprint is for the PRO cube version + ONLY and is not fully compatible with the original version of the cube. Links + for those are in my [GIT repository](https://github.com/SirGoodenough/HA_Blueprints).\nThis + cube has an 'Action' mode (Shown below with this graphic: \U0001FAF3 ACTION MODE + ONLY \U0001FAF3 )\nand a 'Scene' mode (Shown below with this graphic: \U0001F440 + SCENE MODE ONLY \U0001F440 ).\nMake sure the cube is in the mode you think it + is in when triggering it. The functions are separate. IE 'Action - slide' will + do something different from 'Scene - slide'.\nThe split out of functions gives + you the ability to assign local scripts or functions to do the things you want + the remote to do.\nFunctions that are left empty will simply do nothing.\n#### + \U0001F34E There is a set of 36 + 18 actions that will trigger on specific actions + on specific sides that are listed as **Group 1 \U0001F34E**\n#### \U0001F352 There + is a set of 6 + 3 actions that will trigger on specific actions on *ANY* side + that are listed as **Group 2 \U0001F352**\n#### \U0001F350 There is a set of 30 + + 30 event functions that will trigger on cube flips to & from specific sides + that are listed as **Group 3 \U0001F350**\n#### \U0001F355 There are 5 actions + (shake & throw for each mode & hold in scene mode) that only occur once in each + mode & are OK to be combined with any other group in that mode. Listed as **Group + 4 sections \U0001F355**.\nPlease be aware that ALL actions except the 4 listed + above, \U0001F355 will trigger an action in **ALL 3 groups in the matching mode + at the same time** every time. Therefore I suggest if you just have a couple of + things you want this remote to do that you choose the *ANY / Group 2 / \U0001F352* + events. Action events and Scene events are separate from each other.\nIf you + want more than a few events, you should select actions in **Group 1 / \U0001F34E + OR Group 3 / \U0001F350**.\nWith careful selection you can use mixed groups, but + you run the risk of a single cube action triggering more than 1 Home Assistant + action and making a mess of things \U0001F371.\n\n#### NOTICE: This cube *can* + be triggered 124 ways, but only 38(+54) of them are unique (with templating).\n##### + This blueprint has been known to freak out when there are spaces or odd characters + in the MQTT Topic. Make sure there are no spaces, ONLY a single word, and ONLY + A thru Z, a thru z, and 0 thru 9 in the topic. (``` / ``` is ok between device + and topic) If there are, you will need to change the name of the cube to remove + those characters.\n### IF YOU SEE --> TemplateError: Must provide a device or + entity ID\nIf you get an error like that, The friendly_name in Z2M likely does + not match the friendly_name on HA. To fix go into the Z2M ```Open web UI``` and + set the friendly_name there. Setting this in just HA or in Z2M without ticking + the update HA box will cause this. See the blueprint documentation page for details.\n\nThere + is sample code to make the template sensor in the help file on GitHib named [Zigbee2MQTT + - Xiaomi Cube Controller MQTT Triggered.md](https://github.com/SirGoodenough/HA_Blueprints/blob/master/Automations/Zigbee2MQTT%20-%20Xiaomi%20Cube%20Controller%20MQTT%20Triggered.md) + & in the [community page related to this](https://community.home-assistant.io/t/zigbee2mqtt-xiaomi-cube-controller/393203).\n\nWithin + this blueprint there is an event handler that will latch the last command that + the blueprint finds and sends that to the event buss. From there a simple Template + sensor can grab it and show you the last action sent. Thie will help when setting + up new functions and to troubleshoot strange behaviours.\n\n> This was forked + from https://community.home-assistant.io/t/z2m-xiaomi-cube-controller/263006 1.2 + project authored by luckypoppy and the friends he pulled together to create the + base. I sincerely thank Him (Them) for their work. I felt there needed to be more + documentation for rookie users to properly set this up. I had quite a few questions + and when I saw a few questions in that chat from people struggling, I wanted to + help. I also had a better idea for troubleshooting info.\n\n[Community link for + this blueprint](https://community.home-assistant.io/t/zigbee2mqtt-aqara-magic-cube-t1-pro-ctp-r01-xiaomi-lumi-cagl02/525111)\n\n## + \U0001F5FFNotice\U0001F46E\U0001F3FF‍♂️:\n* Copies of the original Blueprint that + were converted via the 'Take Control' feature or other means are officially not + supported by me.\n\n* I may or may not be able to support you when you have a + problem after you make changes to my code, as some of the code is no longer mine.\n\n* + I & my license also require attribution as a link back to the original should + you use this code in your own creation.\n\n* [Here is a link to my license & the + original github post](https://github.com/SirGoodenough/HA_Blueprints?tab=License-1-ov-file) + expected to be followed & referenced as attribution should you use this code elsewhere.\n" + source_url: https://github.com/SirGoodenough/HA_Blueprints/blob/master/Automations/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.yaml + domain: automation + homeassistant: + min_version: 2024.6.0 + input: + required_input: + name: "Required input \U0001F440" + icon: mdi:alert-box + collapsed: false + description: "This blueprint has been known to freak out when there are spaces + or odd characters\n in the MQTT Topic. Make sure there are no spaces, ONLY + a single word,\n and ONLY A thru Z, a thru z, and 0 thru 9 in the topic.\n + \ (``` / ``` is ok between device and topic)\n\n If there are other characters, + you will need to change the name of the cube to\n remove those characters.\n\n + \ ### IF YOU SEE --> TemplateError: Must provide a device or entity ID\n\n + \ If you get an error like that, The friendly_name in Z2M likely does not + match the \n friendly_name on HA. To fix go into the Z2M ```Open web UI``` + and set the \n friendly_name there. Setting this in just HA or in Z2M without + ticking the update HA\n box will cause this. See the blueprint documentation + page for details.\n" + input: + topic: + name: Topic + description: "The main MQTT Topic for your cube. \nDetails on finding the + correct topic can be found here: \n[Find my Cube Topic](https://github.com/SirGoodenough/HA_Blueprints/blob/master/Automations/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.md#to-make-the-blueprint-work-it-will-need)\n" + default: zigbee2mqtt/? + selector: + text: + multiline: false + multiple: false + optional_conditions: + name: "Optional conditions\U0001F913" + icon: mdi:alert-octagon-outline + collapsed: true + description: "This input was added by user request. With it you can add your + own condition.\n If you only want the cube to work like this in the morning,\n + \ then after 6pm you want it to do different actions, add a condition that\n + \ will only let this run from 6am until 6pm. Then have another copy of\n the + blueprint that will only run from 6pm until 6am. (for example)\n\n Any valid + conditions you can dream up, you should be able to add here.\n\n Leave it + as the default of you don't want to add a condition of your own.\n" + input: + additional_conditions: + name: Additional conditions + description: "Extra conditions you may want to add to this automation \n(Example: + Home occupied, TV on, etc)\n" + default: [] + selector: + condition: {} + action_side_1_group_1_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 1 Group 1 actions \U0001F34E" + icon: mdi:numeric-1-box + collapsed: true + description: "These are Group 1 actions that end with Face 1\n (the one with + the 'Aqara' LOGO) of the cube up to start.\n" + input: + action_slide_face_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Slide with + face 1 up" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + action_doubletap_face_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Double + tap with face 1 up" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + action_flipped90_face_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 90 + degrees to face 1" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + action_flipped180_face_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 180 + degrees to face 1" + description: '**NOTE:** This action is trick to master, and should be used + sparingly... Face 1 is the one with the ''Aqara'' LOGO' + default: [] + selector: + action: {} + action_flip_from_any_to_face_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip From + any face to face 1" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + action_rotate_cw_face_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CW with face 1 up" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + action_rotate_ccw_face_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CCW with face 1 up" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + action_side_2_group_1_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 2 Group 1 actions \U0001F34E" + icon: mdi:numeric-2-box + collapsed: true + description: 'These are Group 1 actions that end with Face 2 + + ' + input: + action_slide_face_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Slide with + face 2 up" + default: [] + selector: + action: {} + action_doubletap_face_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Double + tap with face 2 up" + default: [] + selector: + action: {} + action_flipped90_face_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 90 + degrees to face 2" + default: [] + selector: + action: {} + action_flipped180_face_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 180 + degrees to face 2" + description: '**NOTE:** This action is trick to master, and should be used + sparingly...' + default: [] + selector: + action: {} + action_flip_from_any_to_face_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip From + any face to face 2" + default: [] + selector: + action: {} + action_rotate_cw_face_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CW with face 2 up" + default: [] + selector: + action: {} + action_rotate_ccw_face_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CCW with face 2 up" + default: [] + selector: + action: {} + action_side_3_group_1_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 3 Group 1 actions \U0001F34E" + icon: mdi:numeric-3-box + collapsed: true + description: 'These are Group 1 actions that end with Face 3 + + ' + input: + action_slide_face_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Slide with + face 3 up" + default: [] + selector: + action: {} + action_doubletap_face_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Double + tap with face 3 up" + default: [] + selector: + action: {} + action_flipped90_face_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 90 + degrees to face 3" + default: [] + selector: + action: {} + action_flipped180_face_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 180 + degrees to face 3" + description: '**NOTE:** This action is trick to master, and should be used + sparingly...' + default: [] + selector: + action: {} + action_flip_from_any_to_face_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip From + any face to face 3" + default: [] + selector: + action: {} + action_rotate_cw_face_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CW with face 3 up" + default: [] + selector: + action: {} + action_rotate_ccw_face_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CCW with face 3 up" + default: [] + selector: + action: {} + action_side_4_group_1_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 4 Group 1 actions \U0001F34E" + icon: mdi:numeric-4-box + collapsed: true + description: 'These are Group 1 actions that end with Face 4 + + ' + input: + action_slide_face_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Slide with + face 4 up" + default: [] + selector: + action: {} + action_doubletap_face_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Double + tap with face 4 up" + default: [] + selector: + action: {} + action_flipped90_face_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 90 + degrees to face 4" + default: [] + selector: + action: {} + action_flipped180_face_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 180 + degrees to face 4" + description: '**NOTE:** This action is trick to master, and should be used + sparingly...' + default: [] + selector: + action: {} + action_flip_from_any_to_face_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip From + any face to face 4" + default: [] + selector: + action: {} + action_rotate_cw_face_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CW with face 4 up" + default: [] + selector: + action: {} + action_rotate_ccw_face_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CCW with face 4 up" + default: [] + selector: + action: {} + action_side_5_group_1_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 5 Group 1 actions \U0001F34E" + icon: mdi:numeric-5-box + collapsed: true + description: 'These are Group 1 actions that end with Face 5 + + ' + input: + action_slide_face_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Slide with + face 5 up" + default: [] + selector: + action: {} + action_doubletap_face_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Double + tap with face 5 up" + default: [] + selector: + action: {} + action_flipped90_face_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 90 + degrees to face 5" + default: [] + selector: + action: {} + action_flipped180_face_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 180 + degrees to face 5" + description: '**NOTE:** This action is trick to master, and should be used + sparingly...' + default: [] + selector: + action: {} + action_flip_from_any_to_face_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip From + any face to face 5" + default: [] + selector: + action: {} + action_rotate_cw_face_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CW with face 5 up" + default: [] + selector: + action: {} + action_rotate_ccw_face_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CCW with face 5 up" + default: [] + selector: + action: {} + action_side_6_group_1_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 6 Group 1 actions \U0001F34E" + icon: mdi:numeric-6-box + collapsed: true + description: 'These are Group 1 actions that end with Face 6 + + ' + input: + action_slide_face_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Slide with + face 6 up" + default: [] + selector: + action: {} + action_doubletap_face_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Double + tap with face 6 up" + default: [] + selector: + action: {} + action_flipped90_face_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 90 + degrees to face 6" + default: [] + selector: + action: {} + action_flipped180_face_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip 180 + degrees to face 6" + description: '**NOTE:** This action is trick to master, and should be used + sparingly...' + default: [] + selector: + action: {} + action_flip_from_any_to_face_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Flip From + any face to face 6" + default: [] + selector: + action: {} + action_rotate_cw_face_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CW with face 6 up" + default: [] + selector: + action: {} + action_rotate_ccw_face_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 1 \U0001F34E Rotate + CCW with face 6 up" + default: [] + selector: + action: {} + action_any_side_group_2_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Any side Group 2 actions \U0001F352" + icon: mdi:fruit-cherries + collapsed: true + description: "\U0001F352 These are a set of 6 event functions that will trigger + on specific actions\n on *ANY* side. **Group 2 actions \U0001F352**\n" + input: + action_slide_any: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 2 \U0001F352 Slide on + any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_doubletap_any: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 2 \U0001F352 Double + tap on any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_flipped90_any: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 2 \U0001F352 Flip 90 + degrees to any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_flipped180_any: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 2 \U0001F352 Flip 180 + degrees to any side" + description: "NOTE: This action is trick to master, and should be used sparingly... + Side doesn't matter on this one.\n**!!Warning!!** The automations for + the specific side will also trigger. \U0001F34E\U0001F350\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_rotate_cw_any: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 2 \U0001F352 Rotate + CW on any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_rotate_ccw_any: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 2 \U0001F352 Rotate + CCW on any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_side_1_group_3_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 1 Group 3 actions \U0001F350" + icon: mdi:numeric-1-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 1 as the ending + point.\n All are flips from another side to side 1.\n" + input: + action_1_from_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 1 from side 6" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_1_from_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 1 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_1_from_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 1 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_1_from_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 1 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_1_from_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 1 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_side_2_group_3_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 2 Group 3 actions \U0001F350" + icon: mdi:numeric-2-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 2 as the ending + point.\n All are flips from another side to side 2.\n" + input: + action_2_from_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 2 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_2_from_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 2 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_2_from_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 2 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_2_from_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 2 from side 5" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_2_from_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 2 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_side_3_group_3_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 3 Group 3 actions \U0001F350" + icon: mdi:numeric-3-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 3 as the ending + point.\n All are flips from another side to side 3.\n" + input: + action_3_from_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 3 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_3_from_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 3 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_3_from_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 3 from side 4" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_3_from_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 3 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_3_from_6: + name: "G\U0001FAF3 ACTION MODE ONLY \U0001FAF3 roup 3 \U0001F350 Flip to + side 3 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_side_4_group_3_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 4 Group 3 actions \U0001F350" + icon: mdi:numeric-4-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 4 as the ending + point.\n All are flips from another side to side 4.\n" + input: + action_4_from_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 4 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_4_from_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 4 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_4_from_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 4 from side 3" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_4_from_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 4 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_4_from_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 4 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_side_5_group_3_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 5 Group 3 actions \U0001F350" + icon: mdi:numeric-5-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 5 as the ending + point.\n All are flips from another side to side 5.\n" + input: + action_5_from_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 5 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger. \U0001F34E\U0001F350\nI + suggest using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_5_from_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 5 from side 2" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_5_from_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 5 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_5_from_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 5 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_5_from_6: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 5 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_side_6_group_3_actions: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Side 2 Group 3 actions \U0001F350" + icon: mdi:numeric-6-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 6 as the ending + point.\n All are flips from another side to side 6.\n" + input: + action_6_from_1: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 6 from side 1" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_6_from_2: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 6 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_6_from_3: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 6 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_6_from_4: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 6 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + action_6_from_5: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Group 3 \U0001F350 Flip to + side 6 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + group_4_actions: + name: "Group 4 actions only used once (no side) \U0001F355" + icon: mdi:pizza + collapsed: true + description: "These are Group 4 actions only used once (no side) \U0001F355\n + \ they can be safely compined with any other action mode command.\n" + input: + action_shake: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Shake the cube \U0001F355 + Group 4" + description: 'This trigger only occurs once in the set-up. + + It can be combined in any group.' + default: [] + selector: + action: {} + action_throw: + name: "\U0001FAF3 ACTION MODE ONLY \U0001FAF3 Throw the cube \U0001F355 + Group 4" + description: ' You don''t actually ''Throw'' the cube. The action is as + follows: + + > Pick up the cube firmly. + + > Make a throwing motion with it but do not motion back towards yourself. + + > Hold it there for a second. + + > The throw trigger should be sent to Z2M. + + + This trigger only occurs once in the set-up. + + It can be combined in any group.' + default: [] + selector: + action: {} + scene_side_1_group_1_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 1 Group 1 actions \U0001F34E" + icon: mdi:numeric-1-box + collapsed: true + description: "These are Group 1 actions that end with Face 1\n (the one with + the 'Aqara' LOGO) of the cube up to start.\n" + input: + scene_flip_to_face_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Flip to + face 1 from any other face" + description: Face 1 is the one with the ''Aqara'' LOGO' + default: [] + selector: + action: {} + scene_rotate_cw_face_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CW with face 1 up" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + scene_rotate_ccw_face_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CCW with face 1 up" + description: Face 1 is the one with the 'Aqara' LOGO + default: [] + selector: + action: {} + scene_side_2_group_1_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 2 Group 1 actions \U0001F34E" + icon: mdi:numeric-2-box + collapsed: true + description: 'These are Group 1 actions that end with Face 2 + + ' + input: + scene_flip_to_face_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Flip to + face 2 from any other face" + default: [] + selector: + action: {} + scene_rotate_cw_face_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CW with face 2 up" + default: [] + selector: + action: {} + scene_rotate_ccw_face_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CCW with face 2 up" + default: [] + selector: + action: {} + scene_side_3_group_1_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 3 Group 1 actions \U0001F34E" + icon: mdi:numeric-3-box + collapsed: true + description: 'These are Group 1 actions that end with Face 3 + + ' + input: + scene_flip_to_face_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Flip to + face 3 from any other face" + default: [] + selector: + action: {} + scene_rotate_cw_face_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CW with face 3 up" + default: [] + selector: + action: {} + scene_rotate_ccw_face_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CCW with face 3 up" + default: [] + selector: + action: {} + scene_side_4_group_1_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 4 Group 1 actions \U0001F34E" + icon: mdi:numeric-4-box + collapsed: true + description: 'These are Group 1 actions that end with Face 4 + + ' + input: + scene_flip_to_face_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Flip to + face 4 from any other face" + default: [] + selector: + action: {} + scene_rotate_cw_face_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CW with face 4 up" + default: [] + selector: + action: {} + scene_rotate_ccw_face_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CCW with face 4 up" + default: [] + selector: + action: {} + scene_side_5_group_1_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 5 Group 1 actions \U0001F34E" + icon: mdi:numeric-5-box + collapsed: true + description: 'These are Group 1 actions that end with Face 5 + + ' + input: + scene_flip_to_face_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Flip to + face 5 from any other face" + default: [] + selector: + action: {} + scene_rotate_cw_face_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CW with face 5 up" + default: [] + selector: + action: {} + scene_rotate_ccw_face_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CCW with face 5 up" + default: [] + selector: + action: {} + scene_side_6_group_1_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 6 Group 1 actions \U0001F34E" + icon: mdi:numeric-6-box + collapsed: true + description: 'These are Group 1 actions that end with Face 6 + + ' + input: + scene_flip_to_face_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Flip to + face 6 from any other face" + default: [] + selector: + action: {} + scene_rotate_cw_face_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CW with face 6 up" + default: [] + selector: + action: {} + scene_rotate_ccw_face_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 1 \U0001F34E Rotate cube + CCW with face 6 up" + default: [] + selector: + action: {} + scene_any_side_group_2_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Any side Group 2 actions \U0001F352" + icon: mdi:fruit-cherries + collapsed: true + description: "\U0001F352 These are a set of 6 event functions that will trigger + on specific actions\n on *ANY* side. **Group 2 actions \U0001F352**\n" + input: + scene_flip_to_side_any: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 2 \U0001F352 Flip to + any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_rotate_cw_any: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 2 \U0001F352 Rotate cube + CW with any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_rotate_ccw_any: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 2 \U0001F352 Rotate cube + CCW with any side" + description: "Side doesn't matter on this one.\n**!!Warning!!** The automations + for the specific side will also trigger. \U0001F34E\U0001F350\nI suggest + using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_side_1_group_3_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 1 Group 3 actions \U0001F350" + icon: mdi:numeric-1-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 1 as the ending + point.\n All are flips from another side to side 1.\n" + input: + scene_1_from_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 1 from side 6" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_1_from_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 1 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_1_from_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 1 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_1_from_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 1 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_1_from_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 1 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 1 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_side_2_group_3_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 2 Group 3 actions \U0001F350" + icon: mdi:numeric-2-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 2 as the ending + point.\n All are flips from another side to side 2.\n" + input: + scene_2_from_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 2 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_2_from_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 2 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_2_from_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 2 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_2_from_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 2 from side 5" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_2_from_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 2 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 2 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_side_3_group_3_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 3 Group 3 actions \U0001F350" + icon: mdi:numeric-3-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 3 as the ending + point.\n All are flips from another side to side 3.\n" + input: + scene_3_from_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 3 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_3_from_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 3 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_3_from_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 3 from side 4" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_3_from_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 3 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_3_from_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 3 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 3 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_side_4_group_3_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 4 Group 3 actions \U0001F350" + icon: mdi:numeric-4-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 4 as the ending + point.\n All are flips from another side to side 4.\n" + input: + scene_4_from_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 4 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_4_from_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 4 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_4_from_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 4 from side 3" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_4_from_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 4 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_4_from_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 4 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 4 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_side_5_group_3_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 5 Group 3 actions \U0001F350" + icon: mdi:numeric-5-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 5 as the ending + point.\n All are flips from another side to side 5.\n" + input: + scene_5_from_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 5 from side 1" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger. \U0001F34E\U0001F350\nI + suggest using only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_5_from_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 5 from side 2" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_5_from_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 5 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_5_from_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 5 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_5_from_6: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 5 from side 6" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 5 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_side_6_group_3_actions: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Side 6 Group 3 actions \U0001F350" + icon: mdi:numeric-6-circle + collapsed: true + description: "These are Group 3 actions that happen with Face 6 as the ending + point.\n All are flips from another side to side 6.\n" + input: + scene_6_from_1: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 6 from side 1" + description: "**!!Warning!!** The flip 180 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_6_from_2: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 6 from side 2" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_6_from_3: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 6 from side 3" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_6_from_4: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 6 from side 4" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_6_from_5: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Group 3 \U0001F350 Flip to + side 6 from side 5" + description: "**!!Warning!!** The flip 90 automations \U0001F34E for side + 6 and the ANY \U0001F352 automations will also trigger.\nI suggest using + only one of the three groups of triggers, not multiple groups..." + default: [] + selector: + action: {} + scene_group_4_actions: + name: "Group 4 actions only used once (no side) \U0001F355" + icon: mdi:pizza + collapsed: true + description: "These are Group 4 actions only used once (no side) \U0001F355\n + \ they can be safely compined with any other scene mode command.\n" + input: + scene_hold: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Lift up the cube and hold it + still \U0001F355" + description: 'This trigger only occurs once in the set-up. + + It can be combined in any group.' + default: [] + selector: + action: {} + scene_shake: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Shake the cube \U0001F355" + description: 'This trigger only occurs once in the set-up. + + It can be combined in any group.' + default: [] + selector: + action: {} + scene_throw: + name: "\U0001F440 SCENE MODE ONLY \U0001F440 Throw the cube \U0001F355" + description: ' You don''t actually ''Throw'' the cube. The action is as + follows: + + > Pick up the cube firmly. + + > Make a throwing motion with it but do not motion back towards yourself. + + > Hold it there for a second. + + > The throw trigger should be sent to Z2M. + + + This trigger only occurs once in the set-up. + + It can be combined in any group.' + default: [] + selector: + action: {} +triggers: +- trigger: mqtt + topic: !input topic +variables: + action: "{% if trigger.payload_json.action is defined %}\n {{ trigger.payload_json.action + }}\n{% else %}\n none\n{% endif %}\n" + angle: "{% if trigger.payload_json.action_angle is number %}\n {{ trigger.payload_json.action_angle + | float(0.0) }}\n{% else %}\n 0.0\n{% endif %}\n" + side: "{% if trigger.payload_json.side is number %}\n {{ trigger.payload_json.side + | int(0) }}\n{% else %}\n 0\n{% endif %}\n" + op_mode: "{% if trigger.payload_json.operation_mode is defined %}\n {{ trigger.payload_json.operation_mode + }}\n{% else %}\n none\n{% endif %}\n" + topic_var: !input topic + friendly_name: '{{ topic_var.split(''/'')[1] | regex_replace("[^A-Za-z0-9_/./-]", + "") }}' + device_id: '{{ device_id( friendly_name ) }}' + ieee_id_long: '{{(( device_attr( device_id, ''identifiers'') |list)[0][1]) }}' + ieee_id: '{{ieee_id_long.split(''_'')[1] }}' + num_name: last side + num_ha_name: '{{ ''number.'' + friendly_name + ''_last_side'' }}' + num_ha_val: '{{ states(num_ha_name) | default(0) | int(0) }}' + last_side: "{# The first time it will not be there, so make sure its 0 instead of + undefined. #} {% if not num_ha_val in [\"undefined\", \"unknown\", \"unavailable\", + \"none\", \"null\", \"\"] %}\n {{ num_ha_val }}\n{% else %}\n 0\n{% endif %}\n" + mdl: Number Helper for Z2M Cube T1-Pro Integration + mf: SirGoodenough + sa: house + sw: '2023-12-09' + hw: https://github.com/SirGoodenough/HA_Blueprints/blob/master/Automations/Zigbee2MQTT-Aqara-Magic-Cube-T1-Pro-CTP-R01-Xiaomi-Lumi.yaml + cu: https://community.home-assistant.io/t/zigbee2mqtt-aqara-magic-cube-t1-pro-ctp-r01-xiaomi-lumi-cagl02/525111 + config_topic_2: '{{ ''homeassistant/number/'' + ieee_id + ''/last_side/config'' + }}' + topic_2: '{{ ''homeassistant/number/'' + ieee_id + ''/last_side'' }}' +conditions: +- condition: or + conditions: + - condition: template + alias: Check for valid Action Mode triggers + value_template: "{{ trigger.payload_json is defined and\n trigger.payload_json.operation_mode + is defined and\n trigger.payload_json.action is defined and\n trigger.payload_json.operation_mode + == \"action_mode\" and\n trigger.payload_json.action in [\"rotate_right\", + \"rotate_left\",\n \"flip90\", \"flip180\", \"slide\", \"tap\", \"shake\", + \"throw\"] }}\n" + - condition: template + alias: Check for valid Scene Mode triggers + value_template: "{{ trigger.payload_json is defined and\n trigger.payload_json.operation_mode + is defined and\n trigger.payload_json.action is defined and\n trigger.payload_json.operation_mode + == \"scene_mode\" and\n trigger.payload_json.action in [\"rotate_right\", \"rotate_left\",\n + \ \"flip_to_side\", \"shake\", \"throw\", \"hold\"] }}\n" +- alias: User pick + condition: !input additional_conditions +actions: +- alias: Make number helper to store last side + action: mqtt.publish + data: + topic: '{{ config_topic_2 }}' + retain: true + payload: "{ \"name\": \"{{ num_name }}\",\n \"avty_t\": \"homeassistant/status\",\n + \ \"uniq_id\": \"{{ friendly_name + '-' + device_id }}\",\n \"cmd_t\": \"{{ + topic_2 }}\",\n \"sta_t\": \"{{ topic_2 }}\",\n \"min\": \"0\",\n \"max\": + \"6\",\n \"step\": \"1\",\n \"mode\": \"box\",\n \"ret\": true,\n \"dev\": + {\n \"name\": \"{{ friendly_name }}\",\n \"mdl\": \"{{ mdl }}\",\n \"mf\": + \"{{ mf }}\",\n \"sa\": \"{{ sa }}\",\n \"sw\": \"{{ sw }}\",\n \"hw\": + \"{{ hw }}\",\n \"cu\": \"{{ cu }}\",\n \"ids\": [\n \"{{ ieee_id + + '-' + num_name }}\"\n ]\n }\n}\n" +- alias: Fire Last Action event. This is for optional troubleshooting data. + event: cube_last_action + event_data: + action: '{{ action }}' + side: '{{ side }}' + last_side: '{{ last_side }}' + friendly_name: '{{ friendly_name }}' + device_id: '{{ device_id }}' + ieee_id: '{{ ieee_id }}' + angle: '{{ angle }}' + mode: '{{ op_mode }}' +- alias: Store the current_side as the last_side for the next trigger + action: number.set_value + target: + entity_id: '{{ num_ha_name }}' + data: + value: '{{ side }}' +- alias: Choose Action Mode or Scene Mode + choose: + - conditions: '{{ op_mode == "action_mode" }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 2 \U0001F352 and No Group \U0001F355 + These are action events for ANY Side" + choose: + - conditions: '{{ action == "slide" }}' + sequence: !input action_slide_any + - conditions: '{{ action == "tap" }}' + sequence: !input action_doubletap_any + - conditions: '{{ action == "rotate_right" }}' + sequence: !input action_rotate_cw_any + - conditions: '{{ action == "rotate_left" }}' + sequence: !input action_rotate_ccw_any + - conditions: '{{ action == "shake" }}' + sequence: !input action_shake + - conditions: '{{ action == "throw" }}' + sequence: !input action_throw + - conditions: '{{ action == "flip90" }}' + sequence: + - parallel: + - !input action_flipped90_any + - alias: These are flip to specific side from any side events + choose: &id001 + - conditions: '{{ side == 1 }}' + sequence: !input action_flip_from_any_to_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input action_flip_from_any_to_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input action_flip_from_any_to_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input action_flip_from_any_to_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input action_flip_from_any_to_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input action_flip_from_any_to_face_6 + - conditions: '{{ action == "flip180" }}' + sequence: + - parallel: + - !input action_flipped180_any + - alias: These are flip to specific side from any side events + choose: *id001 + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 1 \U0001F34E These are action + events based on the action broken down by side" + choose: + - conditions: '{{ action == "slide" }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 1 \U0001F34E These are + slide events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input action_slide_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input action_slide_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input action_slide_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input action_slide_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input action_slide_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input action_slide_face_6 + - conditions: '{{ action == "tap" }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 1 \U0001F34E These are + doubletap events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input action_doubletap_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input action_doubletap_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input action_doubletap_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input action_doubletap_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input action_doubletap_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input action_doubletap_face_6 + - conditions: '{{ action == "flip90" }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 1 \U0001F34E These are + flipped90 events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input action_flipped90_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input action_flipped90_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input action_flipped90_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input action_flipped90_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input action_flipped90_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input action_flipped90_face_6 + - conditions: '{{ action == "flip180" }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 1 \U0001F34E These are + flipped180 events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input action_flipped180_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input action_flipped180_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input action_flipped180_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input action_flipped180_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input action_flipped180_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input action_flipped180_face_6 + - conditions: '{{ action == "rotate_right" }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 1 \U0001F34E These are + rotate CW events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input action_rotate_cw_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input action_rotate_cw_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input action_rotate_cw_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input action_rotate_cw_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input action_rotate_cw_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input action_rotate_cw_face_6 + - conditions: '{{ action == "rotate_left" }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 1 \U0001F34E These are + rotate CCW events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input action_rotate_ccw_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input action_rotate_ccw_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input action_rotate_ccw_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input action_rotate_ccw_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input action_rotate_ccw_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input action_rotate_ccw_face_6 + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 3 \U0001F350 Side to side jump + events" + choose: + - conditions: '{{ side == 1 }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 3 \U0001F350 To side 1 from + any events" + choose: + - conditions: '{{ last_side == 2 }}' + sequence: !input action_1_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input action_1_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input action_1_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input action_1_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input action_1_from_6 + - conditions: '{{ side == 2 }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 3 \U0001F350 To side 2 from + any events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input action_2_from_1 + - conditions: '{{ last_side == 3 }}' + sequence: !input action_2_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input action_2_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input action_2_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input action_2_from_6 + - conditions: '{{ side == 3 }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 3 \U0001F350 To side 3 from + any events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input action_3_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input action_3_from_2 + - conditions: '{{ last_side == 4 }}' + sequence: !input action_3_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input action_3_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input action_3_from_6 + - conditions: '{{ side == 4 }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 3 \U0001F350 To side 4 from + any events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input action_4_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input action_4_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input action_4_from_3 + - conditions: '{{ last_side == 5 }}' + sequence: !input action_4_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input action_4_from_6 + - conditions: '{{ side == 5 }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 3 \U0001F350 To side 5 from + any events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input action_5_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input action_5_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input action_5_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input action_5_from_4 + - conditions: '{{ last_side == 6 }}' + sequence: !input action_5_from_6 + - conditions: '{{ side == 6 }}' + sequence: + - alias: "\U0001FAF3 ACTION MODE \U0001FAF3 Group 3 \U0001F350 To side 6 from + any events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input action_6_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input action_6_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input action_6_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input action_6_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input action_6_from_5 + - conditions: '{{ op_mode == "scene_mode" }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 2 \U0001F352 and No Group \U0001F355 + These are action events for ANY Side" + choose: + - conditions: '{{ action == "flip_to_side" }}' + sequence: !input scene_flip_to_side_any + - conditions: '{{ action == "rotate_right" }}' + sequence: !input scene_rotate_cw_any + - conditions: '{{ action == "rotate_left" }}' + sequence: !input scene_rotate_ccw_any + - conditions: '{{ action == "hold" }}' + sequence: !input scene_hold + - conditions: '{{ action == "shake" }}' + sequence: !input scene_shake + - conditions: '{{ action == "throw" }}' + sequence: !input scene_throw + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 1 \U0001F34E These are action + events based on the action broken down by side" + choose: + - conditions: '{{ action == "flip_to_side" }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 1 \U0001F34E These are flip_to_side + events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input scene_flip_to_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input scene_flip_to_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input scene_flip_to_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input scene_flip_to_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input scene_flip_to_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input scene_flip_to_face_6 + - conditions: '{{ action == "rotate_right" }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 1 \U0001F34E These are rotate + CW events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input scene_rotate_cw_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input scene_rotate_cw_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input scene_rotate_cw_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input scene_rotate_cw_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input scene_rotate_cw_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input scene_rotate_cw_face_6 + - conditions: '{{ action == "rotate_left" }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 1 \U0001F34E These are rotate + CCW events" + choose: + - conditions: '{{ side == 1 }}' + sequence: !input scene_rotate_ccw_face_1 + - conditions: '{{ side == 2 }}' + sequence: !input scene_rotate_ccw_face_2 + - conditions: '{{ side == 3 }}' + sequence: !input scene_rotate_ccw_face_3 + - conditions: '{{ side == 4 }}' + sequence: !input scene_rotate_ccw_face_4 + - conditions: '{{ side == 5 }}' + sequence: !input scene_rotate_ccw_face_5 + - conditions: '{{ side == 6 }}' + sequence: !input scene_rotate_ccw_face_6 + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 3 \U0001F350 Side to side jump + events" + choose: + - conditions: '{{ side == 1 }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 3 \U0001F350 To side 1 from + specific events" + choose: + - conditions: '{{ last_side == 2 }}' + sequence: !input scene_1_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input scene_1_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input scene_1_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input scene_1_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input scene_1_from_6 + - conditions: '{{ side == 2 }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 3 \U0001F350 To side 2 from + specific events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input scene_2_from_1 + - conditions: '{{ last_side == 3 }}' + sequence: !input scene_2_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input scene_2_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input scene_2_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input scene_2_from_6 + - conditions: '{{ side == 3 }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 3 \U0001F350 To side 3 from + specific events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input scene_3_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input scene_3_from_2 + - conditions: '{{ last_side == 4 }}' + sequence: !input scene_3_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input scene_3_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input scene_3_from_6 + - conditions: '{{ side == 4 }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 3 \U0001F350 To side 4 from + specific events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input scene_4_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input scene_4_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input scene_4_from_3 + - conditions: '{{ last_side == 5 }}' + sequence: !input scene_4_from_5 + - conditions: '{{ last_side == 6 }}' + sequence: !input scene_4_from_6 + - conditions: '{{ side == 5 }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 3 \U0001F350 To side 5 from + specific events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input scene_5_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input scene_5_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input scene_5_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input scene_5_from_4 + - conditions: '{{ last_side == 6 }}' + sequence: !input scene_5_from_6 + - conditions: '{{ side == 6 }}' + sequence: + - alias: "\U0001F440 SCENE MODE \U0001F440 Group 3 \U0001F350 To side 6 from + specific events" + choose: + - conditions: '{{ last_side == 1 }}' + sequence: !input scene_6_from_1 + - conditions: '{{ last_side == 2 }}' + sequence: !input scene_6_from_2 + - conditions: '{{ last_side == 3 }}' + sequence: !input scene_6_from_3 + - conditions: '{{ last_side == 4 }}' + sequence: !input scene_6_from_4 + - conditions: '{{ last_side == 5 }}' + sequence: !input scene_6_from_5 +- alias: Delay ⏱ for debounce so toggle functions work. Also blueprint is in single + mode. + delay: 00:00:01 +mode: single +max_exceeded: silent diff --git a/blueprints/script/SirGoodenough/dimmer_control_4_magic_cube.yaml b/blueprints/script/SirGoodenough/dimmer_control_4_magic_cube.yaml new file mode 100644 index 0000000..4531b8d --- /dev/null +++ b/blueprints/script/SirGoodenough/dimmer_control_4_magic_cube.yaml @@ -0,0 +1,153 @@ +blueprint: + name: dimmer_control_4_magic_cube_and_other_things - 2023-12-07 + author: SirGoodenough + domain: script + homeassistant: + min_version: 2023.8.0 + source_url: https://github.com/SirGoodenough/HA_Blueprints/blob/master/Scripts/dimmer_control_4_magic_cube.yaml + description: "This script was invented to be used with my Magic Cube Blueprints. + I released it as a standalone Script Blueprint because I saw others struggling + with how so convert cube rotation to light dimming in other places.\n\nTo use + this blueprint, install it from the source in the normal way. After that add your + unique name for the Script, and add an icon and change the entity_id if desired. + You should only need to run the blueprint once, as you will be calling the unique + name/entity you added above to use the code. This script has 2 inputs in the Blueprint + screen to set the sensitivity of the rotation action and the minimum bbrightness + level for your lights. If the rotation is too sensitive the lights will change + from dim to bright in too short of a rotation so make it less sensitive. If the + lights flicker at low brightness, raise the minimum brightness level to stop it. + The data is sent to the generated script to control the action live at each use. + You only need to ececute this blueprint generator script if you wish to change + in input values or create another dimmer script.\n\nThis requires you call this + script wth 2 data values. One is a positive or negative number between 360 and + -360 that in the original, represents the input angle from the cube movement. + The other data point is the entity of the light or light group that you are trying + to control.\n***NOTE:*** the trigger variable below will be different for Z2M + and ZHA and others. Look at my documentation for that cube integration help if + you need it.\n\n## \U0001F5FFNotice\U0001F46E\U0001F3FF‍♂️:\n* Copies of the original + Blueprint that were converted via the 'Take Control' feature or other means are + officially not supported by me.\n\n* I may or may not be able to support you when + you have a problem after you make changes to my code, as some of the code is no + longer mine.\n\n* I & my license also require attribution as a link back to the + original should you use this code in your own creation.\n\n* [Here is a link to + my license & the original github post](https://github.com/SirGoodenough/HA_Blueprints?tab=License-1-ov-file) + expected to be followed & referenced as attribution should you use this code elsewhere.\n\n + \ Sample call / use of this script for ZHA:\n ```yaml\n - action: script.cube_dimmer_control_bp\n + \ data:\n angle: '{{ trigger.event.data.args.relative_degrees | default(0.1) + | float(0.2) }}'\n light: light.grp_studio\n ```\n\n Sample call / + use of this script for Z2M:\n ```yaml\n - action: script.cube_dimmer_control_bp\n + \ data:\n angle: '{{ trigger.payload_json.action_angle | default(0.1) + | float(0.2) }}'\n light: light.grp_kitch\n ```\n" + input: + sensi: + name: Sensitivity of the cube rotational motion. + description: 'This sets how ''fast'' the changes happen compared to how far + you move the cube. Higher percentage is faster, lower is slower. + + I have found that 40% is a good number, but you can adjust as desired. + + ' + default: 40 + selector: + number: + min: 1.0 + max: 200.0 + step: 1.0 + mode: box + min_b: + name: Minimum Brighness + description: 'This sets the lowest brightness it will set before it turns off. + Some bulbs react poorly below a certain percent, you can adjust that here. + + I have found that 10% is a good number, but you can adjust as desired. + + ' + default: 10 + selector: + number: + min: 5.0 + max: 90.0 + step: 1.0 + mode: slider + transition_i: + name: Transition Input + description: Seconds to take to change light one level to another + default: [] + selector: + number: + min: 0.0 + max: 1800.0 + step: 0.5 + mode: slider +fields: + angle: + name: Angle to represent new brightness + description: ' This is the new angle to be used in the calculation for Brightness' + required: true + example: '-120.7' + default: -120.7 + selector: + number: + min: -360 + max: 360 + step: 0.1 + mode: box + light: + name: Light entity to control + description: Add the light entity you want to control here + required: true + example: light.grp_studio + default: light.grp_studio + selector: + entity: + multiple: false + filter: + domain: light + transition: + name: Transition Field + description: Seconds to take to change light one level to another + required: false + example: '4' + selector: + number: + min: 0 + max: 1800 + step: 0.5 +variables: + sensi_var: !input sensi + sens_d: '{{ sensi_var / 100 | default( 0.4 ) }}' + min_b_var: !input min_b + bright_p: "{% set step_size = angle * sens_d %}\n {# Get brightness as a percent. + #}\n{% set cb = (state_attr( light, 'brightness') | float(10) / 255.0) * 100.0 + %} {% set new_brightness = cb | int(10) + step_size %} {% if new_brightness < + 5 %}\n {# If it gets really low set to 0. Adjust if needed for your lights. #}\n + \ 0\n{% elif new_brightness <= min_b_var %}\n {# If it's not quite 0, set to + a minimum working brightness. #}\n {{ min_b_var }}\n{% elif 91 <= new_brightness + < (90 + step_size) %}\n {# If it's almost full brightness, set to 100%. #}\n + \ 100\n{% else %}\n {# Send actual calculated value. #}\n{{ new_brightness }} + {% endif %}" + trans_i: !input transition_i + transition_var: "{% if is_number(transition | default('none')) %}\n {{ transition + }}\n{% elif is_number(trans_i | default('none')) %}\n {{ trans_i }}\n{% else + %}\n none\n{% endif %}" +sequence: +- if: + - alias: turn the light & set brightness + condition: template + value_template: '{{ is_number(transition_var) }}' + then: + - alias: Transition number IS available + action: light.turn_on + data: + entity_id: '{{ light }}' + brightness_pct: '{{ bright_p }}' + transition: '{{ transition_var }}' + else: + - alias: NO transition number available + action: light.turn_on + data: + entity_id: '{{ light }}' + brightness_pct: '{{ bright_p }}' +icon: mdi:lightbulb-on-50 +mode: queued +max: 10 diff --git a/configuration.yaml b/configuration.yaml index 7a3894e..43fd883 100644 --- a/configuration.yaml +++ b/configuration.yaml @@ -60,13 +60,13 @@ template: device_class: power state_class: measurement state: > - {% set hvac = states.climate | selectattr('entity_id', 'search', 'living_room') | map(attribute='entity_id') | first %} - {% if hvac %} + {% set hvac = 'climate.living_room_living_room' %} + {% if states(hvac) not in ['unknown', 'unavailable'] %} {% set action = state_attr(hvac, 'hvac_action') | string | lower %} {% if action == 'cooling' %} 2.12 {% elif action == 'heating' %} - {% if is_state_attr(hvac, 'is_aux_heat', true) %} + {% if is_state_attr(hvac, 'preset_mode', 'eco') or is_state_attr(hvac, 'aux_heat', 'on') %} 20.5 {% else %} 2.5 @@ -116,9 +116,7 @@ http: use_x_forwarded_for: true trusted_proxies: - 192.168.122.1 - - 192.168.1.130 - - 192.168.1.125 - - 192.168.1.135 + - 192.168.1.0/24 # Legacy and Integration Sensor Section sensor: diff --git a/custom_components/album_slideshow/__init__.py b/custom_components/album_slideshow/__init__.py new file mode 100644 index 0000000..5647de8 --- /dev/null +++ b/custom_components/album_slideshow/__init__.py @@ -0,0 +1,431 @@ +from __future__ import annotations + +import asyncio +import json +import logging +import os + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED +from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er + +from .const import DOMAIN, SERVICE_NEXT_SLIDE, SERVICE_REFRESH_ALBUM, ATTR_ENTRY_ID +from .store import SlideshowStore + +_LOGGER = logging.getLogger(__name__) + +PLATFORMS: list[str] = ["camera", "sensor", "button", "number", "select", "text", "switch"] + +CARD_STATIC_PATH = "/album_slideshow_static" +CARD_FILE = "album-slideshow-card.js" + + +async def _async_register_card(hass: HomeAssistant) -> None: + """Serve the Lovelace card JS and register it as a frontend module. + + Idempotent: only the first config entry to load triggers registration + for the HA session. The card lets dashboards cross-fade between slides + in the browser (GPU compositor) instead of forcing the camera entity + to render a JPEG burst on the event loop. + """ + if hass.data.get(DOMAIN, {}).get("card_registered"): + return + + integration_dir = os.path.dirname(__file__) + www_dir = os.path.join(integration_dir, "www") + card_path = os.path.join(www_dir, CARD_FILE) + + if not os.path.isfile(card_path): + # Some HACS upgrade paths (and broken zip extractors) drop the + # ``www/`` subdirectory. Try to recover by checking whether the + # integration root has the file under a literal-backslash name + # (a symptom of zips written with Windows path separators) or + # directly at the root, and salvage it into ``www/`` so the + # rest of the registration can proceed. + recovered = await hass.async_add_executor_job( + _recover_card_from_root, integration_dir, www_dir, card_path + ) + if not recovered: + _LOGGER.warning( + "Album Slideshow card missing on disk (%s). Re-install" + " the integration via HACS (3-dot menu -> Redownload)" + " or copy %s/%s into the album_slideshow folder." + " The custom:album-slideshow-card type will not be" + " available until this is fixed.", + card_path, + "www", + CARD_FILE, + ) + return + + try: + from homeassistant.components.http import StaticPathConfig + + await hass.http.async_register_static_paths( + [StaticPathConfig(CARD_STATIC_PATH, www_dir, False)] + ) + except Exception: # noqa: BLE001 - many possible failure modes here + _LOGGER.exception( + "Failed to register static path for Album Slideshow card" + ) + return + + # Cache-bust the card URL with the integration version so dashboards + # always pick up the script that matches the running integration + # rather than a stale copy from a previous release. + version = await hass.async_add_executor_job( + _read_manifest_version, integration_dir + ) + card_url = f"{CARD_STATIC_PATH}/{CARD_FILE}" + if version: + card_url = f"{card_url}?v={version}" + + # Prefer registering the card as a Lovelace resource for storage-mode + # dashboards. Resources are loaded as part of the Lovelace bootstrap, + # before any dashboard renders custom cards, which removes the race + # where the dashboard can hit "Custom element doesn't exist" if the + # browser hasn't finished loading the module yet (a real risk after + # HA restarts and integration upgrades that bust the cache). + # + # The storage-mode resources collection is only consumed by + # storage-mode dashboards; YAML-mode dashboards (whether the user + # has ``lovelace.mode: yaml`` globally or per-dashboard + # ``mode: yaml`` entries) read only the resources declared in their + # own YAML and would otherwise never load the card. That's why we + # *always* also call ``add_extra_js_url``: the frontend injects a + # `` + +`; + + const win = window.open("", "_blank", "width=900,height=1100"); + if (!win) { + // Popup blocker — fall back to the legacy in-place print so the user + // still gets *something* (with the HA shell drawback they reported). + window.print(); + return; + } + win.document.open(); + win.document.write(html); + win.document.close(); + } + + private _escapeHtml(s: string): string { + return s.replace(/[&<>"']/g, (c) => ({ + "&": "&", "<": "<", ">": ">", '"': """, "'": "'", + }[c] as string)); + } + + private _renderImportExport(L: string) { + return html` +
+

${t("settings_import_export", L)}

+
+ +
+
+ + + +
+
+ +
+ +
+
+
+ `; + } + + // --- Export / Import actions --- + + private async _exportJson(): Promise { + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/export", + format: "json", + include_history: this._includeHistory, + }) as { data: string }; + const ts = new Date().toISOString().slice(0, 10); + this._downloadFile(result.data, `maintenance_export_${ts}.json`, "application/json"); + this._showToast(t("settings_export_success", this._lang)); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + private async _exportYaml(): Promise { + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/export", + format: "yaml", + include_history: this._includeHistory, + }) as { data: string }; + const ts = new Date().toISOString().slice(0, 10); + this._downloadFile(result.data, `maintenance_export_${ts}.yaml`, "application/yaml"); + this._showToast(t("settings_export_success", this._lang)); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + private async _exportCsv(): Promise { + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/csv/export", + }) as { csv: string }; + const ts = new Date().toISOString().slice(0, 10); + this._downloadFile(result.csv, `maintenance_export_${ts}.csv`, "text/csv"); + this._showToast(t("settings_export_success", this._lang)); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + private async _importCsvAction(): Promise { + const content = this._importCsv.trim(); + if (!content) return; + this._importLoading = true; + try { + // CSV exports start with the "object_name" header; anything else + // (JSON `{`/`[` or YAML `version:`) goes to the structured importer, + // which parses JSON and YAML alike. + const isCsv = content.startsWith("object_name"); + const result = await this.hass.connection.sendMessagePromise( + isCsv + ? { type: "maintenance_supporter/csv/import", csv_content: content } + : { type: "maintenance_supporter/json/import", json_content: content } + ) as { created: number }; + const count = result.created ?? 0; + this._showToast(t("settings_import_success", this._lang).replace("{count}", String(count))); + this._importCsv = ""; + this.dispatchEvent(new CustomEvent("settings-changed")); + } catch { + this._showToast(t("action_error", this._lang)); + } + this._importLoading = false; + } + + // --- Styles --- + + static styles = css` + :host { display: block; } + + .settings-loading { + text-align: center; + padding: 32px; + color: var(--secondary-text-color); + } + + .settings-section { + margin-bottom: 24px; + padding: 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color, #e0e0e0); + } + .settings-section h3 { + margin: 0 0 4px 0; + font-size: 16px; + } + .section-desc { + font-size: 13px; + color: var(--secondary-text-color); + margin: 0 0 16px 0; + } + + .setting-row { + display: flex; + align-items: center; + justify-content: space-between; + padding: 10px 0; + border-bottom: 1px solid var(--divider-color, #e0e0e0); + cursor: pointer; + gap: 12px; + } + .setting-row:last-child { border-bottom: none; } + .setting-row.sub-row { + padding-left: 16px; + } + + .setting-label { font-size: 14px; display: block; } + .setting-desc { font-size: 12px; color: var(--secondary-text-color); display: block; } + + .setting-row input[type="checkbox"] { + width: 18px; height: 18px; flex-shrink: 0; + } + .setting-row input[type="number"], + .setting-row input[type="text"], + .setting-row input[type="time"] { + width: 120px; + padding: 6px 8px; + border: 1px solid var(--divider-color, #e0e0e0); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 14px; + flex-shrink: 0; + } + .setting-row input[type="number"] { + text-align: right; + } + .setting-row select { + padding: 6px 8px; + border: 1px solid var(--divider-color, #e0e0e0); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 14px; + flex-shrink: 0; + } + + .settings-actions { + display: flex; gap: 8px; flex-wrap: wrap; margin-top: 12px; + } + .settings-actions button { + padding: 8px 16px; + border-radius: 8px; + border: 1px solid var(--divider-color, #e0e0e0); + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + cursor: pointer; + font-size: 14px; + } + .settings-actions button:hover { + background: var(--secondary-background-color, #f5f5f5); + } + .settings-actions button[disabled] { + opacity: 0.5; + cursor: not-allowed; + } + + .export-history-toggle { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + cursor: pointer; + } + .export-history-toggle input { width: 16px; height: 16px; } + + .import-section { margin-top: 16px; } + + .import-area { + width: 100%; + min-height: 120px; + padding: 8px; + border: 1px solid var(--divider-color, #e0e0e0); + border-radius: 8px; + font-family: monospace; + font-size: 12px; + resize: vertical; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + box-sizing: border-box; + } + + .settings-toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + background: var(--primary-color, #03a9f4); + color: #fff; + padding: 10px 24px; + border-radius: 8px; + font-size: 14px; + z-index: 1000; + box-shadow: 0 2px 8px rgba(0,0,0,.3); + animation: toast-in .3s ease; + } + @keyframes toast-in { + from { opacity: 0; transform: translateX(-50%) translateY(16px); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } + } + + /* ─── Vacation mode section (v1.2.0) ─── */ + + .vacation-section h3 { + display: flex; + align-items: center; + gap: 8px; + } + .vac-badge { + font-size: 11px; + font-weight: 600; + letter-spacing: 0.4px; + text-transform: uppercase; + padding: 2px 8px; + border-radius: 10px; + } + .vac-badge.active { + background: var(--success-color, #4caf50); + color: #fff; + } + .vac-badge.stale { + background: var(--warning-color, #ff9800); + color: #fff; + } + .vac-toggle { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + margin: 8px 0 12px; + } + .vac-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 12px; + margin-bottom: 12px; + } + .vac-field { + display: flex; + flex-direction: column; + gap: 4px; + } + .vac-field input { + padding: 6px 8px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + } + .vac-exempt-panel { + border: 1px solid var(--divider-color); + border-radius: 6px; + padding: 10px; + margin: 12px 0; + } + .vac-exempt-panel summary { + cursor: pointer; + font-weight: 500; + display: flex; + align-items: center; + gap: 8px; + } + .section-badge { + background: var(--primary-color, #03a9f4); + color: #fff; + font-size: 11px; + font-weight: 600; + padding: 1px 8px; + border-radius: 10px; + } + .vac-task-list { + max-height: 280px; + overflow-y: auto; + margin-top: 8px; + } + .vac-task-group { + margin: 8px 0; + } + .vac-task-group-name { + font-size: 12px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.4px; + color: var(--secondary-text-color); + padding: 4px 0; + } + .vac-task-row { + display: flex; + align-items: center; + gap: 8px; + padding: 4px 8px; + cursor: pointer; + border-radius: 4px; + } + .vac-task-row:hover { background: var(--secondary-background-color, rgba(127,127,127,0.1)); } + .vac-preview-toolbar { + display: flex; + align-items: center; + gap: 12px; + margin: 12px 0 8px; + } + .vac-preview-count { + color: var(--secondary-text-color); + font-size: 13px; + } + .vac-preview-list { + display: flex; + flex-direction: column; + gap: 6px; + } + .vac-preview-row { + display: flex; + gap: 12px; + padding: 10px 12px; + background: var(--secondary-background-color, rgba(127,127,127,0.08)); + border-radius: 6px; + border-left: 3px solid var(--warning-color, #ff9800); + } + .vac-preview-row.exempt { + border-left-color: var(--success-color, #4caf50); + } + .vac-preview-info { flex: 1; } + .vac-preview-name { font-size: 14px; } + .vac-preview-kind { + font-size: 11px; + color: var(--secondary-text-color); + margin-left: 6px; + } + .vac-preview-events { + font-size: 12px; + color: var(--secondary-text-color); + margin-top: 2px; + } + .vac-preview-actions { + display: flex; + gap: 6px; + align-items: center; + flex-wrap: wrap; + } + .vac-preview-actions button { + font-size: 12px; + padding: 4px 10px; + } + .vac-notify-on { background: var(--success-color, #4caf50) !important; color: #fff; } + .vac-end-now { + margin-top: 12px; + background: var(--error-color, #f44336); + color: #fff; + } + + /* ─── Print QR codes section (v1.1.0) ─── */ + + .qr-filter-panel { + border: 1px solid var(--divider-color); + border-radius: 6px; + padding: 12px; + margin-top: 8px; + } + .qr-filter-panel > summary { + cursor: pointer; + font-weight: 500; + } + .qr-filter-group { + margin-top: 12px; + } + .qr-filter-label { + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.4px; + color: var(--secondary-text-color); + margin-bottom: 4px; + } + .qr-object-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 4px 12px; + max-height: 240px; + overflow-y: auto; + } + .qr-object-row { + display: flex; + align-items: center; + gap: 6px; + padding: 4px 6px; + cursor: pointer; + border-radius: 4px; + } + .qr-object-row:hover { background: var(--secondary-background-color, rgba(127,127,127,0.1)); } + .qr-object-row > span:nth-of-type(1) { flex: 1; } + .qr-task-count { + color: var(--secondary-text-color); + font-size: 12px; + font-variant-numeric: tabular-nums; + } + + .qr-action-chips { + display: flex; + gap: 6px; + flex-wrap: wrap; + } + .qr-action-chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border-radius: 14px; + border: 1px solid var(--divider-color); + cursor: pointer; + user-select: none; + } + .qr-action-chip.active { + background: var(--primary-color, #03a9f4); + color: #fff; + border-color: transparent; + } + .qr-action-chip input { accent-color: currentColor; } + + .qr-filter-actions { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + } + .qr-estimate { font-size: 13px; } + .qr-estimate.error { color: var(--error-color, #f44336); } + + .qr-results-toolbar { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 16px; + padding: 8px 12px; + background: var(--secondary-background-color, rgba(127,127,127,0.1)); + border-radius: 6px; + } + + .qr-print-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(180px, 1fr)); + gap: 16px; + margin-top: 16px; + } + .qr-print-cell { + border: 1px solid var(--divider-color); + border-radius: 6px; + padding: 8px; + display: flex; + flex-direction: column; + align-items: center; + text-align: center; + background: #fff; + color: #000; + } + .qr-print-cell .qr-svg { + width: 100%; + max-width: 160px; + } + .qr-print-cell .qr-svg svg { width: 100%; height: auto; display: block; } + .qr-label { + margin-top: 6px; + font-size: 11px; + line-height: 1.3; + } + .qr-label-obj { font-weight: 600; } + .qr-label-task { color: #444; } + .qr-label-action { + margin-top: 2px; + text-transform: uppercase; + letter-spacing: 0.5px; + font-size: 10px; + color: #777; + } + + /* ─── Print stylesheet ─── */ + @media print { + /* Strip everything except the QR grid itself */ + :host { color: #000; background: #fff; } + .qr-print-section h3, + .qr-print-section .section-desc, + .qr-filter-panel, + .qr-results-toolbar, + .settings-section:not(.qr-print-section), + .settings-toast { + display: none !important; + } + .qr-print-section { padding: 0; margin: 0; } + .qr-print-grid { + grid-template-columns: repeat(3, 1fr); + gap: 12mm 8mm; + margin: 0; + } + .qr-print-cell { + border: none; + padding: 0; + page-break-inside: avoid; + } + .qr-print-cell .qr-svg { max-width: 48mm; } + .qr-label { font-size: 9pt; } + } + `; +} + +customElements.define("maintenance-settings-view", MaintenanceSettingsView); diff --git a/custom_components/maintenance_supporter/frontend-src/components/storage-section-card.ts b/custom_components/maintenance_supporter/frontend-src/components/storage-section-card.ts new file mode 100644 index 0000000..f0828a3 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/components/storage-section-card.ts @@ -0,0 +1,361 @@ +/** Document-storage overview card (panel overview). + * + * Shows the physical footprint (real backup cost), the dedup saving, and a + * per-object drill-down sorted by size. Object ids from the WS summary are + * mapped to names via the panel's already-loaded objects. Self-hides when no + * documents exist, so it never clutters the overview for non-users. + */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import { t, ensureLocale } from "../styles"; +import { describeWsError } from "../ws-errors"; +import { formatBytes } from "../helpers/format-bytes"; +import type { HomeAssistant } from "../types"; + +interface StorageSummary { + total_bytes: number; + dedup_savings_bytes: number; + file_count: number; + link_count: number; + document_count: number; + by_object: Record; +} + +interface PanelObject { + entry_id: string; + object?: { id?: string; name?: string }; +} + +interface SearchResult { + id: string; + entry_id: string; + object_name: string; + kind: "file" | "weblink"; + title: string; + filename?: string; + url?: string; + size?: number; + tags?: string[]; +} + +export class MaintenanceStorageSectionCard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public objects: PanelObject[] = []; + + @state() private _summary: StorageSummary | null = null; + @state() private _loaded = false; + @state() private _busy = false; + @state() private _error = ""; + @state() private _query = ""; + @state() private _results: SearchResult[] = []; + @state() private _expanded = false; + + private _initiallyLoaded = false; + private _searchTimer = 0; + + private get _lang(): string { + return this.hass?.language || "en"; + } + + updated(changed: Map): void { + super.updated(changed); + if (changed.has("hass") && this.hass && !this._initiallyLoaded) { + this._initiallyLoaded = true; + void this._load(); + // Re-render once the runtime locale JSON arrives (t() falls back to + // English until then; this card can paint before the panel's fetch lands). + void ensureLocale(this._lang).then(() => this.requestUpdate()); + } + } + + private async _load(): Promise { + this._busy = true; + try { + this._summary = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/documents/storage", + }); + this._error = ""; + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._loaded = true; + this._busy = false; + } + } + + private _nameFor(objectId: string): string { + const o = this.objects.find((x) => x.object?.id === objectId); + return o?.object?.name || objectId.slice(0, 8); + } + + private _entryFor(objectId: string): string | undefined { + return this.objects.find((x) => x.object?.id === objectId)?.entry_id; + } + + private _toggle(): void { + this._expanded = !this._expanded; + } + + /** Ask the panel (which owns navigation) to open an object's detail view. */ + private _openObject(entryId: string): void { + this.dispatchEvent( + new CustomEvent("open-object", { + detail: { entry_id: entryId }, + bubbles: true, + composed: true, + }), + ); + } + + private _onSearch(e: Event): void { + this._query = (e.target as HTMLInputElement).value; + clearTimeout(this._searchTimer); + this._searchTimer = window.setTimeout(() => void this._doSearch(), 250); + } + + private async _doSearch(): Promise { + const q = this._query.trim(); + if (!q) { + this._results = []; + return; + } + try { + const r = await this.hass.connection.sendMessagePromise<{ results: SearchResult[] }>({ + type: "maintenance_supporter/documents/search", + query: q, + }); + this._results = r.results || []; + } catch (e) { + this._error = describeWsError(e, this._lang); + this._results = []; + } + } + + private async _openResult(doc: SearchResult): Promise { + if (doc.kind === "weblink") { + window.open(doc.url, "_blank", "noopener"); + return; + } + const win = window.open("about:blank", "_blank"); + try { + const s = await this.hass.connection.sendMessagePromise<{ path: string }>({ + type: "auth/sign_path", + path: `/api/maintenance_supporter/document/${doc.id}`, + expires: 300, + }); + // Absolute URL so it navigates the blank popup reliably (about:blank base). + if (win) win.location.href = new URL(s.path, window.location.origin).href; + } catch (e) { + if (win) win.close(); + this._error = describeWsError(e, this._lang); + } + } + + private _renderResult(doc: SearchResult, L: string) { + return html` +
this._openResult(doc)}> + +
+
${doc.title || doc.filename || doc.url}
+
${doc.object_name}
+
+ +
+ `; + } + + render() { + if (!this._loaded || !this._summary) return nothing; + const s = this._summary; + if (s.document_count === 0) return nothing; // self-hide when unused + const L = this._lang; + + const rows = Object.entries(s.by_object) + .filter(([, v]) => v.files > 0 || v.links > 0) + .map(([id, v]) => ({ id, name: this._nameFor(id), entry: this._entryFor(id), ...v })) + .sort((a, b) => b.bytes - a.bytes); + + return html` + +
+
+ + +
+ + ${this._expanded + ? html` +
+
+
+
${formatBytes(s.total_bytes)}
+
+ ${s.file_count} + ${s.link_count} +
+
+ ${s.dedup_savings_bytes > 0 + ? html`
+
−${formatBytes(s.dedup_savings_bytes)}
+
${t("doc_storage_saved", L)}
+
` + : nothing} +
+ + + + ${this._error ? html`
${this._error}
` : nothing} + + ${this._query.trim() + ? this._results.length + ? html`
${this._results.map((d) => this._renderResult(d, L))}
` + : html`
${t("doc_search_none", L)}
` + : rows.length + ? html`
${rows.map((r) => this._renderObjRow(r, L))}
` + : nothing} +
+ ` + : nothing} +
+
+ `; + } + + private _renderObjRow( + r: { id: string; name: string; entry?: string; bytes: number; files: number; links: number }, + L: string, + ) { + const eid = r.entry; + return html` +
this._openObject(eid) : undefined} + @keydown=${eid + ? (e: KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + this._openObject(eid); + } + } + : undefined} + > + ${r.name} + + ${r.files > 0 ? html`${r.files}` : nothing} + ${r.links > 0 ? html`${r.links}` : nothing} + + ${formatBytes(r.bytes)} + ${eid ? html`` : nothing} +
+ `; + } + + static styles = css` + ha-card { margin-top: 16px; } + .card-content { padding: 16px; } + .doc-search { + display: flex; align-items: center; gap: 6px; margin: 10px 0 4px; + padding: 2px 10px; border-radius: 8px; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.06)); + border: 1px solid var(--divider-color); + } + .doc-search ha-icon { --mdc-icon-size: 18px; color: var(--secondary-text-color, #888); } + .doc-search input { + flex: 1; border: none; background: transparent; font: inherit; outline: none; + color: var(--primary-text-color); padding: 6px 0; + } + .result-row { cursor: pointer; } + .result-row > ha-icon { color: var(--primary-color); --mdc-icon-size: 20px; flex: none; } + .result-info { flex: 1; min-width: 0; } + .result-title { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .result-obj { font-size: 12px; color: var(--secondary-text-color, #888); } + .result-open { color: var(--secondary-text-color, #888); --mdc-icon-size: 18px; flex: none; } + .search-empty { color: var(--secondary-text-color, #888); font-size: 13px; padding: 8px 2px; } + .header { display: flex; align-items: center; justify-content: space-between; gap: 8px; } + .toggle { + display: flex; align-items: center; gap: 8px; flex: 1; min-width: 0; + background: none; border: none; padding: 4px 0; margin: 0; cursor: pointer; + font: inherit; color: var(--primary-text-color); text-align: left; + } + .toggle:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; border-radius: 6px; } + .chevron { --mdc-icon-size: 22px; color: var(--secondary-text-color, #888); flex: none; } + .title-text { font-size: 16px; font-weight: 500; } + .header-summary { + margin-left: auto; display: flex; align-items: center; gap: 8px; + font-size: 14px; font-weight: 600; white-space: nowrap; + } + .header-summary .saved { color: var(--success-color, #4caf50); font-weight: 500; } + .emoji { font-size: 18px; } + .body { margin-top: 4px; } + .totals { display: flex; gap: 24px; margin: 12px 0 8px; flex-wrap: wrap; } + .stat-value { font-size: 22px; font-weight: 600; } + .stat-value.saved { color: var(--success-color, #4caf50); } + .stat-label { + font-size: 12px; color: var(--secondary-text-color, #888); + display: flex; align-items: center; gap: 4px; + } + .stat-label ha-icon { --mdc-icon-size: 15px; } + .obj-list { display: flex; flex-direction: column; gap: 2px; margin-top: 8px; } + .obj-row { + display: flex; align-items: center; gap: 10px; + padding: 6px 8px; border-radius: 6px; + } + .obj-row:nth-child(odd) { background: var(--secondary-background-color, rgba(0,0,0,0.04)); } + .obj-row.clickable { cursor: pointer; } + .obj-row.clickable:hover { background: var(--secondary-background-color, rgba(0,0,0,0.10)); } + .obj-row.clickable:focus-visible { outline: 2px solid var(--primary-color); outline-offset: -2px; } + .obj-go { --mdc-icon-size: 18px; color: var(--secondary-text-color, #888); flex: none; } + .obj-name { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 14px; } + .obj-meta { + display: flex; align-items: center; gap: 4px; + color: var(--secondary-text-color, #888); font-size: 13px; + } + .obj-meta ha-icon { --mdc-icon-size: 15px; } + .obj-size { font-variant-numeric: tabular-nums; font-size: 13px; min-width: 64px; text-align: right; } + .icon-btn { + display: inline-flex; align-items: center; justify-content: center; + width: 32px; height: 32px; border-radius: 8px; cursor: pointer; + background: transparent; border: none; color: var(--primary-text-color); + } + .icon-btn:hover { background: var(--secondary-background-color, rgba(0,0,0,0.06)); } + .icon-btn[disabled] { opacity: 0.4; pointer-events: none; } + .error { color: var(--error-color, #f44336); font-size: 13px; margin-top: 6px; } + `; +} + +if (!customElements.get("maintenance-storage-section-card")) { + customElements.define("maintenance-storage-section-card", MaintenanceStorageSectionCard); +} diff --git a/custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts b/custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts new file mode 100644 index 0000000..5ec6d72 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/components/task-dialog.ts @@ -0,0 +1,1791 @@ +/** Dialog for creating/editing a maintenance task. */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import type { AdaptiveConfig, HomeAssistant, MaintenanceTask, TriggerConfig, HAUser } from "../types"; +import { t, weekdayName } from "../styles"; +import { UserService } from "../user-service"; + +import { describeWsError } from "../ws-errors"; +import "./ms-textfield"; + +const MAINTENANCE_TYPE_KEYS = ["cleaning", "inspection", "replacement", "calibration", "service", "reading", "custom"]; +const PRIORITY_KEYS = ["low", "normal", "high"]; +const SCHEDULE_TYPE_KEYS = ["time_based", "weekdays", "nth_weekday", "day_of_month", "sensor_based", "one_time", "manual"]; +const CALENDAR_KINDS = ["weekdays", "nth_weekday", "day_of_month"]; +const TRIGGER_TYPE_KEYS = ["threshold", "counter", "state_change", "runtime"]; +// The type selector additionally offers "compound" (a group of conditions +// joined by AND/OR); its per-condition sub-type is limited to the flat kinds. +const TRIGGER_TYPE_KEYS_WITH_COMPOUND = [...TRIGGER_TYPE_KEYS, "compound"]; + +/** One condition of a compound trigger — a flat trigger the user edits inline. + * String-typed like the top-level fields (form inputs); coerced on save. */ +interface CompoundConditionDraft { + entityIds: string; // comma-separated raw input + type: string; // threshold | counter | state_change | runtime + above: string; + below: string; + forMinutes: string; + targetValue: string; + deltaMode: boolean; + fromState: string; + toState: string; + targetChanges: string; + runtimeHours: string; +} + +function emptyCondition(): CompoundConditionDraft { + return { + entityIds: "", type: "threshold", above: "", below: "", forMinutes: "0", + targetValue: "", deltaMode: false, fromState: "", toState: "", + targetChanges: "", runtimeHours: "", + }; +} + +/** Map a persisted compound condition (storage shape) to an editable draft. */ +function conditionToDraft(c: TriggerConfig): CompoundConditionDraft { + const ids = c.entity_ids || (c.entity_id ? [c.entity_id] : []); + return { + entityIds: ids.join(", "), + type: c.type || "threshold", + above: c.trigger_above?.toString() ?? "", + below: c.trigger_below?.toString() ?? "", + forMinutes: c.trigger_for_minutes?.toString() ?? "0", + targetValue: c.trigger_target_value?.toString() ?? "", + deltaMode: c.trigger_delta_mode || false, + fromState: c.trigger_from_state || "", + toState: c.trigger_to_state || "", + targetChanges: c.trigger_target_changes?.toString() ?? "", + runtimeHours: c.trigger_runtime_hours?.toString() ?? "", + }; +} + +/** Build the persisted condition (storage shape) from an editable draft. + * Returns null when the condition has no entity (skip it). */ +function draftToCondition(d: CompoundConditionDraft): TriggerConfig | null { + const ids = d.entityIds.split(",").map((s) => s.trim()).filter(Boolean); + if (ids.length === 0) return null; + const c: TriggerConfig = { entity_id: ids[0], entity_ids: ids, type: d.type }; + if (d.type === "threshold") { + const a = parseFloat(d.above); if (!isNaN(a)) c.trigger_above = a; + const b = parseFloat(d.below); if (!isNaN(b)) c.trigger_below = b; + const f = parseInt(d.forMinutes, 10); if (!isNaN(f)) c.trigger_for_minutes = f; + } else if (d.type === "counter") { + const v = parseFloat(d.targetValue); if (!isNaN(v)) c.trigger_target_value = v; + c.trigger_delta_mode = d.deltaMode; + } else if (d.type === "state_change") { + if (d.fromState) c.trigger_from_state = d.fromState; + if (d.toState) c.trigger_to_state = d.toState; + const n = parseInt(d.targetChanges, 10); if (!isNaN(n)) c.trigger_target_changes = n; + } else if (d.type === "runtime") { + const h = parseFloat(d.runtimeHours); if (!isNaN(h)) c.trigger_runtime_hours = h; + } + return c; +} + +/** Short localized weekday names (0=Mon … 6=Sun) — uses the shared weekdayName (DRY). */ +function weekdayNames(lang?: string): string[] { + return Array.from({ length: 7 }, (_, i) => weekdayName(i, lang, "short")); +} + +export class MaintenanceTaskDialog extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @property({ type: Boolean, attribute: "checklists-enabled" }) public checklistsEnabled = false; + @property({ type: Boolean, attribute: "schedule-time-enabled" }) public scheduleTimeEnabled = false; + @property({ type: Boolean, attribute: "completion-actions-enabled" }) public completionActionsEnabled = false; + @property({ type: Number, attribute: "default-warning-days" }) public defaultWarningDays = 7; + @state() private _open = false; + @state() private _loading = false; + @state() private _error = ""; + @state() private _entryId = ""; + @state() private _taskId: string | null = null; // null = create + // When openCreate is called without an entry_id and a list of objects is supplied, + // the dialog renders an Object selector dropdown so the user can pick the parent. + @state() private _objectChoices: Array<{ entry_id: string; name: string }> = []; + + // Task fields + @state() private _name = ""; + @state() private _type = "custom"; + @state() private _scheduleType = "time_based"; + @state() private _intervalDays = "30"; + @state() private _intervalUnit = "days"; + @state() private _dueDate = ""; + @state() private _warningDays = "7"; + @state() private _earliestCompletionDays = ""; + @state() private _intervalAnchor: "completion" | "planned" = "completion"; + // Calendar kinds (Phase 4): weekdays (0=Mon…6=Sun), nth_weekday, day_of_month + @state() private _weekdays: number[] = []; + @state() private _nth = "1"; // "1".."5" or "-1" (last) + @state() private _nthWeekday = "5"; // Saturday + @state() private _domDay = "1"; + // (#83) end-of-month options: last day / business-day rollback / ±N offset. + @state() private _domLastDay = false; + @state() private _domBusiness = false; + @state() private _calOffset = "0"; + @state() private _notes = ""; + @state() private _documentationUrl = ""; + @state() private _customIcon = ""; + @state() private _priority = "normal"; + @state() private _labels = ""; + @state() private _enabled = true; + + // Trigger fields + @state() private _triggerEntityId = ""; + @state() private _triggerEntityIds: string[] = []; + @state() private _triggerEntityLogic: "any" | "all" = "any"; + @state() private _triggerAttribute = ""; + @state() private _triggerType = "threshold"; + @state() private _triggerAbove = ""; + @state() private _triggerBelow = ""; + @state() private _triggerForMinutes = "0"; + @state() private _triggerTargetValue = ""; + @state() private _triggerDeltaMode = false; + @state() private _autoCompleteOnRecovery = false; + @state() private _triggerFromState = ""; + @state() private _triggerToState = ""; + @state() private _triggerTargetChanges = ""; + @state() private _triggerRuntimeHours = ""; + // Compound trigger (type === "compound"): a list of conditions + AND/OR logic + @state() private _compoundLogic: "AND" | "OR" = "AND"; + @state() private _compoundConditions: CompoundConditionDraft[] = []; + + // Entity attribute introspection + @state() private _suggestedAttributes: string[] = []; + @state() private _availableAttributes: Array<{ name: string; value: unknown; numeric: boolean }> = []; + @state() private _entityDomain = ""; + + // NFC + @state() private _lastPerformed = ""; + @state() private _nfcTagId = ""; + // v2.20 (#83): unit for `reading`-type tasks ("kWh", "m³", ...) + @state() private _readingUnit = ""; + @state() private _availableTags: Array<{id: string; name: string}> = []; + + // User assignment + @state() private _responsibleUserId: string | null = null; + @state() private _assigneePool: string[] = []; + @state() private _rotationStrategy = ""; + @state() private _availableUsers: HAUser[] = []; + + // Checklist (newline-separated steps, one per line) + @state() private _checklistText = ""; + + // Schedule time (HH:MM, advanced feature) + @state() private _scheduleTime = ""; + + // v1.3.0: on_complete_action (gated by completionActionsEnabled) + // v1.3.1: _actionData is the parsed object (was: _actionDataJson string) + // so ha-form can drive the data fields when the service schema is known. + @state() private _actionService = ""; + @state() private _actionTargetEntity = ""; + @state() private _actionData: Record = {}; + @state() private _actionDataJsonFallback = ""; + @state() private _actionTesting = false; + @state() private _actionTestResult: "" | "ok" | "error" = ""; + @state() private _actionTestError = ""; + + // v1.3.0: quick_complete_defaults (gated by completionActionsEnabled) + @state() private _qcNotes = ""; + @state() private _qcCost = ""; + @state() private _qcDuration = ""; + @state() private _qcFeedback: "" | "needed" | "not_needed" = ""; + + // Environmental entity (adaptive_config) + @state() private _environmentalEntity = ""; + @state() private _environmentalAttribute = ""; + private _environmentalInitial = ""; // for change detection on save + private _environmentalAttributeInitial = ""; + private _userService: UserService | null = null; + + private get _lang(): string { + return this.hass?.language ?? navigator.language.split("-")[0] ?? "en"; + } + + public async openCreate(entryId: string, objects?: Array<{ entry_id: string; object: { name: string } }>): Promise { + this._entryId = entryId; + this._taskId = null; + this._error = ""; + // If no entryId is preset but caller passed objects, expose them as a dropdown. + // Sort alphabetically by name so the user doesn't have to scan for a target + // object in creation order (#40). First object after sort becomes the + // default selection so save can work without forced UI. + if (!entryId && objects && objects.length > 0) { + this._objectChoices = objects + .map(o => ({ entry_id: o.entry_id, name: o.object.name })) + .sort((a, b) => a.name.localeCompare(b.name)); + this._entryId = this._objectChoices[0].entry_id; + } else { + this._objectChoices = []; + } + this._resetFields(); + await Promise.all([this._loadUsers(), this._loadTags()]); + this._open = true; + } + + public async openEdit(entryId: string, task: MaintenanceTask): Promise { + this._entryId = entryId; + this._taskId = task.id; + this._error = ""; + this._name = task.name; + this._type = task.type; + this._scheduleType = task.schedule_type; + // Preserve null (= no safety interval set) instead of forcing "30". + // Bug #42: the old `?.toString() || "30"` fallback reverted a cleared + // safety_interval back to 30 on next edit, silently overwriting the + // user's intent the moment they touched any other field and saved. + this._intervalDays = task.interval_days != null ? String(task.interval_days) : ""; + this._intervalUnit = task.interval_unit || "days"; + this._dueDate = task.due_date || ""; + // Calendar kinds read the nested schedule (the only place they live). + const sched = task.schedule; + this._weekdays = sched?.kind === "weekdays" ? [...(sched.weekdays ?? [])] : []; + this._nth = sched?.kind === "nth_weekday" ? String(sched.nth ?? 1) : "1"; + this._nthWeekday = sched?.kind === "nth_weekday" ? String(sched.weekday ?? 5) : "5"; + this._domDay = sched?.kind === "day_of_month" && (sched.day ?? 1) >= 1 ? String(sched.day ?? 1) : "1"; + this._domLastDay = sched?.kind === "day_of_month" && sched.day === -1; + this._domBusiness = sched?.kind === "day_of_month" && sched.business === true; + this._calOffset = sched?.offset ? String(sched.offset) : "0"; + this._warningDays = task.warning_days.toString(); + this._earliestCompletionDays = + task.earliest_completion_days != null ? String(task.earliest_completion_days) : ""; + this._intervalAnchor = task.interval_anchor || "completion"; + this._notes = task.notes || ""; + this._documentationUrl = task.documentation_url || ""; + this._customIcon = task.custom_icon || ""; + this._priority = task.priority || "normal"; + this._labels = (task.labels || []).join(", "); + this._enabled = task.enabled !== false; + this._lastPerformed = task.last_performed || ""; + this._nfcTagId = task.nfc_tag_id || ""; + this._readingUnit = task.reading_unit || ""; + this._responsibleUserId = task.responsible_user_id || null; + this._assigneePool = [...(task.assignee_pool || [])]; + this._rotationStrategy = task.rotation_strategy || ""; + + this._checklistText = (task.checklist || []).join("\n"); + this._scheduleTime = task.schedule_time || ""; + + // v1.3.0: hydrate on_complete_action + quick_complete_defaults + const oca = task.on_complete_action; + if (oca && oca.service) { + this._actionService = oca.service; + const tgt = oca.target?.entity_id; + this._actionTargetEntity = Array.isArray(tgt) ? (tgt[0] || "") : (tgt || ""); + this._actionData = (oca.data && typeof oca.data === "object") ? { ...oca.data } : {}; + this._actionDataJsonFallback = ""; + } else { + this._actionService = ""; + this._actionTargetEntity = ""; + this._actionData = {}; + this._actionDataJsonFallback = ""; + } + const qcd = task.quick_complete_defaults; + this._qcNotes = qcd?.notes || ""; + this._qcCost = qcd?.cost != null ? String(qcd.cost) : ""; + this._qcDuration = qcd?.duration != null ? String(qcd.duration) : ""; + this._qcFeedback = (qcd?.feedback as "needed" | "not_needed" | undefined) || ""; + + const ac: Partial = task.adaptive_config || {}; + this._environmentalEntity = ac.environmental_entity || ""; + this._environmentalAttribute = ac.environmental_attribute || ""; + this._environmentalInitial = this._environmentalEntity; + this._environmentalAttributeInitial = this._environmentalAttribute; + + if (task.trigger_config) { + const tc = task.trigger_config; + this._triggerEntityId = tc.entity_id || ""; + this._triggerEntityIds = tc.entity_ids || (tc.entity_id ? [tc.entity_id] : []); + this._triggerEntityLogic = tc.entity_logic || "any"; + this._triggerAttribute = tc.attribute || ""; + this._triggerType = tc.type || "threshold"; + this._triggerAbove = tc.trigger_above?.toString() || ""; + this._triggerBelow = tc.trigger_below?.toString() || ""; + this._triggerForMinutes = tc.trigger_for_minutes?.toString() || "0"; + this._triggerTargetValue = tc.trigger_target_value?.toString() || ""; + this._triggerDeltaMode = tc.trigger_delta_mode || false; + this._autoCompleteOnRecovery = tc.auto_complete_on_recovery || false; + this._triggerFromState = tc.trigger_from_state || ""; + this._triggerToState = tc.trigger_to_state || ""; + this._triggerTargetChanges = tc.trigger_target_changes?.toString() || ""; + this._triggerRuntimeHours = tc.trigger_runtime_hours?.toString() || ""; + if (tc.type === "compound") { + this._compoundLogic = tc.compound_logic === "OR" ? "OR" : "AND"; + this._compoundConditions = (tc.conditions || []).map(conditionToDraft); + } else { + this._compoundLogic = "AND"; + this._compoundConditions = []; + } + } else { + this._resetTriggerFields(); + } + + // Fetch entity attributes if trigger entity is set + if (this._triggerEntityId) { + this._fetchEntityAttributes(this._triggerEntityId); + } + + await Promise.all([this._loadUsers(), this._loadTags()]); + this._open = true; + } + + private _resetFields(): void { + this._name = ""; + this._type = "custom"; + this._scheduleType = "time_based"; + this._intervalDays = "30"; + this._intervalUnit = "days"; + this._dueDate = ""; + this._warningDays = String(this.defaultWarningDays); + this._earliestCompletionDays = ""; + this._intervalAnchor = "completion"; + this._weekdays = []; + this._nth = "1"; + this._nthWeekday = "5"; + this._domDay = "1"; + this._domLastDay = false; + this._domBusiness = false; + this._calOffset = "0"; + this._notes = ""; + this._documentationUrl = ""; + this._customIcon = ""; + this._priority = "normal"; + this._labels = ""; + this._enabled = true; + this._lastPerformed = ""; + this._nfcTagId = ""; + this._readingUnit = ""; + this._responsibleUserId = null; + this._assigneePool = []; + this._rotationStrategy = ""; + this._checklistText = ""; + this._scheduleTime = ""; + this._environmentalEntity = ""; + this._environmentalAttribute = ""; + this._environmentalInitial = ""; + this._environmentalAttributeInitial = ""; + // v1.3.0 + this._actionService = ""; + this._actionTargetEntity = ""; + this._actionData = {}; + this._actionDataJsonFallback = ""; + this._actionTesting = false; + this._actionTestResult = ""; + this._qcNotes = ""; + this._qcCost = ""; + this._qcDuration = ""; + this._qcFeedback = ""; + this._resetTriggerFields(); + } + + private _resetTriggerFields(): void { + this._triggerEntityId = ""; + this._triggerEntityIds = []; + this._triggerEntityLogic = "any"; + this._triggerAttribute = ""; + this._suggestedAttributes = []; + this._availableAttributes = []; + this._entityDomain = ""; + this._triggerType = "threshold"; + this._triggerAbove = ""; + this._triggerBelow = ""; + this._triggerForMinutes = "0"; + this._triggerTargetValue = ""; + this._triggerDeltaMode = false; + this._autoCompleteOnRecovery = false; + this._triggerFromState = ""; + this._triggerToState = ""; + this._triggerTargetChanges = ""; + this._triggerRuntimeHours = ""; + this._compoundLogic = "AND"; + this._compoundConditions = []; + } + + private async _loadUsers(): Promise { + if (!this._userService) { + this._userService = new UserService(this.hass); + } + try { + this._availableUsers = await this._userService.getUsers(); + } catch (error) { + console.error("Failed to load users:", error); + this._availableUsers = []; + } + } + + private _toggleAssignee(userId: string): void { + this._assigneePool = this._assigneePool.includes(userId) + ? this._assigneePool.filter((u) => u !== userId) + : [...this._assigneePool, userId]; + } + + // v1.3.0: fire the configured action immediately so the user can verify + // it works before saving the task. Doesn't persist anything. + private async _testAction(): Promise { + // VALIDATE-ONLY — does NOT actually fire the service-call. + // + // Earlier this method called hass.callService(...) for real, which + // had real side effects: counter.increment incremented the counter, + // input_button.press fired the button (which on the user's vacuum + // robot would actually reset the dirty-time sensor as if maintenance + // had been performed). User pushback (correct): + // *"setzt Test auch den zustand? dann würde beim testen bereits so + // getan als ob eine Wartung ausgeführt wurde — das ist nicht gut"* + // + // The Test button is for verifying the *configuration* is correct + // before saving — not for triggering the action. The user can mark + // the task complete to actually fire it. + // + // Checks performed (all without side effects): + // 1. Service format matches `domain.service` regex + // 2. Service exists in hass.services registry + // 3. Domain whitelist for cross-domain services + // 4. Service-entity domain match (otherwise HA silently drops the + // call at fire-time with "Referenced entities ... missing or + // not currently available") + // 5. Target entity exists in hass.states (when target set) + const svc = this._actionService.trim(); + if (!svc || !/^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(svc)) { + this._actionTestResult = "error"; + this._actionTestError = "Invalid service format (expected 'domain.service')"; + setTimeout(() => { this._actionTestResult = ""; this._actionTestError = ""; }, 5000); + return; + } + const [domain, name] = svc.split("."); + + // 2. Service exists? + if (!this.hass?.services?.[domain]?.[name]) { + this._actionTestResult = "error"; + this._actionTestError = `Service "${svc}" is not registered in Home Assistant. Check spelling and that the integration providing it is loaded.`; + setTimeout(() => { this._actionTestResult = ""; this._actionTestError = ""; }, 8000); + return; + } + + const tgt = this._actionTargetEntity.trim(); + + // 3 + 4. Cross-domain whitelist + domain match. + // homeassistant.* / scene.* / notify.* / persistent_notification.* + // legitimately accept cross-domain targets; everything else needs the + // entity domain to match the service domain. + if (tgt) { + const entityDomain = tgt.split(".")[0]; + const crossDomainServices = new Set([ + "homeassistant", "scene", "notify", "persistent_notification", + ]); + if (entityDomain !== domain && !crossDomainServices.has(domain)) { + this._actionTestResult = "error"; + this._actionTestError = `Service "${svc}" only works on ${domain}.* entities; entity "${tgt}" is in ${entityDomain}.* — pick a service that matches the entity domain (e.g. ${entityDomain}.${name})`; + setTimeout(() => { this._actionTestResult = ""; this._actionTestError = ""; }, 8000); + return; + } + // 5. Entity exists? + if (!this.hass.states?.[tgt]) { + this._actionTestResult = "error"; + this._actionTestError = `Target entity "${tgt}" not found in Home Assistant — the entity may have been renamed or its integration removed.`; + setTimeout(() => { this._actionTestResult = ""; this._actionTestError = ""; }, 8000); + return; + } + } + + // All checks passed — configuration is valid. The action will fire + // when the task is marked complete (not now). + this._actionTestResult = "ok"; + setTimeout(() => { this._actionTestResult = ""; this._actionTestError = ""; }, 5000); + } + + // v1.3.1: derive the data dict from either the schema-driven _actionData + // (preferred) or the JSON fallback textfield. Returns {} on any parse + // problem so the caller still gets a usable empty object. + private _buildActionData(): Record { + if (this._actionDataJsonFallback.trim()) { + try { + const parsed = JSON.parse(this._actionDataJsonFallback); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + return parsed as Record; + } + } catch { /* fallthrough to ha-form data */ } + } + return { ...this._actionData }; + } + + // v1.3.1: look up the selected service in hass.services and convert its + // `fields` map into the schema shape ha-form expects. Returns null when + // the service is unknown or has no fields metadata — caller falls back + // to a free-form JSON textfield. + private _serviceSchema(): Array<{ + name: string; + required: boolean; + selector: Record; + }> | null { + const svc = this._actionService.trim(); + if (!svc || !/^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(svc)) return null; + const [domain, name] = svc.split("."); + const fields = this.hass?.services?.[domain]?.[name]?.fields; + if (!fields || Object.keys(fields).length === 0) return null; + return Object.entries(fields).map(([fname, def]) => ({ + name: fname, + required: !!def.required, + selector: (def.selector as Record) || { text: {} }, + })); + } + + private _renderCompletionActionsSection(L: string) { + if (!this.completionActionsEnabled) return nothing; + const schema = this._serviceSchema(); + return html` +
+ ${t("on_complete_action_title", L)} +

${t("on_complete_action_desc", L)}

+ { + this._actionService = e.detail.value || ""; + // Schema changed → drop fields the new service doesn't accept. + const newSchema = this._serviceSchema(); + if (newSchema) { + const allowed = new Set(newSchema.map(f => f.name)); + this._actionData = Object.fromEntries( + Object.entries(this._actionData).filter(([k]) => allowed.has(k)), + ); + } + }} + > + t("on_complete_action_target", L)} + @value-changed=${(e: CustomEvent) => { + const v = e.detail.value as { target_entity?: string }; + this._actionTargetEntity = v.target_entity || ""; + }} + > +

+ ${t("on_complete_action_target_hint", L)} +

+ ${schema + ? html` + { + this._actionData = { ...(e.detail.value as Record) }; + }} + > + ` + : html` + { this._actionDataJsonFallback = (e.target as HTMLInputElement).value; }} + > + `} +
+ + ${this._actionTestResult === "ok" + ? html`${t("on_complete_action_test_success", L)}` + : nothing} + ${this._actionTestResult === "error" + ? html`
+ ${t("on_complete_action_test_failed", L)} + ${this._actionTestError + ? html`
${this._actionTestError}
` + : nothing} +
` + : nothing} +
+
+ +
+ ${t("quick_complete_defaults_title", L)} +

${t("quick_complete_defaults_desc", L)}

+ { this._qcNotes = (e.target as HTMLInputElement).value; }} + > + { this._qcCost = (e.target as HTMLInputElement).value; }} + > + { this._qcDuration = (e.target as HTMLInputElement).value; }} + > + +
+ `; + } + + private async _loadTags(): Promise { + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/tags/list", + }) as { tags: Array<{id: string; name: string}> }; + this._availableTags = result.tags || []; + } catch { + this._availableTags = []; + } + } + + private async _fetchEntityAttributes(entityId: string): Promise { + if (!entityId || !this.hass) { + this._suggestedAttributes = []; + this._availableAttributes = []; + this._entityDomain = ""; + return; + } + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/entity/attributes", + entity_id: entityId, + }) as { + domain: string; + suggested_attributes: string[]; + available_attributes: Array<{ name: string; value: unknown; numeric: boolean }>; + }; + this._entityDomain = result.domain || ""; + this._suggestedAttributes = result.suggested_attributes || []; + this._availableAttributes = result.available_attributes || []; + } catch { + this._suggestedAttributes = []; + this._availableAttributes = []; + this._entityDomain = ""; + } + } + + private async _save(): Promise { + if (this._loading) return; // synchronous re-entry guard (double-click) + if (!this._name.trim()) return; + this._loading = true; + this._error = ""; + try { + const data: Record = { + type: this._taskId + ? "maintenance_supporter/task/update" + : "maintenance_supporter/task/create", + entry_id: this._entryId, + name: this._name, + task_type: this._type, + schedule_type: this._scheduleType, + warning_days: parseInt(this._warningDays, 10) || 7, + }; + const ecd = this._earliestCompletionDays.trim(); + data.earliest_completion_days = ecd === "" ? null : Math.max(0, parseInt(ecd, 10) || 0); + + if (this._taskId) data.task_id = this._taskId; + + if (this._scheduleType === "one_time") { + data.due_date = this._dueDate || null; + data.interval_days = null; + } else if (CALENDAR_KINDS.includes(this._scheduleType)) { + // Calendar kinds are sent as the nested schedule; the backend prefers + // it over the flat fields and clears any stale interval/due_date. + data.schedule = this._buildSchedule(); + data.interval_days = null; + if (this._taskId) data.due_date = null; + } else { + // Switching away from one-time clears the stale due_date on edit. + if (this._taskId) data.due_date = null; + if (this._scheduleType !== "manual" && this._intervalDays) { + data.interval_days = parseInt(this._intervalDays, 10); + data.interval_unit = this._intervalUnit; + data.interval_anchor = this._intervalAnchor; + } else if (this._taskId) { + data.interval_days = null; + data.interval_anchor = "completion"; + } + } + + data.notes = this._notes || null; + data.documentation_url = this._documentationUrl || null; + data.custom_icon = this._customIcon || null; + data.priority = this._priority; + data.labels = this._labels + .split(",") + .map((s) => s.trim()) + .filter(Boolean); + data.enabled = this._enabled; + data.last_performed = this._lastPerformed || null; + data.nfc_tag_id = this._nfcTagId || null; + data.reading_unit = this._readingUnit.trim() || null; + data.responsible_user_id = this._responsibleUserId; + data.assignee_pool = this._assigneePool; + data.rotation_strategy = + this._assigneePool.length >= 2 && this._rotationStrategy + ? this._rotationStrategy + : null; + + if (this._scheduleType === "sensor_based" && this._triggerType === "compound") { + // Compound: a group of conditions joined by AND/OR. Each condition + // carries its own entity + type + params; the top-level entity picker + // does not apply here. + const conditions = this._compoundConditions + .map(draftToCondition) + .filter((c): c is TriggerConfig => c !== null); + if (conditions.length > 0) { + const triggerConfig: TriggerConfig = { + type: "compound", + compound_logic: this._compoundLogic, + conditions, + }; + if (this._autoCompleteOnRecovery) triggerConfig.auto_complete_on_recovery = true; + data.trigger_config = triggerConfig; + } else if (this._taskId) { + data.trigger_config = null; + } + } else if (this._scheduleType === "sensor_based" && this._triggerEntityId) { + const entityIds = this._triggerEntityIds.length > 0 + ? this._triggerEntityIds + : [this._triggerEntityId]; + const triggerConfig: TriggerConfig = { + entity_id: entityIds[0], + entity_ids: entityIds, + type: this._triggerType, + }; + if (this._triggerAttribute) triggerConfig.attribute = this._triggerAttribute; + if (this._autoCompleteOnRecovery) triggerConfig.auto_complete_on_recovery = true; + + // Multi-entity: store entity_logic for all trigger types + if (entityIds.length > 1) { + triggerConfig.entity_logic = this._triggerEntityLogic; + } + + if (this._triggerType === "threshold") { + if (this._triggerAbove) { const v = parseFloat(this._triggerAbove); if (!isNaN(v)) triggerConfig.trigger_above = v; } + if (this._triggerBelow) { const v = parseFloat(this._triggerBelow); if (!isNaN(v)) triggerConfig.trigger_below = v; } + if (this._triggerForMinutes) { const v = parseInt(this._triggerForMinutes, 10); if (!isNaN(v)) triggerConfig.trigger_for_minutes = v; } + } else if (this._triggerType === "counter") { + if (this._triggerTargetValue) { const v = parseFloat(this._triggerTargetValue); if (!isNaN(v)) triggerConfig.trigger_target_value = v; } + triggerConfig.trigger_delta_mode = this._triggerDeltaMode; + } else if (this._triggerType === "state_change") { + if (this._triggerFromState) triggerConfig.trigger_from_state = this._triggerFromState; + if (this._triggerToState) triggerConfig.trigger_to_state = this._triggerToState; + if (this._triggerTargetChanges) { const v = parseInt(this._triggerTargetChanges, 10); if (!isNaN(v)) triggerConfig.trigger_target_changes = v; } + } else if (this._triggerType === "runtime") { + if (this._triggerRuntimeHours) { const v = parseFloat(this._triggerRuntimeHours); if (!isNaN(v)) triggerConfig.trigger_runtime_hours = v; } + } + + data.trigger_config = triggerConfig; + } else if (this._taskId) { + data.trigger_config = null; + } + + // Schedule time only sent when feature is enabled — empty string clears. + if (this.scheduleTimeEnabled && this._scheduleType === "time_based") { + const t = this._scheduleTime.trim(); + data.schedule_time = /^([01]\d|2[0-3]):[0-5]\d$/.test(t) ? t : null; + } + + if (this.checklistsEnabled) { + const items = this._checklistText + .split("\n") + .map((l) => l.trim()) + .filter(Boolean) + .slice(0, 100); + data.checklist = items.length ? items : null; + } + + // v1.3.0: on_complete_action + quick_complete_defaults (gated) + if (this.completionActionsEnabled) { + const svc = this._actionService.trim(); + if (svc && /^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(svc)) { + const action: Record = { service: svc }; + const tgt = this._actionTargetEntity.trim(); + if (tgt) action.target = { entity_id: tgt }; + const dataDict = this._buildActionData(); + if (Object.keys(dataDict).length > 0) { + action.data = dataDict; + } + data.on_complete_action = action; + } else { + data.on_complete_action = null; + } + + const qcd: Record = {}; + if (this._qcNotes.trim()) qcd.notes = this._qcNotes.trim(); + const cost = parseFloat(this._qcCost); + if (!isNaN(cost) && cost >= 0) qcd.cost = cost; + const dur = parseInt(this._qcDuration, 10); + if (!isNaN(dur) && dur >= 0) qcd.duration = dur; + if (this._qcFeedback) qcd.feedback = this._qcFeedback; + data.quick_complete_defaults = Object.keys(qcd).length ? qcd : null; + } + + const result = await this.hass.connection.sendMessagePromise(data) as { task_id?: string }; + const savedTaskId = this._taskId || result?.task_id; + + // Environmental entity lives in adaptive_config (Store-managed), + // so it has a dedicated endpoint. Only call it when something + // actually changed, and only for sensor_based tasks. + const envChanged = + this._environmentalEntity !== this._environmentalInitial + || this._environmentalAttribute !== this._environmentalAttributeInitial; + if ( + savedTaskId + && this._scheduleType === "sensor_based" + && envChanged + ) { + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/set_environmental_entity", + entry_id: this._entryId, + task_id: savedTaskId, + environmental_entity: this._environmentalEntity || null, + environmental_attribute: this._environmentalAttribute || null, + }); + this._environmentalInitial = this._environmentalEntity; + this._environmentalAttributeInitial = this._environmentalAttribute; + } catch { + /* non-fatal — task itself saved */ + } + } + + this._open = false; + this.dispatchEvent(new CustomEvent("task-saved")); + } catch (e) { + this._error = describeWsError(e, this._lang, t("save_error", this._lang)); + } finally { + this._loading = false; + } + } + + private _close(): void { + this._open = false; + } + + private _renderTriggerFields() { + if (this._scheduleType !== "sensor_based") return nothing; + const L = this._lang; + + const isCompound = this._triggerType === "compound"; + return html` +

${t("trigger_configuration", L)}

+
+ + +
+ ${isCompound ? this._renderCompoundEditor() : html` + 0 ? this._triggerEntityIds.join(", ") : this._triggerEntityId} + @input=${(e: Event) => { + const raw = (e.target as HTMLInputElement).value; + const ids = raw.split(",").map((s: string) => s.trim()).filter(Boolean); + this._triggerEntityId = ids[0] || ""; + this._triggerEntityIds = ids; + if (ids[0]) this._fetchEntityAttributes(ids[0]); + }} + > + ${this._triggerEntityIds.length > 1 ? html` +
+ + +
+ ` : nothing} + ${this._availableAttributes.length > 0 + ? html` +
+ + +
+ ` + : html` + (this._triggerAttribute = (e.target as HTMLInputElement).value)} + > + ` + } + ${this._renderTriggerTypeFields()} + `} + +
${t("auto_complete_on_recovery_help", L)}
+ (this._intervalDays = (e.target as HTMLInputElement).value)} + > + ${this._intervalDays ? this._renderUnitSelect() : nothing} + `; + } + + /** Immutably patch one compound condition and trigger a re-render. */ + private _patchCondition(index: number, patch: Partial): void { + this._compoundConditions = this._compoundConditions.map((c, i) => + i === index ? { ...c, ...patch } : c + ); + } + + private _addCondition(): void { + this._compoundConditions = [...this._compoundConditions, emptyCondition()]; + } + + private _removeCondition(index: number): void { + this._compoundConditions = this._compoundConditions.filter((_, i) => i !== index); + } + + /** Compound trigger editor: AND/OR logic + a list of inline conditions. */ + private _renderCompoundEditor() { + const L = this._lang; + return html` +
+ + +
+
${t("compound_help", L)}
+ ${this._compoundConditions.length === 0 + ? html`
${t("compound_no_conditions", L)}
` + : this._compoundConditions.map((c, i) => this._renderCondition(c, i))} + + `; + } + + /** One compound condition row: entity + sub-type + type-specific params. */ + private _renderCondition(c: CompoundConditionDraft, i: number) { + const L = this._lang; + const num = i + 1; + return html` +
+
+ ${t("compound_condition", L)} ${num} + +
+ this._patchCondition(i, { entityIds: (e.target as HTMLInputElement).value })} + > +
+ + +
+ ${this._renderConditionTypeFields(c, i)} +
+ `; + } + + /** Type-specific inputs for a single compound condition (mirrors the flat + * per-type fields, bound to the condition draft). */ + private _renderConditionTypeFields(c: CompoundConditionDraft, i: number) { + const L = this._lang; + if (c.type === "threshold") { + return html` + this._patchCondition(i, { above: (e.target as HTMLInputElement).value })}> + this._patchCondition(i, { below: (e.target as HTMLInputElement).value })}> + this._patchCondition(i, { forMinutes: (e.target as HTMLInputElement).value })}> + `; + } + if (c.type === "counter") { + return html` + this._patchCondition(i, { targetValue: (e.target as HTMLInputElement).value })}> + + `; + } + if (c.type === "state_change") { + return html` + this._patchCondition(i, { fromState: (e.target as HTMLInputElement).value })}> + this._patchCondition(i, { toState: (e.target as HTMLInputElement).value })}> + this._patchCondition(i, { targetChanges: (e.target as HTMLInputElement).value })}> + `; + } + if (c.type === "runtime") { + return html` + this._patchCondition(i, { runtimeHours: (e.target as HTMLInputElement).value })}> + `; + } + return nothing; + } + + /** Shared interval-unit dropdown (DRY: time-based interval + sensor safety + * interval). Bound to _intervalUnit; options localized via unit_* keys. */ + private _renderUnitSelect() { + const L = this._lang; + return html` +
+ + +
`; + } + + private _toggleWeekday(i: number): void { + this._weekdays = this._weekdays.includes(i) + ? this._weekdays.filter((d) => d !== i) + : [...this._weekdays, i]; + } + + /** Build the nested `schedule` object for the selected calendar kind. */ + private _buildSchedule(): Record { + const withOffset = (schedule: Record) => { + const off = parseInt(this._calOffset, 10) || 0; + if (off) schedule.offset = Math.max(-15, Math.min(off, 15)); + return schedule; + }; + if (this._scheduleType === "weekdays") { + return withOffset({ kind: "weekdays", weekdays: [...this._weekdays].sort((a, b) => a - b) }); + } + if (this._scheduleType === "nth_weekday") { + return withOffset({ + kind: "nth_weekday", + nth: parseInt(this._nth, 10), + weekday: parseInt(this._nthWeekday, 10), + }); + } + // (#83) "last day" wins over the day number; business rolls weekends back. + const schedule: Record = { + kind: "day_of_month", + day: this._domLastDay ? -1 : (parseInt(this._domDay, 10) || 1), + }; + if (this._domBusiness) schedule.business = true; + return withOffset(schedule); + } + + /** Per-kind field groups for the calendar recurrence kinds. */ + private _renderCalendarFields() { + const L = this._lang; + const days = weekdayNames(L); + if (this._scheduleType === "weekdays") { + return html` + +
+ ${days.map((name, i) => html` + `)} +
+ ${this._renderCalOffsetField()}`; + } + if (this._scheduleType === "nth_weekday") { + const nths: Array<[string, string]> = [ + ["1", t("ord_1", L)], ["2", t("ord_2", L)], ["3", t("ord_3", L)], + ["4", t("ord_4", L)], ["5", t("ord_5", L)], ["-1", t("ord_last", L)], + ]; + return html` +
+ + +
+
+ + +
+ ${this._renderCalOffsetField()}`; + } + if (this._scheduleType === "day_of_month") { + return html` + ${this._domLastDay ? nothing : html` + (this._domDay = (e.target as HTMLInputElement).value)} + >`} + + + ${this._renderCalOffsetField()}`; + } + return nothing; + } + + /** (#83) ±N-day shift shared by all calendar kinds. */ + private _renderCalOffsetField() { + const L = this._lang; + return html` + (this._calOffset = (e.target as HTMLInputElement).value)} + >`; + } + + private _renderTriggerTypeFields() { + const L = this._lang; + if (this._triggerType === "threshold") { + return html` + (this._triggerAbove = (e.target as HTMLInputElement).value)} + > + (this._triggerBelow = (e.target as HTMLInputElement).value)} + > + (this._triggerForMinutes = (e.target as HTMLInputElement).value)} + > + `; + } + if (this._triggerType === "counter") { + return html` + (this._triggerTargetValue = (e.target as HTMLInputElement).value)} + > + + `; + } + if (this._triggerType === "state_change") { + return html` + (this._triggerFromState = (e.target as HTMLInputElement).value)} + > +
${t("state_value_help", L)}
+ (this._triggerToState = (e.target as HTMLInputElement).value)} + > + (this._triggerTargetChanges = (e.target as HTMLInputElement).value)} + > +
${t("target_changes_help", L)}
+ `; + } + if (this._triggerType === "runtime") { + return html` + (this._triggerRuntimeHours = (e.target as HTMLInputElement).value)} + > + `; + } + return nothing; + } + + render() { + if (!this._open) return html``; + const L = this._lang; + const title = this._taskId ? t("edit_task", L) : t("new_task", L); + return html` + +
${title}
+
+ ${this._error ? html`
${this._error}
` : nothing} + ${this._objectChoices.length > 0 ? html` +
+ + +
+ ` : nothing} + (this._name = (e.target as HTMLInputElement).value)} + > +
+ + +
+ ${this._type === "reading" + ? html` + (this._readingUnit = (e.target as HTMLInputElement).value)} + > +
${t("reading_unit_help", L)}
+ ` + : nothing} +
+ + +
+
+ + (this._labels = (e.target as HTMLInputElement).value)} + /> +
${t("labels_help", L)}
+
+
+ + +
+ ${this._scheduleType === "time_based" + ? html` + (this._intervalDays = (e.target as HTMLInputElement).value)} + > + ${this._renderUnitSelect()} +
+ + +
+ ${this.scheduleTimeEnabled ? html` + (this._scheduleTime = (e.target as HTMLInputElement).value)} + > + ` : nothing} + ` + : nothing} + ${this._renderCalendarFields()} + ${this._scheduleType === "one_time" + ? html` + (this._dueDate = (e.target as HTMLInputElement).value)} + > + ` + : nothing} + (this._warningDays = (e.target as HTMLInputElement).value)} + > + (this._earliestCompletionDays = (e.target as HTMLInputElement).value)} + > + ${this.checklistsEnabled ? html` +

${t("checklist_steps_optional", L)}

+ +
${t("checklist_help", L)}
+ ` : nothing} + (this._lastPerformed = (e.target as HTMLInputElement).value)} + > +
+ + +
+ ${this._availableUsers.length >= 2 ? html` +
+ +
${t("shared_with_help", L)}
+
+ ${this._availableUsers.map((user) => html` + `)} +
+
+ ${this._assigneePool.length >= 2 ? html` +
+ + +
` : nothing} + ` : nothing} + ${this._renderTriggerFields()} + ${this._scheduleType === "sensor_based" ? html` + (this._environmentalEntity = (e.target as HTMLInputElement).value.trim())} + > + ${this._environmentalEntity ? html` + (this._environmentalAttribute = (e.target as HTMLInputElement).value.trim())} + > + ` : nothing} + ` : nothing} + (this._notes = (e.target as HTMLInputElement).value)} + > + (this._documentationUrl = (e.target as HTMLInputElement).value)} + > + + (this._customIcon = (e.detail.value as string) || "")} + > + ${this._availableTags.length > 0 + ? html` +
+ + + +
+ ` + : html` + (this._nfcTagId = (e.target as HTMLInputElement).value)} + > +
+ ${t("nfc_tags_empty_help", L)} + ${t("nfc_tags_open_settings", L)} + · + +
+ ` + } + + ${this._renderCompletionActionsSection(L)} +
+
+ ${t("cancel", L)} + + ${this._loading ? t("saving", L) : t("save", L)} + +
+
+ `; + } + + static styles = css` + .dialog-title { + font-size: 18px; + font-weight: 500; + padding-bottom: 12px; + } + /* v1.3.0: completion-action sections */ + .ca-section { + border: 1px solid var(--divider-color); + border-radius: 6px; + padding: 8px 12px; + margin-top: 8px; + } + .ca-section > summary { + cursor: pointer; + font-weight: 500; + } + .ca-section ms-textfield, + .ca-section ha-entity-picker, + .ca-section ha-service-picker, + .ca-section ha-form, + .ca-section .qc-feedback { + width: 100%; + margin-top: 8px; + display: block; + } + .ca-section .qc-feedback { + padding: 8px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + } + .ca-test-row { + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; + } + .ca-test-ok { color: var(--success-color, #4caf50); font-size: 13px; } + .ca-test-error { color: var(--error-color, #f44336); font-size: 13px; font-weight: 500; } + .ca-test-error-block { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; } + .ca-test-error-detail { + font-size: 12px; + color: var(--secondary-text-color); + background: rgba(244, 67, 54, 0.08); + padding: 6px 8px; border-radius: 4px; + line-height: 1.4; + word-break: break-word; + } + .content { + display: flex; + flex-direction: column; + gap: 12px; + min-width: 350px; + max-height: 70vh; + overflow-y: auto; + } + @media (max-width: 600px) { + .content { + min-width: 0; + max-height: none; + } + } + .dialog-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 16px; + } + ms-textfield { + display: block; + } + .field-label { + font-size: 12px; + color: var(--secondary-text-color); + } + .checklist-textarea { + width: 100%; + min-height: 88px; + padding: 8px; + font-family: inherit; + font-size: 14px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color); + color: var(--primary-text-color); + resize: vertical; + box-sizing: border-box; + } + .field-help { + font-size: 12px; + color: var(--secondary-text-color); + } + .field-help a, + .link-button { + background: none; + border: 0; + padding: 0; + color: var(--primary-color); + cursor: pointer; + font: inherit; + text-decoration: underline; + } + .field-help a:hover, + .link-button:hover { + text-decoration: none; + } + /* Smaller refresh icon-button when shown next to the dropdown. */ + .select-row .link-button { + margin-left: 8px; + text-decoration: none; + font-size: 16px; + } + .select-row .link-button:hover { + color: var(--primary-color); + opacity: 0.7; + } + h3 { + margin: 8px 0 0; + font-size: 14px; + color: var(--primary-color); + } + .select-row { + display: flex; + flex-direction: column; + gap: 4px; + } + .assignee-pool { + display: flex; + flex-wrap: wrap; + gap: 6px 14px; + margin-top: 4px; + } + .checkbox-row { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13px; + cursor: pointer; + margin: 2px 0; + } + .checkbox-row input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + } + .pool-item { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + cursor: pointer; + } + .pool-item input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + } + .select-row label { + font-size: 12px; + color: var(--secondary-text-color); + } + .select-row select { + padding: 8px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 14px; + } + .field-label { + font-size: 12px; + color: var(--secondary-text-color); + } + .weekday-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + .weekday-chip { + padding: 6px 12px; + border: 1px solid var(--divider-color); + border-radius: 16px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 13px; + cursor: pointer; + } + .weekday-chip.selected { + background: var(--primary-color, #03a9f4); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color, #03a9f4); + } + .error { + color: var(--error-color, #f44336); + font-size: 13px; + } + .toggle-row { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + cursor: pointer; + } + `; +} + +if (!customElements.get("maintenance-task-dialog")) { + customElements.define("maintenance-task-dialog", MaintenanceTaskDialog); +} diff --git a/custom_components/maintenance_supporter/frontend-src/components/task-documents.ts b/custom_components/maintenance_supporter/frontend-src/components/task-documents.ts new file mode 100644 index 0000000..c2e7009 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/components/task-documents.ts @@ -0,0 +1,339 @@ +/** Documents linked to a maintenance task. + * + * A filtered view over the object's document pool via each doc's `task_ids`: + * link/unlink existing object documents to the task and open/download them. Full + * upload + management lives at the object level (documents-section); this keeps + * the manual / spare-parts list right where the maintenance work happens. Hides + * itself entirely when the object has no documents at all. + */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import { t, ensureLocale } from "../styles"; +import { describeWsError } from "../ws-errors"; +import { downloadUrl } from "../helpers/download"; +import { formatBytes } from "../helpers/format-bytes"; +import type { HomeAssistant } from "../types"; + +interface Doc { + id: string; + kind: "file" | "weblink"; + title: string; + filename?: string; + url?: string; + mime?: string; + size?: number; + tags?: string[]; + task_ids?: string[]; + task_pages?: Record; +} + +const CATEGORIES = ["manual", "warranty", "invoice", "spare_parts", "photo", "other"] as const; +const CATEGORY_ICONS: Record = { + manual: "mdi:book-open-variant", + warranty: "mdi:shield-check", + invoice: "mdi:receipt-text-outline", + spare_parts: "mdi:cog-outline", + photo: "mdi:image-outline", + other: "mdi:file-document-outline", +}; + +export class MaintenanceTaskDocuments extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @property({ attribute: false }) public entryId!: string; + @property({ attribute: false }) public taskId!: string; + @property({ type: Boolean }) public canWrite = false; + + @state() private _docs: Doc[] = []; + @state() private _loaded = false; + @state() private _busy = false; + @state() private _error = ""; + @state() private _attachId = ""; + + private _loadedKey = ""; + private _localeReady = false; + + private get _lang(): string { + return this.hass?.language || "en"; + } + + updated(changed: Map): void { + super.updated(changed); + if (this.hass && !this._localeReady) { + this._localeReady = true; + void ensureLocale(this._lang).then(() => this.requestUpdate()); + } + const key = `${this.entryId}|${this.taskId}`; + if (this.hass && this.entryId && this.taskId && this._loadedKey !== key) { + this._loadedKey = key; + void this._load(); + } + } + + private async _load(): Promise { + try { + const r = await this.hass.connection.sendMessagePromise<{ documents: Doc[] }>({ + type: "maintenance_supporter/documents/list", + entry_id: this.entryId, + }); + this._docs = r.documents || []; + this._loaded = true; + this._error = ""; + } catch (e) { + this._error = describeWsError(e, this._lang); + this._loaded = true; + } + } + + private _linked(): Doc[] { + return this._docs.filter((d) => (d.task_ids || []).includes(this.taskId)); + } + + private _available(): Doc[] { + return this._docs.filter((d) => !(d.task_ids || []).includes(this.taskId)); + } + + private async _setTaskIds(doc: Doc, taskIds: string[]): Promise { + this._busy = true; + this._error = ""; + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/documents/update", + doc_id: doc.id, + task_ids: taskIds, + }); + await this._load(); + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._busy = false; + } + } + + private _link(): void { + const doc = this._docs.find((d) => d.id === this._attachId); + if (!doc) return; + this._attachId = ""; + void this._setTaskIds(doc, [...(doc.task_ids || []), this.taskId]); + } + + private _unlink(doc: Doc): void { + void this._setTaskIds(doc, (doc.task_ids || []).filter((x) => x !== this.taskId)); + } + + private _isPdf(doc: Doc): boolean { + return doc.mime === "application/pdf" || (doc.filename || "").toLowerCase().endsWith(".pdf"); + } + + /** The page this doc should open at for the current task, if set (PDFs only). */ + private _pageFor(doc: Doc): number | undefined { + return this._isPdf(doc) ? doc.task_pages?.[this.taskId] : undefined; + } + + private async _open(doc: Doc): Promise { + if (doc.kind === "weblink") { + window.open(doc.url, "_blank", "noopener"); + return; + } + // A per-task page hint jumps straight to the relevant page via the PDF + // viewer's #page=N fragment (client-side, so it never breaks the signature). + const page = this._pageFor(doc); + const frag = page ? `#page=${page}` : ""; + const win = window.open("about:blank", "_blank"); + try { + const s = await this.hass.connection.sendMessagePromise<{ path: string }>({ + type: "auth/sign_path", + path: `/api/maintenance_supporter/document/${doc.id}`, + expires: 300, + }); + // Absolute URL: a fragment on a *root-relative* path won't resolve against + // the blank popup's about:blank base, so it would silently stay blank. + if (win) win.location.href = new URL(s.path + frag, window.location.origin).href; + } catch (e) { + if (win) win.close(); + this._error = describeWsError(e, this._lang); + } + } + + /** Set (page >= 1) or clear (0) the jump-to page for this doc's task link. */ + private async _setPage(doc: Doc, page: number): Promise { + this._busy = true; + this._error = ""; + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/documents/update", + doc_id: doc.id, + task_pages: { [this.taskId]: page }, + }); + await this._load(); + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._busy = false; + } + } + + private async _download(doc: Doc): Promise { + try { + const s = await this.hass.connection.sendMessagePromise<{ path: string }>({ + type: "auth/sign_path", + path: `/api/maintenance_supporter/document/${doc.id}`, + expires: 30, + }); + downloadUrl(s.path, doc.filename || doc.title || "document"); + } catch (e) { + this._error = describeWsError(e, this._lang); + } + } + + render() { + // Hide entirely for objects with no documents — no clutter on every task. + if (!this._loaded || this._docs.length === 0) return nothing; + const L = this._lang; + const linked = this._linked(); + const available = this._available(); + return html` +
+

${t("documents", L)} (${linked.length})

+ ${this._error ? html`
${this._error}
` : nothing} + ${linked.length === 0 + ? html`
${t("doc_task_none", L)}
` + : html`
${linked.map((d) => this._renderRow(d, L))}
`} + ${this.canWrite && available.length + ? html`
+ + +
` + : nothing} +
+ `; + } + + private _renderRow(doc: Doc, L: string) { + const isFile = doc.kind === "file"; + const isPdf = this._isPdf(doc); + const page = doc.task_pages?.[this.taskId]; + const cat = (doc.tags || []).find((x) => (CATEGORIES as readonly string[]).includes(x)) || "other"; + const meta = isFile ? formatBytes(doc.size) : t("doc_link_badge", L); + return html` +
+ +
this._open(doc)} + @keydown=${(e: KeyboardEvent) => { + if (e.key === "Enter" || e.key === " ") { + e.preventDefault(); + void this._open(doc); + } + }} + > +
${doc.title || doc.filename || doc.url}
+
+ ${meta}${page ? html` · ${t("doc_page", L)} ${page}` : nothing} +
+
+ ${this.canWrite && isPdf + ? html` { + const v = parseInt((e.target as HTMLInputElement).value, 10); + void this._setPage(doc, Number.isFinite(v) && v >= 1 ? v : 0); + }} + />` + : nothing} + + ${isFile + ? html`` + : nothing} + ${this.canWrite + ? html`` + : nothing} +
+ `; + } + + static styles = css` + :host { display: block; } + .task-docs { margin-top: 20px; } + h3 { + display: flex; align-items: center; gap: 6px; margin: 0 0 8px; + font-size: 15px; color: var(--primary-text-color); + } + h3 ha-icon { --mdc-icon-size: 18px; color: var(--secondary-text-color, #888); } + .tdoc-empty { color: var(--secondary-text-color, #888); font-size: 13px; padding: 2px 0 8px; } + .tdoc-error { color: var(--error-color, #f44336); font-size: 13px; margin: 4px 0; } + .tdoc-list { display: flex; flex-direction: column; gap: 4px; } + .tdoc-row { + display: flex; align-items: center; gap: 10px; padding: 6px 10px; + border: 1px solid var(--divider-color); border-radius: 8px; + } + .tdoc-icon { color: var(--primary-color); --mdc-icon-size: 22px; flex: none; } + .tdoc-info { flex: 1; min-width: 0; cursor: pointer; border-radius: 6px; } + .tdoc-info:hover .tdoc-title { text-decoration: underline; } + .tdoc-info:focus-visible { outline: 2px solid var(--primary-color); outline-offset: 2px; } + .tdoc-title { font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .tdoc-meta { font-size: 12px; color: var(--secondary-text-color, #888); } + .tdoc-pagetag { color: var(--primary-color); font-weight: 500; } + .tdoc-page { + flex: none; width: 76px; padding: 5px 8px; border-radius: 6px; font: inherit; font-size: 13px; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.06)); + color: var(--primary-text-color); border: 1px solid var(--divider-color); + } + .tdoc-page:disabled { opacity: 0.5; } + .icon-btn { + display: inline-flex; align-items: center; justify-content: center; + width: 34px; height: 34px; border-radius: 8px; cursor: pointer; + background: transparent; border: none; color: var(--primary-text-color); + } + .icon-btn:hover { background: var(--secondary-background-color, rgba(0, 0, 0, 0.06)); } + .icon-btn[disabled] { opacity: 0.4; pointer-events: none; } + .icon-btn ha-icon { --mdc-icon-size: 20px; } + .tdoc-attach { display: flex; gap: 8px; margin-top: 8px; flex-wrap: wrap; } + .tdoc-select { + flex: 1; min-width: 160px; padding: 6px 10px; border-radius: 6px; font: inherit; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.06)); + color: var(--primary-text-color); border: 1px solid var(--divider-color); + } + .tdoc-btn { + display: inline-flex; align-items: center; gap: 6px; cursor: pointer; + padding: 6px 12px; border-radius: 6px; font: inherit; font-size: 13px; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.06)); + color: var(--primary-text-color); border: 1px solid var(--divider-color); + } + .tdoc-btn ha-icon { --mdc-icon-size: 18px; } + .tdoc-btn[disabled] { opacity: 0.5; pointer-events: none; } + `; +} + +if (!customElements.get("maintenance-task-documents")) { + customElements.define("maintenance-task-documents", MaintenanceTaskDocuments); +} diff --git a/custom_components/maintenance_supporter/frontend-src/components/task-quick-actions-dialog.ts b/custom_components/maintenance_supporter/frontend-src/components/task-quick-actions-dialog.ts new file mode 100644 index 0000000..8c87cc5 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/components/task-quick-actions-dialog.ts @@ -0,0 +1,815 @@ +/** v2.3.0 — Task Quick-Actions Dialog. + * + * Opens from the Lovelace card / strategy when the user clicks a task row. + * Surfaces every action the panel's Task-Detail header offers, in-place, + * without forcing a panel-roundtrip: + * + * • Quick info — name + status + next due + last performed + interval + * • Primary actions — Complete (existing dialog), Skip (inline), Reset (inline) + * • Secondary (admin) — Edit settings (existing dialog), QR Code, Delete + * • Footer — "Open in Maintenance Panel" deep-link for History/Statistics + * + * Mounted via dialog-mount.openTaskQuickActions(entryId, taskId). + */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import { sharedStyles, t, STATUS_COLORS, formatDate, formatDateTime, formatRecurrence } from "../styles"; +import { describeWsError } from "../ws-errors"; +import { renderWeibullSection } from "../renderers/weibull"; +import { renderPredictionSection } from "../renderers/prediction"; +import { renderRecommendationBars } from "../renderers/recommendation"; +import { + renderSeasonalCardCompact, + renderSeasonalCardExpanded, +} from "../renderers/seasonal"; +import type { + HomeAssistant, + HistoryEntry, + MaintenanceObjectResponse, + MaintenanceTask, + AdvancedFeatures, +} from "../types"; + +interface MaintenanceObjectFull { + entry_id: string; + object: { id: string; name: string }; + tasks: MaintenanceTask[]; +} + +export class MaintenanceTaskQuickActionsDialog extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _open = false; + @state() private _entryId: string | null = null; + @state() private _taskId: string | null = null; + @state() private _task: MaintenanceTask | null = null; + @state() private _objectName = ""; + @state() private _busy = false; + @state() private _error = ""; + @state() private _showSkip = false; + @state() private _showReset = false; + @state() private _showDetails = false; + @state() private _showAdaptive = false; + @state() private _skipReason = ""; + @state() private _resetDate = ""; + @state() private _features: AdvancedFeatures = { + adaptive: false, predictions: false, seasonal: false, environmental: false, + budget: false, groups: false, checklists: false, schedule_time: false, + completion_actions: false, + }; + @state() private _toast = ""; + private _featuresLoaded = false; + + private get _lang(): string { + return this.hass?.language || "en"; + } + + /** Open the dialog. Loads fresh data from /object via WS so dialog stays in + * sync even if the underlying card has stale data. */ + public async openFor(entryId: string, taskId: string): Promise { + this._entryId = entryId; + this._taskId = taskId; + this._error = ""; + this._showSkip = false; + this._showReset = false; + this._showAdaptive = false; + this._skipReason = ""; + this._resetDate = new Date().toISOString().slice(0, 10); + this._open = true; + await Promise.all([this._loadTask(), this._loadFeatures()]); + } + + /** Pull the active feature flags so adaptive sections only render when + * Adaptive / Seasonal / Environmental are actually enabled (matches the + * panel's behaviour). Cached after first load. */ + private async _loadFeatures(): Promise { + if (this._featuresLoaded) return; + try { + const r = await this.hass.connection.sendMessagePromise<{ + features?: Partial; + }>({ type: "maintenance_supporter/settings" }); + if (r?.features) { + this._features = { ...this._features, ...r.features }; + } + this._featuresLoaded = true; + } catch { + // Settings endpoint unavailable — leave defaults (all false), the + // adaptive panel will then stay hidden. + } + } + + public close(): void { + this._open = false; + this._task = null; + this._error = ""; + } + + private async _loadTask(): Promise { + if (!this._entryId || !this._taskId) return; + try { + const r = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/object", + entry_id: this._entryId, + }); + this._objectName = r.object?.name || ""; + const found = (r.tasks || []).find((t) => t.id === this._taskId); + this._task = found ?? null; + } catch (e) { + this._error = describeWsError(e, this._lang); + } + } + + private async _runWs(payload: Record): Promise { + this._busy = true; + this._error = ""; + try { + await this.hass.connection.sendMessagePromise(payload); + this._busy = false; + return true; + } catch (e) { + this._error = describeWsError(e, this._lang); + this._busy = false; + return false; + } + } + + private _notifyChanged(action: string): void { + this.dispatchEvent( + new CustomEvent("task-action-fired", { + detail: { entry_id: this._entryId, task_id: this._taskId, action }, + bubbles: true, + composed: true, + }), + ); + } + + private _onComplete(): void { + if (!this._entryId || !this._taskId || !this._task) return; + // Reuse the existing rich complete-dialog by mounting it on body + import("../dialog-mount").then(({ openCompleteDialog }) => { + const ok = openCompleteDialog({ + entry_id: this._entryId!, + task_id: this._taskId!, + task_name: this._task!.name, + checklist: this._task!.checklist || [], + adaptive_enabled: !!this._task!.adaptive_config?.enabled, + }); + if (ok) { + this._notifyChanged("complete"); + this.close(); + } + }); + } + + private async _onSkipConfirm(): Promise { + if (!this._entryId || !this._taskId) return; + const ok = await this._runWs({ + type: "maintenance_supporter/task/skip", + entry_id: this._entryId, + task_id: this._taskId, + reason: this._skipReason.trim() || null, + }); + if (ok) { + this._notifyChanged("skip"); + this.close(); + } + } + + private async _onResetConfirm(): Promise { + if (!this._entryId || !this._taskId) return; + const ok = await this._runWs({ + type: "maintenance_supporter/task/reset", + entry_id: this._entryId, + task_id: this._taskId, + date: this._resetDate || undefined, + }); + if (ok) { + this._notifyChanged("reset"); + this.close(); + } + } + + private _onEdit(): void { + if (!this._entryId || !this._taskId) return; + import("../dialog-mount").then(({ openEditTaskDialog }) => { + openEditTaskDialog(this._entryId!, this._taskId!); + this.close(); + }); + } + + private _onQr(): void { + if (!this._entryId || !this._taskId || !this._task) return; + import("../dialog-mount").then(({ openQrDialog }) => { + openQrDialog({ + entry_id: this._entryId!, + task_id: this._taskId!, + task_name: this._task!.name, + object_name: this._objectName, + }); + this.close(); + }); + } + + private async _onDelete(): Promise { + if (!this._entryId || !this._taskId) return; + const confirmText = t("delete_task_confirm", this._lang) + || `Delete "${this._task?.name}"?`; + if (!window.confirm(confirmText)) return; + const ok = await this._runWs({ + type: "maintenance_supporter/task/delete", + entry_id: this._entryId, + task_id: this._taskId, + }); + if (ok) { + this._notifyChanged("delete"); + this.close(); + } + } + + private async _onArchive(): Promise { + if (!this._entryId || !this._taskId) return; + const ok = await this._runWs({ + type: "maintenance_supporter/task/archive", + entry_id: this._entryId, + task_id: this._taskId, + }); + if (ok) { + this._notifyChanged("archive"); + this.close(); + } + } + + private async _onUnarchive(): Promise { + if (!this._entryId || !this._taskId) return; + const ok = await this._runWs({ + type: "maintenance_supporter/task/unarchive", + entry_id: this._entryId, + task_id: this._taskId, + }); + if (ok) { + this._notifyChanged("unarchive"); + this.close(); + } + } + + private _onOpenInPanel(): void { + if (!this._entryId || !this._taskId) return; + const path = `/maintenance-supporter?entry_id=${encodeURIComponent(this._entryId)}` + + `&task_id=${encodeURIComponent(this._taskId)}`; + history.pushState(null, "", path); + window.dispatchEvent(new CustomEvent("location-changed")); + this.close(); + } + + private async _applySuggestion(): Promise { + if (!this._entryId || !this._taskId || !this._task?.suggested_interval) return; + const ok = await this._runWs({ + type: "maintenance_supporter/task/apply_suggestion", + entry_id: this._entryId, + task_id: this._taskId, + interval: this._task.suggested_interval, + }); + if (ok) { + this._toast = t("suggestion_applied", this._lang) || "Applied"; + this._notifyChanged("apply_suggestion"); + // Refresh local task so the recommendation card hides + await this._loadTask(); + setTimeout(() => { this._toast = ""; }, 2500); + } + } + + private async _reanalyzeInterval(): Promise { + if (!this._entryId || !this._taskId) return; + this._busy = true; + this._error = ""; + try { + const r = await this.hass.connection.sendMessagePromise<{ + recommended_interval: number | null; + confidence: string; + data_points: number; + }>({ + type: "maintenance_supporter/task/analyze_interval", + entry_id: this._entryId, + task_id: this._taskId, + }); + this._toast = r.recommended_interval + ? `${t("reanalyze_result", this._lang) || "Recomputed"}: ${r.recommended_interval}d (${r.data_points} pts)` + : (t("reanalyze_insufficient_data", this._lang) || "Not enough data"); + await this._loadTask(); + setTimeout(() => { this._toast = ""; }, 3500); + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._busy = false; + } + } + + private _onEditHistoryEntry(entry: HistoryEntry): void { + if (!this._entryId || !this._taskId) return; + import("../dialog-mount").then(({ openHistoryEditDialog }) => { + openHistoryEditDialog({ + entry_id: this._entryId!, + task_id: this._taskId!, + original_timestamp: entry.timestamp, + type: entry.type, + timestamp: entry.timestamp, + notes: entry.notes ?? null, + cost: entry.cost ?? null, + duration: entry.duration ?? null, + completed_by: entry.completed_by ?? null, + }); + }); + } + + /** Inline recommendation card (Current vs Suggested with apply/reanalyze). + * Bars + confidence badge come from the shared renderer; the action row + * uses native + + + + `; + } + + /** Adaptive section: prediction + recommendation + Weibull + seasonal, + * reusing the panel's renderers. Only renders blocks that have data. */ + private _renderAdaptive(task: MaintenanceTask) { + const L = this._lang; + const hasRecommendation = this._features.adaptive + && task.suggested_interval + && task.suggested_interval !== task.interval_days; + const hasPrediction = (task.degradation_trend != null + && task.degradation_trend !== "insufficient_data") + || task.days_until_threshold != null + || (task.environmental_factor != null && task.environmental_factor !== 1.0); + const hasWeibull = this._features.adaptive + && task.interval_analysis?.weibull_beta != null + && task.interval_analysis?.weibull_eta != null; + const hasSeasonal = this._features.seasonal + && task.seasonal_factor + && task.seasonal_factor !== 1.0; + + if (!hasRecommendation && !hasPrediction && !hasWeibull && !hasSeasonal) { + return html`
+ ${t("adaptive_no_data", L) || "Not enough completion history yet for adaptive analysis."} +
`; + } + return html` +
+ ${this._toast + ? html`
${this._toast}
` + : nothing} + ${hasRecommendation ? this._renderRecommendation(task) : nothing} + ${hasPrediction ? renderPredictionSection(task, L, this._features) : nothing} + ${hasWeibull ? renderWeibullSection(task, L) : nothing} + ${hasSeasonal ? html` + ${renderSeasonalCardCompact(task, L, this._features)} + ${task.seasonal_factors?.length === 12 + || task.interval_analysis?.seasonal_factors?.length === 12 + ? renderSeasonalCardExpanded(task, L) + : nothing} + ` : nothing} +
+ `; + } + + /** Read-only details panel: stats + history. Shown when the user clicks + * "Show details" in the dialog. Edit-buttons on history entries open the + * existing history-edit dialog (which lives in the same dialog-mount). */ + private _renderDetails(task: MaintenanceTask) { + const L = this._lang; + const history = (task.history || []) as HistoryEntry[]; + const completed = history.filter((h) => h.type === "completed"); + const totalCost = completed.reduce( + (s, h) => s + (typeof h.cost === "number" ? h.cost : 0), + 0, + ); + const avgDuration = (() => { + const durs = completed + .map((h) => (typeof h.duration === "number" ? h.duration : null)) + .filter((d): d is number => d != null); + if (!durs.length) return null; + return Math.round(durs.reduce((s, d) => s + d, 0) / durs.length); + })(); + + return html` +
+
+
+ ${t("times_performed", L) || "Performed"} + ${completed.length} +
+
+ ${t("total_cost", L) || "Total cost"} + ${totalCost.toFixed(2)} +
+
+ ${t("avg_duration", L) || "Avg duration"} + ${avgDuration != null ? `${avgDuration}m` : "—"} +
+
+
+ ${t("history", L) || "History"} + ${history.length} +
+ ${history.length === 0 + ? html`
${t("history_empty", L) || "No history yet."}
` + : html` +
+ ${[...history].reverse().slice(0, 20).map((entry) => { + const editable = ["completed", "reset", "skipped"].includes(entry.type); + return html` +
+
+ ${t(entry.type, L)} + ${formatDateTime(entry.timestamp, L)} + ${editable + ? html`` + : nothing} +
+ ${entry.notes + ? html`
${entry.notes}
` + : nothing} + ${entry.cost != null || entry.duration != null + ? html`
+ ${entry.cost != null ? html`💰 ${entry.cost.toFixed(2)}` : nothing} + ${entry.duration != null ? html`⏱️ ${entry.duration}m` : nothing} +
` + : nothing} +
+ `; + })} + ${history.length > 20 + ? html`
… +${history.length - 20} ${t("older_entries", L) || "older"}
` + : nothing} +
+ `} +
+ `; + } + + render() { + if (!this._open) return nothing; + const L = this._lang; + const task = this._task; + const isAdmin = (this.hass?.user?.is_admin ?? true) as boolean; + + return html` +
+ + `; + } + + static styles = [sharedStyles, css` + :host { display: contents; } + .backdrop { + position: fixed; inset: 0; z-index: 100; + background: rgba(0,0,0,0.5); + } + .dialog { + position: fixed; left: 50%; top: 50%; + transform: translate(-50%, -50%); + width: 95vw; max-width: 460px; + max-height: 92vh; overflow: auto; + background: var(--card-background-color, var(--ha-card-background, #1c1c1c)); + color: var(--primary-text-color); + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0,0,0,0.4); + padding: 20px; + display: flex; flex-direction: column; gap: 14px; + z-index: 101; + } + .header { display: flex; flex-direction: column; gap: 6px; } + .title { display: flex; align-items: center; gap: 10px; } + .status-dot { width: 12px; height: 12px; border-radius: 50%; flex-shrink: 0; } + .task-name { font-size: 18px; font-weight: 600; } + .object { font-size: 13px; color: var(--secondary-text-color); } + .link-inline { + background: transparent; border: none; padding: 0; cursor: pointer; + color: var(--primary-color); font-size: inherit; font-family: inherit; + } + .link-inline:hover { text-decoration: underline; } + .quick-info { + display: flex; flex-wrap: wrap; gap: 12px; + font-size: 12px; color: var(--secondary-text-color); + padding-top: 4px; border-top: 1px solid var(--divider-color); + } + .quick-info strong { color: var(--primary-text-color); font-weight: 500; } + .actions { display: flex; gap: 8px; } + .actions.primary-row { gap: 6px; } + .actions.primary-row .btn { flex: 1; } + /* Edit + QR are admin-tools — left-align as a group; Delete is destructive + so it gets pushed to the far right with margin-left:auto for visual + separation. Earlier this row was flex-end which left a strange empty + gap on the left (user feedback). */ + .actions.secondary-row { + padding-top: 8px; border-top: 1px solid var(--divider-color); + justify-content: flex-start; + } + .actions.secondary-row .btn.danger { + margin-left: auto; + } + .btn { + padding: 8px 12px; font-size: 14px; + border-radius: 6px; cursor: pointer; + border: 1px solid var(--divider-color); + background: var(--secondary-background-color, transparent); + color: var(--primary-text-color); + font-weight: 500; + display: inline-flex; align-items: center; gap: 6px; + transition: background 0.12s; + } + .btn:hover { background: var(--state-icon-color, rgba(255,255,255,0.06)); } + .btn[disabled] { opacity: 0.5; cursor: wait; } + .btn.primary { + background: var(--primary-color); + color: var(--text-primary-color, white); + border-color: var(--primary-color); + } + .btn.cancel { background: transparent; } + .btn.ghost { padding: 6px 10px; font-size: 13px; } + .btn.danger { color: var(--error-color); } + .btn ha-icon { --mdc-icon-size: 18px; } + .inline-form { display: flex; flex-direction: column; gap: 8px; } + .inline-form label { font-size: 13px; color: var(--secondary-text-color); } + .inline-form input { + padding: 8px; font-size: 14px; + background: var(--secondary-background-color, #2c2c2c); + color: var(--primary-text-color); + border: 1px solid var(--divider-color, #444); + border-radius: 6px; + } + .inline-actions { display: flex; gap: 8px; justify-content: flex-end; } + .footer { display: flex; justify-content: center; padding-top: 4px; } + .link { + background: transparent; border: none; cursor: pointer; + color: var(--primary-color); font-size: 13px; + display: inline-flex; align-items: center; gap: 4px; + } + .link:hover { text-decoration: underline; } + .link ha-icon { --mdc-icon-size: 14px; } + .loading { padding: 24px; text-align: center; color: var(--secondary-text-color); } + .error { + padding: 8px; border-radius: 6px; + background: rgba(211,47,47,0.1); + color: var(--error-color, #d32f2f); font-size: 13px; + } + + /* Details (expandable Show details section) */ + .details-toggle { display: flex; justify-content: center; margin-top: 4px; } + .details { + display: flex; flex-direction: column; gap: 12px; + border-top: 1px solid var(--divider-color); + padding-top: 12px; + } + .stats-grid { + display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 8px; + } + .stat { + display: flex; flex-direction: column; gap: 2px; + background: var(--secondary-background-color, rgba(255,255,255,0.04)); + padding: 8px; border-radius: 6px; + align-items: center; + } + .stat-label { font-size: 11px; color: var(--secondary-text-color); text-transform: uppercase; letter-spacing: 0.5px; } + .stat-value { font-size: 16px; font-weight: 600; } + .history-header { + display: flex; align-items: baseline; gap: 8px; + font-size: 14px; + } + .history-count { + font-size: 12px; color: var(--secondary-text-color); + background: var(--secondary-background-color); padding: 2px 8px; border-radius: 999px; + } + .history-empty { color: var(--secondary-text-color); font-style: italic; font-size: 13px; } + .history-list { display: flex; flex-direction: column; gap: 8px; max-height: 280px; overflow: auto; } + .history-entry { + padding: 6px 8px; border-radius: 6px; + background: var(--secondary-background-color, rgba(255,255,255,0.03)); + font-size: 13px; + } + .history-line { + display: flex; align-items: center; gap: 8px; + justify-content: space-between; + } + .history-type { + font-weight: 600; font-size: 11px; + padding: 2px 6px; border-radius: 4px; + text-transform: uppercase; letter-spacing: 0.5px; + } + .type-completed { background: rgba(46,125,50,0.2); color: #66bb6a; } + .type-skipped { background: rgba(158,158,158,0.2); color: var(--secondary-text-color); } + .type-reset { background: rgba(33,150,243,0.2); color: #64b5f6; } + .type-triggered { background: rgba(255,87,34,0.2); color: #ff8a65; } + .history-date { font-size: 11px; color: var(--secondary-text-color); flex: 1; text-align: right; } + .history-edit { + background: transparent; border: none; cursor: pointer; + padding: 4px; border-radius: 4px; + color: var(--secondary-text-color); + } + .history-edit:hover { background: var(--state-icon-color, rgba(255,255,255,0.06)); color: var(--primary-color); } + .history-edit ha-icon { --mdc-icon-size: 14px; } + .history-notes { margin-top: 4px; color: var(--primary-text-color); } + .history-meta { display: flex; gap: 12px; margin-top: 4px; color: var(--secondary-text-color); font-size: 11px; } + .history-more { padding: 8px; text-align: center; font-size: 12px; color: var(--secondary-text-color); font-style: italic; } + + /* Adaptive section — wraps the panel renderers (which assume sharedStyles + are present) and adds dialog-specific layout. */ + .adaptive-stack { + display: flex; flex-direction: column; gap: 12px; + border-top: 1px solid var(--divider-color); + padding-top: 12px; + } + .adaptive-empty { + padding: 16px; text-align: center; + color: var(--secondary-text-color); + font-style: italic; font-size: 13px; + border-top: 1px solid var(--divider-color); + } + .toast { + padding: 8px 12px; border-radius: 6px; + background: rgba(76, 175, 80, 0.15); + color: #4caf50; font-size: 13px; font-weight: 500; + } + /* The panel's recommendation-card uses ha-button. We use plain ` + : nothing} + ${RANGES.map( + (r) => html``, + )} + ` + : nothing} + ${pts.length < 2 + ? html`
+ ${t("loading_chart", L)} +
` + : this._renderSvg(W, pts)} + + `; + } + + private _renderSvg(W: number, pts: ChartPoint[]) { + const L = this.lang; + const plotW = W - PAD_L - PAD_R; + const plotB = H - PAD_B; + const plotH = plotB - PAD_T; + + // Domain: data ∪ min/max band ∪ reference lines ∪ optional zero floor. + let lo = Infinity; + let hi = -Infinity; + for (const p of pts) { + lo = Math.min(lo, p.min ?? p.val); + hi = Math.max(hi, p.max ?? p.val); + } + if (this.thresholdAbove != null) { lo = Math.min(lo, this.thresholdAbove); hi = Math.max(hi, this.thresholdAbove); } + if (this.thresholdBelow != null) { lo = Math.min(lo, this.thresholdBelow); hi = Math.max(hi, this.thresholdBelow); } + if (this.targetValue != null) { lo = Math.min(lo, this.targetValue); hi = Math.max(hi, this.targetValue); } + if (this.forceZero) lo = Math.min(lo, 0); + const pad = (hi - lo || 1) * 0.06; + // A progress axis never dips below zero — padding must not create a -50 tick. + const loPadded = this.forceZero && lo >= 0 ? 0 : lo - pad; + let { ticks, niceMin, niceMax } = niceTicks(loPadded, hi + pad, 4); + if (this.forceZero && lo >= 0 && niceMin < 0) { + niceMin = 0; + ticks = ticks.filter((v) => v >= 0); + } + + const tsMin = pts[0].ts; + const tsMax = pts[pts.length - 1].ts; + const tsSpan = tsMax - tsMin || 1; + const withYear = needsYear(tsMin, tsMax); + + const toX = (ts: number) => PAD_L + ((ts - tsMin) / tsSpan) * plotW; + const toY = (v: number) => PAD_T + (1 - (v - niceMin) / (niceMax - niceMin || 1)) * plotH; + + const linePts = pts.map((p) => `${toX(p.ts).toFixed(1)},${toY(p.val).toFixed(1)}`).join(" "); + const areaPath = + `M${toX(pts[0].ts).toFixed(1)},${plotB} ` + + pts.map((p) => `L${toX(p.ts).toFixed(1)},${toY(p.val).toFixed(1)}`).join(" ") + + ` L${toX(pts[pts.length - 1].ts).toFixed(1)},${plotB} Z`; + + // Optional min/max band behind the line. + let bandPath = ""; + const band = pts.filter((p) => p.min != null && p.max != null); + if (band.length >= 2) { + const up = band.map((p) => `${toX(p.ts).toFixed(1)},${toY(p.max!).toFixed(1)}`); + const dn = [...band].reverse().map((p) => `${toX(p.ts).toFixed(1)},${toY(p.min!).toFixed(1)}`); + bandPath = `M${up[0]} ` + up.slice(1).map((x) => `L${x}`).join(" ") + ` L${dn.join(" L")} Z`; + } + + // Danger zones: rect (shading + line-color clip), the threshold line y, + // and where its label sits (just inside the zone). + const zones: { y: number; h: number; lineY: number; label: string; labelY: number }[] = []; + if (this.thresholdBelow != null) { + const zy = toY(this.thresholdBelow); + zones.push({ y: zy, h: Math.max(0, plotB - zy), lineY: zy, label: `▼ ${fmtNum(this.thresholdBelow)}`, labelY: Math.min(plotB - 4, zy + 13) }); + } + if (this.thresholdAbove != null) { + const zy = toY(this.thresholdAbove); + zones.push({ y: PAD_T, h: Math.max(0, zy - PAD_T), lineY: zy, label: `▲ ${fmtNum(this.thresholdAbove)}`, labelY: Math.max(PAD_T + 11, zy - 5) }); + } + + const lastP = pts[pts.length - 1]; + const events = (this.events || []).filter((e) => e.ts >= tsMin && e.ts <= tsMax); + const xTicks = timeTicks(tsMin, tsMax, Math.max(2, Math.min(5, Math.floor(plotW / 110) + 1))); + const hover = this._hover; + + return html` +
+ this._onPointer(e, pts, toX, toY, W)} + @pointerdown=${(e: PointerEvent) => this._onPointer(e, pts, toX, toY, W)} + @pointerleave=${() => (this._hover = null)} + > + + + ${zones.length + ? svg`${zones.map((z) => svg``)}` + : nothing} + + + + + + + + ${ticks.map((v) => { + const y = toY(v); + if (y < PAD_T - 1 || y > plotB + 1) return nothing; + return svg` + + ${fmtNum(v)}`; + })} + + ${zones.map( + (z) => svg``, + )} + + ${bandPath ? svg`` : nothing} + + + ${zones.length + ? svg`` + : nothing} + + ${zones.map( + (z) => svg` + + ${z.label}`, + )} + + ${this.targetValue != null + ? svg` + ◆ ${fmtNum(this.targetValue)} ${this.unit}` + : nothing} + + ${this.projection && this.projection.length === 2 + ? svg`` + : nothing} + + ${xTicks.map((ts, i) => { + const x = toX(ts); + const anchor = i === 0 ? "start" : i === xTicks.length - 1 ? "end" : "middle"; + return svg`${fmtDateTick(ts, L, withYear)}`; + })} + + + + ${events.map((e) => { + const x = toX(e.ts); + const color = + e.type === "completed" + ? "var(--success-color, #4caf50)" + : e.type === "skipped" + ? "var(--warning-color, #ff9800)" + : "var(--info-color, #2196f3)"; + return svg` + + + ${fmtDateTime(e.ts, L)} + `; + })} + + ${hover + ? svg` + + ` + : svg``} + + ${hover + ? html`
+
${fmtDateTime(hover.p.ts, L)}
+
+ ${fmtVal(hover.p.val, this.unit, L)} + ${hover.p.min != null && hover.p.max != null + ? html`(${fmtNum(hover.p.min)}–${fmtNum(hover.p.max)})` + : nothing} +
+
` + : nothing} +
+ `; + } + + private _onPointer( + e: PointerEvent, + pts: ChartPoint[], + toX: (ts: number) => number, + toY: (v: number) => number, + W: number, + ): void { + const svgEl = e.currentTarget as SVGSVGElement; + const rect = svgEl.getBoundingClientRect(); + const px = ((e.clientX - rect.left) / rect.width) * W; + if (px < PAD_L - 8 || px > W - PAD_R + 8) { + this._hover = null; + return; + } + let best = pts[0]; + let bestD = Infinity; + for (const p of pts) { + const d = Math.abs(toX(p.ts) - px); + if (d < bestD) { + bestD = d; + best = p; + } + } + this._hover = { x: toX(best.ts), y: toY(best.val), p: best }; + } + + static styles = css` + :host { display: block; width: 100%; } + .chart-wrap { position: relative; } + .range-chips { display: flex; gap: 4px; justify-content: flex-end; margin-bottom: 2px; } + .range-chip { + font: inherit; font-size: 11.5px; padding: 2px 9px; border-radius: 12px; cursor: pointer; + border: 1px solid var(--divider-color); background: transparent; + color: var(--secondary-text-color); + } + /* Outlier toggle sits left of the range chips as an icon button. */ + .outlier-chip { margin-right: auto; padding: 2px 7px; display: inline-flex; align-items: center; } + .outlier-chip ha-icon { --mdc-icon-size: 15px; } + .range-chip.active { + background: var(--primary-color); border-color: var(--primary-color); + color: var(--text-primary-color, #fff); + } + .range-chip[disabled] { opacity: 0.5; pointer-events: none; } + .svg-holder { position: relative; } + .chart-svg { display: block; touch-action: pan-y; } + .tick-label { fill: var(--secondary-text-color); font-size: 10.5px; } + .zone-label { fill: var(--error-color, #f44336); font-size: 11px; font-weight: 600; } + .chart-empty { + display: flex; align-items: center; justify-content: center; gap: 8px; height: 120px; + color: var(--secondary-text-color); font-size: 12.5px; + } + .chart-empty ha-icon { --mdc-icon-size: 17px; } + .hover-chip { + position: absolute; top: 0; transform: translateX(-50%); + background: var(--card-background-color, #fff); border: 1px solid var(--divider-color); + border-radius: 8px; padding: 4px 9px; pointer-events: none; white-space: nowrap; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); z-index: 3; + } + .hover-date { font-size: 10.5px; color: var(--secondary-text-color); } + .hover-val { font-size: 12.5px; font-weight: 600; color: var(--primary-text-color); } + .hover-range { font-weight: 400; color: var(--secondary-text-color); font-size: 11px; } + `; +} + +if (!customElements.get("maintenance-trigger-chart")) { + customElements.define("maintenance-trigger-chart", MaintenanceTriggerChart); +} diff --git a/custom_components/maintenance_supporter/frontend-src/components/vacation-section-card.ts b/custom_components/maintenance_supporter/frontend-src/components/vacation-section-card.ts new file mode 100644 index 0000000..363fbca --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/components/vacation-section-card.ts @@ -0,0 +1,327 @@ +/** v2.4.0 — Interactive Vacation Section Card. + * + * Replaces the read-only markdown card from Phase 5. Lets the user toggle + * vacation mode + edit start/end/buffer inline, without leaving Lovelace. + * + * Used as a section-strategy card (HA 2026.5+ sections) AND as a stand-alone + * card the user can drop onto any dashboard. + * + * Permissions: only admins see edit affordances; non-admins get a read-only + * view with a deep-link to the panel (vacation config is admin-only at the + * WS layer too). + */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import { t, ensureLocale } from "../styles"; +import { describeWsError } from "../ws-errors"; +import { sectionCardSharedStyles } from "./section-card-shared-styles"; +import type { HomeAssistant } from "../types"; + +interface VacationState { + enabled?: boolean; + is_active?: boolean; + start?: string | null; + end?: string | null; + buffer_days?: number; + window_end?: string | null; + exempt_task_ids?: string[]; +} + +interface CardConfig { + type: string; + title?: string; +} + +export class MaintenanceVacationSectionCard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @state() private _config: CardConfig = { type: "" }; + @state() private _state: VacationState | null = null; + @state() private _busy = false; + @state() private _error = ""; + @state() private _localStart = ""; + @state() private _localEnd = ""; + @state() private _localBuffer = 7; + @state() private _dirty = false; + + private _loaded = false; + + setConfig(config: CardConfig): void { + this._config = config; + } + + getCardSize(): number { + return 2; + } + + private get _lang(): string { + return this.hass?.language || "en"; + } + + private get _isAdmin(): boolean { + return (this.hass?.user?.is_admin ?? true) as boolean; + } + + updated(changedProps: Map): void { + super.updated(changedProps); + if (changedProps.has("hass") && this.hass && !this._loaded) { + this._loaded = true; + void this._load(); + void ensureLocale(this._lang).then(() => this.requestUpdate()); + } + } + + private async _load(): Promise { + try { + const r = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/vacation/state", + }); + this._state = r; + this._localStart = r.start || ""; + this._localEnd = r.end || ""; + this._localBuffer = r.buffer_days ?? 7; + this._dirty = false; + } catch (e) { + this._error = describeWsError(e, this._lang); + } + } + + private async _toggleEnabled(on: boolean): Promise { + this._busy = true; + this._error = ""; + try { + const r = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/vacation/update", + enabled: on, + }); + this._state = r; + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._busy = false; + } + } + + private async _save(): Promise { + if (!this._isAdmin) return; + this._busy = true; + this._error = ""; + try { + const r = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/vacation/update", + start: this._localStart || null, + end: this._localEnd || null, + buffer_days: this._localBuffer, + }); + this._state = r; + this._dirty = false; + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._busy = false; + } + } + + private async _endNow(): Promise { + if (!this._isAdmin) return; + if (!window.confirm(t("vacation_end_now_confirm", this._lang) + || "End vacation immediately?")) return; + this._busy = true; + try { + const r = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/vacation/end_now", + }); + this._state = r; + this._localStart = r.start || ""; + this._localEnd = r.end || ""; + } catch (e) { + this._error = describeWsError(e, this._lang); + } finally { + this._busy = false; + } + } + + private _onDeepLink(): void { + const path = "/maintenance-supporter?ms_action=open_vacation"; + history.pushState(null, "", path); + window.dispatchEvent(new CustomEvent("location-changed")); + } + + render() { + const L = this._lang; + const s = this._state; + if (!s) { + return html`
${t("loading", L) || "Loading…"}
`; + } + const active = s.is_active === true; + const enabled = s.enabled === true; + const exemptCount = s.exempt_task_ids?.length ?? 0; + const statusLabel = active + ? (t("vacation_status_active", L) || "Active now") + : enabled + ? (t("vacation_status_scheduled", L) || "Scheduled") + : (t("vacation_status_inactive", L) || "Inactive"); + const statusClass = active ? "active" : enabled ? "scheduled" : "inactive"; + + return html` + +
+
+
+ 🏖️ + ${this._config.title || (t("vacation_mode", L) || "Vacation mode")} +
+ ${statusLabel} +
+ + ${this._error ? html`
${this._error}
` : nothing} + + ${this._isAdmin + ? html` +
+ + + this._toggleEnabled((e.target as HTMLInputElement).checked)} + > +
+ +
+
+ + { + this._localStart = (e.target as HTMLInputElement).value; + this._dirty = true; + }} /> +
+
+ + { + this._localEnd = (e.target as HTMLInputElement).value; + this._dirty = true; + }} /> +
+
+ + { + this._localBuffer = parseInt( + (e.target as HTMLInputElement).value, 10) || 0; + this._dirty = true; + }} /> +
+
+ +
+ + ${active + ? html`` + : nothing} + ${exemptCount > 0 + ? html`` + : html``} +
+ ` + : html` +
+ ${enabled && s.start && s.end + ? html`
${s.start} → ${s.end}
` + : nothing} + +
+ `} +
+
+ `; + } + + static styles = [sectionCardSharedStyles, css` + .status-pill { + font-size: 11px; font-weight: 600; + padding: 3px 8px; border-radius: 999px; + text-transform: uppercase; letter-spacing: 0.5px; + } + .status-pill.active { + background: rgba(76, 175, 80, 0.15); + color: #4caf50; + } + .status-pill.scheduled { + background: rgba(255, 152, 0, 0.15); + color: #ff9800; + } + .status-pill.inactive { + background: rgba(158, 158, 158, 0.15); + color: var(--secondary-text-color); + } + .row.toggle-row { + display: flex; align-items: center; justify-content: space-between; + } + .row.toggle-row label { + font-size: 14px; color: var(--primary-text-color); + } + .dates-row { + display: grid; grid-template-columns: 1fr 1fr 100px; gap: 10px; + } + .date-field.buffer label { white-space: nowrap; } + .date-field { display: flex; flex-direction: column; gap: 4px; } + .date-field label { + font-size: 11px; color: var(--secondary-text-color); + text-transform: uppercase; letter-spacing: 0.3px; + } + .date-field input { + padding: 6px 8px; font-size: 13px; + background: var(--secondary-background-color, #2c2c2c); + color: var(--primary-text-color); + border: 1px solid var(--divider-color); border-radius: 6px; + font-family: inherit; + } + .date-field input:disabled { opacity: 0.5; cursor: not-allowed; } + .actions { + display: flex; gap: 8px; align-items: center; flex-wrap: wrap; + } + .readonly { display: flex; flex-direction: column; gap: 8px; } + `]; +} + +if (!customElements.get("maintenance-vacation-section-card")) { + customElements.define( + "maintenance-vacation-section-card", + MaintenanceVacationSectionCard, + ); +} + +(window as { customCards?: unknown[] }).customCards = + (window as { customCards?: unknown[] }).customCards || []; +((window as { customCards?: unknown[] }).customCards!).push({ + type: "maintenance-vacation-section-card", + name: "Maintenance Supporter — Vacation", + description: "Inline vacation mode toggle + dates", + preview: false, +}); diff --git a/custom_components/maintenance_supporter/frontend-src/deeplink_test.mjs b/custom_components/maintenance_supporter/frontend-src/deeplink_test.mjs new file mode 100644 index 0000000..8981603 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/deeplink_test.mjs @@ -0,0 +1,50 @@ +import { chromium } from "playwright"; + +const HA = "http://homeassistant-dev:8123"; +const REFRESH = process.env.HA_REFRESH_TOKEN; +if (!REFRESH) { console.error("Set HA_REFRESH_TOKEN env var"); process.exit(1); } + +const browser = await chromium.connect("ws://localhost:3000"); +const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 }, locale: "de" }); +const page = await ctx.newPage(); + +await page.goto(HA); +await page.waitForTimeout(1000); +await page.evaluate((refresh) => { + localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: "http://homeassistant-dev:8123", + clientId: "http://homeassistant-dev:8123/", + refresh_token: refresh, access_token: "", token_type: "Bearer", + expires_in: 1800, expires: 0, + })); +}, REFRESH); + +// Test 1: Deep-link with valid entry_id + task_id +console.log("=== Test 1: Valid deep-link ==="); +await page.goto(HA + "/maintenance-supporter?entry_id=01KJTX1VHBT6TN5JMF9W6WADTJ&task_id=49c49ecf36204031be94c8f132aa6479"); +await page.waitForTimeout(6000); +var url1 = page.url(); +console.log("URL after deep-link:", url1); +console.log("Query params cleaned:", !url1.includes("entry_id")); +await page.screenshot({ path: "screenshots/deeplink_valid.png" }); + +// Test 2: Deep-link with invalid entry_id +console.log("\n=== Test 2: Invalid entry_id ==="); +await page.goto(HA + "/maintenance-supporter?entry_id=NONEXISTENT"); +await page.waitForTimeout(6000); +var url2 = page.url(); +console.log("URL after invalid deep-link:", url2); +console.log("Query params cleaned:", !url2.includes("entry_id")); +await page.screenshot({ path: "screenshots/deeplink_invalid.png" }); + +// Test 3: Deep-link with valid entry but invalid task +console.log("\n=== Test 3: Invalid task_id ==="); +await page.goto(HA + "/maintenance-supporter?entry_id=01KJTX1VHBT6TN5JMF9W6WADTJ&task_id=NONEXISTENT"); +await page.waitForTimeout(6000); +var url3 = page.url(); +console.log("URL after invalid task deep-link:", url3); +await page.screenshot({ path: "screenshots/deeplink_invalid_task.png" }); + +await ctx.close(); +await browser.close(); +console.log("\nDone!"); diff --git a/custom_components/maintenance_supporter/frontend-src/dialog-mount.ts b/custom_components/maintenance_supporter/frontend-src/dialog-mount.ts new file mode 100644 index 0000000..059b9bc --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/dialog-mount.ts @@ -0,0 +1,300 @@ +/** Standalone dialog mounting helper. + * + * Mounts the existing MaintenanceObjectDialog / MaintenanceTaskDialog onto + * document.body so they can be opened from any Lovelace context — without + * the user navigating to the panel first. + * + * Usage from a strategy or card click handler: + * + * import { openCreateObjectDialog, openTaskDialog } from "./dialog-mount"; + * openCreateObjectDialog(); + * + * The hass instance is pulled from .hass at open time and + * re-injected on every Lovelace re-render so the dialog stays connected to + * a fresh WS connection across HA reconnects. + * + * Each dialog is created once and re-used. Closing the dialog (via its own + * Cancel/Save buttons) unsets ``_open`` internally; we never destroy it. + */ + +import "./components/object-dialog"; +import "./components/task-dialog"; +import "./components/complete-dialog"; +import "./components/history-edit-dialog"; +import "./components/qr-dialog"; +import "./components/task-quick-actions-dialog"; +import "./components/object-quick-actions-dialog"; +import type { MaintenanceObjectDialog } from "./components/object-dialog"; +import type { MaintenanceTaskDialog } from "./components/task-dialog"; +import type { + MaintenanceHistoryEditDialog, + HistoryEntryDraft, +} from "./components/history-edit-dialog"; +import type { MaintenanceCompleteDialog } from "./components/complete-dialog"; +import type { MaintenanceQrDialog } from "./components/qr-dialog"; +import type { MaintenanceTaskQuickActionsDialog } from "./components/task-quick-actions-dialog"; +import type { MaintenanceObjectQuickActionsDialog } from "./components/object-quick-actions-dialog"; +import type { HomeAssistant, MaintenanceObject } from "./types"; +import { ensureLocale, isLocaleLoaded } from "./styles"; + +const OBJECT_DIALOG_TAG = "maintenance-object-dialog"; +const TASK_DIALOG_TAG = "maintenance-task-dialog"; +const HISTORY_EDIT_DIALOG_TAG = "maintenance-history-edit-dialog"; +const COMPLETE_DIALOG_TAG = "maintenance-complete-dialog"; +const QR_DIALOG_TAG = "maintenance-qr-dialog"; +const QUICK_ACTIONS_DIALOG_TAG = "maintenance-task-quick-actions-dialog"; +const OBJECT_QUICK_ACTIONS_DIALOG_TAG = "maintenance-object-quick-actions-dialog"; + +interface HassRoot extends HTMLElement { + hass?: HomeAssistant; +} + +function getHass(): HomeAssistant | undefined { + const root = document.querySelector("home-assistant"); + return root?.hass; +} + +function getOrCreate(tag: string): T { + let el = document.body.querySelector(tag); + if (!el) { + el = document.createElement(tag) as T; + document.body.appendChild(el); + } + return el; +} + +function syncHass(el: HTMLElement & { hass?: HomeAssistant }): boolean { + const hass = getHass(); + if (!hass) return false; + el.hass = hass; + // Dialogs opened from the dashboard strategy live in a lazily-loaded chunk + // that never ran the panel's ensureLocale — so without this the popup renders + // in English even when the rest of HA is localized. Fetch the user's locale + // (idempotent + cached) and force a re-render once it's in, so the first paint + // (which may be English) is corrected to the user's language. + const lang = hass.language || "en"; + if (!isLocaleLoaded(lang)) { + void ensureLocale(lang).then(() => { + (el as unknown as { requestUpdate?: () => void }).requestUpdate?.(); + }); + } + return true; +} + +/** Settings cache used to populate task-dialog's feature-gated sections + * (checklists / schedule_time / completion_actions) AND its + * defaultWarningDays prop when the dialog is mounted from Lovelace. + * + * Without this, every section that's gated on a feature flag stays + * hidden (default false) — that's the "ich habe nicht alles wie im + * Panel gefunden" symptom. The panel reads the same settings object + * via maintenance-panel.ts and passes it through as element properties. + * + * Cache lifetime: page session. Invalidated on full HA reload (which + * also drops our custom-element registry). The settings WS itself is + * cheap (~10ms) so even uncached this isn't a hot path concern, but + * caching means re-opening the dialog feels instant. + */ +interface SettingsCache { + features: { + adaptive: boolean; predictions: boolean; seasonal: boolean; + environmental: boolean; budget: boolean; groups: boolean; + checklists: boolean; schedule_time: boolean; completion_actions: boolean; + }; + defaultWarningDays: number; +} + +const FALLBACK_SETTINGS: SettingsCache = { + features: { + adaptive: false, predictions: false, seasonal: false, + environmental: false, budget: false, groups: false, + checklists: false, schedule_time: false, completion_actions: false, + }, + defaultWarningDays: 7, +}; + +let _cachedSettings: Promise | null = null; + +function fetchSettingsOnce(hass: HomeAssistant): Promise { + if (_cachedSettings) return _cachedSettings; + _cachedSettings = hass.connection + .sendMessagePromise<{ + features?: SettingsCache["features"]; + general?: { default_warning_days?: number }; + }>({ type: "maintenance_supporter/settings" }) + .then((r) => ({ + features: r.features ?? FALLBACK_SETTINGS.features, + defaultWarningDays: r.general?.default_warning_days ?? 7, + })) + .catch(() => FALLBACK_SETTINGS); + return _cachedSettings; +} + +export function openCreateObjectDialog(): boolean { + const dlg = getOrCreate(OBJECT_DIALOG_TAG); + if (!syncHass(dlg)) return false; + // openCreate is defined on MaintenanceObjectDialog + dlg.openCreate(); + return true; +} + +export function openEditObjectDialog( + entryId: string, + obj: MaintenanceObject, +): boolean { + const dlg = getOrCreate(OBJECT_DIALOG_TAG); + if (!syncHass(dlg)) return false; + dlg.openEdit(entryId, obj); + return true; +} + +export function openCreateTaskDialog(): boolean { + const dlg = getOrCreate(TASK_DIALOG_TAG); + if (!syncHass(dlg)) return false; + const hass = getHass(); + if (!hass) return false; + void (async () => { + const settings = await fetchSettingsOnce(hass); + const dlgFull = dlg as MaintenanceTaskDialog & { + checklistsEnabled: boolean; + scheduleTimeEnabled: boolean; + completionActionsEnabled: boolean; + defaultWarningDays: number; + openCreate: (entryId?: string) => void; + }; + dlgFull.checklistsEnabled = settings.features.checklists; + dlgFull.scheduleTimeEnabled = settings.features.schedule_time; + dlgFull.completionActionsEnabled = settings.features.completion_actions; + dlgFull.defaultWarningDays = settings.defaultWarningDays; + dlgFull.openCreate(); + })(); + return true; +} + +export function openEditTaskDialog( + entryId: string, + taskId: string, +): boolean { + const dlg = getOrCreate(TASK_DIALOG_TAG); + if (!syncHass(dlg)) return false; + + // openEdit on the panel-side expects a FULL MaintenanceTask (it accesses + // task.warning_days.toString(), task.checklist, task.adaptive_config etc. + // unconditionally on hydrate). For the Lovelace-mount path we only have + // (entry_id, task_id) at the click site, so we MUST fetch the full task + // via WS before calling openEdit. + // + // Earlier the comment claimed "the dialog re-loads task data via WS in + // its own openEdit handler when given just an id" — that was aspirational + // but never implemented. Calling openEdit with a stub `{id: taskId}` + // crashed silently with TypeError: Cannot read properties of undefined + // (reading 'toString'). Reported as #50-followup ("strategy dashboard + // edit button geht nicht mehr"). + // + // The fetch is fire-and-forget — boolean return is kept for back-compat + // (true = mount succeeded, false = no hass yet). Errors during fetch are + // logged + the dialog stays closed. + const hass = getHass(); + if (!hass) return false; + void (async () => { + try { + // Fetch features + task in parallel — both are needed before openEdit. + // Without features, the completion-action / checklist / schedule-time + // sections stay hidden (their @property defaults to false), which is + // the "im Dashboard nicht alles wie im Panel" symptom. + const [r, settings] = await Promise.all([ + hass.connection.sendMessagePromise<{ + tasks?: Array<{ id: string } & Record>; + }>({ type: "maintenance_supporter/object", entry_id: entryId }), + fetchSettingsOnce(hass), + ]); + const fullTask = (r.tasks || []).find((t) => t.id === taskId); + if (!fullTask) { + // eslint-disable-next-line no-console + console.warn(`openEditTaskDialog: task ${taskId} not found in entry ${entryId}`); + return; + } + const dlgFull = dlg as MaintenanceTaskDialog & { + checklistsEnabled: boolean; + scheduleTimeEnabled: boolean; + completionActionsEnabled: boolean; + defaultWarningDays: number; + openEdit: (entryId: string, task: unknown) => Promise; + }; + dlgFull.checklistsEnabled = settings.features.checklists; + dlgFull.scheduleTimeEnabled = settings.features.schedule_time; + dlgFull.completionActionsEnabled = settings.features.completion_actions; + dlgFull.defaultWarningDays = settings.defaultWarningDays; + await dlgFull.openEdit(entryId, fullTask); + } catch (e) { + // eslint-disable-next-line no-console + console.warn("openEditTaskDialog: failed to load task/features", e); + } + })(); + return true; +} + +/** v2.2.0: open the history-entry editor in place, e.g. from a calendar + * card past-event click. The caller fetches the existing entry data via + * the maintenance_supporter/object WS first (or uses what the calendar + * event already carries). */ +export function openHistoryEditDialog(draft: HistoryEntryDraft): boolean { + const dlg = getOrCreate(HISTORY_EDIT_DIALOG_TAG); + if (!syncHass(dlg)) return false; + dlg.openEdit(draft); + return true; +} + +/** v2.3.0: open the rich complete-dialog from any Lovelace context. */ +export function openCompleteDialog(args: { + entry_id: string; + task_id: string; + task_name: string; + checklist?: string[]; + adaptive_enabled?: boolean; +}): boolean { + const dlg = getOrCreate(COMPLETE_DIALOG_TAG); + if (!syncHass(dlg)) return false; + dlg.entryId = args.entry_id; + dlg.taskId = args.task_id; + dlg.taskName = args.task_name; + dlg.checklist = args.checklist ?? []; + dlg.adaptiveEnabled = !!args.adaptive_enabled; + dlg.lang = (getHass()?.language) || "en"; + dlg.open(); + return true; +} + +/** v2.3.0: open the QR dialog for a single task. */ +export function openQrDialog(args: { + entry_id: string; + task_id: string; + task_name: string; + object_name: string; +}): boolean { + const dlg = getOrCreate(QR_DIALOG_TAG); + if (!syncHass(dlg)) return false; + dlg.openForTask(args.entry_id, args.task_id, args.object_name, args.task_name); + return true; +} + +/** v2.3.0: open the task quick-actions dialog (Complete/Skip/Reset/Edit/QR/Delete). + * This is the primary entry point from a card row click. */ +export function openTaskQuickActions(entryId: string, taskId: string): boolean { + const dlg = getOrCreate( + QUICK_ACTIONS_DIALOG_TAG, + ); + if (!syncHass(dlg)) return false; + void dlg.openFor(entryId, taskId); + return true; +} + +/** v2.3.0 Phase 3: open the object quick-actions dialog (Edit/Add-task/Delete + read-only meta + task list). */ +export function openObjectQuickActions(entryId: string): boolean { + const dlg = getOrCreate( + OBJECT_QUICK_ACTIONS_DIALOG_TAG, + ); + if (!syncHass(dlg)) return false; + void dlg.openFor(entryId); + return true; +} diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-checklist-preview.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-checklist-preview.mjs new file mode 100644 index 0000000..aae59db --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-checklist-preview.mjs @@ -0,0 +1,116 @@ +/** + * Verifies the read-only checklist preview in the task detail (overview tab). + * Adds a task with a checklist via WS, navigates to its detail, asserts the + * preview card with all 3 steps is in the DOM. + */ +import { setup, ws, cleanup } from "./e2e-helpers.mjs"; + +const RUN = Date.now().toString(36); +const OBJ_NAME = `Checklist Preview ${RUN}`; +const STEPS = ["Drain water", "Replace filter cartridge", "Run flush cycle"]; + +const log = (...a) => console.log(...a); +let pass = 0, fail = 0; +const check = (label, ok, detail) => { + if (ok) { pass++; log(` ✓ ${label}`); } + else { fail++; log(` ✗ ${label}${detail ? " — " + detail : ""}`); } +}; + +const PANEL_EVAL = ` +window._ha=document.querySelector('home-assistant'); +window._main=_ha.shadowRoot.querySelector('home-assistant-main'); +window._drawer=_main.shadowRoot.querySelector('ha-drawer'); +window._resolver=_drawer.querySelector('partial-panel-resolver'); +window._custom=_resolver.querySelector('ha-panel-custom'); +window._panel=_custom.querySelector('maintenance-supporter-panel'); +window._sr=_panel.shadowRoot; +`; + +async function main() { + const { browser, ctx, page } = await setup({ mobile: false }); + + log("\n== Step 1: enable Checklists feature"); + await ws(page, { + type: "maintenance_supporter/global/update", + settings: { advanced_checklists_visible: true }, + }); + + log("\n== Step 2: create object + task with checklist"); + const obj = await ws(page, { type: "maintenance_supporter/object/create", name: OBJ_NAME }); + const taskRes = await ws(page, { + type: "maintenance_supporter/task/create", + entry_id: obj.entry_id, name: "Filter Maintenance", interval_days: 30, + checklist: STEPS, + }); + log(" created task_id =", taskRes.task_id); + + log("\n== Step 3: wait for panel to refresh + select the task"); + await page.waitForTimeout(2500); + await page.evaluate(({ eid, tid, fn }) => { + eval(fn); + window._panel._selectedEntryId = eid; + window._panel._selectedTaskId = tid; + window._panel._view = "task"; + window._panel.requestUpdate(); + }, { eid: obj.entry_id, tid: taskRes.task_id, fn: PANEL_EVAL }); + await page.waitForTimeout(1500); + + log("\n== Step 4: inspect overview tab DOM for the checklist preview card"); + const result = await page.evaluate(({ fn }) => { + eval(fn); + const sr = window._sr; + const card = sr.querySelector(".checklist-preview-card"); + const header = card?.querySelector(".checklist-preview-header")?.textContent?.trim(); + const items = [...(card?.querySelectorAll("li") || [])].map((li) => li.textContent?.trim()); + return { + hasCard: !!card, + header, + items, + cardOffsetTop: card?.offsetTop, + }; + }, { fn: PANEL_EVAL }); + log(" hasCard:", result.hasCard); + log(" header:", JSON.stringify(result.header)); + log(" items:", JSON.stringify(result.items)); + log(" offsetTop:", result.cardOffsetTop); + + check("preview card rendered in task detail", result.hasCard === true, ""); + check("header shows count = 3", result.header?.includes("(3)"), `header="${result.header}"`); + check("all 3 items rendered in order", + JSON.stringify(result.items) === JSON.stringify(STEPS), + `items=${JSON.stringify(result.items)}`); + + await page.screenshot({ path: "checklist-preview.png", fullPage: false }); + log(" saved checklist-preview.png"); + + log("\n== Step 5: verify preview is HIDDEN when checklist is empty"); + const obj2 = await ws(page, { type: "maintenance_supporter/object/create", name: `Empty ${RUN}` }); + const t2 = await ws(page, { + type: "maintenance_supporter/task/create", + entry_id: obj2.entry_id, name: "Bare Task", interval_days: 7, + }); + await page.waitForTimeout(1500); + await page.evaluate(({ eid, tid, fn }) => { + eval(fn); + window._panel._selectedEntryId = eid; + window._panel._selectedTaskId = tid; + window._panel._view = "task"; + window._panel.requestUpdate(); + }, { eid: obj2.entry_id, tid: t2.task_id, fn: PANEL_EVAL }); + await page.waitForTimeout(1000); + const empty = await page.evaluate(({ fn }) => { + eval(fn); + return !!window._sr.querySelector(".checklist-preview-card"); + }, { fn: PANEL_EVAL }); + check("preview hidden for tasks without checklist", empty === false, ""); + + log("\n== Cleanup"); + await ws(page, { type: "maintenance_supporter/object/delete", entry_id: obj.entry_id }); + await ws(page, { type: "maintenance_supporter/object/delete", entry_id: obj2.entry_id }); + await cleanup(browser, ctx); + + log(`\n=== ${pass} passed, ${fail} failed ===`); + process.exit(fail === 0 ? 0 : 1); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-checklist-visibility.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-checklist-visibility.mjs new file mode 100644 index 0000000..6e7081c --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-checklist-visibility.mjs @@ -0,0 +1,178 @@ +/** + * Reproduces issue #32 item 1: enable Checklists feature, then verify + * the textarea is actually visible in the task dialog. + * + * Run via: + * docker exec playwright-server sh -c \ + * 'cd /tmp && node /work/frontend-src/e2e-checklist-visibility.mjs' + * or from host with the helper container. + */ +import { setup, ws, cleanup } from "./e2e-helpers.mjs"; + +const RUN = Date.now().toString(36); +const OBJ_NAME = `Checklist Probe ${RUN}`; + +const log = (...args) => console.log(...args); +let pass = 0, fail = 0; +const check = (label, ok, detail) => { + if (ok) { pass++; log(` ✓ ${label}`); } + else { fail++; log(` ✗ ${label}${detail ? " — " + detail : ""}`); } +}; + +const PANEL_EVAL = ` +window._ha=document.querySelector('home-assistant'); +window._main=_ha.shadowRoot.querySelector('home-assistant-main'); +window._drawer=_main.shadowRoot.querySelector('ha-drawer'); +window._resolver=_drawer.querySelector('partial-panel-resolver'); +window._custom=_resolver.querySelector('ha-panel-custom'); +window._panel=_custom.querySelector('maintenance-supporter-panel'); +window._sr=_panel.shadowRoot; +`; + +async function main() { + const { browser, ctx, page } = await setup({ mobile: false }); + + log("\n== Step 1: read /settings (initial)"); + let settings = await ws(page, { type: "maintenance_supporter/settings" }); + log(" features.checklists =", settings.features.checklists); + + log("\n== Step 2: enable Checklists via /global/update"); + settings = await ws(page, { + type: "maintenance_supporter/global/update", + settings: { advanced_checklists_visible: true }, + }); + check("backend reports checklists=true", settings.features.checklists === true, + `actual=${settings.features.checklists}`); + + log("\n== Step 3: wait for panel to react to settings event"); + await page.waitForTimeout(2500); + + log("\n== Step 4: read panel's _features"); + const panelFeatures = await page.evaluate((fn) => { + eval(fn); + return window._panel._features; + }, PANEL_EVAL); + log(" _features =", JSON.stringify(panelFeatures)); + check("panel._features.checklists is true", panelFeatures?.checklists === true, + `panel sees checklists=${panelFeatures?.checklists}`); + + log("\n== Step 5: create object + task to open dialog against"); + const obj = await ws(page, { type: "maintenance_supporter/object/create", name: OBJ_NAME }); + const taskRes = await ws(page, { + type: "maintenance_supporter/task/create", + entry_id: obj.entry_id, name: "Probe Task", interval_days: 7, + }); + await page.waitForTimeout(2000); + + log("\n== Step 6: open task-dialog in EDIT mode"); + const objWithTasks = await ws(page, { + type: "maintenance_supporter/object", entry_id: obj.entry_id, + }); + const task = objWithTasks.tasks.find(t => t.id === taskRes.task_id); + await page.evaluate(({ eid, task, fn }) => { + eval(fn); + window._sr.querySelector("maintenance-task-dialog").openEdit(eid, task); + }, { eid: obj.entry_id, task, fn: PANEL_EVAL }); + await page.waitForTimeout(1500); + + log("\n== Step 7: inspect the dialog DOM for the checklist textarea"); + const dlgState = await page.evaluate((fn) => { + eval(fn); + const dlg = window._sr.querySelector("maintenance-task-dialog"); + const dlgSr = dlg?.shadowRoot; + return { + dialogExists: !!dlg, + checklistsEnabled: dlg?.checklistsEnabled, + open: dlg?._open, + hasTextarea: !!dlgSr?.querySelector("textarea.checklist-textarea"), + hasFieldLabel: !!dlgSr?.querySelector('label[for="checklist-textarea"]'), + labelText: dlgSr?.querySelector('label[for="checklist-textarea"]')?.textContent?.trim(), + // grab the rendered HTML around the env field for reference + contentSnippet: dlgSr?.querySelector(".content")?.innerHTML?.slice(0, 1500), + }; + }, PANEL_EVAL); + + log(" dialogExists:", dlgState.dialogExists); + log(" checklistsEnabled prop:", dlgState.checklistsEnabled); + log(" dialog open:", dlgState.open); + log(" hasTextarea:", dlgState.hasTextarea); + log(" hasFieldLabel:", dlgState.hasFieldLabel); + log(" labelText:", dlgState.labelText); + check("checklistsEnabled prop is true on the dialog", + dlgState.checklistsEnabled === true, + `prop=${dlgState.checklistsEnabled}`); + check("checklist textarea is in the DOM", + dlgState.hasTextarea === true, + "the textarea.checklist-textarea was not found inside the dialog"); + check("label exists for checklist textarea", + dlgState.hasFieldLabel === true, + "the label for=\"checklist-textarea\" was not found"); + + if (!dlgState.hasTextarea) { + log("\n --- dialog content snippet (first 1500 chars) ---"); + log(dlgState.contentSnippet); + } + + log("\n== Step 8: take screenshot of edit dialog"); + await page.screenshot({ path: "/tmp/checklist-edit.png", fullPage: false }); + log(" saved /tmp/checklist-edit.png"); + + log("\n== Step 9: scroll inside dialog so the textarea is visible"); + const scrollInfo = await page.evaluate((fn) => { + eval(fn); + const dlgSr = window._sr.querySelector("maintenance-task-dialog").shadowRoot; + const content = dlgSr.querySelector(".content"); + const ta = dlgSr.querySelector("textarea.checklist-textarea"); + return { + contentScrollHeight: content?.scrollHeight, + contentClientHeight: content?.clientHeight, + contentOverflow: getComputedStyle(content).overflowY, + textareaOffsetTop: ta?.offsetTop, + textareaVisible: ta && ta.getBoundingClientRect().top < window.innerHeight, + }; + }, PANEL_EVAL); + log(" scroll info:", JSON.stringify(scrollInfo)); + check("dialog content is scrollable", + scrollInfo.contentOverflow === "auto" || scrollInfo.contentOverflow === "scroll", + `overflow-y=${scrollInfo.contentOverflow}`); + check("textarea is reachable by scrolling within dialog", + scrollInfo.textareaOffsetTop > 0, + `offsetTop=${scrollInfo.textareaOffsetTop}`); + + log("\n== Step 10: test CREATE mode (new task — does the field appear there?)"); + await page.evaluate((fn) => { + eval(fn); + window._sr.querySelector("maintenance-task-dialog")._open = false; + }, PANEL_EVAL); + await page.waitForTimeout(500); + await page.evaluate(({ eid, fn }) => { + eval(fn); + window._sr.querySelector("maintenance-task-dialog").openCreate(eid); + }, { eid: obj.entry_id, fn: PANEL_EVAL }); + await page.waitForTimeout(1500); + + const createState = await page.evaluate((fn) => { + eval(fn); + const dlgSr = window._sr.querySelector("maintenance-task-dialog").shadowRoot; + return { + hasTextarea: !!dlgSr?.querySelector("textarea.checklist-textarea"), + labelText: dlgSr?.querySelector('label[for="checklist-textarea"]')?.textContent?.trim(), + }; + }, PANEL_EVAL); + log(" CREATE mode:", JSON.stringify(createState)); + check("checklist textarea also visible in CREATE dialog", + createState.hasTextarea === true, ""); + await page.screenshot({ path: "/tmp/checklist-create.png", fullPage: false }); + log(" saved /tmp/checklist-create.png"); + + log("\n== Cleanup"); + await ws(page, { + type: "maintenance_supporter/object/delete", entry_id: obj.entry_id, + }); + await cleanup(browser, ctx); + + log(`\n=== ${pass} passed, ${fail} failed ===`); + process.exit(fail === 0 ? 0 : 1); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-error-messages.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-error-messages.mjs new file mode 100644 index 0000000..5e481f3 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-error-messages.mjs @@ -0,0 +1,98 @@ +/** + * Verifies that dialogs surface localized error messages when the WS schema + * rejects an input. Covers the two most common shapes: length cap and range + * cap. Asserts the error string contains the translated field label + the + * translated constraint (e.g. "max 200 characters"), not raw voluptuous text. + */ +import { setup, ws, cleanup } from "./e2e-helpers.mjs"; + +const RUN = Date.now().toString(36); +const log = (...a) => console.log(...a); +let pass = 0, fail = 0; +const check = (label, ok, detail) => { + if (ok) { pass++; log(` ✓ ${label}`); } + else { fail++; log(` ✗ ${label}${detail ? " — " + detail : ""}`); } +}; + +const PANEL_EVAL = ` +window._ha=document.querySelector('home-assistant'); +window._main=_ha.shadowRoot.querySelector('home-assistant-main'); +window._drawer=_main.shadowRoot.querySelector('ha-drawer'); +window._resolver=_drawer.querySelector('partial-panel-resolver'); +window._custom=_resolver.querySelector('ha-panel-custom'); +window._panel=_custom.querySelector('maintenance-supporter-panel'); +window._sr=_panel.shadowRoot; +`; + +async function main() { + const { browser, ctx, page } = await setup({ mobile: false }); + + log("\n== Direct WS call with oversize name (expect rejection)"); + let wsErr; + try { + await ws(page, { + type: "maintenance_supporter/object/create", + name: "X".repeat(500), + }); + } catch (e) { + wsErr = e; + } + check("WS rejected oversize name", !!wsErr, "no error raised"); + log(" raw error:", String(wsErr?.message || "").slice(0, 120)); + + log("\n== Call describeWsError from the panel with the raw error"); + const localized = await page.evaluate(({ rawMsg, fn }) => { + eval(fn); + // The ws-errors module is bundled into the panel; reach it via the panel instance. + // We can approximate the test by driving the object-dialog: open it, submit + // an oversize name, and read back the dialog's displayed error. + return rawMsg; // just echoed for now + }, { rawMsg: String(wsErr?.message || wsErr), fn: PANEL_EVAL }); + log(" echoed:", localized?.slice(0, 120)); + + log("\n== UI path: open object-dialog, enter oversize name, hit save"); + await page.evaluate((fn) => { + eval(fn); + window._sr.querySelector("maintenance-object-dialog").openCreate(); + }, PANEL_EVAL); + await page.waitForTimeout(800); + await page.evaluate(({ name, fn }) => { + eval(fn); + const dlg = window._sr.querySelector("maintenance-object-dialog"); + dlg._name = name; + dlg.requestUpdate(); + }, { name: "X".repeat(500), fn: PANEL_EVAL }); + await page.waitForTimeout(300); + await page.evaluate(({ fn }) => { + eval(fn); + const dlg = window._sr.querySelector("maintenance-object-dialog"); + dlg._save(); + }, { fn: PANEL_EVAL }); + await page.waitForTimeout(1500); + + const errText = await page.evaluate(({ fn }) => { + eval(fn); + const dlg = window._sr.querySelector("maintenance-object-dialog"); + return dlg._error; + }, { fn: PANEL_EVAL }); + + log(" dialog _error:", errText); + check( + "dialog error is localized (contains 'too long' in EN)", + typeof errText === "string" + && errText.toLowerCase().includes("too long") + && errText.includes("200"), + `got: "${errText}"`, + ); + check( + "dialog error is NOT the raw voluptuous string", + typeof errText === "string" && !errText.includes("dictionary value @"), + `got raw voluptuous: "${errText}"`, + ); + + await cleanup(browser, ctx); + log(`\n=== ${pass} passed, ${fail} failed ===`); + process.exit(fail === 0 ? 0 : 1); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-helpers.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-helpers.mjs new file mode 100644 index 0000000..dcc1267 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-helpers.mjs @@ -0,0 +1,127 @@ +/** + * Shared helpers for Playwright E2E tests. + * + * Usage: + * import { setup, ws, panelSR, cleanup } from "./e2e-helpers.mjs"; + * const { browser, page } = await setup({ mobile: false }); + */ +import { chromium } from "playwright"; + +const HA = "http://homeassistant-dev:8123"; + +export async function getRefreshToken() { + const http = await import("http"); + function post(path, body, contentType = "application/json") { + return new Promise((resolve, reject) => { + const data = typeof body === "string" ? body : JSON.stringify(body); + const opts = { + hostname: "localhost", port: 8125, path, method: "POST", + headers: { "Content-Type": contentType, "Content-Length": Buffer.byteLength(data) }, + }; + const req = http.request(opts, (res) => { + let d = ""; + res.on("data", (c) => (d += c)); + res.on("end", () => resolve(JSON.parse(d))); + }); + req.on("error", reject); + req.write(data); + req.end(); + }); + } + const flow = await post("/auth/login_flow", { client_id: `${HA}/`, handler: ["homeassistant", null], redirect_uri: `${HA}/` }); + const auth = await post(`/auth/login_flow/${flow.flow_id}`, { client_id: `${HA}/`, username: "dev", password: "dev" }); + const tokens = await post("/auth/token", `grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`, "application/x-www-form-urlencoded"); + return tokens.refresh_token; +} + +export async function setup({ mobile = false, timezone = undefined } = {}) { + const refreshToken = process.env.HA_REFRESH_TOKEN || await getRefreshToken(); + const browser = await chromium.connect("ws://localhost:3000"); + const ctxOpts = { + viewport: mobile ? { width: 375, height: 812 } : { width: 1280, height: 900 }, + locale: "en-US", + colorScheme: "dark", + ...(mobile ? { isMobile: true } : {}), + ...(timezone ? { timezoneId: timezone } : {}), + }; + const ctx = await browser.newContext(ctxOpts); + const page = await ctx.newPage(); + + await page.goto(HA); + await page.waitForTimeout(1000); + await page.evaluate((args) => { + localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: args.ha, clientId: args.ha + "/", + refresh_token: args.r, access_token: "", + token_type: "Bearer", expires_in: 1800, expires: 0, + })); + }, { ha: HA, r: refreshToken }); + await page.goto(HA + "/maintenance-supporter"); + await page.waitForTimeout(7000); + + return { browser, ctx, page }; +} + +export async function ws(page, cmd) { + return page.evaluate(async (c) => { + const ha = document.querySelector("home-assistant"); + if (!ha?.hass?.connection) throw new Error("no hass connection"); + try { + return await ha.hass.connection.sendMessagePromise(c); + } catch (e) { + // Surface backend errors as readable JSON + throw new Error("WS error: " + JSON.stringify(e)); + } + }, cmd); +} + +export function panelSR(page) { + return page.evaluate(() => { + const ha = document.querySelector("home-assistant"); + const main = ha?.shadowRoot?.querySelector("home-assistant-main"); + const drawer = main?.shadowRoot?.querySelector("ha-drawer"); + const resolver = drawer?.querySelector("partial-panel-resolver"); + const custom = resolver?.querySelector("ha-panel-custom"); + const panel = custom?.querySelector("maintenance-supporter-panel"); + return !!panel?.shadowRoot; + }); +} + +const PANEL_EVAL = "window._ha=document.querySelector('home-assistant');window._main=_ha.shadowRoot.querySelector('home-assistant-main');window._drawer=_main.shadowRoot.querySelector('ha-drawer');window._resolver=_drawer.querySelector('partial-panel-resolver');window._custom=_resolver.querySelector('ha-panel-custom');window._panel=_custom.querySelector('maintenance-supporter-panel');window._sr=_panel.shadowRoot;"; + +export async function openEditDialog(page, entryId, task) { + await page.evaluate(({ eid, task, fn }) => { + eval(fn); + window._sr.querySelector("maintenance-task-dialog").openEdit(eid, task); + }, { eid: entryId, task, fn: PANEL_EVAL }); + await page.waitForTimeout(1500); +} + +export async function readDialogSelects(page) { + return page.evaluate(({ fn }) => { + eval(fn); + const dlgSr = window._sr?.querySelector("maintenance-task-dialog")?.shadowRoot; + if (!dlgSr) return []; + return [...dlgSr.querySelectorAll("select")].map((sel) => { + const label = sel.closest(".select-row")?.querySelector("label")?.textContent?.trim() || "unknown"; + const selected = [...sel.options].find((o) => o.selected); + return { label, value: sel.value, selectedValue: selected?.value, match: sel.value === selected?.value }; + }); + }, { fn: PANEL_EVAL }); +} + +export async function readDialogFields(page) { + return page.evaluate(({ fn }) => { + eval(fn); + const dlgSr = window._sr?.querySelector("maintenance-task-dialog")?.shadowRoot; + if (!dlgSr) return []; + return [...dlgSr.querySelectorAll("ha-textfield")].map((f) => ({ + label: f.getAttribute("label"), type: f.getAttribute("type"), value: f.value, + })); + }, { fn: PANEL_EVAL }); +} + +export async function cleanup(browser, ctx) { + try { await ctx.close(); } catch { /* ignore */ } + try { await browser.close(); } catch { /* ignore */ } +} diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-issues-30-31.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-issues-30-31.mjs new file mode 100644 index 0000000..6c831e3 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-issues-30-31.mjs @@ -0,0 +1,292 @@ +/** + * E2E test scenarios for issues #30 and #31. + * + * #30: Task without last_performed should anchor next_due on created_at, + * not "today" every refresh. + * #31: Reset prompt strings should make it clear that the date is the + * "last performed" date, not the next due date. + * + * Usage: + * docker compose up -d homeassistant-dev playwright + * docker restart playwright-server + * cd custom_components/maintenance_supporter/frontend-src + * node e2e-issues-30-31.mjs + */ +import { setup, ws, cleanup } from "./e2e-helpers.mjs"; +import { readFile } from "fs/promises"; + +let passed = 0, failed = 0; +function check(name, ok, detail) { + if (ok) { passed++; console.log(" PASS " + name); } + else { failed++; console.log(" FAIL " + name + (detail ? " — " + detail : "")); } +} +async function runTest(name, fn) { + console.log("\n=== " + name + " ==="); + try { await fn(); } + catch (e) { failed++; console.log(" ERROR: " + e.message + (e.stack ? "\n" + e.stack : "")); } +} + +const { browser, ctx, page } = await setup(); +const RUN = `${Date.now().toString(36)}`; + +// Helper: create a temp object + task on a fresh entry, return ids. +async function wsLog(cmd) { + try { + return await ws(page, cmd); + } catch (e) { + const detail = e.message + " · cmd=" + JSON.stringify(cmd); + throw new Error(detail); + } +} +async function createScenarioObject(name, taskOptions) { + const created = await wsLog({ + type: "maintenance_supporter/object/create", name, + }); + const entryId = created.entry_id; + const taskRes = await wsLog({ + type: "maintenance_supporter/task/create", + entry_id: entryId, + ...taskOptions, + }); + return { entryId, taskId: taskRes.task_id }; +} +async function readTask(entryId, taskId) { + const obj = await ws(page, { type: "maintenance_supporter/object", entry_id: entryId }); + return obj.tasks.find(t => t.id === taskId); +} +async function deleteEntry(entryId) { + try { + await page.evaluate(async (eid) => { + const ha = document.querySelector("home-assistant"); + const conn = ha.__hass.connection; + await conn.sendMessagePromise({ + type: "config_entries/remove", + entry_id: eid, + }); + }, entryId); + } catch (e) { + console.log(" cleanup: ", e.message); + } +} + +// ===================================================================== +// SCENARIO #30: created_at fallback for tasks without last_performed +// ===================================================================== + +await runTest("#30 — Task ohne last_performed bekommt created_at=today", async () => { + const { entryId, taskId } = await createScenarioObject(`E2E Issue30 A ${RUN}`, { + name: "Daily Task", task_type: "cleaning", + schedule_type: "time_based", interval_days: 1, warning_days: 0, enabled: true, + }); + const task = await readTask(entryId, taskId); + console.log(` debug: next_due=${task.next_due} days_until_due=${task.days_until_due} last_performed=${task.last_performed}`); + // The frontend doesn't expose created_at, but we know that with last_performed=null + // and a fresh anchor, days_until_due must equal interval_days (1 here). + check("days_until_due equals interval_days", task.days_until_due === 1, + `got ${task.days_until_due} (expected 1)`); + check("status is OK with warning_days=0", task.status === "ok", + `got ${task.status}`); + + await deleteEntry(entryId); +}); + +await runTest("#30 — Interval=30 ergibt days_until_due=30", async () => { + const { entryId, taskId } = await createScenarioObject(`E2E Issue30 B ${RUN}`, { + name: "Monthly Task", task_type: "cleaning", + schedule_type: "time_based", interval_days: 30, warning_days: 7, enabled: true, + }); + const task = await readTask(entryId, taskId); + console.log(` debug: next_due=${task.next_due} days_until_due=${task.days_until_due}`); + check("days_until_due equals interval_days (30)", task.days_until_due === 30, + `got ${task.days_until_due}`); + check("status is OK (not DUE_SOON)", task.status === "ok", + `got ${task.status}`); + + await deleteEntry(entryId); +}); + +await runTest("#30 — Refresh ändert next_due nicht (Anker stabil)", async () => { + const { entryId, taskId } = await createScenarioObject(`E2E Issue30 C ${RUN}`, { + name: "Stable Date", task_type: "cleaning", + schedule_type: "time_based", interval_days: 7, enabled: true, + }); + const t1 = await readTask(entryId, taskId); + // Force a refresh by sleeping briefly + await page.waitForTimeout(1500); + const t2 = await readTask(entryId, taskId); + check("next_due stable across refreshes", t1.next_due === t2.next_due, + `t1=${t1.next_due}, t2=${t2.next_due}`); + + await deleteEntry(entryId); +}); + +await runTest("#30 — Task mit last_performed nutzt last_performed (nicht created_at)", async () => { + const past = new Date(Date.now() - 60 * 86400000).toISOString().slice(0, 10); + const { entryId, taskId } = await createScenarioObject(`E2E Issue30 D ${RUN}`, { + name: "Past LP Task", task_type: "cleaning", + schedule_type: "time_based", interval_days: 30, + last_performed: past, enabled: true, + }); + const task = await readTask(entryId, taskId); + // last_performed=60 days ago + interval=30 → next_due was 30 days ago → OVERDUE + check("status is OVERDUE", task.status === "overdue", `got ${task.status}`); + check("days_until_due negative", task.days_until_due < 0, `got ${task.days_until_due}`); + + await deleteEntry(entryId); +}); + +// ===================================================================== +// SCENARIO #31: Reset prompt strings clearer +// ===================================================================== + +await runTest("#31 — Reset-Prompt Label sagt 'Last performed date'", async () => { + // Create a task to reset + const { entryId, taskId } = await createScenarioObject(`E2E Issue31 ${RUN}`, { + name: "Reset Demo", task_type: "cleaning", + schedule_type: "time_based", interval_days: 30, enabled: true, + }); + await page.goto("http://homeassistant-dev:8123/maintenance-supporter"); + await page.waitForTimeout(3000); + + // Verify shipped strings via the built JS file (runs on host fs, not browser). + const builtJs = await readFile( + new URL("../frontend/maintenance-panel.js", import.meta.url), + "utf-8", + ); + check("EN reset_date_optional shipped", + builtJs.includes("Last performed date (optional, defaults to today)")); + check("EN reset_date_prompt shipped", + builtJs.includes("Mark task as performed?")); + check("DE reset_date_optional shipped", + builtJs.includes("Letztes Erledigungs-Datum")); + check("Old confusing string removed", + !builtJs.includes("Reset this task?"), + "old 'Reset this task?' still present"); + + // Reset via WS using the new label semantics: setting the date sets last_performed. + const resetDate = "2025-01-15"; + await ws(page, { + type: "maintenance_supporter/task/reset", entry_id: entryId, task_id: taskId, + date: resetDate, + }); + const task = await readTask(entryId, taskId); + check("reset sets last_performed", task.last_performed === resetDate, + `got ${task.last_performed}`); + // next_due = last_performed + interval = 2025-01-15 + 30 = 2025-02-14 + check("next_due = last_performed + interval", task.next_due === "2025-02-14", + `got ${task.next_due}`); + + await deleteEntry(entryId); +}); + +// ===================================================================== +// EDGE CASES: TZ + lifecycle interaction +// ===================================================================== + +await runTest("EDGE — Lifecycle: create → complete → next_due aktualisiert", async () => { + const { entryId, taskId } = await createScenarioObject(`E2E Edge LC ${RUN}`, { + name: "Lifecycle Task", task_type: "cleaning", + schedule_type: "time_based", interval_days: 30, warning_days: 7, enabled: true, + }); + // Initial state: no last_performed, days_until_due == interval_days + const t1 = await readTask(entryId, taskId); + check("initial days_until_due == interval_days", t1.days_until_due === 30, + `got ${t1.days_until_due}`); + check("initial last_performed is null", t1.last_performed === null, + `got ${t1.last_performed}`); + + // Complete via WS + await ws(page, { + type: "maintenance_supporter/task/complete", + entry_id: entryId, task_id: taskId, + }); + const t2 = await readTask(entryId, taskId); + check("after complete: last_performed set", t2.last_performed !== null, + `got ${t2.last_performed}`); + check("after complete: days_until_due == interval", t2.days_until_due === 30, + `got ${t2.days_until_due}`); + // History entry written + check("after complete: history has entry", t2.history.length === 1, + `len=${t2.history.length}`); + // Timestamp must be TZ-aware + const ts = t2.history[0].timestamp; + check("history timestamp has TZ suffix", + /[+-]\d{2}:\d{2}$|Z$/.test(ts), + `ts=${ts}`); + + await deleteEntry(entryId); +}); + +await runTest("EDGE — Reset zu vergangenem Datum: last_performed + history", async () => { + const { entryId, taskId } = await createScenarioObject(`E2E Edge RST ${RUN}`, { + name: "Reset Edge", task_type: "cleaning", + schedule_type: "time_based", interval_days: 30, enabled: true, + }); + const past = "2025-01-15"; + await ws(page, { + type: "maintenance_supporter/task/reset", + entry_id: entryId, task_id: taskId, date: past, + }); + const t = await readTask(entryId, taskId); + check("reset: last_performed = past date", t.last_performed === past, + `got ${t.last_performed}`); + check("reset: next_due = past + interval (overdue)", + t.next_due === "2025-02-14", + `got ${t.next_due}`); + check("reset: status is overdue", t.status === "overdue", + `got ${t.status}`); + check("reset: history has reset entry", + t.history.some(h => h.type === "reset"), + `types=${t.history.map(h => h.type).join(",")}`); + + await deleteEntry(entryId); +}); + +await runTest("EDGE — Skip via WS: history entry + last_performed bumped", async () => { + const { entryId, taskId } = await createScenarioObject(`E2E Edge SKP ${RUN}`, { + name: "Skip Edge", task_type: "cleaning", + schedule_type: "time_based", interval_days: 14, enabled: true, + }); + await ws(page, { + type: "maintenance_supporter/task/skip", + entry_id: entryId, task_id: taskId, reason: "user busy", + }); + const t = await readTask(entryId, taskId); + check("skip: last_performed set (today)", t.last_performed !== null); + check("skip: history has skip entry", + t.history.some(h => h.type === "skipped"), + `types=${t.history.map(h => h.type).join(",")}`); + check("skip: next_due == today + interval (14)", + t.days_until_due === 14, + `got ${t.days_until_due}`); + + await deleteEntry(entryId); +}); + +await runTest("EDGE — Update last_performed setzt next_due neu", async () => { + const { entryId, taskId } = await createScenarioObject(`E2E Edge UPD ${RUN}`, { + name: "Update Edge", task_type: "cleaning", + schedule_type: "time_based", interval_days: 30, enabled: true, + }); + const past = "2024-06-01"; + await ws(page, { + type: "maintenance_supporter/task/update", + entry_id: entryId, task_id: taskId, + last_performed: past, + }); + const t = await readTask(entryId, taskId); + check("update: last_performed reflected", t.last_performed === past, + `got ${t.last_performed}`); + check("update: next_due = last + interval", + t.next_due === "2024-07-01", + `got ${t.next_due}`); + + await deleteEntry(entryId); +}); + +// ===================================================================== +await cleanup(browser, ctx); +console.log("\n" + "═".repeat(50)); +console.log(`RESULTS: ${passed} passed, ${failed} failed`); +if (failed > 0) { console.log("SOME TESTS FAILED!"); process.exit(1); } +else console.log("ALL TESTS PASSED!"); diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-new-features.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-new-features.mjs new file mode 100644 index 0000000..e195e47 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-new-features.mjs @@ -0,0 +1,292 @@ +/** + * E2E tests for features that previously had WS endpoints without UI. + * + * Usage: + * docker compose up -d homeassistant-dev playwright + * docker restart playwright-server + * cd custom_components/maintenance_supporter/frontend-src + * node e2e-new-features.mjs + */ +import { setup, ws, cleanup } from "./e2e-helpers.mjs"; + +let passed = 0, failed = 0; +function check(name, ok, detail) { + if (ok) { passed++; console.log(" PASS " + name); } + else { failed++; console.log(" FAIL " + name + (detail ? " — " + detail : "")); } +} +async function runTest(name, fn) { + console.log("\n=== " + name + " ==="); + try { await fn(); } + catch (e) { failed++; console.log(" ERROR: " + e.message); } +} + +const { browser, ctx, page } = await setup(); + +// ===================================================================== +// Batch A: Test-Notification Button in Settings +// ===================================================================== + +await runTest("Test-Notification: WS endpoint reachable", async () => { + const res = await ws(page, { type: "maintenance_supporter/global/test_notification" }); + check("WS response has success field", typeof res.success === "boolean", + `got ${JSON.stringify(res)}`); + // When no notify_service is configured the backend returns success=false + // with a "no_service" message. Either outcome is a valid response — + // what matters is that the endpoint is reachable. + check("WS response has message field", typeof res.message === "string" || res.success, + `got ${JSON.stringify(res)}`); +}); + +await runTest("Test-Notification: Button renders in Settings view", async () => { + // Navigate to maintenance panel and open settings. In the base panel the + // settings tab is not always pre-rendered; we verify the string keys exist + // in the bundled JS so the button can be rendered when the tab is opened. + const { readFile } = await import("fs/promises"); + const js = await readFile( + new URL("../frontend/maintenance-panel.js", import.meta.url), + "utf-8", + ); + check("EN send_test string shipped", js.includes("Send test")); + check("EN test_notification string shipped", js.includes("Test notification")); + check("DE test_notification string shipped", js.includes("Test-Benachrichtigung")); + check("testing state string shipped", + js.includes("Sending\\u2026") || js.includes("Sending…")); +}); + +// ===================================================================== +// Batch B: Re-analyze Button in Task Detail view +// ===================================================================== + +await runTest("Re-analyze: WS endpoint returns analysis", async () => { + const objs = await ws(page, { type: "maintenance_supporter/objects" }); + const obj = objs.objects[0]; + const t1 = obj.tasks[0]; + if (!t1) { console.log(" SKIP: no task in first object"); return; } + + const res = await ws(page, { + type: "maintenance_supporter/task/analyze_interval", + entry_id: obj.entry_id, task_id: t1.id, + }); + check("analysis has current_interval field", + "current_interval" in res, `got keys=${Object.keys(res).join(",")}`); + check("analysis has confidence field", typeof res.confidence === "string"); + check("analysis has data_points number", typeof res.data_points === "number"); +}); + +await runTest("Re-analyze: Button strings shipped", async () => { + const { readFile } = await import("fs/promises"); + const js = await readFile( + new URL("../frontend/maintenance-panel.js", import.meta.url), + "utf-8", + ); + check("EN reanalyze label shipped", js.includes("Re-analyze")); + check("DE reanalyze label shipped", js.includes("Neu analysieren")); + check("reanalyze_result string shipped", js.includes("New analysis")); + check("reanalyze_insufficient_data shipped", + js.includes("Not enough data to produce")); +}); + +// ===================================================================== +// Batch B.2: Environmental Entity Selector (Task Dialog, sensor_based) +// ===================================================================== + +await runTest("Environmental entity: WS endpoint persists adaptive_config", async () => { + // Create a sensor-based task, then set and clear the env entity. + const obj = await ws(page, { type: "maintenance_supporter/object/create", name: `env ${Date.now().toString(36)}` }); + const task = await ws(page, { + type: "maintenance_supporter/task/create", + entry_id: obj.entry_id, name: "env-task", + schedule_type: "sensor_based", interval_days: 30, + trigger_config: { type: "threshold", entity_id: "sensor.dummy", trigger_above: 10 }, + enabled: true, + }); + + // Set + const r1 = await ws(page, { + type: "maintenance_supporter/task/set_environmental_entity", + entry_id: obj.entry_id, task_id: task.task_id, + environmental_entity: "sensor.outdoor_temperature", + environmental_attribute: null, + }); + check("set succeeds", r1.success === true, JSON.stringify(r1)); + + const detail = await ws(page, { + type: "maintenance_supporter/object", entry_id: obj.entry_id, + }); + const t = detail.tasks.find(x => x.id === task.task_id); + check("adaptive_config reflects env entity", + t?.adaptive_config?.environmental_entity === "sensor.outdoor_temperature", + `got ${JSON.stringify(t?.adaptive_config)}`); + + // Clear + await ws(page, { + type: "maintenance_supporter/task/set_environmental_entity", + entry_id: obj.entry_id, task_id: task.task_id, + environmental_entity: null, + }); + + // Cleanup (as admin via config_entries) + try { + await page.evaluate(async (eid) => { + const ha = document.querySelector("home-assistant"); + await ha.hass.connection.sendMessagePromise({ + type: "config_entries/remove", entry_id: eid, + }); + }, obj.entry_id); + } catch { /* best-effort */ } +}); + +await runTest("Environmental entity: dialog strings shipped", async () => { + const { readFile } = await import("fs/promises"); + const js = await readFile( + new URL("../frontend/maintenance-panel.js", import.meta.url), + "utf-8", + ); + check("EN environmental_entity label", js.includes("Environmental sensor")); + check("DE environmental_entity label", js.includes("Umgebungs-Sensor")); + check("helper text shipped", js.includes("adjusts the interval")); +}); + +// ===================================================================== +// Batch C: Seasonal Overrides Dialog +// ===================================================================== + +await runTest("Seasonal overrides: WS roundtrip set + clear", async () => { + const obj = await ws(page, { type: "maintenance_supporter/object/create", name: `seas ${Date.now().toString(36)}` }); + const task = await ws(page, { + type: "maintenance_supporter/task/create", + entry_id: obj.entry_id, name: "seas-task", + schedule_type: "time_based", interval_days: 30, enabled: true, + }); + + // Set overrides for Jan + Jul + const r1 = await ws(page, { + type: "maintenance_supporter/task/seasonal_overrides", + entry_id: obj.entry_id, task_id: task.task_id, + overrides: { 1: 2.0, 7: 0.5 }, + }); + check("set succeeds", r1.success === true, JSON.stringify(r1)); + + const detail = await ws(page, { + type: "maintenance_supporter/object", entry_id: obj.entry_id, + }); + const t = detail.tasks.find(x => x.id === task.task_id); + const ov = t?.adaptive_config?.seasonal_overrides; + check("adaptive_config has overrides", + ov && (ov["1"] === 2.0 || ov[1] === 2.0) && (ov["7"] === 0.5 || ov[7] === 0.5), + `got ${JSON.stringify(ov)}`); + + // Clear + const r2 = await ws(page, { + type: "maintenance_supporter/task/seasonal_overrides", + entry_id: obj.entry_id, task_id: task.task_id, + overrides: {}, + }); + check("clear succeeds", r2.success === true); + + try { + await page.evaluate(async (eid) => { + const ha = document.querySelector("home-assistant"); + await ha.hass.connection.sendMessagePromise({ + type: "config_entries/remove", entry_id: eid, + }); + }, obj.entry_id); + } catch { /* best-effort */ } +}); + +await runTest("Seasonal overrides: dialog + button strings shipped", async () => { + const { readFile } = await import("fs/promises"); + const js = await readFile( + new URL("../frontend/maintenance-panel.js", import.meta.url), + "utf-8", + ); + check("EN edit button", js.includes("Edit seasonal factors")); + check("EN title", js.includes("Seasonal factors (override)")); + check("clear_all string", js.includes("Clear all")); + check("DE title", js.includes("Saisonale Faktoren")); +}); + +// ===================================================================== +// Batch D: Groups CRUD UI +// ===================================================================== + +await runTest("Groups: WS roundtrip create + update + delete", async () => { + // Need an object + task to reference + const obj = await ws(page, { type: "maintenance_supporter/object/create", name: `grp-obj ${Date.now().toString(36)}` }); + const task = await ws(page, { + type: "maintenance_supporter/task/create", + entry_id: obj.entry_id, name: "grp-task", + schedule_type: "time_based", interval_days: 30, enabled: true, + }); + + // Create group + const create = await ws(page, { + type: "maintenance_supporter/group/create", + name: "E2E Group", + description: "created by e2e", + task_refs: [{ entry_id: obj.entry_id, task_id: task.task_id }], + }); + check("group/create returns group_id", typeof create.group_id === "string", + JSON.stringify(create)); + const gid = create.group_id; + + // Read back + const list = await ws(page, { type: "maintenance_supporter/groups" }); + check("group appears in list", !!list.groups[gid], + `got ${Object.keys(list.groups).join(",")}`); + check("task_refs persisted", + list.groups[gid].task_refs.length === 1, + JSON.stringify(list.groups[gid])); + + // Update + const update = await ws(page, { + type: "maintenance_supporter/group/update", + group_id: gid, + name: "E2E Group renamed", + task_refs: [], + }); + check("group/update succeeds", update.success === true); + + const list2 = await ws(page, { type: "maintenance_supporter/groups" }); + check("group renamed", list2.groups[gid].name === "E2E Group renamed"); + check("task_refs cleared", list2.groups[gid].task_refs.length === 0); + + // Delete + const del = await ws(page, { + type: "maintenance_supporter/group/delete", group_id: gid, + }); + check("group/delete succeeds", del.success === true); + + const list3 = await ws(page, { type: "maintenance_supporter/groups" }); + check("group gone", !list3.groups[gid]); + + // Cleanup object + try { + await page.evaluate(async (eid) => { + const ha = document.querySelector("home-assistant"); + await ha.hass.connection.sendMessagePromise({ + type: "config_entries/remove", entry_id: eid, + }); + }, obj.entry_id); + } catch { /* best-effort */ } +}); + +await runTest("Groups: dialog + buttons strings shipped", async () => { + const { readFile } = await import("fs/promises"); + const js = await readFile( + new URL("../frontend/maintenance-panel.js", import.meta.url), + "utf-8", + ); + check("EN new_group", js.includes("New group")); + check("EN edit_group", js.includes("Edit group")); + check("EN no_groups", js.includes("No groups yet")); + check("EN group_select_tasks", js.includes("Select tasks")); + check("DE new_group", js.includes("Neue Gruppe")); +}); + +// ===================================================================== +await cleanup(browser, ctx); +console.log("\n" + "═".repeat(50)); +console.log(`RESULTS: ${passed} passed, ${failed} failed`); +if (failed > 0) { console.log("SOME TESTS FAILED!"); process.exit(1); } +else console.log("ALL TESTS PASSED!"); diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-schedule-time.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-schedule-time.mjs new file mode 100644 index 0000000..556a554 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-schedule-time.mjs @@ -0,0 +1,143 @@ +/** + * E2E for the Uhrzeit / time-of-day scheduling feature. + * + * Verifies: + * 1. Feature flag off by default → dialog has NO time input → WS summary has + * schedule_time=null. + * 2. Toggling the flag on → dialog shows the field → a value round-trips to + * WS (create + /object) and back to the dialog in edit mode. + * 3. Feature flag off again → WS-computed _status ignores the stored time + * (backend gating confirms "OFF means midnight semantic"). + */ +import { setup, ws, cleanup } from "./e2e-helpers.mjs"; + +const RUN = Date.now().toString(36); +const OBJ_NAME = `Schedule Time ${RUN}`; + +const log = (...a) => console.log(...a); +let pass = 0, fail = 0; +const check = (label, ok, detail) => { + if (ok) { pass++; log(` ✓ ${label}`); } + else { fail++; log(` ✗ ${label}${detail ? " — " + detail : ""}`); } +}; + +const PANEL_EVAL = ` +window._ha=document.querySelector('home-assistant'); +window._main=_ha.shadowRoot.querySelector('home-assistant-main'); +window._drawer=_main.shadowRoot.querySelector('ha-drawer'); +window._resolver=_drawer.querySelector('partial-panel-resolver'); +window._custom=_resolver.querySelector('ha-panel-custom'); +window._panel=_custom.querySelector('maintenance-supporter-panel'); +window._sr=_panel.shadowRoot; +`; + +async function main() { + const { browser, ctx, page } = await setup({ mobile: false }); + + log("\n== Step 1: feature flag starts OFF"); + let settings = await ws(page, { + type: "maintenance_supporter/global/update", + settings: { advanced_schedule_time_visible: false }, + }); + check("backend reports schedule_time=false", settings.features.schedule_time === false); + // Force panel to pick up the flag change (direct WS doesn't trigger listener). + await page.evaluate((fn) => { eval(fn); return window._panel._loadData(); }, PANEL_EVAL); + await page.waitForTimeout(1000); + + log("\n== Step 2: create obj + time-based task"); + const obj = await ws(page, { type: "maintenance_supporter/object/create", name: OBJ_NAME }); + const taskRes = await ws(page, { + type: "maintenance_supporter/task/create", + entry_id: obj.entry_id, name: "Daily chore", interval_days: 1, + }); + await page.waitForTimeout(2000); + + log("\n== Step 3: open edit dialog, field should NOT be visible"); + const task = (await ws(page, { type: "maintenance_supporter/object", entry_id: obj.entry_id })) + .tasks.find(t => t.id === taskRes.task_id); + await page.evaluate(({ eid, task, fn }) => { + eval(fn); + window._sr.querySelector("maintenance-task-dialog").openEdit(eid, task); + }, { eid: obj.entry_id, task, fn: PANEL_EVAL }); + await page.waitForTimeout(1200); + + const hiddenState = await page.evaluate((fn) => { + eval(fn); + const dlgSr = window._sr.querySelector("maintenance-task-dialog").shadowRoot; + return { + scheduleTimeEnabled: window._sr.querySelector("maintenance-task-dialog").scheduleTimeEnabled, + hasTimeInput: !!dlgSr.querySelector('ha-textfield[type="time"]'), + }; + }, PANEL_EVAL); + check("scheduleTimeEnabled prop is false on dialog", hiddenState.scheduleTimeEnabled === false); + check("time input is NOT in the DOM when feature is off", hiddenState.hasTimeInput === false); + + log("\n== Step 4: enable the feature flag, re-open edit"); + settings = await ws(page, { + type: "maintenance_supporter/global/update", + settings: { advanced_schedule_time_visible: true }, + }); + check("backend reports schedule_time=true after toggle", settings.features.schedule_time === true); + await page.waitForTimeout(1000); + // Direct WS update doesn't fire the panel's "settings-changed" listener, + // so force a reload so _features picks up the flip. + await page.evaluate((fn) => { + eval(fn); + return window._panel._loadData(); + }, PANEL_EVAL); + await page.waitForTimeout(1500); + // Close dialog, reopen (the prop is bound from _features on the parent panel) + await page.evaluate((fn) => { + eval(fn); + window._sr.querySelector("maintenance-task-dialog")._open = false; + }, PANEL_EVAL); + await page.waitForTimeout(500); + await page.evaluate(({ eid, task, fn }) => { + eval(fn); + window._sr.querySelector("maintenance-task-dialog").openEdit(eid, task); + }, { eid: obj.entry_id, task, fn: PANEL_EVAL }); + await page.waitForTimeout(1200); + const visibleState = await page.evaluate((fn) => { + eval(fn); + const dlgSr = window._sr.querySelector("maintenance-task-dialog").shadowRoot; + return { + scheduleTimeEnabled: window._sr.querySelector("maintenance-task-dialog").scheduleTimeEnabled, + hasTimeInput: !!dlgSr.querySelector('ha-textfield[type="time"]'), + }; + }, PANEL_EVAL); + check("scheduleTimeEnabled prop is true after feature on", visibleState.scheduleTimeEnabled === true); + check("time input IS in the DOM after feature on", visibleState.hasTimeInput === true); + + log("\n== Step 5: set time via WS (skip the DOM event roundtrip), re-read task"); + await ws(page, { + type: "maintenance_supporter/task/update", + entry_id: obj.entry_id, task_id: taskRes.task_id, schedule_time: "14:00", + }); + await page.waitForTimeout(1500); + const updated = (await ws(page, { type: "maintenance_supporter/object", entry_id: obj.entry_id })) + .tasks.find(t => t.id === taskRes.task_id); + check("summary contains schedule_time=14:00", updated.schedule_time === "14:00", + `got ${updated.schedule_time}`); + + log("\n== Step 6: reject malformed schedule_time at the WS layer"); + let rejected = false; + try { + await ws(page, { + type: "maintenance_supporter/task/update", + entry_id: obj.entry_id, task_id: taskRes.task_id, schedule_time: "25:99", + }); + } catch { + rejected = true; + } + check("malformed HH:MM rejected by schema", rejected); + + log("\n== Step 7: cleanup"); + await ws(page, { type: "maintenance_supporter/object/delete", entry_id: obj.entry_id }); + // Leave the flag on — test fixture state is not restored, next run will re-toggle. + await cleanup(browser, ctx); + + log(`\n=== ${pass} passed, ${fail} failed ===`); + process.exit(fail === 0 ? 0 : 1); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/custom_components/maintenance_supporter/frontend-src/e2e-test.mjs b/custom_components/maintenance_supporter/frontend-src/e2e-test.mjs new file mode 100644 index 0000000..4052633 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/e2e-test.mjs @@ -0,0 +1,197 @@ +/** + * Comprehensive E2E test suite for the Maintenance Supporter panel. + * + * Prerequisites: + * docker compose up -d # ha-maint + playwright + * docker restart playwright-server # clean session + * + * Usage: + * cd custom_components/maintenance_supporter/frontend-src + * node e2e-test.mjs # auto-fetches refresh token + * HA_REFRESH_TOKEN=... node e2e-test.mjs # explicit token + */ +import { setup, ws, openEditDialog, readDialogSelects, readDialogFields, cleanup } from "./e2e-helpers.mjs"; + +let passed = 0, failed = 0, skipped = 0; +function check(name, ok) { + if (ok) { passed++; console.log(" PASS " + name); } + else { failed++; console.log(" FAIL " + name); } +} + +async function runTest(name, fn) { + console.log("\n=== " + name + " ==="); + try { await fn(); } + catch (e) { failed++; console.log(" ERROR: " + e.message); } +} + +// ═══════════════════════════════════════════════════════════════ +// DESKTOP TESTS +// ═══════════════════════════════════════════════════════════════ +const { browser, ctx, page } = await setup(); +const ENTRY = (await ws(page, { type: "maintenance_supporter/objects" })).objects[0].entry_id; + +await runTest("Sort dropdown exists with 4 modes", async () => { + const selects = await page.evaluate(() => { + const ha = document.querySelector("home-assistant"); + const main = ha?.shadowRoot?.querySelector("home-assistant-main"); + const drawer = main?.shadowRoot?.querySelector("ha-drawer"); + const resolver = drawer?.querySelector("partial-panel-resolver"); + const custom = resolver?.querySelector("ha-panel-custom"); + const panel = custom?.querySelector("maintenance-supporter-panel"); + const sr = panel?.shadowRoot; + const filterBar = sr?.querySelector(".filter-bar"); + if (!filterBar) return []; + return [...filterBar.querySelectorAll("select")].map(s => ({ + options: [...s.options].map(o => o.value), + })); + }); + const sortSelect = selects.find(s => s.options.includes("due_date")); + check("sort dropdown found", !!sortSelect); + check("has 4 sort options", sortSelect?.options.length === 4); +}); + +await runTest("All Objects view via KPI click", async () => { + const clicked = await page.evaluate(() => { + const ha = document.querySelector("home-assistant"); + const sr = ha?.shadowRoot?.querySelector("home-assistant-main")?.shadowRoot?.querySelector("ha-drawer")?.querySelector("partial-panel-resolver")?.querySelector("ha-panel-custom")?.querySelector("maintenance-supporter-panel")?.shadowRoot; + const item = sr?.querySelector(".stat-item.clickable"); + if (item) { item.click(); return true; } + return false; + }); + check("KPI clickable", clicked); + await page.waitForTimeout(1000); + const cards = await page.evaluate(() => { + const ha = document.querySelector("home-assistant"); + const sr = ha?.shadowRoot?.querySelector("home-assistant-main")?.shadowRoot?.querySelector("ha-drawer")?.querySelector("partial-panel-resolver")?.querySelector("ha-panel-custom")?.querySelector("maintenance-supporter-panel")?.shadowRoot; + return sr?.querySelectorAll(".object-card")?.length ?? 0; + }); + check("object cards rendered", cards > 0); +}); + +// Reload panel to reset view state after All Objects test +await page.goto("http://homeassistant-dev:8123/maintenance-supporter"); +await page.waitForTimeout(5000); + +await runTest("Select dropdowns preserve values on edit", async () => { + const objs = await ws(page, { type: "maintenance_supporter/objects" }); + let triggerTask = null, triggerEntry = null; + for (const o of objs.objects) { + for (const t of o.tasks) { + if (t.trigger_config?.type) { triggerTask = t; triggerEntry = o.entry_id; break; } + } + if (triggerTask) break; + } + if (!triggerTask) { skipped++; console.log(" SKIP: no sensor-trigger task"); return; } + await openEditDialog(page, triggerEntry, triggerTask); + const selects = await readDialogSelects(page); + for (const s of selects) { + check(s.label + " selected matches value", s.match); + } + await page.keyboard.press("Escape"); +}); + +await runTest("Last performed field in task dialog", async () => { + const cr = await ws(page, { + type: "maintenance_supporter/task/create", entry_id: ENTRY, + name: "E2E LP Test", task_type: "service", schedule_type: "time_based", + interval_days: 30, last_performed: "2025-11-01", enabled: true, + }); + // Reload so panel picks up the new task + await page.goto("http://homeassistant-dev:8123/maintenance-supporter"); + await page.waitForTimeout(5000); + const obj = await ws(page, { type: "maintenance_supporter/object", entry_id: ENTRY }); + const task = obj.tasks.find(t => t.id === cr.task_id); + await openEditDialog(page, ENTRY, task); + const fields = await readDialogFields(page); + console.log(" Fields found:", fields.length, fields.map(f => f.type + ":" + f.label?.slice(0, 20)).join(", ")); + const dateField = fields.find(f => f.type === "date"); + check("date field exists", !!dateField); + check("date value correct", dateField?.value === "2025-11-01"); + await page.keyboard.press("Escape"); + await ws(page, { type: "maintenance_supporter/task/delete", entry_id: ENTRY, task_id: cr.task_id }); +}); + +await runTest("Task create with enabled field (Bug #14 regression)", async () => { + const r = await ws(page, { + type: "maintenance_supporter/task/create", entry_id: ENTRY, + name: "E2E Enabled Test", task_type: "cleaning", schedule_type: "time_based", + interval_days: 30, enabled: true, + }); + check("create with enabled succeeds", !!r.task_id); + if (r.task_id) await ws(page, { type: "maintenance_supporter/task/delete", entry_id: ENTRY, task_id: r.task_id }); +}); + +await runTest("Reset with date field (Bug #12 regression)", async () => { + const objs = await ws(page, { type: "maintenance_supporter/objects" }); + const t = objs.objects[0].tasks[0]; + try { + await ws(page, { type: "maintenance_supporter/task/reset", entry_id: ENTRY, task_id: t.id, date: "2026-04-01" }); + check("reset with date succeeds", true); + } catch (e) { + check("reset with date succeeds", false); + } +}); + +await runTest("Desktop header bar hidden on overview", async () => { + await page.goto("http://homeassistant-dev:8123/maintenance-supporter"); + await page.waitForTimeout(3000); + const headerVisible = await page.evaluate(() => { + const ha = document.querySelector("home-assistant"); + const sr = ha?.shadowRoot?.querySelector("home-assistant-main")?.shadowRoot?.querySelector("ha-drawer")?.querySelector("partial-panel-resolver")?.querySelector("ha-panel-custom")?.querySelector("maintenance-supporter-panel")?.shadowRoot; + const header = sr?.querySelector(".header"); + if (!header) return false; + const style = window.getComputedStyle(header); + return style.display !== "none" && header.offsetHeight > 0; + }); + check("header NOT visible on desktop overview", !headerVisible); +}); + +await cleanup(browser, ctx); + +// Reconnect for timezone + mobile tests +import { chromium } from "playwright"; +const browser2 = await chromium.connect("ws://localhost:3000"); + +// ═══════════════════════════════════════════════════════════════ +// TIMEZONE + MOBILE TESTS (reuse same browser connection) +// ═══════════════════════════════════════════════════════════════ +await runTest("Date formatting in US timezone (Bug #21)", async () => { + const tzCtx = await browser2.newContext({ viewport: { width: 800, height: 600 }, locale: "en-US", timezoneId: "America/New_York" }); + const tzPage = await tzCtx.newPage(); + await tzPage.goto("about:blank"); + const r = await tzPage.evaluate(() => ({ + utc: new Date("2025-11-01").toLocaleDateString("en-US"), + local: new Date("2025-11-01T00:00:00").toLocaleDateString("en-US"), + })); + check("UTC parse shows wrong date (Oct 31)", r.utc.includes("10/31")); + check("Local parse shows correct date (Nov)", r.local.includes("11/")); + await tzCtx.close(); +}); + +await runTest("Mobile hamburger menu button", async () => { + const mobCtx = await browser2.newContext({ viewport: { width: 375, height: 812 }, locale: "en-US", colorScheme: "dark", isMobile: true }); + const mobPage = await mobCtx.newPage(); + await mobPage.goto("http://homeassistant-dev:8123"); + await mobPage.waitForTimeout(1000); + await mobPage.evaluate((r) => localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: "http://homeassistant-dev:8123", clientId: "http://homeassistant-dev:8123/", + refresh_token: r, access_token: "", token_type: "Bearer", expires_in: 1800, expires: 0, + })), process.env.HA_REFRESH_TOKEN); + await mobPage.goto("http://homeassistant-dev:8123/maintenance-supporter"); + await mobPage.waitForTimeout(7000); + const hasMenu = await mobPage.evaluate(() => { + const ha = document.querySelector("home-assistant"); + const sr = ha?.shadowRoot?.querySelector("home-assistant-main")?.shadowRoot?.querySelector("ha-drawer")?.querySelector("partial-panel-resolver")?.querySelector("ha-panel-custom")?.querySelector("maintenance-supporter-panel")?.shadowRoot; + return !!sr?.querySelector("ha-menu-button"); + }); + check("ha-menu-button visible on mobile", hasMenu); + await mobCtx.close(); +}); + +await cleanup(browser2, { close() {} }); + +// ═══════════════════════════════════════════════════════════════ +console.log("\n" + "═".repeat(50)); +console.log(`RESULTS: ${passed} passed, ${failed} failed, ${skipped} skipped`); +if (failed > 0) { console.log("SOME TESTS FAILED!"); process.exit(1); } +else console.log("ALL TESTS PASSED!"); diff --git a/custom_components/maintenance_supporter/frontend-src/esbuild.mjs b/custom_components/maintenance_supporter/frontend-src/esbuild.mjs new file mode 100644 index 0000000..c922aa8 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/esbuild.mjs @@ -0,0 +1,93 @@ +import { build } from "esbuild"; +import { cpSync, mkdirSync, rmSync } from "fs"; + +// Content-hashed chunk names change every time their inputs change; esbuild +// writes the new files but never removes the old ones. Clear the chunks dir up +// front so stale, unreferenced chunks don't accumulate in git (they did before +// this — every frontend change left orphans behind). +rmSync("../frontend/strategy/chunks", { recursive: true, force: true }); + +const common = { + bundle: true, + format: "esm", + target: "es2021", + minify: true, + sourcemap: false, + external: [], +}; + +// Panel +await build({ + ...common, + entryPoints: ["maintenance-panel.ts"], + outfile: "../frontend/maintenance-panel.js", +}); + +// Lovelace Card +await build({ + ...common, + entryPoints: ["maintenance-card.ts"], + outfile: "../frontend/maintenance-card.js", +}); + +// Dashboard Strategy (HA 2026.5+ — silent no-op on older HA) +// +// v2.3.4 (issue #52): we MUST split dynamic imports out into separate +// chunks. HA's strategy loader has a 5 s timeout on +// customElements.whenDefined("ll-strategy-dashboard-..."); if the strategy +// bundle takes longer than that to download + parse + execute, the +// dashboard fails to open. Without splitting, esbuild inlines every +// dynamic import — strategy file ballooned to 510 KB pulling in 7 dialog +// components + 3 section cards. With splitting enabled, the strategy +// entry stays small (~13 KB) and registers customElements instantly; +// heavy components load lazily as separate chunks only when they're +// actually rendered. +// +// All output lands in ../frontend/strategy/ so HA can serve the directory +// as a single static-path mount and the entry's relative ./chunks/ imports +// resolve naturally without per-chunk URL registration. +await build({ + ...common, + entryPoints: ["maintenance-dashboard-strategy.ts"], + outdir: "../frontend/strategy", + splitting: true, + entryNames: "[name]", + chunkNames: "chunks/[name]-[hash]", +}); + +// Dashboard-strategy registration shim (v2.8.1 — strategy timeout fix). +// +// HA loads frontend_extra_module_url entries as fire-and-forget import() with +// no ordering/await, then only waits ~5 s via whenDefined for a custom: strategy +// element. Under heavy HACS plugin load the full strategy bundle could lose that +// race (element downloaded but not yet executed). This tiny zero-import shim is +// what we register as the extra_module_url: it defines the dashboard tag +// synchronously (a few hundred bytes, no import graph) and lazy-import()s the +// heavy bundle above only on first generate()/getConfigElement(). +await build({ + ...common, + entryPoints: ["maintenance-strategy-shim.ts"], + outfile: "../frontend/maintenance-strategy-shim.js", +}); + +// Calendar Card (v1.9.0+ — Lovelace card extracted from panel's Calendar tab) +await build({ + ...common, + entryPoints: ["maintenance-calendar-card.ts"], + outfile: "../frontend/maintenance-calendar-card.js", +}); + +// Copy the runtime-loaded locale tables into the served frontend dir. Only EN +// is bundled (imported by styles.ts); the other languages are fetched at +// runtime from frontend/locales/, so editing a translation needs no rebuild. +mkdirSync("../frontend/locales", { recursive: true }); +cpSync("locales", "../frontend/locales", { recursive: true }); + +// v2.21: vendor pdf.js for the work sheet's inline manual excerpt. Copied, +// not bundled — the work-sheet page loads it on demand from VENDOR_URL, so +// the panel/card bundles stay untouched. +mkdirSync("../frontend/vendor", { recursive: true }); +cpSync("node_modules/pdfjs-dist/legacy/build/pdf.min.mjs", "../frontend/vendor/pdf.min.mjs"); +cpSync("node_modules/pdfjs-dist/legacy/build/pdf.worker.min.mjs", "../frontend/vendor/pdf.worker.min.mjs"); + +console.log("Build complete."); diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/calendar-bucket.ts b/custom_components/maintenance_supporter/frontend-src/helpers/calendar-bucket.ts new file mode 100644 index 0000000..452d34e --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/calendar-bucket.ts @@ -0,0 +1,387 @@ +/** + * Pure helper for the panel's Calendar tab (v1.5.0+). + * + * Given the loaded objects, a window in days, and an optional user filter, + * produce a flat day-bucketed event list ready for rendering. + * + * Recurring projection (time-based tasks): the first occurrence is the task's + * next_due (or "today" if overdue/triggered). Subsequent occurrences are + * projected by adding interval_days repeatedly until the window end, capped + * at MAX_OCCURRENCES_PER_TASK to prevent absurdly small intervals (e.g. a + * 1-day-interval task in a 30-day window would otherwise produce 30 entries). + * + * Sensor-triggered tasks: only next_due is shown — we have no honest way to + * predict when a sensor will next fire. + */ + +import type { MaintenanceObjectResponse } from "../types"; +import { intervalSpanDays } from "./interval"; + +export const MAX_OCCURRENCES_PER_TASK = 5; + +export interface CalendarEvent { + /** ISO date string (YYYY-MM-DD) — the day this event is bucketed under. */ + date: string; + entry_id: string; + task_id: string; + task_name: string; + object_name: string; + status: string; // "ok" | "due_soon" | "overdue" | "triggered" + days_until_due: number | null; + /** True if this is a projected recurrence (not the actual next_due). */ + projected: boolean; + schedule_type: string; + interval_days: number | null; + /** v2.6.3 (#59): interval unit so the "every N …" label + projection match. */ + interval_unit?: string | null; + responsible_user_id: string | null; + avg_cost: number | null; + /** v1.5.1: source indicator. */ + adaptive_enabled: boolean; + /** v1.5.1: sensor prediction confidence ("low" | "medium" | "high") or null + * for time-based / no prediction available. */ + prediction_confidence: string | null; + /** v2.2.0 — for past-window events, the original ISO timestamp of the + * underlying history entry. The history-edit WS uses this as the stable + * identifier (see ws_update_history_entry / original_timestamp). null + * for forward-projected / next_due events. */ + history_timestamp?: string | null; + /** v2.2.0 — history entry type for past events + * ("completed" | "reset" | "skipped" | "triggered"). null for future. */ + history_type?: string | null; + /** v2.2.0 — original cost / notes / duration from the history entry, + * so the past-events list shows what really happened (not avg_cost). */ + history_cost?: number | null; + history_notes?: string | null; + history_duration?: number | null; +} + +export interface CalendarDayBucket { + date: string; // ISO date YYYY-MM-DD + events: CalendarEvent[]; +} + +/** + * Format a Date as ISO YYYY-MM-DD using LOCAL time components — matches what + * the user sees on their wall clock, not UTC. The naive `.toISOString().slice(0,10)` + * approach silently shifts dates near midnight in non-UTC timezones. + */ +export function isoDateLocal(d: Date): string { + const y = d.getFullYear(); + const m = String(d.getMonth() + 1).padStart(2, "0"); + const day = String(d.getDate()).padStart(2, "0"); + return `${y}-${m}-${day}`; +} + +/** Build the list of N consecutive ISO dates starting today (local). */ +export function buildWindowDates(today: Date, windowDays: number): string[] { + const out: string[] = []; + for (let i = 0; i < windowDays; i++) { + const d = new Date(today); + d.setDate(d.getDate() + i); + d.setHours(0, 0, 0, 0); + out.push(isoDateLocal(d)); + } + return out; +} + +/** Add `days` to an ISO date (local), return new ISO date. */ +function addDaysIso(iso: string, days: number): string { + const [y, m, d] = iso.split("-").map(Number); + const date = new Date(y, m - 1, d); + date.setDate(date.getDate() + days); + return isoDateLocal(date); +} + +/** Average recorded cost across history rows that HAVE a cost (mean of + * non-null `cost` values). Note this differs from the panel KPI, which divides + * total_cost by times_performed — completions without a recorded cost are + * counted differently. This mean is used only for the calendar tooltip. */ +function computeAvgCost(history: Array<{ cost?: number | null }> | undefined): number | null { + if (!history || history.length === 0) return null; + const costs = history.map((h) => h.cost).filter((c): c is number => typeof c === "number"); + if (costs.length === 0) return null; + return costs.reduce((a, b) => a + b, 0) / costs.length; +} + +interface ProjectionInput { + windowStart: string; + windowEnd: string; // inclusive + task: any; // panel's task shape from MaintenanceObjectResponse + entryId: string; + objectName: string; +} + +/** + * Project up to MAX_OCCURRENCES_PER_TASK occurrences for a single task. + * Returns events keyed by their bucketed date. + */ +function projectTask(input: ProjectionInput): CalendarEvent[] { + const { windowStart, windowEnd, task, entryId, objectName } = input; + const out: CalendarEvent[] = []; + + const baseEvent = (date: string, projected: boolean): CalendarEvent => ({ + date, + entry_id: entryId, + task_id: task.id, + task_name: task.name, + object_name: objectName, + // Projected recurrences are hypothetical future occurrences that assume + // the current cycle resolves on schedule. If the parent task is currently + // overdue/triggered, carrying that status forward to the projection (e.g. + // "OVERDUE 211d" on the May 7 projection of a 7-day-interval task) is + // misleading — the projection IS the assumption that the user completes + // it today, so the projected slot should read as a fresh "ok" event. + status: projected && (task.status === "overdue" || task.status === "triggered") + ? "ok" + : task.status, + days_until_due: projected ? null : (task.days_until_due ?? null), + projected, + schedule_type: task.schedule_type, + interval_days: task.interval_days ?? null, + interval_unit: task.interval_unit ?? null, + responsible_user_id: task.responsible_user_id ?? null, + avg_cost: computeAvgCost(task.history), + adaptive_enabled: !!task.adaptive_config?.enabled, + prediction_confidence: task.threshold_prediction_confidence ?? null, + }); + + // Step between projected occurrences, unit-aware (issue #59): a weeks/months/ + // years interval must advance by its real day-span, not the raw count (else a + // "1 year" task projects at 1-day steps and spams the window). + const stepDays = Math.max( + 1, Math.round(intervalSpanDays(task.interval_days, task.interval_unit)), + ); + + // Overdue / triggered → bucket on today (windowStart) regardless of next_due + if (task.status === "overdue" || task.status === "triggered") { + out.push(baseEvent(windowStart, false)); + // Continue projecting from "today" forward for time-based tasks + if (task.schedule_type === "time_based" && task.interval_days && task.interval_days > 0) { + let cursor = addDaysIso(windowStart, stepDays); + let count = 1; + while (cursor <= windowEnd && count < MAX_OCCURRENCES_PER_TASK) { + out.push(baseEvent(cursor, true)); + count++; + cursor = addDaysIso(cursor, stepDays); + } + } + return out; + } + + // Non-actionable status (ok / due_soon): need a next_due + const nextDue = task.next_due; + if (typeof nextDue !== "string" || !nextDue) return out; + const firstDate = nextDue.slice(0, 10); // strip time portion if any + + // First occurrence must be in window + if (firstDate >= windowStart && firstDate <= windowEnd) { + out.push(baseEvent(firstDate, false)); + } else if (firstDate > windowEnd) { + return out; // entire occurrence chain is past window + } + + // Subsequent projected occurrences (time-based only, with interval_days) + if (task.schedule_type === "time_based" && task.interval_days && task.interval_days > 0) { + let cursor = addDaysIso(firstDate, stepDays); + let count = out.length; + while (cursor <= windowEnd && count < MAX_OCCURRENCES_PER_TASK) { + // Skip occurrences before window start (when next_due itself was past) + if (cursor >= windowStart) { + out.push(baseEvent(cursor, true)); + count++; + } + cursor = addDaysIso(cursor, stepDays); + } + } + + return out; +} + +/** Status sort priority: lower = shown first. */ +const STATUS_RANK: Record = { + overdue: 0, + triggered: 1, + due_soon: 2, + ok: 3, +}; + +/** + * Main entry point: build the day-bucketed event list for the Calendar tab. + * + * @param objects The panel's loaded objects (with nested tasks). + * @param today "Today" as a Date (caller passes new Date() — easier for tests). + * @param windowDays Number of days to include (7 / 14 / 30). + * @param userFilter null/empty = all; otherwise filter tasks by responsible_user_id. + */ +export function buildCalendarBuckets( + objects: MaintenanceObjectResponse[], + today: Date, + windowDays: number, + userFilter: string | null = null, +): CalendarDayBucket[] { + const days = buildWindowDates(today, windowDays); + const windowStart = days[0]; + const windowEnd = days[days.length - 1]; + + const allEvents: CalendarEvent[] = []; + for (const obj of objects) { + const objectName = obj.object?.name || ""; + const entryId = obj.entry_id; + const tasks = obj.tasks || []; + for (const task of tasks) { + // User filter + if (userFilter && task.responsible_user_id !== userFilter) continue; + // Disabled tasks never produce calendar events + if (task.enabled === false) continue; + const projected = projectTask({ + windowStart, windowEnd, task, entryId, objectName, + }); + allEvents.push(...projected); + } + } + + // Bucket by date + const byDate = new Map(); + for (const day of days) byDate.set(day, []); + for (const ev of allEvents) { + const bucket = byDate.get(ev.date); + if (bucket) bucket.push(ev); + } + + // Sort within day: status priority first, then projected last, then by name + for (const [, evs] of byDate) { + evs.sort((a, b) => { + const rA = STATUS_RANK[a.status] ?? 99; + const rB = STATUS_RANK[b.status] ?? 99; + if (rA !== rB) return rA - rB; + if (a.projected !== b.projected) return a.projected ? 1 : -1; + const cmp = a.object_name.localeCompare(b.object_name); + if (cmp !== 0) return cmp; + return a.task_name.localeCompare(b.task_name); + }); + } + + return days.map((d) => ({ date: d, events: byDate.get(d) ?? [] })); +} + +// ── v2.2.0 — past-window bucketer ────────────────────────────────────────── +// +// Same shape as buildCalendarBuckets but walks the task HISTORY (not next_due) +// and buckets entries by their actual completion/reset/skip/trigger date. +// Used by the calendar card's past-mode chip ("← 30 d") to surface +// recently-performed maintenance for review or correction. +// +// Window: from (today − pastDays + 1) to today, inclusive — so passing 30 +// gives 30 day-buckets ending today. + +const HISTORY_TYPE_TO_STATUS: Record = { + completed: "ok", + reset: "ok", + skipped: "due_soon", + triggered: "triggered", + trigger_replaced: "triggered", + trigger_removed: "ok", // config change, not a due/triggered event +}; + +interface HistoryEntryShape { + timestamp?: string; + type?: string; + notes?: string; + cost?: number; + duration?: number; +} + +/** Build a list of N consecutive ISO dates ending today (local). */ +export function buildPastWindowDates(today: Date, pastDays: number): string[] { + const out: string[] = []; + for (let i = pastDays - 1; i >= 0; i--) { + const d = new Date(today); + d.setDate(d.getDate() - i); + d.setHours(0, 0, 0, 0); + out.push(isoDateLocal(d)); + } + return out; +} + +/** + * Build day-bucketed events for the *past* N days using task history. + * + * Each history entry whose timestamp falls inside the window becomes one + * CalendarEvent with `history_timestamp` set — the calendar card uses that + * timestamp as the stable key when dispatching an edit-history click event. + */ +export function buildPastBuckets( + objects: MaintenanceObjectResponse[], + today: Date, + pastDays: number, + userFilter: string | null = null, +): CalendarDayBucket[] { + const days = buildPastWindowDates(today, pastDays); + const windowStart = days[0]; + const windowEnd = days[days.length - 1]; + + const byDate = new Map(); + for (const day of days) byDate.set(day, []); + + for (const obj of objects) { + const objectName = obj.object?.name || ""; + const entryId = obj.entry_id; + const tasks = obj.tasks || []; + for (const task of tasks) { + if (userFilter && task.responsible_user_id !== userFilter) continue; + const history = (task.history || []) as HistoryEntryShape[]; + for (const h of history) { + if (typeof h?.timestamp !== "string") continue; + const dateKey = h.timestamp.slice(0, 10); // YYYY-MM-DD + if (dateKey < windowStart || dateKey > windowEnd) continue; + const bucket = byDate.get(dateKey); + if (!bucket) continue; + const evType = h.type ?? "completed"; + bucket.push({ + date: dateKey, + entry_id: entryId, + task_id: task.id, + task_name: task.name, + object_name: objectName, + status: HISTORY_TYPE_TO_STATUS[evType] ?? "ok", + days_until_due: null, + projected: false, + schedule_type: task.schedule_type, + interval_days: task.interval_days ?? null, + responsible_user_id: task.responsible_user_id ?? null, + avg_cost: typeof h.cost === "number" ? h.cost : null, + adaptive_enabled: !!task.adaptive_config?.enabled, + prediction_confidence: null, + history_timestamp: h.timestamp, + history_type: evType, + history_cost: typeof h.cost === "number" ? h.cost : null, + history_notes: typeof h.notes === "string" ? h.notes : null, + history_duration: typeof h.duration === "number" ? h.duration : null, + }); + } + } + } + + // Sort within day: type priority (completed first, triggered last) then by + // object/task name. Past events are never "projected" so no opacity dimming. + const PAST_TYPE_RANK: Record = { + completed: 0, + reset: 1, + skipped: 2, + triggered: 3, + trigger_replaced: 4, + }; + for (const [, evs] of byDate) { + evs.sort((a, b) => { + const rA = PAST_TYPE_RANK[a.history_type ?? ""] ?? 99; + const rB = PAST_TYPE_RANK[b.history_type ?? ""] ?? 99; + if (rA !== rB) return rA - rB; + const cmp = a.object_name.localeCompare(b.object_name); + if (cmp !== 0) return cmp; + return a.task_name.localeCompare(b.task_name); + }); + } + + return days.map((d) => ({ date: d, events: byDate.get(d) ?? [] })); +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/download.ts b/custom_components/maintenance_supporter/frontend-src/helpers/download.ts new file mode 100644 index 0000000..1d4d69f --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/download.ts @@ -0,0 +1,50 @@ +/** + * Download a text payload as a file — Companion-app safe. + * + * The naive pattern (a detached ``, `a.click()`, then an immediate + * `URL.revokeObjectURL`) silently fails inside the Home Assistant Companion + * app's WebView (especially iOS WKWebView): the anchor must be in the DOM, + * carry `target="_blank"` so the app's download handler engages, and the + * object URL must NOT be revoked before the (asynchronous) download has + * started. This mirrors home-assistant-frontend's own `fileDownload` helper. + */ +export function downloadTextFile( + content: string, + filename: string, + mime: string, +): void { + const blob = new Blob([content], { type: mime }); + const url = URL.createObjectURL(blob); + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.target = "_blank"; + a.rel = "noopener"; + a.style.display = "none"; + document.body.appendChild(a); + a.dispatchEvent(new MouseEvent("click")); + document.body.removeChild(a); + // Revoke late: slower WebViews (the Companion app) kick off the download + // asynchronously, and revoking the blob URL immediately cancels it. + setTimeout(() => URL.revokeObjectURL(url), 60_000); +} + +/** + * Download a (same-origin) URL as a file — Companion-app safe. + * + * For binary blobs served by our authenticated view: fetch a signed path via + * `auth/sign_path` first, then hand the signed URL here. Same DOM-anchored, + * `target="_blank"` trick as {@link downloadTextFile} so the Companion WebView + * engages its download handler; no blob URL to revoke. + */ +export function downloadUrl(url: string, filename: string): void { + const a = document.createElement("a"); + a.href = url; + a.download = filename; + a.target = "_blank"; + a.rel = "noopener"; + a.style.display = "none"; + document.body.appendChild(a); + a.dispatchEvent(new MouseEvent("click")); + document.body.removeChild(a); +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/format-bytes.ts b/custom_components/maintenance_supporter/frontend-src/helpers/format-bytes.ts new file mode 100644 index 0000000..d841a10 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/format-bytes.ts @@ -0,0 +1,7 @@ +/** Human-readable byte size (B / KB / MB) for document storage figures. */ +export function formatBytes(bytes: number | undefined): string { + const b = bytes ?? 0; + if (b < 1024) return `${b} B`; + if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)} KB`; + return `${(b / (1024 * 1024)).toFixed(1)} MB`; +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/interval.ts b/custom_components/maintenance_supporter/frontend-src/helpers/interval.ts new file mode 100644 index 0000000..70519c1 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/interval.ts @@ -0,0 +1,41 @@ +/** + * Unit-aware interval math (issue #59) for the frontend's progress bars and + * calendar projection stepping. Uses an AVERAGE day-span per unit (below), an + * intentional approximation — the backend `helpers/dates.add_interval` is + * calendar-exact (Jan+1mo = 31 days), so display-side spans can differ by a day + * or two. Cosmetic only. Pure functions — unit-tested in interval.test.ts. + */ + +/** Approximate (average) days per interval unit — mean Gregorian month / Julian + * year. NOT calendar-exact; see the module note above. */ +export const UNIT_DAYS: Record = { + days: 1, + weeks: 7, + months: 30.4368, + years: 365.25, +}; + +/** Approximate length of `intervalDays` of `unit` in days; 0 when no interval. */ +export function intervalSpanDays( + intervalDays: number | null | undefined, + unit?: string | null, +): number { + if (!intervalDays || intervalDays <= 0) return 0; + return intervalDays * (UNIT_DAYS[unit || "days"] ?? 1); +} + +/** + * Progress through the current cycle, unit-aware. + * Returns a clamped 0–100 % plus `overflow` (raw % > 100, i.e. past due). + * Falls back to {0, false} when there is no usable interval/countdown. + */ +export function daysProgress( + intervalDays: number | null | undefined, + daysUntilDue: number | null | undefined, + unit?: string | null, +): { pct: number; overflow: boolean } { + const span = intervalSpanDays(intervalDays, unit); + if (span <= 0 || daysUntilDue == null) return { pct: 0, overflow: false }; + const raw = ((span - daysUntilDue) / span) * 100; + return { pct: Math.max(0, Math.min(100, raw)), overflow: raw > 100 }; +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/object-columns.ts b/custom_components/maintenance_supporter/frontend-src/helpers/object-columns.ts new file mode 100644 index 0000000..afec03b --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/object-columns.ts @@ -0,0 +1,65 @@ +/** (#67) Objects-table column catalog. + * + * Shared by the panel's table view and the Settings column-config UI. Keep + * KNOWN/DEFAULT in lockstep with const.py (KNOWN_OBJECT_TABLE_COLUMNS / + * DEFAULT_OBJECTS_TABLE_COLUMNS) — the backend sanitises to the same set. + * Parity is enforced by tests/test_frontend_const_parity.py (drift fails CI). + */ + +export interface ObjectColumnDef { + /** Stable column key (matches the persisted setting + object field). */ + key: string; + /** i18n key for the header cell + the Settings checkbox label. */ + labelKey: string; + /** Columns the user may not remove (always rendered). */ + required?: boolean; +} + +/** Canonical order + label mapping. Most labels reuse existing i18n keys. */ +export const OBJECT_COLUMNS: ObjectColumnDef[] = [ + { key: "name", labelKey: "name", required: true }, + { key: "manufacturer", labelKey: "manufacturer" }, + { key: "model", labelKey: "model" }, + { key: "serial_number", labelKey: "serial_number_label" }, + { key: "installation_date", labelKey: "installed" }, + { key: "warranty_expiry", labelKey: "warranty" }, + { key: "area_id", labelKey: "area" }, + { key: "documentation_url", labelKey: "documentation_url_label" }, + { key: "notes", labelKey: "object_notes_label" }, + { key: "task_count", labelKey: "tasks" }, + { key: "actions", labelKey: "actions" }, +]; + +export const KNOWN_OBJECT_COLUMNS: string[] = OBJECT_COLUMNS.map((c) => c.key); + +export const DEFAULT_OBJECTS_TABLE_COLUMNS: string[] = [ + "name", + "manufacturer", + "model", + "serial_number", + "installation_date", + "warranty_expiry", + "area_id", + "task_count", + "actions", +]; + +/** + * Sanitise a stored/configured column list to known keys (order preserved, + * deduped). Falls back to the defaults when empty/invalid, and always keeps + * the required `name` column so the table stays usable. Mirrors the backend. + */ +export function sanitizeColumns(cols: unknown): string[] { + if (!Array.isArray(cols)) return [...DEFAULT_OBJECTS_TABLE_COLUMNS]; + const seen = new Set(); + const out: string[] = []; + for (const c of cols) { + if (typeof c === "string" && KNOWN_OBJECT_COLUMNS.includes(c) && !seen.has(c)) { + seen.add(c); + out.push(c); + } + } + if (!out.length) return [...DEFAULT_OBJECTS_TABLE_COLUMNS]; + if (!out.includes("name")) out.unshift("name"); + return out; +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/report.ts b/custom_components/maintenance_supporter/frontend-src/helpers/report.ts new file mode 100644 index 0000000..6e11588 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/report.ts @@ -0,0 +1,110 @@ +/** Self-contained printable maintenance report for one object. + * + * Produces a full HTML document (inline print CSS) that the panel opens in a new + * tab via a Blob URL — the user then prints or "Save as PDF" from the browser. + * No PDF dependency, works offline. All user content is HTML-escaped. + */ + +import type { MaintenanceObject, MaintenanceTask } from "../types"; + +export interface ReportLabels { + title: string; + generated: string; + manufacturer: string; + model: string; + serial: string; + installed: string; + warranty: string; + area: string; + notes: string; + tasksHeading: string; + colTask: string; + colType: string; + colStatus: string; + colSchedule: string; + colLastDone: string; + colNextDue: string; + colCost: string; + colTimes: string; + totalCost: string; + /** Human schedule label for a task — pass formatRecurrence so calendar + * kinds (weekdays/nth_weekday/…) render properly, not just intervals. */ + scheduleLabel: (t: MaintenanceTask) => string; + statusLabel: (s: string) => string; + typeLabel: (t: string) => string; + none: string; +} + +function esc(v: unknown): string { + return String(v ?? "").replace(/[&<>"']/g, (c) => + ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c] as string)); +} + +export function buildObjectReportHtml( + obj: MaintenanceObject, + tasks: MaintenanceTask[], + labels: ReportLabels, + fmtDate: (iso: string | null | undefined) => string, + currencySymbol: string, + nowIso: string, +): string { + const meta = ([ + [labels.manufacturer, obj.manufacturer], + [labels.model, obj.model], + [labels.serial, obj.serial_number], + [labels.installed, obj.installation_date ? fmtDate(obj.installation_date) : null], + [labels.warranty, obj.warranty_expiry ? fmtDate(obj.warranty_expiry) : null], + ] as Array<[string, string | null | undefined]>).filter(([, v]) => !!v); + + const rows = tasks.map((t) => { + const schedule = labels.scheduleLabel(t); + return ` + ${esc(t.name)} + ${esc(labels.typeLabel(t.type))} + ${esc(labels.statusLabel(t.status))} + ${esc(schedule)} + ${esc(t.last_performed ? fmtDate(t.last_performed) : labels.none)} + ${esc(t.next_due ? fmtDate(t.next_due) : labels.none)} + ${t.times_performed ?? 0} + ${(t.total_cost ?? 0).toFixed(2)} ${esc(currencySymbol)} + `; + }).join(""); + + const totalCost = tasks.reduce((n, t) => n + (t.total_cost ?? 0), 0); + + return ` +${esc(labels.title)} — ${esc(obj.name)} + +

${esc(obj.name)}

+

${esc(labels.title)} · ${esc(labels.generated)}: ${esc(fmtDate(nowIso))}

+ ${meta.length ? `
${meta.map(([k, v]) => + `
${esc(k)}
${esc(v)}
`).join("")}
` : ""} +

${esc(labels.tasksHeading)} (${tasks.length})

+ + + + + + + ${rows || ``} + +
${esc(labels.colTask)}${esc(labels.colType)}${esc(labels.colStatus)}${esc(labels.colSchedule)}${esc(labels.colLastDone)}${esc(labels.colNextDue)}${esc(labels.colTimes)}${esc(labels.colCost)}
${esc(labels.none)}
${esc(labels.totalCost)}${totalCost.toFixed(2)} ${esc(currencySymbol)}
+ ${obj.notes ? `
${esc(labels.notes)}:\n${esc(obj.notes)}
` : ""} +`; +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/virtual-window.ts b/custom_components/maintenance_supporter/frontend-src/helpers/virtual-window.ts new file mode 100644 index 0000000..1cf39b7 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/virtual-window.ts @@ -0,0 +1,69 @@ +/** Windowing math for the virtualized dashboard task table (500+ tasks). + * + * Pure functions only — the panel measures the scroll container / row height + * and renders `rows.slice(start, end)` between two grid-spanning spacers whose + * heights keep the scrollbar honest. The window start/end snap to a `step` + * grid so a 1-row scroll doesn't rebind the whole slice on every frame. + * + * Why windowing at all: the table's cross-row column alignment comes from CSS + * subgrid, which is incompatible with `content-visibility`/`size` containment + * (the cheap skip-offscreen-paint trick used elsewhere). So for large installs + * the initial O(n) render is paid on every keystroke/filter change unless the + * DOM itself is windowed. + */ + +/** Rows below this count render normally — windowing has bookkeeping cost and + * only pays off when the O(n) render actually hurts. */ +export const VIRTUAL_MIN_ROWS = 120; + +export interface VirtualWindowInput { + /** Scroll offset of the scrolling container. */ + scrollTop: number; + /** Visible (client) height of the scrolling container. */ + viewportHeight: number; + /** Top of the table relative to the container's content (scrollTop space). */ + listTop: number; + /** Measured uniform row height in px (clamped to >= 1). */ + rowHeight: number; + /** Total row count. */ + total: number; + /** Extra rows rendered above/below the viewport. Default 12. */ + overscan?: number; + /** Start/end snap to this grid to avoid re-render churn. Default 6. */ + step?: number; +} + +export interface VirtualWindow { + start: number; + end: number; + padTop: number; + padBottom: number; +} + +export function computeWindow(i: VirtualWindowInput): VirtualWindow { + if (i.total <= 0) return { start: 0, end: 0, padTop: 0, padBottom: 0 }; + const overscan = i.overscan ?? 12; + const step = Math.max(1, i.step ?? 6); + const rh = Math.max(1, i.rowHeight); + + const firstVisible = Math.floor((i.scrollTop - i.listTop) / rh); + const visibleCount = Math.ceil(i.viewportHeight / rh) + 1; + + let start = Math.max(0, firstVisible - overscan); + start = Math.floor(start / step) * step; + let end = Math.min(i.total, Math.max(firstVisible, 0) + visibleCount + overscan); + end = Math.min(i.total, Math.ceil(end / step) * step); + + // Degenerate inputs (table far above/below viewport): keep a sane window. + if (start >= end) { + start = Math.min(start, Math.max(0, i.total - 1)); + end = Math.min(i.total, start + Math.max(visibleCount, 1)); + } + + return { + start, + end, + padTop: start * rh, + padBottom: (i.total - end) * rh, + }; +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/warranty.ts b/custom_components/maintenance_supporter/frontend-src/helpers/warranty.ts new file mode 100644 index 0000000..1df1017 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/warranty.ts @@ -0,0 +1,41 @@ +/** (#67) Warranty status computation for object asset tracking. + * + * Pure, side-effect-free, and `today`-injectable so it can be unit-tested + * deterministically without faking the system clock. + */ + +export type WarrantyStatusKind = "valid" | "expiring" | "expired" | "none"; + +export interface WarrantyStatus { + kind: WarrantyStatusKind; + /** Whole days until expiry (negative once expired); null when no date. */ + days: number | null; + /** The ISO date string echoed back, or null when absent/invalid. */ + date: string | null; +} + +/** Days before expiry at which the warranty is flagged as "expiring" (amber). */ +export const WARRANTY_WARN_DAYS = 60; + +/** + * Classify an object's warranty expiry date relative to `today`. + * + * @param iso ISO `YYYY-MM-DD` warranty expiry date (null/undefined/""/invalid → "none"). + * @param today Reference date; defaults to now. Injectable for deterministic tests. + */ +export function warrantyStatus( + iso: string | null | undefined, + today: Date = new Date(), +): WarrantyStatus { + if (!iso) return { kind: "none", days: null, date: null }; + const exp = new Date(`${iso}T00:00:00`); + if (isNaN(exp.getTime())) return { kind: "none", days: null, date: null }; + // Normalize both ends to local midnight so the difference is whole days + // regardless of the time-of-day component of `today`. + const t0 = Date.UTC(today.getFullYear(), today.getMonth(), today.getDate()); + const t1 = Date.UTC(exp.getFullYear(), exp.getMonth(), exp.getDate()); + const days = Math.round((t1 - t0) / 86400000); + if (days < 0) return { kind: "expired", days, date: iso }; + if (days <= WARRANTY_WARN_DAYS) return { kind: "expiring", days, date: iso }; + return { kind: "valid", days, date: iso }; +} diff --git a/custom_components/maintenance_supporter/frontend-src/helpers/worksheet.ts b/custom_components/maintenance_supporter/frontend-src/helpers/worksheet.ts new file mode 100644 index 0000000..bad14bb --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/helpers/worksheet.ts @@ -0,0 +1,157 @@ +/** Task work sheet (v2.21) — a printable one-pager for a single task. + * + * Everything needed to actually DO the task, on one sheet of paper: object + * + task details, the checklist as real tick boxes, the notes, and a QR + * pair (open the task / complete it) so the paper links back to the panel. + * When the task has a linked PDF manual with a page hint, the manual + * excerpt pages are rendered INLINE (downscaled, two per row) via the + * vendored pdf.js legacy build — the whole work sheet prints as one + * document. The PDF link stays as the fallback when rendering fails. + * + * Mirrors helpers/report.ts: pure function building a self-contained HTML + * document; the panel opens it in a new tab where the user prints or saves + * as PDF. All user strings are escaped; labels arrive translated. + */ + +import type { MaintenanceTask } from "../types"; + +export interface WorksheetLabels { + title: string; // "Work sheet" + object: string; + type: string; + interval: string; + nextDue: string; + lastDone: string; + priority: string; + checklist: string; + notes: string; + scanView: string; // "Scan to open the task" + scanComplete: string; // "Scan to complete" + manualExcerpt: string; // "Manual excerpt" + pages: string; // "pages" + printedOn: string; // "Printed" + never: string; + typeLabel: (t: string) => string; + statusLabel: (s: string) => string; +} + +export interface WorksheetExcerpt { + title: string; + startPage: number; + endPage: number; + url: string; // signed excerpt-endpoint URL (absolute) + /** Absolute base URL of the vendored pdf.js assets; when set, the sheet + * renders the excerpt pages inline (downscaled 2-up) via pdf.js. */ + vendorBase?: string; +} + +const esc = (v: unknown): string => + String(v ?? "").replace(/[&<>"']/g, (c) => + ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[c] as string); + +export function buildTaskWorksheetHtml( + task: MaintenanceTask, + objectName: string, + L: WorksheetLabels, + formatDate: (iso: string) => string, + formatRecurrence: (task: MaintenanceTask) => string, + qrViewDataUri: string | null, + qrCompleteDataUri: string | null, + excerpt: WorksheetExcerpt | null, + nowIso: string, +): string { + const meta: Array<[string, string]> = [ + [L.object, esc(objectName)], + [L.type, esc(L.typeLabel(task.type))], + [L.interval, esc(formatRecurrence(task))], + [L.nextDue, task.next_due ? esc(formatDate(task.next_due)) : "—"], + [L.lastDone, task.last_performed ? esc(formatDate(task.last_performed)) : esc(L.never)], + ]; + if (task.priority && task.priority !== "normal") { + meta.push([L.priority, esc(task.priority)]); + } + + const checklist = (task.checklist || []) + .map((item) => `
  • ${esc(item)}
  • `) + .join(""); + + const qr = (uri: string | null, caption: string) => + uri + ? `
    ${esc(caption)}
    ` + : ""; + + return ` +${esc(task.name)} — ${esc(L.title)} + + +
    +
    +

    ${esc(task.name)}

    +
    ${esc(objectName)}
    +
    +
    + ${qr(qrViewDataUri, L.scanView)} + ${qr(qrCompleteDataUri, L.scanComplete)} +
    +
    + + ${meta.map(([k, v]) => ``).join("")} +
    ${esc(k)}${v}
    + ${checklist ? `

    ${esc(L.checklist)}

      ${checklist}
    ` : ""} + ${task.notes ? `

    ${esc(L.notes)}

    ${esc(task.notes)}
    ` : ""} + ${excerpt ? `

    ${esc(L.manualExcerpt)}

    +
    +
    + ${excerpt.vendorBase ? `` : ""}` : ""} +
    ${esc(objectName)} · ${esc(task.name)} · ${esc(L.printedOn)} ${esc(nowIso.slice(0, 10))}
    +`; +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/cs.json b/custom_components/maintenance_supporter/frontend-src/locales/cs.json new file mode 100644 index 0000000..5225db4 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/cs.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Údržba", + "objects": "Objekty", + "tasks": "Úkoly", + "overdue": "Po termínu", + "due_soon": "Brzy", + "triggered": "Spuštěno", + "ok": "OK", + "all": "Vše", + "new_object": "+ Nový objekt", + "templates_from": "Ze šablony", + "templates_title": "Začít ze šablony", + "templates_task_count": "{n} úkolů", + "template_created": "Vytvořeno ze šablony", + "onboard_hint": "Přidejte první objekt a začněte sledovat údržbu.", + "edit": "Upravit", + "duplicate": "Duplikovat", + "task_duplicated": "Úkol duplikován", + "object_duplicated": "Objekt duplikován", + "delete": "Smazat", + "add_task": "+ Přidat úkol", + "complete": "Dokončit", + "completed": "Dokončeno", + "skip": "Přeskočit", + "skipped": "Přeskočeno", + "missed": "Missed", + "reset": "Reset", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Zrušit", + "bulk_select": "Vybrat", + "bulk_select_all": "Vybrat vše", + "bulk_n_selected": "{n} vybráno", + "bulk_completed": "Dokončeno {n} úkolů", + "bulk_archived": "Archivováno {n} úkolů", + "completing": "Dokončování…", + "interval": "Interval", + "warning": "Upozornění", + "last_performed": "Naposledy provedeno", + "next_due": "Další termín", + "days_until_due": "Dnů do termínu", + "avg_duration": "Prům. trvání", + "trigger": "Spouštěč", + "trigger_type": "Typ spouštěče", + "threshold_above": "Horní limit", + "threshold_below": "Dolní limit", + "threshold": "Práh", + "counter": "Čítač", + "state_change": "Změna stavu", + "runtime": "Doba běhu", + "runtime_hours": "Cílová doba běhu (hodiny)", + "target_value": "Cílová hodnota", + "baseline": "Základní hodnota", + "target_changes": "Cílový počet změn", + "for_minutes": "Po dobu (minut)", + "time_based": "Časový", + "sensor_based": "Založený na senzoru", + "manual": "Manuální", + "one_time": "Jednorázově", + "weekdays": "Dny v týdnu", + "nth_weekday": "N-tý den v týdnu v měsíci", + "day_of_month": "Den v měsíci", + "recurrence_on_days": "Opakovat v", + "recurrence_occurrence": "Výskyt", + "recurrence_weekday": "Den v týdnu", + "recurrence_day": "Den v měsíci (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Poslední", + "day_word": "Den", + "interval_value": "Interval", + "interval_unit": "Jednotka", + "unit_days": "Dny", + "unit_weeks": "Týdny", + "unit_months": "Měsíce", + "unit_years": "Roky", + "due_date": "Datum splnění", + "cleaning": "Čištění", + "inspection": "Inspekce", + "replacement": "Výměna", + "calibration": "Kalibrace", + "service": "Servis", + "reading": "Odečet", + "custom": "Vlastní", + "history": "Historie", + "cost": "Náklady", + "report_button": "Zpráva", + "report_title": "Zpráva o údržbě", + "report_generated": "Vytvořeno", + "report_times_done": "Provedeno", + "report_total_cost": "Celkové náklady", + "report_every": "každých {n} {unit}", + "report_notes": "Poznámky", + "report_col_type": "Typ", + "report_col_status": "Stav", + "report_col_schedule": "Plán", + "duration": "Doba trvání", + "both": "Obojí", + "trigger_val": "Hodnota spouštěče", + "complete_title": "Dokončit: ", + "checklist": "Kontrolní seznam", + "checklist_steps_optional": "Kroky kontrolního seznamu (volitelné)", + "checklist_placeholder": "Vyčistit filtr\nVyměnit těsnění\nOtestovat tlak", + "checklist_help": "Jeden krok na řádek. Max 100 položek.", + "err_too_long": "{field}: příliš dlouhé (max {n} znaků)", + "err_too_short": "{field}: příliš krátké (min {n} znaků)", + "err_value_too_high": "{field}: příliš velké (max {n})", + "err_value_too_low": "{field}: příliš malé (min {n})", + "err_required": "{field}: povinné", + "err_wrong_type": "{field}: špatný typ (očekáván: {type})", + "err_invalid_choice": "{field}: nepovolená hodnota", + "err_invalid_value": "{field}: neplatná hodnota", + "feat_schedule_time": "Plánování podle denní doby", + "feat_schedule_time_desc": "Úkoly se stanou po termínu v určenou denní dobu místo o půlnoci.", + "schedule_time_optional": "Termín v čase (volitelné, HH:MM)", + "schedule_time_help": "Prázdné = půlnoc (výchozí). Časové pásmo HA.", + "at_time": "v", + "notes_optional": "Poznámky (volitelné)", + "cost_optional": "Náklady (volitelné)", + "duration_minutes": "Doba trvání v minutách (volitelné)", + "days": "dní", + "day": "den", + "today": "Dnes", + "d_overdue": "d po termínu", + "no_tasks": "Zatím žádné úkoly údržby. Vytvořte objekt pro začátek.", + "no_tasks_short": "Žádné úkoly", + "no_history": "Zatím žádné záznamy historie.", + "show_all": "Zobrazit vše", + "cost_duration_chart": "Náklady a doba trvání", + "installed": "Nainstalováno", + "confirm_delete_object": "Smazat tento objekt a všechny jeho úkoly?", + "confirm_delete_task": "Smazat tento úkol?", + "min": "Min", + "max": "Max", + "save": "Uložit", + "saving": "Ukládání…", + "edit_task": "Upravit úkol", + "new_task": "Nový úkol údržby", + "task_name": "Název úkolu", + "maintenance_type": "Typ údržby", + "priority": "Priorita", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Nízká", + "priority_normal": "Normální", + "priority_high": "Vysoká", + "schedule_type": "Typ rozvrhu", + "interval_days": "Interval (dny)", + "warning_days": "Dny upozornění", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Naposledy provedeno (volitelné)", + "interval_anchor": "Ukotvení intervalu", + "anchor_completion": "Od data dokončení", + "anchor_planned": "Od plánovaného data (bez posunu)", + "edit_object": "Upravit objekt", + "name": "Název", + "manufacturer_optional": "Výrobce (volitelné)", + "model_optional": "Model (volitelné)", + "serial_number_optional": "Sériové číslo (volitelné)", + "serial_number_label": "S/N", + "documentation_url_label": "Návod", + "object_notes_label": "Poznámky", + "sort_due_date": "Termín", + "sort_object": "Název objektu", + "sort_type": "Typ", + "sort_task_name": "Název úkolu", + "all_objects": "Všechny objekty", + "tasks_lower": "úkolů", + "no_tasks_yet": "Zatím žádné úkoly", + "add_first_task": "Přidat první úkol", + "trigger_configuration": "Konfigurace spouštěče", + "entity_id": "ID entity", + "comma_separated": "oddělené čárkami", + "entity_logic": "Logika entit", + "entity_logic_any": "Spustí libovolná entita", + "entity_logic_all": "Všechny entity musí spustit", + "entities": "entity", + "attribute_optional": "Atribut (volitelný, prázdný = stav)", + "use_entity_state": "Použít stav entity (bez atributu)", + "trigger_above": "Spustit nad", + "trigger_below": "Spustit pod", + "for_at_least_minutes": "Po dobu alespoň (minut)", + "safety_interval_days": "Bezpečnostní interval (dny, volitelný)", + "safety_interval": "Bezpečnostní interval (volitelný)", + "delta_mode": "Režim delta", + "from_state_optional": "Ze stavu (volitelné)", + "to_state_optional": "Do stavu (volitelné)", + "documentation_url_optional": "URL dokumentace (volitelné)", + "object_notes_optional": "Poznámky (volitelné)", + "nfc_tag_id_optional": "ID NFC tagu (volitelné)", + "nfc_tags_empty_help": "V Home Assistant zatím nejsou registrovány žádné NFC tagy.", + "nfc_tags_open_settings": "Otevřít nastavení tagů", + "nfc_tags_refresh": "Obnovit", + "environmental_entity_optional": "Senzor prostředí (volitelný)", + "environmental_entity_helper": "např. sensor.outdoor_temperature — upravuje interval podle podmínek prostředí", + "environmental_attribute_optional": "Atribut prostředí (volitelný)", + "nfc_tag_id": "ID NFC tagu", + "nfc_linked": "NFC tag propojen", + "nfc_link_hint": "Klikněte pro propojení NFC tagu", + "responsible_user": "Zodpovědný uživatel", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Žádný uživatel přiřazen)", + "all_users": "Všichni uživatelé", + "my_tasks": "Moje úkoly", + "tab_calendar": "Kalendář", + "cal_no_events": "Bez údržby", + "cal_window_7": "7 dní", + "cal_window_14": "14 dní", + "cal_window_30": "30 dní", + "cal_window_365": "1 rok", + "cal_every_n_days": "každých {n} dní", + "cal_source_time": "Čas", + "cal_source_time_adaptive": "Čas (adaptivní)", + "cal_source_sensor": "Senzor", + "cal_predicted": "predikce", + "cal_confidence_high": "vysoká přesnost", + "cal_confidence_medium": "střední přesnost", + "cal_confidence_low": "nízká přesnost", + "budget_monthly": "Měsíční rozpočet", + "budget_yearly": "Roční rozpočet", + "groups": "Skupiny", + "new_group": "Nová skupina", + "edit_group": "Upravit skupinu", + "no_groups": "Zatím žádné skupiny", + "delete_group": "Smazat skupinu", + "delete_group_confirm": "Smazat skupinu '{name}'?", + "group_select_tasks": "Vybrat úkoly", + "group_name_required": "Název je povinný", + "description_optional": "Popis (volitelný)", + "selected": "Vybráno", + "loading_chart": "Načítání dat grafu...", + "hide_outliers": "Skrýt odlehlé hodnoty (chyby čidla)", + "was_maintenance_needed": "Byla tato údržba potřeba?", + "feedback_needed": "Potřebná", + "feedback_not_needed": "Nepotřebná", + "feedback_not_sure": "Nejsem si jistý", + "suggested_interval": "Navrhovaný interval", + "apply_suggestion": "Použít", + "reanalyze": "Znovu analyzovat", + "reanalyze_result": "Nová analýza", + "reanalyze_insufficient_data": "Nedostatek dat pro vytvoření doporučení", + "data_points": "datových bodů", + "dismiss_suggestion": "Zavřít", + "confidence_low": "Nízká", + "confidence_medium": "Střední", + "confidence_high": "Vysoká", + "recommended": "doporučeno", + "seasonal_awareness": "Sezónní povědomí", + "edit_seasonal_overrides": "Upravit sezónní faktory", + "seasonal_overrides_title": "Sezónní faktory (přepsání)", + "seasonal_overrides_hint": "Faktor na měsíc (0.1–5.0). Prázdné = naučeno automaticky.", + "seasonal_override_invalid": "Neplatná hodnota", + "seasonal_override_range": "Faktor musí být mezi 0.1 a 5.0", + "clear_all": "Vymazat vše", + "seasonal_chart_title": "Sezónní faktory", + "seasonal_learned": "Naučené", + "seasonal_manual": "Manuální", + "month_jan": "Led", + "month_feb": "Úno", + "month_mar": "Bře", + "month_apr": "Dub", + "month_may": "Kvě", + "month_jun": "Čer", + "month_jul": "Čvc", + "month_aug": "Srp", + "month_sep": "Zář", + "month_oct": "Říj", + "month_nov": "Lis", + "month_dec": "Pro", + "sensor_prediction": "Predikce senzoru", + "degradation_trend": "Trend", + "trend_rising": "Rostoucí", + "trend_falling": "Klesající", + "trend_stable": "Stabilní", + "trend_insufficient_data": "Nedostatek dat", + "days_until_threshold": "Dnů do prahu", + "threshold_exceeded": "Práh překročen", + "environmental_adjustment": "Faktor prostředí", + "sensor_prediction_urgency": "Senzor předpovídá práh za ~{days} dní", + "day_short": "d", + "weibull_reliability_curve": "Křivka spolehlivosti", + "weibull_failure_probability": "Pravděpodobnost selhání", + "weibull_r_squared": "Shoda R²", + "beta_early_failures": "Časná selhání", + "beta_random_failures": "Náhodná selhání", + "beta_wear_out": "Opotřebení", + "beta_highly_predictable": "Vysoce předvídatelné", + "confidence_interval": "Interval spolehlivosti", + "confidence_conservative": "Konzervativní", + "confidence_aggressive": "Optimistický", + "current_interval_marker": "Aktuální interval", + "recommended_marker": "Doporučený", + "characteristic_life": "Charakteristická životnost", + "chart_mini_sparkline": "Graf trendu", + "chart_history": "Historie nákladů a doby trvání", + "chart_seasonal": "Sezónní faktory, 12 měsíců", + "chart_weibull": "Weibullova křivka spolehlivosti", + "chart_sparkline": "Graf hodnoty spouštěče senzoru", + "days_progress": "Postup dnů", + "qr_code": "QR kód", + "qr_generating": "Generování QR kódu…", + "qr_error": "Nepodařilo se vygenerovat QR kód.", + "qr_error_no_url": "Není nakonfigurováno URL HA. Nastavte externí nebo interní URL v Nastavení → Systém → Síť.", + "save_error": "Nepodařilo se uložit. Zkuste to znovu.", + "qr_print": "Tisk", + "qr_download": "Stáhnout SVG", + "qr_action": "Akce při skenování", + "qr_action_view": "Zobrazit informace o údržbě", + "qr_action_complete": "Označit údržbu jako dokončenou", + "qr_url_mode": "Typ odkazu", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokální (mDNS)", + "qr_mode_server": "URL serveru", + "overview": "Přehled", + "analysis": "Analýza", + "recent_activities": "Nedávné aktivity", + "search_notes": "Hledat v poznámkách", + "avg_cost": "Prům. náklady", + "no_advanced_features": "Žádné pokročilé funkce nejsou povoleny", + "no_advanced_features_hint": "Povolte „Adaptivní intervaly” nebo „Sezónní vzory” v nastavení integrace pro zobrazení analytických dat.", + "analysis_not_enough_data": "Zatím nedostatek dat pro analýzu.", + "analysis_not_enough_data_hint": "Weibullova analýza vyžaduje alespoň 5 dokončených údržeb; sezónní vzory se stanou viditelné po 6+ datových bodech na měsíc.", + "analysis_manual_task_hint": "Manuální úkoly bez intervalu negenerují analytická data.", + "completions": "dokončení", + "current": "Aktuální", + "shorter": "Kratší", + "longer": "Delší", + "normal": "Normální", + "disabled": "Zakázáno", + "compound_logic": "Složená logika", + "compound": "Složené (více podmínek)", + "compound_logic_and": "A — musí platit všechny podmínky", + "compound_logic_or": "NEBO — stačí libovolná podmínka", + "compound_help": "Zkombinujte několik podmínek senzorů do jednoho spouštěče.", + "compound_no_conditions": "Zatím žádné podmínky — přidejte alespoň jednu.", + "compound_add_condition": "Přidat podmínku", + "compound_condition": "Podmínka", + "compound_remove_condition": "Odebrat podmínku", + "card_title": "Název", + "card_show_header": "Zobrazit záhlaví se statistikami", + "card_show_actions": "Zobrazit tlačítka akcí", + "card_compact": "Kompaktní režim", + "card_max_items": "Max položek (0 = vše)", + "card_filter_status": "Filtrovat podle stavu", + "card_filter_status_help": "Prázdné = zobrazit všechny stavy.", + "card_filter_objects": "Filtrovat podle objektů", + "card_filter_objects_help": "Prázdné = zobrazit všechny objekty.", + "card_filter_entities": "Filtrovat podle entit (entity_ids)", + "card_filter_entities_help": "Vyberte entity sensor / binary_sensor z této integrace. Prázdné = všechny.", + "card_loading_objects": "Načítání objektů…", + "card_load_error": "Nepodařilo se načíst objekty — zkontrolujte WebSocket spojení.", + "card_no_tasks_title": "Zatím žádné úkoly údržby", + "card_no_tasks_cta": "→ Vytvořte v panelu Maintenance", + "no_objects": "Zatím žádné objekty.", + "action_error": "Akce se nezdařila. Zkuste to znovu.", + "area_id_optional": "Oblast (volitelná)", + "installation_date_optional": "Datum instalace (volitelné)", + "warranty_expiry_optional": "Konec záruky (volitelné)", + "warranty": "Záruka", + "warranty_valid_until": "platná do {date}", + "warranty_expires_in": "vyprší za {days} dní", + "warranty_expired": "vypršela", + "cal_past_windows": "Minulá okna", + "cal_forward_windows": "Budoucí okna", + "history_edit_title": "Upravit záznam historie", + "history_edit_timestamp": "Časové razítko", + "manufacturer": "Výrobce", + "model": "Model", + "area": "Oblast", + "actions": "Akce", + "view_mode_label": "Zobrazení", + "view_cards": "Zobrazení karet", + "view_table": "Zobrazení tabulky", + "objects_table_columns_label": "Sloupce tabulky objektů", + "objects_table_columns_hint": "Vyberte, které sloupce se zobrazí v tabulkovém zobrazení objektů.", + "custom_icon_optional": "Ikona (volitelná, např. mdi:wrench)", + "task_enabled": "Úkol povolen", + "skip_reason_prompt": "Přeskočit tento úkol?", + "reason_optional": "Důvod (volitelný)", + "reset_date_prompt": "Označit úkol jako provedený?", + "reset_date_optional": "Datum posledního provedení (volitelné, výchozí dnes)", + "notes_label": "Poznámky", + "documentation_label": "Dokumentace", + "no_nfc_tag": "— Žádný tag —", + "dashboard": "Přehled", + "tab_today": "Dnes", + "palette_placeholder": "Hledat objekty a úkoly…", + "palette_no_results": "Žádné výsledky", + "palette_hint": "↑↓ pohyb · Enter otevřít · Esc zavřít", + "today_all_caught_up": "Vše hotovo! Tento týden nic nezbývá.", + "today_overdue": "Po termínu", + "today_due_today": "Dnes", + "today_this_week": "Tento týden", + "settings": "Nastavení", + "settings_features": "Pokročilé funkce", + "settings_features_desc": "Povolte nebo zakažte pokročilé funkce. Zakázání je skryje z UI, ale nesmaže data.", + "feat_adaptive": "Adaptivní plánování", + "feat_adaptive_desc": "Učte se optimální intervaly z historie údržby", + "feat_predictions": "Predikce senzorů", + "feat_predictions_desc": "Předpovídejte termíny spouštění z degradace senzoru", + "feat_seasonal": "Sezónní úpravy", + "feat_seasonal_desc": "Upravte intervaly podle sezónních vzorů", + "feat_environmental": "Korelace s prostředím", + "feat_environmental_desc": "Korelujte intervaly s teplotou/vlhkostí", + "feat_budget": "Sledování rozpočtu", + "feat_budget_desc": "Sledujte měsíční a roční výdaje na údržbu", + "feat_groups": "Skupiny úkolů", + "feat_groups_desc": "Organizujte úkoly do logických skupin", + "feat_checklists": "Kontrolní seznamy", + "feat_checklists_desc": "Vícestupňové procedury pro dokončení úkolu", + "settings_general": "Obecné", + "settings_default_warning": "Výchozí dny upozornění", + "settings_panel_enabled": "Boční panel", + "settings_panel_title": "Název panelu", + "settings_notifications": "Oznámení", + "settings_notify_service": "Služba oznámení", + "test_notification": "Testovací oznámení", + "send_test": "Odeslat test", + "testing": "Odesílání…", + "test_notification_success": "Testovací oznámení odesláno", + "test_notification_failed": "Testovací oznámení se nezdařilo", + "settings_notify_due_soon": "Oznámit když brzy", + "settings_notify_overdue": "Oznámit když po termínu", + "settings_notify_triggered": "Oznámit když spuštěno", + "settings_interval_hours": "Interval opakování (hodiny, 0 = jednou)", + "settings_quiet_hours": "Tiché hodiny", + "settings_quiet_start": "Začátek", + "settings_quiet_end": "Konec", + "settings_max_per_day": "Max oznámení denně (0 = bez limitu)", + "settings_bundling": "Seskupit oznámení", + "settings_bundle_threshold": "Práh seskupení", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobilní akční tlačítka", + "settings_action_complete": "Zobrazit tlačítko 'Dokončit'", + "settings_action_skip": "Zobrazit tlačítko 'Přeskočit'", + "settings_action_snooze": "Zobrazit tlačítko 'Odložit'", + "settings_weekly_digest": "Týdenní přehled", + "settings_weekly_digest_hint": "Jedno souhrnné oznámení v pondělí ráno, když jsou úkoly.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Doba odložení (hodiny)", + "settings_budget": "Rozpočet", + "settings_currency": "Měna", + "settings_budget_monthly": "Měsíční rozpočet", + "settings_budget_yearly": "Roční rozpočet", + "settings_budget_alerts": "Rozpočtová upozornění", + "settings_budget_threshold": "Práh upozornění (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Exportovat JSON", + "settings_export_yaml": "Exportovat YAML", + "settings_export_csv": "Exportovat CSV", + "settings_import_csv": "Importovat CSV", + "settings_import_placeholder": "Vložte sem obsah JSON nebo CSV…", + "settings_import_btn": "Importovat", + "settings_import_success": "{count} objektů úspěšně importováno.", + "settings_export_success": "Export stažen.", + "settings_saved": "Nastavení uloženo.", + "settings_include_history": "Zahrnout historii", + "sort_alphabetical": "Abecedně", + "sort_due_soonest": "Nejbližší termín", + "sort_task_count": "Počet úkolů", + "sort_area": "Oblast", + "sort_assigned_user": "Přiřazený uživatel", + "sort_group": "Skupina", + "groupby_none": "Bez seskupení", + "groupby_area": "Podle oblasti", + "groupby_group": "Podle skupiny", + "groupby_user": "Podle uživatele", + "filter_label": "Filtr", + "user_label": "Uživatel", + "sort_label": "Řazení", + "group_by_label": "Seskupit podle", + "state_value_help": "Použijte hodnotu stavu HA (obvykle malými písmeny, např. \"on\"/\"off\"). Velikost písmen se při uložení normalizuje.", + "target_changes_help": "Počet odpovídajících přechodů, po kterých se trigger spustí (výchozí: 1).", + "qr_print_title": "Tisk QR kódů", + "qr_print_desc": "Vygeneruj tiskovou stránku s QR kódy k vystřižení a nalepení na zařízení.", + "qr_print_load": "Načíst objekty", + "qr_print_filter": "Filtr", + "qr_print_objects": "Objekty", + "qr_print_actions": "Akce", + "qr_print_url_mode": "Typ odkazu", + "qr_print_estimate": "Odhad QR kódů", + "qr_print_over_limit": "limit 200, zužte filtr", + "qr_print_generate": "Vygenerovat QR kódy", + "qr_print_generating": "Generování…", + "qr_print_ready": "QR kódy připraveny", + "qr_print_print_button": "Tisk", + "qr_print_empty": "Nic k vygenerování", + "qr_action_skip": "Přeskočit", + "vacation_title": "Režim dovolené", + "vacation_active": "aktivní", + "vacation_ended": "ukončeno", + "vacation_desc": "Naplánuj dovolenou: oznámení jsou pozastavena během období plus dny rezervy. Lze definovat výjimky pro jednotlivé úkoly.", + "vacation_enable": "Zapnout režim dovolené", + "vacation_start": "Začátek", + "vacation_end": "Konec", + "vacation_buffer": "Rezerva (dnů)", + "vacation_exempt_title": "Upozorňovat i přes dovolenou", + "vacation_exempt_desc": "Vyber úkoly, které mají upozorňovat i během dovolené (např. kritická chemie bazénu).", + "vacation_load_tasks": "Načíst úkoly", + "vacation_preview_btn": "Zobrazit náhled", + "vacation_preview_affected": "úkolů ovlivněno", + "vacation_event_due_soon": "blíží se termín", + "vacation_event_overdue": "stane se po termínu", + "vacation_event_triggered_est": "možné spuštění senzoru", + "vacation_sensor_based": "(senzorové)", + "vacation_action_notify": "Přesto upozornit", + "vacation_action_unsilence": "Znovu ztlumit", + "vacation_marked_complete": "Označeno jako dokončené", + "vacation_marked_skip": "Přeskočeno", + "vacation_end_now": "Ukončit dovolenou nyní", + "unassigned": "Nepřiřazeno", + "no_area": "Bez oblasti", + "has_overdue": "Má úkoly po termínu", + "object": "Objekt", + "settings_panel_access": "Přístup k panelu", + "settings_panel_access_desc": "Administrátoři mají vždy plný přístup. Chcete-li delegovat vytváření, úpravy a mazání konkrétním uživatelům bez práv administrátora, zapněte tuto možnost a vyberte je níže — ostatní vidí pouze Dokončit a Přeskočit.", + "settings_operator_write": "Povolit vybraným uživatelům vytvářet, upravovat a mazat", + "settings_operator_write_desc": "Vypnuto: obsah mohou měnit pouze administrátoři. Zapnuto: plný přístup získají i vybraní uživatelé níže.", + "no_non_admin_users": "Nenalezeni žádní uživatelé bez admin práv. Přidejte je v Nastavení → Lidé.", + "owner_label": "Vlastník", + "feat_completion_actions": "Akce při dokončení", + "feat_completion_actions_desc": "Akce HA na úlohu při dokončení + QR rychlého dokončení s předvolenými hodnotami.", + "on_complete_action_title": "Při dokončení: spustit akci HA (volitelné)", + "on_complete_action_desc": "Volá službu HA, když je úloha dokončena — např. resetuje čítač na zařízení.", + "on_complete_action_service": "Služba", + "on_complete_action_target": "Cílová entita", + "on_complete_action_data": "Data (JSON, volitelné)", + "on_complete_action_test": "Testovat akci", + "on_complete_action_test_success": "Úspěch", + "on_complete_action_test_failed": "Selhalo", + "quick_complete_defaults_title": "Výchozí hodnoty rychlého dokončení (pro QR skenů, volitelné)", + "quick_complete_defaults_desc": "Předvolené hodnoty pro QR rychlého dokončení. Bez nich QR otevře dialog.", + "quick_complete_defaults_notes": "Poznámky", + "quick_complete_defaults_cost": "Cena", + "quick_complete_defaults_duration": "Trvání (minuty)", + "quick_complete_defaults_feedback_none": "Bez zpětné vazby", + "quick_complete_defaults_feedback_needed": "Bylo potřeba", + "quick_complete_defaults_feedback_not_needed": "Nebylo potřeba", + "quick_complete_success": "Rychle označeno jako hotové", + "trigger_replaced": "Spouštěč nahrazen", + "add": "Přidat", + "show_stats": "Zobrazit statistiky + grafy", + "hide_stats": "Skrýt statistiky", + "adaptive_no_data": "Zatím není dostatek historie dokončení pro adaptivní analýzu. Dokončete tento úkol ještě několikrát, abyste odemkli doporučení intervalu a grafy spolehlivosti.", + "suggestion_applied": "Navržený interval použit", + "vacation_mode": "Režim dovolené", + "vacation_status_active": "Nyní aktivní", + "vacation_status_scheduled": "Naplánováno", + "vacation_status_inactive": "Neaktivní", + "vacation_end_now_confirm": "Ukončit dovolenou okamžitě?", + "vacation_exempt_count": "vyňato", + "vacation_advanced": "Pokročilé…", + "vacation_open_panel": "Otevřít v panelu", + "enable": "Zapnout", + "saved": "Uloženo", + "budget_monthly_set": "Nastavit měsíční", + "budget_yearly_set": "Nastavit roční", + "budget_advanced": "Měna, výstrahy…", + "budget_open_panel": "Otevřít v panelu", + "groups_empty": "Zatím žádné skupiny.", + "group_new_placeholder": "Přidat skupinu…", + "group_delete_confirm": "Smazat skupinu „{name}\"?", + "groups_manage_tasks": "Spravovat přiřazení úkolů…", + "groups_open_panel": "Otevřít v panelu", + "on_complete_action_target_hint": "Poznámka: doména entity musí odpovídat službě — např. 'button.press' funguje jen na button.*, 'counter.increment' jen na counter.*, 'input_button.press' jen na input_button.* atd. Při neshodě akce tiše selže (HA zaznamená 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Zobrazit všechny objekty", + "show_all_tasks": "Vymazat filtr — zobrazit všechny úkoly", + "filter_to_overdue": "Filtrovat seznam jen na po termínu", + "filter_to_due_soon": "Filtrovat seznam jen na blížící se termín", + "filter_to_triggered": "Filtrovat seznam jen na spuštěné", + "open_task": "Otevřít úkol", + "show_details": "Zobrazit historii + statistiky", + "hide_details": "Skrýt podrobnosti", + "history_empty": "Zatím žádná historie.", + "history_edit_button": "Upravit záznam", + "total_cost": "Celkové náklady", + "times_performed": "Provedeno", + "older_entries": "starší", + "open_in_panel": "Otevřít v panelu Údržba", + "skip_reason": "Důvod přeskočení (volitelné)", + "reset_to_date": "Resetovat poslední provedení na", + "delete_task_confirm": "Smazat tento úkol a jeho historii?", + "delete_object_confirm": "Smazat tento objekt a všechny jeho úkoly?", + "loading": "Načítání…", + "archive": "Archivovat", + "undo": "Zpět", + "task_archived": "Úkol archivován", + "object_archived": "Objekt archivován", + "unarchive": "Obnovit z archivu", + "archived": "Archivováno", + "show_archived": "Zobrazit archivované", + "hide_archived": "Skrýt archivované", + "confirm_archive_object": "Archivovat tento objekt a jeho úkoly? Historie zůstane zachována a lze ji později obnovit.", + "settings_archive": "Archiv a uchovávání", + "settings_archive_desc": "Odkládejte dokončené jednorázové úkoly bez mazání. Archivované položky jsou skryté a neaktivní, ale zachovávají historii a náklady.", + "settings_archive_oneoff_days": "Automaticky archivovat dokončené jednorázové úkoly po (dny, 0 = vypnuto)", + "settings_delete_archived_oneoff_days": "Automaticky mazat archivované jednorázové úkoly po (dny, 0 = nikdy)", + "archive_object": "Archivovat objekt", + "unarchive_object": "Obnovit objekt z archivu", + "documents": "Dokumenty", + "documents_empty": "Zatím žádné dokumenty.", + "doc_upload": "Nahrát soubor", + "doc_uploading": "Nahrávání…", + "doc_add_link": "Přidat odkaz", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Název (nepovinné)", + "doc_open": "Otevřít", + "doc_delete_confirm": "Smazat „{name}“?", + "doc_too_large": "Soubor je příliš velký (max. 25 MB).", + "doc_upload_failed": "Nahrání selhalo.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Již uloženo jinde — sdíleno, bez dalšího místa.", + "doc_dup_in_object": "Tento soubor je již připojen k tomuto objektu.", + "doc_link_invalid": "Povoleny jsou pouze odkazy http/https.", + "doc_cat_manual": "Návod", + "doc_cat_warranty": "Záruka", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Náhradní díly", + "doc_cat_photo": "Fotografie", + "doc_cat_other": "Ostatní", + "doc_link_badge": "Odkaz", + "doc_storage_title": "Úložiště dokumentů", + "doc_storage_saved": "Ušetřeno deduplikací", + "doc_storage_refresh": "Obnovit", + "doc_download": "Stáhnout", + "doc_close": "Zavřít", + "doc_camera": "Vyfotit", + "doc_drop_hint": "Přetáhněte soubory sem", + "doc_task_none": "Žádné dokumenty spojené s tímto úkolem.", + "doc_link_existing": "Připojit dokument…", + "doc_attach": "Připojit", + "doc_unlink": "Odpojit", + "doc_page": "Strana", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1r", + "chart_since_service": "od poslední údržby", + "chart_no_stats": "Pro tuto entitu nejsou dlouhodobé statistiky — zobrazují se pouze hodnoty z údržbových záznamů", + "auto_complete_on_recovery": "Automaticky dokončit při zotavení senzoru", + "auto_complete_on_recovery_help": "Zaznamená dokončení (nastaví poslední provedení), jakmile se spouštěč sám vyřeší — např. doplněná sůl, vyměněný filtr.", + "doc_search": "Hledat dokumenty…", + "doc_search_none": "Žádné odpovídající dokumenty", + "link_device_optional": "Propojit s existujícím zařízením (volitelné)", + "parent_object_optional": "Nadřazený objekt (volitelné)", + "parent_none": "(Žádný nadřazený)", + "paused": "Pozastaveno", + "pause_object": "Pozastavit", + "resume_object": "Pokračovat", + "pause_until_prompt": "Zmrazit plány tohoto objektu — nic není splatné a nic neupozorňuje, dokud nebude obnoven. Volitelně nastavte datum automatického obnovení.", + "pause_until_label": "Pokračovat dne (volitelné)", + "object_paused": "Objekt pozastaven", + "object_resumed": "Objekt obnoven — plány restartovány", + "object_paused_badge": "Pozastaveno", + "paused_until_label": "do", + "replace_object": "Nahradit…", + "replace_object_prompt": "Vyřadit tento objekt a vytvořit nástupce. Historie a náklady zůstávají archivované u starého; úkoly a dokumenty přecházejí na nový, počítadla začínají od nuly.", + "replace_name_label": "Název nástupce", + "object_replaced": "Objekt nahrazen — nástupce vytvořen", + "reading_unit_label": "Jednotka odečtu (např. kWh, m³)", + "reading_unit_help": "Zobrazí se vedle zaznamenané hodnoty při dokončení tohoto úkolu.", + "reading_value_label": "Hodnota odečtu", + "reading_label": "Odečet", + "settings_templates_label": "Galerie šablon", + "settings_templates_hint": "Odškrtněte šablony, které nikdy nevyužijete — zmizí z výběru „Ze šablony\" (panel i config flow). Nic jiného se nemění; kdykoli je lze znovu zapnout.", + "worksheet": "Pracovní list", + "worksheet_scan_view": "Naskenujte pro otevření úkolu", + "worksheet_scan_complete": "Naskenujte pro dokončení", + "worksheet_manual_excerpt": "Výňatek z manuálu", + "worksheet_pages": "strany", + "worksheet_printed": "Vytištěno", + "worksheet_never": "Nikdy" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/da.json b/custom_components/maintenance_supporter/frontend-src/locales/da.json new file mode 100644 index 0000000..648bfc0 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/da.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Vedligeholdelse", + "objects": "Objekter", + "tasks": "Opgaver", + "overdue": "Forfalden", + "due_soon": "Snart forfalden", + "triggered": "Udløst", + "trigger_replaced": "Udløser erstattet", + "ok": "OK", + "all": "Alle", + "new_object": "+ Nyt objekt", + "templates_from": "Fra skabelon", + "templates_title": "Start fra en skabelon", + "templates_task_count": "{n} opgaver", + "template_created": "Oprettet fra skabelon", + "onboard_hint": "Tilføj dit første objekt for at spore vedligeholdelse.", + "edit": "Rediger", + "duplicate": "Dupliker", + "task_duplicated": "Opgave duplikeret", + "object_duplicated": "Objekt duplikeret", + "delete": "Slet", + "add_task": "+ Tilføj opgave", + "complete": "Fuldfør", + "completed": "Fuldført", + "skip": "Spring over", + "skipped": "Sprunget over", + "missed": "Missed", + "reset": "Nulstil", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Annuller", + "bulk_select": "Vælg", + "bulk_select_all": "Vælg alle", + "bulk_n_selected": "{n} valgt", + "bulk_completed": "{n} opgaver fuldført", + "bulk_archived": "{n} opgaver arkiveret", + "completing": "Fuldfører…", + "interval": "Interval", + "warning": "Advarsel", + "last_performed": "Senest udført", + "next_due": "Næste forfald", + "days_until_due": "Dage til forfald", + "avg_duration": "Gns. varighed", + "trigger": "Udløser", + "trigger_type": "Udløsertype", + "threshold_above": "Øvre grænse", + "threshold_below": "Nedre grænse", + "threshold": "Tærskel", + "counter": "Tæller", + "state_change": "Tilstandsændring", + "runtime": "Driftstid", + "runtime_hours": "Måldriftstid (timer)", + "target_value": "Målværdi", + "baseline": "Referenceværdi", + "target_changes": "Antal målændringer", + "for_minutes": "I (minutter)", + "time_based": "Tidsbaseret", + "sensor_based": "Sensorbaseret", + "manual": "Manuel", + "one_time": "Engangs", + "weekdays": "Ugedage", + "nth_weekday": "N'te ugedag i måneden", + "day_of_month": "Dag i måneden", + "recurrence_on_days": "Gentag på", + "recurrence_occurrence": "Forekomst", + "recurrence_weekday": "Ugedag", + "recurrence_day": "Dag i måneden (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Sidste", + "day_word": "Dag", + "interval_value": "Interval", + "interval_unit": "Enhed", + "unit_days": "Dage", + "unit_weeks": "Uger", + "unit_months": "Måneder", + "unit_years": "År", + "due_date": "Forfaldsdato", + "cleaning": "Rengøring", + "inspection": "Inspektion", + "replacement": "Udskiftning", + "calibration": "Kalibrering", + "service": "Service", + "reading": "Aflæsning", + "custom": "Tilpasset", + "history": "Historik", + "cost": "Omkostning", + "report_button": "Rapport", + "report_title": "Vedligeholdelsesrapport", + "report_generated": "Genereret", + "report_times_done": "Udført", + "report_total_cost": "Samlede omkostninger", + "report_every": "hver {n}. {unit}", + "report_notes": "Noter", + "report_col_type": "Type", + "report_col_status": "Status", + "report_col_schedule": "Tidsplan", + "duration": "Varighed", + "both": "Begge", + "trigger_val": "Udløserværdi", + "complete_title": "Fuldfør: ", + "checklist": "Tjekliste", + "checklist_steps_optional": "Tjeklistetrin (valgfrit)", + "checklist_placeholder": "Rengør filter\nUdskift pakning\nTest tryk", + "checklist_help": "Ét trin pr. linje. Maks. 100 elementer.", + "err_too_long": "{field}: for lang (maks. {n} tegn)", + "err_too_short": "{field}: for kort (min. {n} tegn)", + "err_value_too_high": "{field}: for stor (maks. {n})", + "err_value_too_low": "{field}: for lille (min. {n})", + "err_required": "{field}: påkrævet", + "err_wrong_type": "{field}: forkert type (forventet: {type})", + "err_invalid_choice": "{field}: ikke en tilladt værdi", + "err_invalid_value": "{field}: ugyldig værdi", + "feat_schedule_time": "Planlægning efter tidspunkt på dagen", + "feat_schedule_time_desc": "Opgaver bliver forfaldne på et bestemt tidspunkt på dagen i stedet for midnat.", + "schedule_time_optional": "Forfalder kl. (valgfrit, TT:MM)", + "schedule_time_help": "Tom = midnat (standard). HA-tidszone.", + "at_time": "kl.", + "notes_optional": "Noter (valgfrit)", + "cost_optional": "Omkostning (valgfrit)", + "duration_minutes": "Varighed i minutter (valgfrit)", + "days": "dage", + "day": "dag", + "today": "I dag", + "d_overdue": "d forsinket", + "no_tasks": "Ingen vedligeholdelsesopgaver endnu. Opret et objekt for at komme i gang.", + "no_tasks_short": "Ingen opgaver", + "no_history": "Ingen historikposter endnu.", + "show_all": "Vis alle", + "cost_duration_chart": "Omkostning og varighed", + "installed": "Installeret", + "confirm_delete_object": "Slet dette objekt og alle dets opgaver?", + "confirm_delete_task": "Slet denne opgave?", + "min": "Min", + "max": "Maks", + "save": "Gem", + "saving": "Gemmer…", + "edit_task": "Rediger opgave", + "new_task": "Ny vedligeholdelsesopgave", + "task_name": "Opgavenavn", + "maintenance_type": "Vedligeholdelsestype", + "priority": "Prioritet", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Lav", + "priority_normal": "Normal", + "priority_high": "Høj", + "schedule_type": "Planlægningstype", + "interval_days": "Interval (dage)", + "warning_days": "Advarselsdage", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Senest udført (valgfrit)", + "interval_anchor": "Intervalanker", + "anchor_completion": "Fra fuldførelsesdato", + "anchor_planned": "Fra planlagt dato (ingen drift)", + "edit_object": "Rediger objekt", + "name": "Navn", + "manufacturer_optional": "Producent (valgfrit)", + "model_optional": "Model (valgfrit)", + "serial_number_optional": "Serienummer (valgfrit)", + "serial_number_label": "S/N", + "documentation_url_label": "Manual", + "object_notes_label": "Noter", + "sort_due_date": "Forfaldsdato", + "sort_object": "Objektnavn", + "sort_type": "Type", + "sort_task_name": "Opgavenavn", + "all_objects": "Alle objekter", + "tasks_lower": "opgaver", + "no_tasks_yet": "Ingen opgaver endnu", + "add_first_task": "Tilføj første opgave", + "trigger_configuration": "Udløserkonfiguration", + "entity_id": "Enheds-ID", + "comma_separated": "kommasepareret", + "entity_logic": "Enhedslogik", + "entity_logic_any": "Enhver enhed udløser", + "entity_logic_all": "Alle enheder skal udløse", + "entities": "enheder", + "attribute_optional": "Attribut (valgfrit, tom = tilstand)", + "use_entity_state": "Brug enhedstilstand (ingen attribut)", + "trigger_above": "Udløs over", + "trigger_below": "Udløs under", + "for_at_least_minutes": "I mindst (minutter)", + "safety_interval_days": "Sikkerhedsinterval (dage, valgfrit)", + "safety_interval": "Sikkerhedsinterval (valgfrit)", + "delta_mode": "Delta-tilstand", + "from_state_optional": "Fra tilstand (valgfrit)", + "to_state_optional": "Til tilstand (valgfrit)", + "documentation_url_optional": "Dokumentations-URL (valgfrit)", + "object_notes_optional": "Noter (valgfrit)", + "nfc_tag_id_optional": "NFC-tag-ID (valgfrit)", + "nfc_tags_empty_help": "Ingen NFC-tags registreret i Home Assistant endnu.", + "nfc_tags_open_settings": "Åbn tag-indstillinger", + "nfc_tags_refresh": "Opdater", + "environmental_entity_optional": "Miljøsensor (valgfrit)", + "environmental_entity_helper": "f.eks. sensor.outdoor_temperature — justerer intervallet baseret på miljøforhold", + "environmental_attribute_optional": "Miljøattribut (valgfrit)", + "nfc_tag_id": "NFC-tag-ID", + "nfc_linked": "NFC-tag tilknyttet", + "nfc_link_hint": "Klik for at tilknytte NFC-tag", + "responsible_user": "Ansvarlig bruger", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ingen bruger tildelt)", + "all_users": "Alle brugere", + "my_tasks": "Mine opgaver", + "tab_calendar": "Kalender", + "cal_no_events": "Ingen vedligeholdelse", + "cal_window_7": "7 dage", + "cal_window_14": "14 dage", + "cal_window_30": "30 dage", + "cal_window_365": "1 år", + "cal_every_n_days": "hver {n}. dag", + "cal_source_time": "Tidsbaseret", + "cal_source_time_adaptive": "Tidsbaseret (adaptiv)", + "cal_source_sensor": "Sensorbaseret", + "cal_predicted": "forudsagt", + "cal_confidence_high": "høj sikkerhed", + "cal_confidence_medium": "mellem sikkerhed", + "cal_confidence_low": "lav sikkerhed", + "budget_monthly": "Månedligt budget", + "budget_yearly": "Årligt budget", + "groups": "Grupper", + "new_group": "Ny gruppe", + "edit_group": "Rediger gruppe", + "no_groups": "Ingen grupper endnu", + "delete_group": "Slet gruppe", + "delete_group_confirm": "Slet gruppen '{name}'?", + "group_select_tasks": "Vælg opgaver", + "group_name_required": "Navn er påkrævet", + "description_optional": "Beskrivelse (valgfrit)", + "selected": "Valgte", + "loading_chart": "Indlæser diagramdata...", + "hide_outliers": "Skjul outliers (sensorfejl)", + "was_maintenance_needed": "Var denne vedligeholdelse nødvendig?", + "feedback_needed": "Nødvendig", + "feedback_not_needed": "Ikke nødvendig", + "feedback_not_sure": "Usikker", + "suggested_interval": "Foreslået interval", + "apply_suggestion": "Anvend", + "reanalyze": "Analyser igen", + "reanalyze_result": "Ny analyse", + "reanalyze_insufficient_data": "Ikke nok data til at give en anbefaling", + "data_points": "datapunkter", + "dismiss_suggestion": "Afvis", + "confidence_low": "Lav", + "confidence_medium": "Mellem", + "confidence_high": "Høj", + "recommended": "anbefalet", + "seasonal_awareness": "Sæsonbevidsthed", + "edit_seasonal_overrides": "Rediger sæsonfaktorer", + "seasonal_overrides_title": "Sæsonfaktorer (tilsidesæt)", + "seasonal_overrides_hint": "Faktor pr. måned (0.1–5.0). Tom = lært automatisk.", + "seasonal_override_invalid": "Ugyldig værdi", + "seasonal_override_range": "Faktor skal være mellem 0.1 og 5.0", + "clear_all": "Ryd alle", + "seasonal_chart_title": "Sæsonfaktorer", + "seasonal_learned": "Lært", + "seasonal_manual": "Manuel", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "Maj", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Dec", + "sensor_prediction": "Sensorforudsigelse", + "degradation_trend": "Tendens", + "trend_rising": "Stigende", + "trend_falling": "Faldende", + "trend_stable": "Stabil", + "trend_insufficient_data": "Utilstrækkelige data", + "days_until_threshold": "Dage til tærskel", + "threshold_exceeded": "Tærskel overskredet", + "environmental_adjustment": "Miljøfaktor", + "sensor_prediction_urgency": "Sensor forudsiger tærskel om ~{days} dage", + "day_short": "d", + "weibull_reliability_curve": "Pålidelighedskurve", + "weibull_failure_probability": "Fejlsandsynlighed", + "weibull_r_squared": "Tilpasning R²", + "beta_early_failures": "Tidlige fejl", + "beta_random_failures": "Tilfældige fejl", + "beta_wear_out": "Slitage", + "beta_highly_predictable": "Meget forudsigelig", + "confidence_interval": "Konfidensinterval", + "confidence_conservative": "Konservativ", + "confidence_aggressive": "Optimistisk", + "current_interval_marker": "Nuværende interval", + "recommended_marker": "Anbefalet", + "characteristic_life": "Karakteristisk levetid", + "chart_mini_sparkline": "Tendensdiagram", + "chart_history": "Omkostnings- og varighedshistorik", + "chart_seasonal": "Sæsonfaktorer, 12 måneder", + "chart_weibull": "Weibull-pålidelighedskurve", + "chart_sparkline": "Diagram over sensorudløserværdi", + "days_progress": "Dagsforløb", + "qr_code": "QR-kode", + "qr_generating": "Genererer QR-kode…", + "qr_error": "Kunne ikke generere QR-kode.", + "qr_error_no_url": "Ingen HA-URL konfigureret. Angiv en ekstern eller intern URL under Indstillinger → System → Netværk.", + "save_error": "Kunne ikke gemme. Prøv igen.", + "qr_print": "Udskriv", + "qr_download": "Download SVG", + "qr_action": "Handling ved scanning", + "qr_action_view": "Vis vedligeholdelsesoplysninger", + "qr_action_complete": "Marker vedligeholdelse som fuldført", + "qr_url_mode": "Linktype", + "qr_mode_companion": "Companion-app", + "qr_mode_local": "Lokal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Oversigt", + "analysis": "Analyse", + "recent_activities": "Seneste aktiviteter", + "search_notes": "Søg i noter", + "avg_cost": "Gns. omkostning", + "no_advanced_features": "Ingen avancerede funktioner aktiveret", + "no_advanced_features_hint": "Aktiver „Adaptive intervaller” eller „Sæsonmønstre” i integrationsindstillingerne for at se analysedata her.", + "analysis_not_enough_data": "Ikke nok data til analyse endnu.", + "analysis_not_enough_data_hint": "Weibull-analyse kræver mindst 5 fuldførte vedligeholdelser; sæsonmønstre bliver synlige efter 6+ datapunkter pr. måned.", + "analysis_manual_task_hint": "Manuelle opgaver uden et interval genererer ikke analysedata.", + "completions": "fuldførelser", + "current": "Nuværende", + "shorter": "Kortere", + "longer": "Længere", + "normal": "Normal", + "disabled": "Deaktiveret", + "compound_logic": "Sammensat logik", + "compound": "Sammensat (flere betingelser)", + "compound_logic_and": "OG — alle betingelser skal udløses", + "compound_logic_or": "ELLER — en betingelse er nok", + "compound_help": "Kombiner flere sensorbetingelser til én udløser.", + "compound_no_conditions": "Ingen betingelser endnu — tilføj mindst én.", + "compound_add_condition": "Tilføj betingelse", + "compound_condition": "Betingelse", + "compound_remove_condition": "Fjern betingelse", + "card_title": "Titel", + "card_show_header": "Vis overskrift med statistik", + "card_show_actions": "Vis handlingsknapper", + "card_compact": "Kompakt tilstand", + "card_max_items": "Maks. elementer (0 = alle)", + "card_filter_status": "Filtrer efter status", + "card_filter_status_help": "Tom = vis alle statusser.", + "card_filter_objects": "Filtrer efter objekter", + "card_filter_objects_help": "Tom = vis alle objekter.", + "card_filter_entities": "Filtrer efter enheder (entity_ids)", + "card_filter_entities_help": "Vælg sensor- / binary_sensor-enheder fra denne integration. Tom = alle.", + "card_loading_objects": "Indlæser objekter…", + "card_load_error": "Kunne ikke indlæse objekter — tjek WebSocket-forbindelsen.", + "card_no_tasks_title": "Ingen vedligeholdelsesopgaver endnu", + "card_no_tasks_cta": "→ Opret en i Vedligeholdelse-panelet", + "no_objects": "Ingen objekter endnu.", + "action_error": "Handlingen mislykkedes. Prøv igen.", + "area_id_optional": "Område (valgfrit)", + "installation_date_optional": "Installationsdato (valgfrit)", + "warranty_expiry_optional": "Garantiens udløb (valgfrit)", + "warranty": "Garanti", + "warranty_valid_until": "gyldig indtil {date}", + "warranty_expires_in": "udløber om {days} dage", + "warranty_expired": "udløbet", + "cal_past_windows": "Tidligere vinduer", + "cal_forward_windows": "Kommende vinduer", + "history_edit_title": "Rediger historikpost", + "history_edit_timestamp": "Tidsstempel", + "manufacturer": "Producent", + "model": "Model", + "area": "Område", + "actions": "Handlinger", + "view_mode_label": "Visning", + "view_cards": "Kortvisning", + "view_table": "Tabelvisning", + "objects_table_columns_label": "Kolonner i objekttabel", + "objects_table_columns_hint": "Vælg, hvilke kolonner der vises i objekttabelvisningen.", + "custom_icon_optional": "Ikon (valgfrit, f.eks. mdi:wrench)", + "task_enabled": "Opgave aktiveret", + "skip_reason_prompt": "Spring denne opgave over?", + "reason_optional": "Årsag (valgfrit)", + "reset_date_prompt": "Marker opgave som udført?", + "reset_date_optional": "Dato for seneste udførelse (valgfrit, standard er i dag)", + "notes_label": "Noter", + "documentation_label": "Dokumentation", + "no_nfc_tag": "— Intet tag —", + "dashboard": "Oversigt", + "tab_today": "I dag", + "palette_placeholder": "Søg objekter og opgaver…", + "palette_no_results": "Ingen match", + "palette_hint": "↑↓ naviger · Enter åbn · Esc luk", + "today_all_caught_up": "Alt er klaret! Intet forfalder i denne uge.", + "today_overdue": "Forfaldne", + "today_due_today": "Forfalder i dag", + "today_this_week": "Denne uge", + "settings": "Indstillinger", + "settings_features": "Avancerede funktioner", + "settings_features_desc": "Aktiver eller deaktiver avancerede funktioner. Deaktivering skjuler dem fra brugerfladen, men sletter ikke data.", + "feat_adaptive": "Adaptiv planlægning", + "feat_adaptive_desc": "Lær optimale intervaller fra vedligeholdelseshistorik", + "feat_predictions": "Sensorforudsigelser", + "feat_predictions_desc": "Forudsig udløsningsdatoer fra sensornedbrydning", + "feat_seasonal": "Sæsonjusteringer", + "feat_seasonal_desc": "Juster intervaller baseret på sæsonmønstre", + "feat_environmental": "Miljøkorrelation", + "feat_environmental_desc": "Korreler intervaller med temperatur/luftfugtighed", + "feat_budget": "Budgetopfølgning", + "feat_budget_desc": "Følg månedlige og årlige vedligeholdelsesudgifter", + "feat_groups": "Opgavegrupper", + "feat_groups_desc": "Organiser opgaver i logiske grupper", + "feat_checklists": "Tjeklister", + "feat_checklists_desc": "Procedurer i flere trin til fuldførelse af opgaver", + "settings_general": "Generelt", + "settings_default_warning": "Standard advarselsdage", + "settings_panel_enabled": "Sidepanel", + "settings_panel_title": "Sidepanelets titel", + "settings_notifications": "Notifikationer", + "settings_notify_service": "Notifikationstjeneste", + "test_notification": "Testnotifikation", + "send_test": "Send test", + "testing": "Sender…", + "test_notification_success": "Testnotifikation sendt", + "test_notification_failed": "Testnotifikation mislykkedes", + "settings_notify_due_soon": "Notificer ved snart forfalden", + "settings_notify_overdue": "Notificer ved forfalden", + "settings_notify_triggered": "Notificer ved udløst", + "settings_interval_hours": "Gentagelsesinterval (timer, 0 = én gang)", + "settings_quiet_hours": "Stilletimer", + "settings_quiet_start": "Start", + "settings_quiet_end": "Slut", + "settings_max_per_day": "Maks. notifikationer pr. dag (0 = ubegrænset)", + "settings_bundling": "Saml notifikationer", + "settings_bundle_threshold": "Samlingstærskel", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobile handlingsknapper", + "settings_action_complete": "Vis 'Fuldfør'-knap", + "settings_action_skip": "Vis 'Spring over'-knap", + "settings_action_snooze": "Vis 'Udsæt'-knap", + "settings_weekly_digest": "Ugentlig oversigt", + "settings_weekly_digest_hint": "Én samlet notifikation mandag morgen, når opgaver forfalder.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Udsættelsesvarighed (timer)", + "settings_budget": "Budget", + "settings_currency": "Valuta", + "settings_budget_monthly": "Månedligt budget", + "settings_budget_yearly": "Årligt budget", + "settings_budget_alerts": "Budgetadvarsler", + "settings_budget_threshold": "Advarselstærskel (%)", + "settings_import_export": "Import / Eksport", + "settings_export_json": "Eksporter JSON", + "settings_export_yaml": "Eksporter YAML", + "settings_export_csv": "Eksporter CSV", + "settings_import_csv": "Importer CSV", + "settings_import_placeholder": "Indsæt JSON- eller CSV-indhold her…", + "settings_import_btn": "Importer", + "settings_import_success": "{count} objekter importeret.", + "settings_export_success": "Eksport downloadet.", + "settings_saved": "Indstilling gemt.", + "settings_include_history": "Inkluder historik", + "sort_alphabetical": "Alfabetisk", + "sort_due_soonest": "Forfalder først", + "sort_task_count": "Antal opgaver", + "sort_area": "Område", + "sort_assigned_user": "Tildelt bruger", + "sort_group": "Gruppe", + "groupby_none": "Ingen gruppering", + "groupby_area": "Efter område", + "groupby_group": "Efter gruppe", + "groupby_user": "Efter bruger", + "filter_label": "Filter", + "user_label": "Bruger", + "sort_label": "Sortering", + "group_by_label": "Grupper efter", + "state_value_help": "Brug HA-tilstandsværdien (normalt med små bogstaver, f.eks. \"on\"/\"off\"). Store/små bogstaver normaliseres ved lagring.", + "target_changes_help": "Antal matchende overgange, før udløseren aktiveres (standard: 1).", + "qr_print_title": "Udskriv QR-koder", + "qr_print_desc": "Generer en udskriftsklar side med QR-koder, der kan klippes ud og sættes på dit udstyr.", + "qr_print_load": "Indlæs objekter", + "qr_print_filter": "Filter", + "qr_print_objects": "Objekter", + "qr_print_actions": "Handlinger", + "qr_print_url_mode": "Linktype", + "qr_print_estimate": "Anslåede QR-koder", + "qr_print_over_limit": "grænsen er 200, indsnævr filteret", + "qr_print_generate": "Generer QR-koder", + "qr_print_generating": "Genererer…", + "qr_print_ready": "QR-koder klar", + "qr_print_print_button": "Udskriv", + "qr_print_empty": "Intet at generere", + "qr_action_skip": "Spring over", + "vacation_title": "Ferietilstand", + "vacation_active": "aktiv", + "vacation_ended": "afsluttet", + "vacation_desc": "Planlæg en ferie: notifikationer sættes på pause i perioden plus et bufferantal dage. Du kan vælge bestemte opgaver til igen.", + "vacation_enable": "Aktiver ferietilstand", + "vacation_start": "Start", + "vacation_end": "Slut", + "vacation_buffer": "Buffer (dage)", + "vacation_exempt_title": "Notificer alligevel under ferie", + "vacation_exempt_desc": "Vælg opgaver, der stadig skal notificere under ferien (f.eks. kritisk poolkemi).", + "vacation_load_tasks": "Indlæs opgaver", + "vacation_preview_btn": "Vis forhåndsvisning", + "vacation_preview_affected": "opgaver berørt", + "vacation_event_due_soon": "bliver snart forfalden", + "vacation_event_overdue": "bliver forfalden", + "vacation_event_triggered_est": "sensorudløsning mulig", + "vacation_sensor_based": "(sensorbaseret)", + "vacation_action_notify": "Notificer alligevel", + "vacation_action_unsilence": "Dæmp igen", + "vacation_marked_complete": "Markeret som fuldført", + "vacation_marked_skip": "Sprunget over", + "vacation_end_now": "Afslut ferie nu", + "add": "Tilføj", + "show_stats": "Vis statistik + grafer", + "hide_stats": "Skjul statistik", + "adaptive_no_data": "Endnu ikke nok fuldførelseshistorik til adaptiv analyse. Fuldfør denne opgave et par gange mere for at låse op for intervalanbefalinger og pålidelighedsdiagrammer.", + "suggestion_applied": "Foreslået interval anvendt", + "vacation_mode": "Ferietilstand", + "vacation_status_active": "Aktiv nu", + "vacation_status_scheduled": "Planlagt", + "vacation_status_inactive": "Inaktiv", + "vacation_end_now_confirm": "Afslut ferie med det samme?", + "vacation_exempt_count": "undtaget", + "vacation_advanced": "Avanceret…", + "vacation_open_panel": "Åbn i panel", + "enable": "Aktiver", + "saved": "Gemt", + "budget_monthly_set": "Angiv måned", + "budget_yearly_set": "Angiv år", + "budget_advanced": "Valuta, advarsler…", + "budget_open_panel": "Åbn i panel", + "groups_empty": "Ingen grupper endnu.", + "group_new_placeholder": "Tilføj gruppe…", + "group_delete_confirm": "Slet gruppen \"{name}\"?", + "groups_manage_tasks": "Administrer opgavetildelinger…", + "groups_open_panel": "Åbn i panel", + "unassigned": "Ikke tildelt", + "no_area": "Intet område", + "has_overdue": "Har forfaldne opgaver", + "object": "Objekt", + "settings_panel_access": "Paneladgang", + "settings_panel_access_desc": "Administratorer har altid fuld adgang. For at delegere oprettelse, redigering og sletning til bestemte ikke-administratorer skal du slå dette til og vælge dem nedenfor — alle andre ser kun Fuldfør og Spring over.", + "settings_operator_write": "Tillad valgte brugere at oprette, redigere og slette", + "settings_operator_write_desc": "Fra: kun administratorer kan ændre indhold. Til: de valgte brugere nedenfor får også fuld adgang.", + "no_non_admin_users": "Ingen ikke-administratorbrugere fundet. Tilføj nogle under Indstillinger → Personer.", + "owner_label": "Ejer", + "feat_completion_actions": "Fuldførelseshandlinger", + "feat_completion_actions_desc": "HA-handling pr. opgave ved fuldførelse + hurtig-fuldfør-QR med forudindstillede værdier.", + "on_complete_action_title": "Ved fuldførelse: udløs HA-handling (valgfrit)", + "on_complete_action_desc": "Kalder en HA-tjeneste, når opgaven fuldføres — f.eks. nulstil en tæller på enheden.", + "on_complete_action_service": "Tjeneste", + "on_complete_action_target": "Målenhed", + "on_complete_action_target_hint": "Bemærk: enhedens domæne skal matche tjenesten — f.eks. virker 'button.press' kun på button.*, 'counter.increment' kun på counter.*, 'input_button.press' kun på input_button.* osv. Ved et misforhold mislykkes handlingen lydløst (HA logger 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Data (JSON, valgfrit)", + "on_complete_action_test": "Valider konfiguration", + "on_complete_action_test_success": "✓ Konfiguration gyldig (handlingen udløses kun ved fuldførelse af opgaven)", + "on_complete_action_test_failed": "Mislykkedes", + "quick_complete_defaults_title": "Standardværdier for hurtig-fuldfør (til QR-scanninger, valgfrit)", + "quick_complete_defaults_desc": "Forudindstillede værdier til hurtig-fuldfør-QR-scanninger. Uden disse åbner QR-koden fuldfør-dialogen.", + "quick_complete_defaults_notes": "Noter", + "quick_complete_defaults_cost": "Omkostning", + "quick_complete_defaults_duration": "Varighed (minutter)", + "quick_complete_defaults_feedback_none": "Ingen feedback", + "quick_complete_defaults_feedback_needed": "Var nødvendig", + "quick_complete_defaults_feedback_not_needed": "Ikke nødvendig", + "quick_complete_success": "Hurtigt markeret som fuldført", + "show_all_objects": "Vis alle objekter", + "show_all_tasks": "Ryd filter — vis alle opgaver", + "filter_to_overdue": "Filtrer opgavelisten til kun forfaldne", + "filter_to_due_soon": "Filtrer opgavelisten til kun snart forfaldne", + "filter_to_triggered": "Filtrer opgavelisten til kun udløste", + "open_task": "Åbn opgave", + "show_details": "Vis historik + statistik", + "hide_details": "Skjul detaljer", + "history_empty": "Ingen historik endnu.", + "history_edit_button": "Rediger post", + "total_cost": "Samlet omkostning", + "times_performed": "Udført", + "older_entries": "ældre", + "open_in_panel": "Åbn i Vedligeholdelse-panelet", + "skip_reason": "Årsag til at springe over (valgfrit)", + "reset_to_date": "Nulstil senest udført til", + "delete_task_confirm": "Slet denne opgave og dens historik?", + "delete_object_confirm": "Slet dette objekt og alle dets opgaver?", + "loading": "Indlæser…", + "archive": "Arkivér", + "undo": "Fortryd", + "task_archived": "Opgave arkiveret", + "object_archived": "Objekt arkiveret", + "unarchive": "Gendan", + "archived": "Arkiveret", + "show_archived": "Vis arkiverede", + "hide_archived": "Skjul arkiverede", + "confirm_archive_object": "Arkivér dette objekt og dets opgaver? Historikken bevares og kan gendannes senere.", + "settings_archive": "Arkiv og opbevaring", + "settings_archive_desc": "Læg færdige engangsopgaver væk uden at slette dem. Arkiverede elementer er skjult og inaktive, men bevarer historik og omkostninger.", + "settings_archive_oneoff_days": "Arkivér automatisk færdige engangsopgaver efter (dage, 0 = fra)", + "settings_delete_archived_oneoff_days": "Slet automatisk arkiverede engangsopgaver efter (dage, 0 = aldrig)", + "archive_object": "Arkivér objekt", + "unarchive_object": "Gendan objekt", + "documents": "Dokumenter", + "documents_empty": "Ingen dokumenter endnu.", + "doc_upload": "Upload fil", + "doc_uploading": "Uploader…", + "doc_add_link": "Tilføj link", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titel (valgfri)", + "doc_open": "Åbn", + "doc_delete_confirm": "Slet \"{name}\"?", + "doc_too_large": "Filen er for stor (maks. 25 MB).", + "doc_upload_failed": "Upload mislykkedes.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Allerede gemt et andet sted — delt, ingen ekstra plads.", + "doc_dup_in_object": "Denne fil er allerede vedhæftet dette objekt.", + "doc_link_invalid": "Kun http/https-links er tilladt.", + "doc_cat_manual": "Vejledning", + "doc_cat_warranty": "Garanti", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Reservedele", + "doc_cat_photo": "Foto", + "doc_cat_other": "Andet", + "doc_link_badge": "Link", + "doc_storage_title": "Dokumentlager", + "doc_storage_saved": "Sparet ved deduplikering", + "doc_storage_refresh": "Opdater", + "doc_download": "Download", + "doc_close": "Luk", + "doc_camera": "Tag billede", + "doc_drop_hint": "Slip filer her", + "doc_task_none": "Ingen dokumenter knyttet til denne opgave.", + "doc_link_existing": "Knyt et dokument…", + "doc_attach": "Knyt", + "doc_unlink": "Fjern kobling", + "doc_page": "Side", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1å", + "chart_since_service": "siden sidste vedligeholdelse", + "chart_no_stats": "Ingen langtidsstatistik for denne enhed — viser kun værdier fra vedligeholdelsesposter", + "auto_complete_on_recovery": "Fuldfør automatisk når sensoren retter sig", + "auto_complete_on_recovery_help": "Registrerer en fuldførelse (sætter senest udført), når udløseren løser sig selv — f.eks. salt påfyldt, filter udskiftet.", + "doc_search": "Søg dokumenter…", + "doc_search_none": "Ingen matchende dokumenter", + "link_device_optional": "Tilknyt eksisterende enhed (valgfrit)", + "parent_object_optional": "Overordnet objekt (valgfrit)", + "parent_none": "(Intet overordnet)", + "paused": "Sat på pause", + "pause_object": "Sæt på pause", + "resume_object": "Genoptag", + "pause_until_prompt": "Frys objektets planer — intet forfalder og intet notificerer, før det genoptages. Angiv eventuelt en dato for automatisk genoptagelse.", + "pause_until_label": "Genoptag den (valgfrit)", + "object_paused": "Objekt sat på pause", + "object_resumed": "Objekt genoptaget — planer genstartet", + "object_paused_badge": "På pause", + "paused_until_label": "indtil", + "replace_object": "Udskift…", + "replace_object_prompt": "Pensioner dette objekt og opret en efterfølger. Historik og omkostninger forbliver arkiveret på det gamle; opgaver og dokumenter følger med til det nye, tællere starter forfra.", + "replace_name_label": "Efterfølgerens navn", + "object_replaced": "Objekt udskiftet — efterfølger oprettet", + "reading_unit_label": "Aflæsningsenhed (f.eks. kWh, m³)", + "reading_unit_help": "Vises ved siden af den registrerede værdi, når opgaven fuldføres.", + "reading_value_label": "Aflæst værdi", + "reading_label": "Aflæsning", + "settings_templates_label": "Skabelongalleri", + "settings_templates_hint": "Fravælg skabeloner, du aldrig får brug for — de forsvinder fra \"Fra skabelon\"-vælgerne (panel og config flow). Intet andet ændres; kan genaktiveres når som helst.", + "worksheet": "Arbejdsark", + "worksheet_scan_view": "Scan for at åbne opgaven", + "worksheet_scan_complete": "Scan for at fuldføre", + "worksheet_manual_excerpt": "Uddrag af manualen", + "worksheet_pages": "sider", + "worksheet_printed": "Udskrevet", + "worksheet_never": "Aldrig" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/de.json b/custom_components/maintenance_supporter/frontend-src/locales/de.json new file mode 100644 index 0000000..72ec8e6 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/de.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Wartung", + "objects": "Objekte", + "tasks": "Aufgaben", + "overdue": "Überfällig", + "due_soon": "Bald fällig", + "triggered": "Ausgelöst", + "trigger_replaced": "Auslöser ersetzt", + "ok": "OK", + "all": "Alle", + "new_object": "+ Neues Objekt", + "templates_from": "Aus Vorlage", + "templates_title": "Mit einer Vorlage starten", + "templates_task_count": "{n} Aufgaben", + "template_created": "Aus Vorlage erstellt", + "onboard_hint": "Füge dein erstes Objekt hinzu, um mit der Wartung zu starten.", + "edit": "Bearbeiten", + "duplicate": "Duplizieren", + "task_duplicated": "Aufgabe dupliziert", + "object_duplicated": "Objekt dupliziert", + "delete": "Löschen", + "add_task": "+ Aufgabe", + "complete": "Erledigt", + "completed": "Abgeschlossen", + "skip": "Überspringen", + "skipped": "Übersprungen", + "missed": "Verpasst", + "reset": "Zurücksetzen", + "snooze": "Schlummern", + "snoozed": "Schlummert", + "cancel": "Abbrechen", + "bulk_select": "Auswählen", + "bulk_select_all": "Alle auswählen", + "bulk_n_selected": "{n} ausgewählt", + "bulk_completed": "{n} Aufgaben erledigt", + "bulk_archived": "{n} Aufgaben archiviert", + "completing": "Wird erledigt…", + "interval": "Intervall", + "warning": "Vorwarnung", + "last_performed": "Zuletzt durchgeführt", + "next_due": "Nächste Fälligkeit", + "days_until_due": "Tage bis fällig", + "avg_duration": "Ø Dauer", + "trigger": "Trigger", + "trigger_type": "Trigger-Typ", + "threshold_above": "Obergrenze", + "threshold_below": "Untergrenze", + "threshold": "Schwellwert", + "counter": "Zähler", + "state_change": "Zustandsänderung", + "runtime": "Laufzeit", + "runtime_hours": "Ziel-Laufzeit (Stunden)", + "target_value": "Zielwert", + "baseline": "Nulllinie", + "target_changes": "Ziel-Änderungen", + "for_minutes": "Für (Minuten)", + "time_based": "Zeitbasiert", + "sensor_based": "Sensorbasiert", + "manual": "Manuell", + "one_time": "Einmalig", + "weekdays": "Wochentage", + "nth_weekday": "N-ter Wochentag im Monat", + "day_of_month": "Tag im Monat", + "recurrence_on_days": "Wiederholen an", + "recurrence_occurrence": "Vorkommen", + "recurrence_weekday": "Wochentag", + "recurrence_day": "Tag im Monat (1–31)", + "recurrence_last_day": "Letzter Tag des Monats", + "recurrence_business_day": "Nur Werktage (vom Wochenende vorziehen)", + "recurrence_offset": "Versatz (Tage, ±)", + "recurrence_offset_help": "Verschiebt das Datum um ±N Tage, z. B. -2 = zwei Tage davor.", + "last_day_month": "Letzter Tag des Monats", + "last_business_day_month": "Letzter Werktag", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Letzter", + "day_word": "Tag", + "interval_value": "Intervall", + "interval_unit": "Einheit", + "unit_days": "Tage", + "unit_weeks": "Wochen", + "unit_months": "Monate", + "unit_years": "Jahre", + "due_date": "Fälligkeitsdatum", + "cleaning": "Reinigung", + "inspection": "Inspektion", + "replacement": "Austausch", + "calibration": "Kalibrierung", + "service": "Service", + "reading": "Ablesung", + "custom": "Benutzerdefiniert", + "history": "Verlauf", + "cost": "Kosten", + "report_button": "Bericht", + "report_title": "Wartungsbericht", + "report_generated": "Erstellt", + "report_times_done": "Erledigt", + "report_total_cost": "Gesamtkosten", + "report_every": "alle {n} {unit}", + "report_notes": "Notizen", + "report_col_type": "Typ", + "report_col_status": "Status", + "report_col_schedule": "Zeitplan", + "duration": "Dauer", + "both": "Beides", + "trigger_val": "Trigger-Wert", + "complete_title": "Erledigt: ", + "checklist": "Checkliste", + "checklist_steps_optional": "Checkliste-Schritte (optional)", + "checklist_placeholder": "Filter reinigen\nDichtung ersetzen\nDruck testen", + "checklist_help": "Ein Schritt pro Zeile. Max. 100 Einträge.", + "err_too_long": "{field}: zu lang (max. {n} Zeichen)", + "err_too_short": "{field}: zu kurz (min. {n} Zeichen)", + "err_value_too_high": "{field}: zu groß (max. {n})", + "err_value_too_low": "{field}: zu klein (min. {n})", + "err_required": "{field}: Pflichtfeld", + "err_wrong_type": "{field}: falscher Typ (erwartet: {type})", + "err_invalid_choice": "{field}: nicht erlaubter Wert", + "err_invalid_value": "{field}: ungültiger Wert", + "feat_schedule_time": "Uhrzeit-Scheduling", + "feat_schedule_time_desc": "Tasks werden zu einer festen Uhrzeit fällig statt um Mitternacht.", + "schedule_time_optional": "Fällig um (optional, HH:MM)", + "schedule_time_help": "Leer = Mitternacht (Default). HA-Zeitzone.", + "at_time": "um", + "notes_optional": "Notizen (optional)", + "cost_optional": "Kosten (optional)", + "duration_minutes": "Dauer in Minuten (optional)", + "days": "Tage", + "day": "Tag", + "today": "Heute", + "d_overdue": "T überfällig", + "no_tasks": "Keine Wartungsaufgaben vorhanden. Erstellen Sie ein Objekt um zu beginnen.", + "no_tasks_short": "Keine Aufgaben", + "no_history": "Noch keine Verlaufseinträge.", + "show_all": "Alle anzeigen", + "cost_duration_chart": "Kosten & Dauer", + "installed": "Installiert", + "confirm_delete_object": "Dieses Objekt und alle zugehörigen Aufgaben löschen?", + "confirm_delete_task": "Diese Aufgabe wirklich löschen?", + "min": "Min", + "max": "Max", + "save": "Speichern", + "saving": "Speichern…", + "edit_task": "Aufgabe bearbeiten", + "new_task": "Neue Wartungsaufgabe", + "task_name": "Aufgabenname", + "maintenance_type": "Wartungstyp", + "priority": "Priorität", + "labels": "Labels", + "labels_placeholder": "z. B. Sicherheit, Saison, Mieter-sichtbar", + "labels_help": "Kommagetrennte Tags zum Filtern und für Berichte.", + "priority_low": "Niedrig", + "priority_normal": "Normal", + "priority_high": "Hoch", + "schedule_type": "Planungsart", + "interval_days": "Intervall (Tage)", + "warning_days": "Warntage", + "earliest_completion_days": "Frühester Abschluss (Tage vor Fälligkeit)", + "earliest_completion_days_help": "Leer lassen, um jederzeit abschließen zu können. 0 = erst am/nach dem Fälligkeitstag.", + "last_performed_optional": "Zuletzt durchgeführt (optional)", + "interval_anchor": "Intervall-Anker", + "anchor_completion": "Ab Erledigung", + "anchor_planned": "Ab geplantem Datum (kein Drift)", + "edit_object": "Objekt bearbeiten", + "name": "Name", + "manufacturer_optional": "Hersteller (optional)", + "model_optional": "Modell (optional)", + "serial_number_optional": "Seriennummer (optional)", + "serial_number_label": "S/N", + "documentation_url_label": "Handbuch", + "object_notes_label": "Notizen", + "sort_due_date": "Fälligkeit", + "sort_object": "Objekt-Name", + "sort_type": "Typ", + "sort_task_name": "Aufgaben-Name", + "all_objects": "Alle Objekte", + "tasks_lower": "Aufgaben", + "no_tasks_yet": "Noch keine Aufgaben", + "add_first_task": "Erste Aufgabe hinzufügen", + "trigger_configuration": "Trigger-Konfiguration", + "entity_id": "Entitäts-ID", + "comma_separated": "kommagetrennt", + "entity_logic": "Entitäts-Logik", + "entity_logic_any": "Beliebige Entität löst aus", + "entity_logic_all": "Alle Entitäten müssen auslösen", + "entities": "Entitäten", + "attribute_optional": "Attribut (optional, leer = Zustand)", + "use_entity_state": "Entitäts-Zustand verwenden (kein Attribut)", + "trigger_above": "Auslösen wenn über", + "trigger_below": "Auslösen wenn unter", + "for_at_least_minutes": "Für mindestens (Minuten)", + "safety_interval_days": "Sicherheitsintervall (Tage, optional)", + "safety_interval": "Sicherheitsintervall (optional)", + "delta_mode": "Delta-Modus", + "from_state_optional": "Von Zustand (optional)", + "to_state_optional": "Zu Zustand (optional)", + "documentation_url_optional": "Dokumentation URL (optional)", + "object_notes_optional": "Notizen (optional)", + "nfc_tag_id_optional": "NFC-Tag-ID (optional)", + "nfc_tags_empty_help": "Noch keine NFC-Tags in Home Assistant registriert.", + "nfc_tags_open_settings": "Tag-Einstellungen öffnen", + "nfc_tags_refresh": "Aktualisieren", + "environmental_entity_optional": "Umgebungs-Sensor (optional)", + "environmental_entity_helper": "z.B. sensor.aussentemperatur — passt das Intervall an Umgebungswerte an", + "environmental_attribute_optional": "Umgebungs-Attribut (optional)", + "nfc_tag_id": "NFC-Tag-ID", + "nfc_linked": "NFC-Tag verknüpft", + "nfc_link_hint": "Klicken um NFC-Tag zu verknüpfen", + "responsible_user": "Verantwortlicher Benutzer", + "shared_with": "Geteilt mit (Rotation)", + "shared_with_help": "Mehrere Personen auswählen, die sich diese Aufgabe teilen; der/die Verantwortliche wechselt bei jedem Abschluss.", + "rotation_strategy": "Rotation", + "rotation_none": "Keine Rotation", + "rotation_round_robin": "Reihum", + "rotation_least_completed": "Am wenigsten erledigt", + "rotation_random": "Zufällig", + "no_user_assigned": "(Kein Benutzer zugewiesen)", + "all_users": "Alle Benutzer", + "my_tasks": "Meine Aufgaben", + "tab_calendar": "Kalender", + "cal_no_events": "Keine Wartung", + "cal_window_7": "7 Tage", + "cal_window_14": "14 Tage", + "cal_window_30": "30 Tage", + "cal_window_365": "1 Jahr", + "cal_every_n_days": "alle {n} Tage", + "cal_source_time": "Zeit-basiert", + "cal_source_time_adaptive": "Zeit-basiert (adaptiv)", + "cal_source_sensor": "Sensor-basiert", + "cal_predicted": "vorhergesagt", + "cal_confidence_high": "hohe Genauigkeit", + "cal_confidence_medium": "mittlere Genauigkeit", + "cal_confidence_low": "niedrige Genauigkeit", + "budget_monthly": "Monatsbudget", + "budget_yearly": "Jahresbudget", + "groups": "Gruppen", + "new_group": "Neue Gruppe", + "edit_group": "Gruppe bearbeiten", + "no_groups": "Keine Gruppen vorhanden", + "delete_group": "Gruppe löschen", + "delete_group_confirm": "Gruppe '{name}' wirklich löschen?", + "group_select_tasks": "Aufgaben auswählen", + "group_name_required": "Name erforderlich", + "description_optional": "Beschreibung (optional)", + "selected": "Ausgewählt", + "loading_chart": "Daten werden geladen...", + "hide_outliers": "Ausreißer ausblenden (Messfehler)", + "was_maintenance_needed": "War diese Wartung nötig?", + "feedback_needed": "Nötig", + "feedback_not_needed": "Nicht nötig", + "feedback_not_sure": "Unsicher", + "suggested_interval": "Empfohlenes Intervall", + "apply_suggestion": "Übernehmen", + "reanalyze": "Neu analysieren", + "reanalyze_result": "Neue Analyse", + "reanalyze_insufficient_data": "Nicht genügend Daten für eine Empfehlung", + "data_points": "Datenpunkte", + "dismiss_suggestion": "Verwerfen", + "confidence_low": "Niedrig", + "confidence_medium": "Mittel", + "confidence_high": "Hoch", + "recommended": "empfohlen", + "seasonal_awareness": "Saisonale Anpassung", + "edit_seasonal_overrides": "Saison-Faktoren bearbeiten", + "seasonal_overrides_title": "Saisonale Faktoren (Override)", + "seasonal_overrides_hint": "Faktor pro Monat (0.1–5.0). Leer = automatisch gelernt.", + "seasonal_override_invalid": "Ungültiger Wert", + "seasonal_override_range": "Faktor muss zwischen 0.1 und 5.0 liegen", + "clear_all": "Alle zurücksetzen", + "seasonal_chart_title": "Saisonale Faktoren", + "seasonal_learned": "Gelernt", + "seasonal_manual": "Manuell", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mär", + "month_apr": "Apr", + "month_may": "Mai", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Dez", + "sensor_prediction": "Sensorvorhersage", + "degradation_trend": "Trend", + "trend_rising": "Steigend", + "trend_falling": "Fallend", + "trend_stable": "Stabil", + "trend_insufficient_data": "Unzureichende Daten", + "days_until_threshold": "Tage bis Schwellwert", + "threshold_exceeded": "Schwellwert überschritten", + "environmental_adjustment": "Umgebungsfaktor", + "sensor_prediction_urgency": "Sensor prognostiziert Schwellwert in ~{days} Tagen", + "day_short": "Tag", + "weibull_reliability_curve": "Zuverlässigkeitskurve", + "weibull_failure_probability": "Ausfallwahrscheinlichkeit", + "weibull_r_squared": "Güte R²", + "beta_early_failures": "Frühausfälle", + "beta_random_failures": "Zufällige Ausfälle", + "beta_wear_out": "Verschleiß", + "beta_highly_predictable": "Hochvorhersagbar", + "confidence_interval": "Konfidenzintervall", + "confidence_conservative": "Konservativ", + "confidence_aggressive": "Optimistisch", + "current_interval_marker": "Aktuelles Intervall", + "recommended_marker": "Empfohlen", + "characteristic_life": "Charakteristische Lebensdauer", + "chart_mini_sparkline": "Trend-Sparkline", + "chart_history": "Kosten- und Dauer-Verlauf", + "chart_seasonal": "Saisonfaktoren, 12 Monate", + "chart_weibull": "Weibull-Zuverlässigkeitskurve", + "chart_sparkline": "Sensor-Triggerwert-Verlauf", + "days_progress": "Tagesfortschritt", + "qr_code": "QR-Code", + "qr_generating": "QR-Code wird generiert…", + "qr_error": "QR-Code konnte nicht generiert werden.", + "qr_error_no_url": "Keine HA-URL konfiguriert. Bitte unter Einstellungen → System → Netzwerk eine externe oder interne URL setzen.", + "save_error": "Fehler beim Speichern. Bitte erneut versuchen.", + "qr_print": "Drucken", + "qr_download": "SVG herunterladen", + "qr_action": "Aktion beim Scannen", + "qr_action_view": "Wartungsinfo anzeigen", + "qr_action_complete": "Wartung als erledigt markieren", + "qr_url_mode": "Link-Typ", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Übersicht", + "analysis": "Analyse", + "recent_activities": "Letzte Aktivitäten", + "search_notes": "Notizen durchsuchen", + "avg_cost": "Ø Kosten", + "no_advanced_features": "Keine erweiterten Funktionen aktiviert", + "no_advanced_features_hint": "Aktiviere „Adaptive Intervalle“ oder „Saisonale Muster“ in den Integrationseinstellungen, um hier Analysedaten zu sehen.", + "analysis_not_enough_data": "Noch nicht genügend Daten für die Analyse vorhanden.", + "analysis_not_enough_data_hint": "Die Weibull-Analyse benötigt mindestens 5 abgeschlossene Wartungen, saisonale Muster werden nach 6+ Datenpunkten pro Monat sichtbar.", + "analysis_manual_task_hint": "Manuelle Aufgaben ohne Intervall erzeugen keine Analysedaten.", + "completions": "Abschlüsse", + "current": "Aktuell", + "shorter": "Kürzer", + "longer": "Länger", + "normal": "Normal", + "disabled": "Deaktiviert", + "compound_logic": "Verknüpfungslogik", + "compound": "Verknüpft (mehrere Bedingungen)", + "compound_logic_and": "UND — alle Bedingungen müssen auslösen", + "compound_logic_or": "ODER — eine Bedingung genügt", + "compound_help": "Mehrere Sensor-Bedingungen zu einem Auslöser kombinieren.", + "compound_no_conditions": "Noch keine Bedingungen — mindestens eine hinzufügen.", + "compound_add_condition": "Bedingung hinzufügen", + "compound_condition": "Bedingung", + "compound_remove_condition": "Bedingung entfernen", + "card_title": "Titel", + "card_show_header": "Kopfzeile mit Statistiken anzeigen", + "card_show_actions": "Aktionsbuttons anzeigen", + "card_compact": "Kompaktmodus", + "card_max_items": "Max. Einträge (0 = alle)", + "card_filter_status": "Nach Status filtern", + "card_filter_status_help": "Leer = alle Status zeigen.", + "card_filter_objects": "Nach Objekten filtern", + "card_filter_objects_help": "Leer = alle Objekte zeigen.", + "card_filter_entities": "Nach Entitäten filtern (entity_ids)", + "card_filter_entities_help": "Wähle Sensor-/Binary-Sensor-Entitäten dieser Integration. Leer = alle.", + "card_loading_objects": "Lade Objekte…", + "card_load_error": "Objekte konnten nicht geladen werden — bitte WS-Verbindung prüfen.", + "card_no_tasks_title": "Noch keine Wartungsaufgaben", + "card_no_tasks_cta": "→ Im Maintenance-Panel anlegen", + "no_objects": "Keine Objekte vorhanden.", + "action_error": "Aktion fehlgeschlagen. Bitte erneut versuchen.", + "area_id_optional": "Bereich (optional)", + "installation_date_optional": "Installationsdatum (optional)", + "warranty_expiry_optional": "Garantie-Ablauf (optional)", + "warranty": "Garantie", + "warranty_valid_until": "gültig bis {date}", + "warranty_expires_in": "läuft in {days} Tagen ab", + "warranty_expired": "abgelaufen", + "cal_past_windows": "Vergangene Fenster", + "cal_forward_windows": "Zukünftige Fenster", + "history_edit_title": "Verlaufseintrag bearbeiten", + "history_edit_timestamp": "Zeitstempel", + "manufacturer": "Hersteller", + "model": "Modell", + "area": "Bereich", + "actions": "Aktionen", + "view_mode_label": "Ansicht", + "view_cards": "Kartenansicht", + "view_table": "Tabellenansicht", + "objects_table_columns_label": "Spalten der Objekt-Tabelle", + "objects_table_columns_hint": "Wähle die Spalten für die Tabellenansicht der Objektübersicht.", + "custom_icon_optional": "Icon (optional, z.B. mdi:wrench)", + "task_enabled": "Aufgabe aktiviert", + "skip_reason_prompt": "Aufgabe überspringen?", + "reason_optional": "Grund (optional)", + "reset_date_prompt": "Aufgabe als ausgeführt markieren?", + "reset_date_optional": "Letztes Erledigungs-Datum (optional, Standard: heute)", + "notes_label": "Notizen", + "documentation_label": "Dokumentation", + "no_nfc_tag": "— Kein Tag —", + "dashboard": "Dashboard", + "tab_today": "Heute", + "palette_placeholder": "Objekte und Aufgaben suchen…", + "palette_no_results": "Keine Treffer", + "palette_hint": "↑↓ Navigieren · Enter Öffnen · Esc Schließen", + "today_all_caught_up": "Alles erledigt! Diese Woche steht nichts an.", + "today_overdue": "Überfällig", + "today_due_today": "Heute fällig", + "today_this_week": "Diese Woche", + "settings": "Einstellungen", + "settings_features": "Erweiterte Funktionen", + "settings_features_desc": "Erweiterte Funktionen ein- oder ausschalten. Deaktivieren blendet sie in der Oberfläche aus, löscht aber keine Daten.", + "feat_adaptive": "Adaptive Intervalle", + "feat_adaptive_desc": "Optimale Intervalle aus Wartungshistorie lernen", + "feat_predictions": "Sensorvorhersagen", + "feat_predictions_desc": "Trigger-Datum anhand von Sensordegradation vorhersagen", + "feat_seasonal": "Saisonale Anpassungen", + "feat_seasonal_desc": "Intervalle basierend auf saisonalen Mustern anpassen", + "feat_environmental": "Umgebungskorrelation", + "feat_environmental_desc": "Intervalle mit Temperatur/Luftfeuchtigkeit korrelieren", + "feat_budget": "Budgetverfolgung", + "feat_budget_desc": "Monatliche und jährliche Wartungsausgaben verfolgen", + "feat_groups": "Aufgabengruppen", + "feat_groups_desc": "Aufgaben in logische Gruppen organisieren", + "feat_checklists": "Checklisten", + "feat_checklists_desc": "Mehrstufige Verfahren zur Aufgabenerlediung", + "settings_general": "Allgemein", + "settings_default_warning": "Standard-Warntage", + "settings_panel_enabled": "Seitenleisten-Panel", + "settings_panel_title": "Panel-Titel", + "settings_notifications": "Benachrichtigungen", + "settings_notify_service": "Benachrichtigungsdienst", + "test_notification": "Test-Benachrichtigung", + "send_test": "Test senden", + "testing": "Sende…", + "test_notification_success": "Test-Benachrichtigung gesendet", + "test_notification_failed": "Test-Benachrichtigung fehlgeschlagen", + "settings_notify_due_soon": "Bei baldiger Fälligkeit benachrichtigen", + "settings_notify_overdue": "Bei Überfälligkeit benachrichtigen", + "settings_notify_triggered": "Bei Auslösung benachrichtigen", + "settings_interval_hours": "Wiederholungsintervall (Stunden, 0 = einmalig)", + "settings_quiet_hours": "Ruhezeiten", + "settings_quiet_start": "Beginn", + "settings_quiet_end": "Ende", + "settings_max_per_day": "Max. Benachrichtigungen pro Tag (0 = unbegrenzt)", + "settings_bundling": "Benachrichtigungen bündeln", + "settings_bundle_threshold": "Bündelungsschwelle", + "settings_reminder_leads": "Zusätzliche Erinnerungen (Tage vor Fälligkeit)", + "settings_reminder_leads_hint": "Kommagetrennte Vorlaufzeiten, z. B. 14, 3, 0 — am jeweiligen Tag wird eine zusätzliche Erinnerung gesendet. Leer = aus.", + "settings_actions": "Mobile Aktionsbuttons", + "settings_action_complete": "\"Erledigt\"-Button anzeigen", + "settings_action_skip": "\"Überspringen\"-Button anzeigen", + "settings_action_snooze": "\"Schlummern\"-Button anzeigen", + "settings_weekly_digest": "Wöchentliche Zusammenfassung", + "settings_weekly_digest_hint": "Eine Zusammenfassungs-Benachrichtigung am Montagmorgen, wenn Aufgaben anstehen.", + "settings_warranty_reminder": "Garantie-Ablauf-Erinnerung", + "settings_warranty_reminder_days": "Tage vor Ablauf", + "settings_warranty_reminder_hint": "Einmal benachrichtigen, wenn die Garantie eines Objekts so viele Tage vor Ablauf steht.", + "settings_snooze_hours": "Schlummerdauer (Stunden)", + "settings_budget": "Budget", + "settings_currency": "Währung", + "settings_budget_monthly": "Monatsbudget", + "settings_budget_yearly": "Jahresbudget", + "settings_budget_alerts": "Budget-Warnungen", + "settings_budget_threshold": "Warnschwelle (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "JSON exportieren", + "settings_export_yaml": "YAML exportieren", + "settings_export_csv": "CSV exportieren", + "settings_import_csv": "CSV importieren", + "settings_import_placeholder": "JSON- oder CSV-Inhalt hier einfügen…", + "settings_import_btn": "Importieren", + "settings_import_success": "{count} Objekte erfolgreich importiert.", + "settings_export_success": "Export heruntergeladen.", + "settings_saved": "Einstellung gespeichert.", + "settings_include_history": "Verlauf einbeziehen", + "sort_alphabetical": "Alphabetisch", + "sort_due_soonest": "Frühestens fällig", + "sort_task_count": "Aufgaben-Anzahl", + "sort_area": "Bereich", + "sort_assigned_user": "Verantwortlicher", + "sort_group": "Gruppe", + "groupby_none": "Keine Gruppierung", + "groupby_area": "Nach Bereich", + "groupby_group": "Nach Gruppe", + "groupby_user": "Nach Verantwortlichem", + "filter_label": "Filter", + "user_label": "Benutzer", + "sort_label": "Sortierung", + "group_by_label": "Gruppieren nach", + "state_value_help": "Verwende den HA-Zustandswert (meist kleingeschrieben, z. B. \"on\"/\"off\"). Groß-/Kleinschreibung wird beim Speichern normalisiert.", + "target_changes_help": "Anzahl der passenden Übergänge, nach denen der Trigger auslöst (Standard: 1).", + "qr_print_title": "QR-Codes drucken", + "qr_print_desc": "Erzeuge eine Druckseite mit QR-Codes zum Ausschneiden und Anbringen an den Geräten.", + "qr_print_load": "Objekte laden", + "qr_print_filter": "Filter", + "qr_print_objects": "Objekte", + "qr_print_actions": "Aktionen", + "qr_print_url_mode": "Link-Typ", + "qr_print_estimate": "Geschätzte QR-Codes", + "qr_print_over_limit": "Obergrenze ist 200, bitte Filter eingrenzen", + "qr_print_generate": "QR-Codes erzeugen", + "qr_print_generating": "Erzeuge…", + "qr_print_ready": "QR-Codes bereit", + "qr_print_print_button": "Drucken", + "qr_print_empty": "Keine QR-Codes zu erzeugen", + "qr_action_skip": "Überspringen", + "vacation_title": "Urlaubsmodus", + "vacation_active": "aktiv", + "vacation_ended": "beendet", + "vacation_desc": "Plane deinen Urlaub: Benachrichtigungen werden während des Zeitraums plus Puffer-Tagen pausiert. Du kannst pro Aufgabe Ausnahmen festlegen.", + "vacation_enable": "Urlaubsmodus aktivieren", + "vacation_start": "Beginn", + "vacation_end": "Ende", + "vacation_buffer": "Puffer (Tage)", + "vacation_exempt_title": "Trotz Urlaubsmodus benachrichtigen", + "vacation_exempt_desc": "Wähle Aufgaben aus, für die auch im Urlaub Benachrichtigungen kommen sollen (z. B. kritische Pool-Chemie).", + "vacation_load_tasks": "Aufgaben laden", + "vacation_preview_btn": "Vorschau anzeigen", + "vacation_preview_affected": "Aufgaben betroffen", + "vacation_event_due_soon": "wird bald fällig", + "vacation_event_overdue": "wird überfällig", + "vacation_event_triggered_est": "Sensor-Trigger möglich", + "vacation_sensor_based": "(sensorbasiert)", + "vacation_action_notify": "Trotzdem benachrichtigen", + "vacation_action_unsilence": "Wieder stummschalten", + "vacation_marked_complete": "Als erledigt markiert", + "vacation_marked_skip": "Übersprungen", + "vacation_end_now": "Urlaub jetzt beenden", + "add": "Hinzufügen", + "show_stats": "Statistiken + Diagramme", + "hide_stats": "Statistiken ausblenden", + "adaptive_no_data": "Noch nicht genug Completion-Historie für die adaptive Auswertung. Schließe diese Aufgabe ein paar Mal mehr ab, um Intervall-Empfehlungen und Zuverlässigkeitskurven freizuschalten.", + "suggestion_applied": "Vorgeschlagenes Intervall übernommen", + "vacation_mode": "Urlaubsmodus", + "vacation_status_active": "Aktiv", + "vacation_status_scheduled": "Geplant", + "vacation_status_inactive": "Inaktiv", + "vacation_end_now_confirm": "Urlaub sofort beenden?", + "vacation_exempt_count": "ausgenommen", + "vacation_advanced": "Erweitert…", + "vacation_open_panel": "Im Panel öffnen", + "enable": "Aktivieren", + "saved": "Gespeichert", + "budget_monthly_set": "Monatsbudget setzen", + "budget_yearly_set": "Jahresbudget setzen", + "budget_advanced": "Währung, Alarme…", + "budget_open_panel": "Im Panel öffnen", + "groups_empty": "Keine Gruppen vorhanden.", + "group_new_placeholder": "Gruppe hinzufügen…", + "group_delete_confirm": "Gruppe \"{name}\" löschen?", + "groups_manage_tasks": "Aufgaben-Zuordnungen verwalten…", + "groups_open_panel": "Im Panel öffnen", + "unassigned": "Nicht zugewiesen", + "no_area": "Kein Bereich", + "has_overdue": "Überfällige Aufgaben", + "object": "Objekt", + "settings_panel_access": "Panel-Zugriff", + "settings_panel_access_desc": "Admins haben immer Vollzugriff. Um Erstellen, Bearbeiten und Löschen an bestimmte Non-Admins zu delegieren, aktiviere dies und wähle sie unten aus — alle anderen sehen nur Abhaken/Überspringen.", + "settings_operator_write": "Ausgewählten Nutzern Erstellen, Bearbeiten & Löschen erlauben", + "settings_operator_write_desc": "Aus: nur Admins können Inhalte ändern. Ein: die ausgewählten Nutzer unten erhalten ebenfalls Vollzugriff.", + "no_non_admin_users": "Keine Non-Admin-User gefunden. Lege welche unter Einstellungen → Personen an.", + "owner_label": "Owner", + "feat_completion_actions": "Completion-Actions", + "feat_completion_actions_desc": "Pro Aufgabe HA-Action beim Abschluss konfigurieren + Quick-Complete-QR mit voreingestellten Werten.", + "on_complete_action_title": "Beim Abschluss: HA-Action auslösen (optional)", + "on_complete_action_desc": "Ruft beim Erledigen der Aufgabe einen HA-Service auf — z. B. einen Zähler am Gerät zurücksetzen.", + "on_complete_action_service": "Service", + "on_complete_action_target": "Ziel-Entität", + "on_complete_action_target_hint": "Achtung: Domain der Entität muss zum Service passen — z. B. 'button.press' nur für button.*, 'counter.increment' nur für counter.*, 'input_button.press' nur für input_button.* etc. Bei Mismatch fired die Aktion nicht (HA loggt 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Daten (JSON, optional)", + "on_complete_action_test": "Konfiguration prüfen", + "on_complete_action_test_success": "✓ Konfiguration gültig (Aktion wird erst beim Abschluss ausgeführt)", + "on_complete_action_test_failed": "Konfigurationsfehler", + "quick_complete_defaults_title": "Schnell-Abschluss-Standardwerte (für QR-Scans, optional)", + "quick_complete_defaults_desc": "Voreingestellte Werte für Schnell-Abschluss-QR. Ohne Werte öffnet der QR den Abschluss-Dialog.", + "quick_complete_defaults_notes": "Notizen", + "quick_complete_defaults_cost": "Kosten", + "quick_complete_defaults_duration": "Dauer (Minuten)", + "quick_complete_defaults_feedback_none": "Kein Feedback", + "quick_complete_defaults_feedback_needed": "War notwendig", + "quick_complete_defaults_feedback_not_needed": "War nicht notwendig", + "quick_complete_success": "Schnell als erledigt markiert", + "show_all_objects": "Alle Objekte anzeigen", + "show_all_tasks": "Filter zurücksetzen — alle Aufgaben anzeigen", + "filter_to_overdue": "Auf überfällige Aufgaben filtern", + "filter_to_due_soon": "Auf bald fällige Aufgaben filtern", + "filter_to_triggered": "Auf ausgelöste Aufgaben filtern", + "open_task": "Aufgabe öffnen", + "show_details": "Verlauf + Statistik anzeigen", + "hide_details": "Details ausblenden", + "history_empty": "Noch keine Einträge.", + "history_edit_button": "Eintrag bearbeiten", + "total_cost": "Gesamtkosten", + "times_performed": "Erledigt", + "older_entries": "ältere", + "open_in_panel": "Im Wartungspanel öffnen", + "skip_reason": "Übersprungen-Grund (optional)", + "reset_to_date": "last_performed setzen auf", + "delete_task_confirm": "Diese Aufgabe und ihren Verlauf löschen?", + "delete_object_confirm": "Dieses Objekt und alle seine Aufgaben löschen?", + "loading": "Laden…", + "archive": "Archivieren", + "undo": "Rückgängig", + "task_archived": "Aufgabe archiviert", + "object_archived": "Objekt archiviert", + "unarchive": "Wiederherstellen", + "archived": "Archiviert", + "show_archived": "Archivierte anzeigen", + "hide_archived": "Archivierte ausblenden", + "confirm_archive_object": "Dieses Objekt und seine Aufgaben archivieren? Verlauf bleibt erhalten, kann später wiederhergestellt werden.", + "settings_archive": "Archiv & Aufbewahrung", + "settings_archive_desc": "Erledigte einmalige Aufgaben archivieren statt löschen. Archivierte Einträge sind ausgeblendet und inaktiv, behalten aber Verlauf und Kosten.", + "settings_archive_oneoff_days": "Erledigte einmalige Aufgaben automatisch archivieren nach (Tage, 0 = aus)", + "settings_delete_archived_oneoff_days": "Archivierte einmalige Aufgaben automatisch löschen nach (Tage, 0 = nie)", + "archive_object": "Objekt archivieren", + "unarchive_object": "Objekt wiederherstellen", + "documents": "Dokumente", + "documents_empty": "Noch keine Dokumente.", + "doc_upload": "Datei hochladen", + "doc_uploading": "Wird hochgeladen…", + "doc_add_link": "Link hinzufügen", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titel (optional)", + "doc_open": "Öffnen", + "doc_delete_confirm": "„{name}“ löschen?", + "doc_too_large": "Datei ist zu groß (max. 25 MB).", + "doc_upload_failed": "Upload fehlgeschlagen.", + "completion_photo_optional": "Abschlussfoto (optional)", + "add_photo": "Foto hinzufügen", + "uploading": "Wird hochgeladen…", + "remove": "Entfernen", + "doc_deduped": "Bereits anderswo gespeichert — geteilt, kein zusätzlicher Speicher.", + "doc_dup_in_object": "Diese Datei ist bereits an diesem Objekt angehängt.", + "doc_link_invalid": "Nur http/https-Links sind erlaubt.", + "doc_cat_manual": "Anleitung", + "doc_cat_warranty": "Garantie", + "doc_cat_invoice": "Rechnung", + "doc_cat_spare_parts": "Ersatzteile", + "doc_cat_photo": "Foto", + "doc_cat_other": "Sonstiges", + "doc_link_badge": "Link", + "doc_storage_title": "Dokumentenspeicher", + "doc_storage_saved": "Durch Deduplizierung gespart", + "doc_storage_refresh": "Aktualisieren", + "doc_download": "Herunterladen", + "doc_close": "Schließen", + "doc_camera": "Foto aufnehmen", + "doc_drop_hint": "Dateien hier ablegen", + "doc_task_none": "Keine Dokumente mit dieser Aufgabe verknüpft.", + "doc_link_existing": "Dokument verknüpfen…", + "doc_attach": "Verknüpfen", + "doc_unlink": "Verknüpfung lösen", + "doc_page": "Seite", + "chart_range_7d": "7T", + "chart_range_30d": "30T", + "chart_range_90d": "90T", + "chart_range_1y": "1J", + "chart_since_service": "seit letzter Wartung", + "chart_no_stats": "Keine Langzeit-Statistik für diese Entity — es werden nur Werte aus Wartungseinträgen angezeigt", + "auto_complete_on_recovery": "Bei Sensor-Erholung automatisch abschließen", + "auto_complete_on_recovery_help": "Verbucht eine Wartung (setzt „zuletzt durchgeführt“), sobald sich der Auslöser von selbst auflöst — z. B. Salz nachgefüllt, Filter getauscht.", + "doc_search": "Dokumente durchsuchen…", + "doc_search_none": "Keine passenden Dokumente", + "link_device_optional": "Mit bestehendem Gerät verknüpfen (optional)", + "parent_object_optional": "Übergeordnetes Objekt (optional)", + "parent_none": "(Kein übergeordnetes Objekt)", + "paused": "Pausiert", + "pause_object": "Pausieren", + "resume_object": "Fortsetzen", + "pause_until_prompt": "Zeitpläne dieses Objekts einfrieren — nichts wird fällig und nichts benachrichtigt, bis es fortgesetzt wird. Optional ein Datum für die automatische Fortsetzung festlegen.", + "pause_until_label": "Fortsetzen am (optional)", + "object_paused": "Objekt pausiert", + "object_resumed": "Objekt fortgesetzt — Zeitpläne neu gestartet", + "object_paused_badge": "Pausiert", + "paused_until_label": "bis", + "replace_object": "Ersetzen…", + "replace_object_prompt": "Dieses Objekt ausmustern und einen Nachfolger anlegen. Historie und Kosten bleiben am alten Objekt archiviert; Aufgaben und Dokumente wandern zum neuen, Zähler starten frisch.", + "replace_name_label": "Name des Nachfolgers", + "object_replaced": "Objekt ersetzt — Nachfolger angelegt", + "reading_unit_label": "Ableseeinheit (z. B. kWh, m³)", + "reading_unit_help": "Wird beim Erledigen dieser Aufgabe neben dem erfassten Wert angezeigt.", + "reading_value_label": "Ablesewert", + "reading_label": "Ablesung", + "settings_templates_label": "Vorlagen-Galerie", + "settings_templates_hint": "Vorlagen abwählen, die du nie brauchst — sie verschwinden aus den \"Aus Vorlage\"-Auswahlen (Panel und Config-Flow). Sonst ändert sich nichts; jederzeit wieder aktivierbar.", + "worksheet": "Arbeitsblatt", + "worksheet_scan_view": "Scannen, um die Aufgabe zu öffnen", + "worksheet_scan_complete": "Scannen zum Erledigen", + "worksheet_manual_excerpt": "Handbuch-Auszug", + "worksheet_pages": "Seiten", + "worksheet_printed": "Gedruckt", + "worksheet_never": "Nie" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/en.json b/custom_components/maintenance_supporter/frontend-src/locales/en.json new file mode 100644 index 0000000..2b373cb --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/en.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Maintenance", + "objects": "Objects", + "tasks": "Tasks", + "overdue": "Overdue", + "due_soon": "Due Soon", + "triggered": "Triggered", + "trigger_replaced": "Trigger replaced", + "ok": "OK", + "all": "All", + "new_object": "+ New Object", + "templates_from": "From template", + "templates_title": "Start from a template", + "templates_task_count": "{n} tasks", + "template_created": "Created from template", + "onboard_hint": "Add your first object to start tracking maintenance.", + "edit": "Edit", + "duplicate": "Duplicate", + "task_duplicated": "Task duplicated", + "object_duplicated": "Object duplicated", + "delete": "Delete", + "add_task": "+ Add Task", + "complete": "Complete", + "completed": "Completed", + "skip": "Skip", + "skipped": "Skipped", + "missed": "Missed", + "reset": "Reset", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Cancel", + "bulk_select": "Select", + "bulk_select_all": "Select all", + "bulk_n_selected": "{n} selected", + "bulk_completed": "{n} tasks completed", + "bulk_archived": "{n} tasks archived", + "completing": "Completing…", + "interval": "Interval", + "warning": "Warning", + "last_performed": "Last performed", + "next_due": "Next due", + "days_until_due": "Days until due", + "avg_duration": "Avg duration", + "trigger": "Trigger", + "trigger_type": "Trigger type", + "threshold_above": "Upper limit", + "threshold_below": "Lower limit", + "threshold": "Threshold", + "counter": "Counter", + "state_change": "State change", + "runtime": "Runtime", + "runtime_hours": "Target runtime (hours)", + "target_value": "Target value", + "baseline": "Baseline", + "target_changes": "Target changes", + "for_minutes": "For (minutes)", + "time_based": "Time-based", + "sensor_based": "Sensor-based", + "manual": "Manual", + "one_time": "One-time", + "weekdays": "Weekdays", + "nth_weekday": "Nth weekday of month", + "day_of_month": "Day of month", + "recurrence_on_days": "Repeat on", + "recurrence_occurrence": "Occurrence", + "recurrence_weekday": "Weekday", + "recurrence_day": "Day of month (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1st", + "ord_2": "2nd", + "ord_3": "3rd", + "ord_4": "4th", + "ord_5": "5th", + "ord_last": "Last", + "day_word": "Day", + "interval_value": "Interval", + "interval_unit": "Unit", + "unit_days": "Days", + "unit_weeks": "Weeks", + "unit_months": "Months", + "unit_years": "Years", + "due_date": "Due date", + "cleaning": "Cleaning", + "inspection": "Inspection", + "replacement": "Replacement", + "calibration": "Calibration", + "service": "Service", + "reading": "Reading", + "custom": "Custom", + "history": "History", + "cost": "Cost", + "report_button": "Report", + "report_title": "Maintenance report", + "report_generated": "Generated", + "report_times_done": "Done", + "report_total_cost": "Total cost", + "report_every": "every {n} {unit}", + "report_notes": "Notes", + "report_col_type": "Type", + "report_col_status": "Status", + "report_col_schedule": "Schedule", + "duration": "Duration", + "both": "Both", + "trigger_val": "Trigger value", + "complete_title": "Complete: ", + "checklist": "Checklist", + "checklist_steps_optional": "Checklist steps (optional)", + "checklist_placeholder": "Clean filter\nReplace seal\nTest pressure", + "checklist_help": "One step per line. Max 100 items.", + "err_too_long": "{field}: too long (max {n} characters)", + "err_too_short": "{field}: too short (min {n} characters)", + "err_value_too_high": "{field}: too large (max {n})", + "err_value_too_low": "{field}: too small (min {n})", + "err_required": "{field}: required", + "err_wrong_type": "{field}: wrong type (expected: {type})", + "err_invalid_choice": "{field}: not an allowed value", + "err_invalid_value": "{field}: invalid value", + "feat_schedule_time": "Time-of-day scheduling", + "feat_schedule_time_desc": "Tasks become overdue at a specific time of day instead of midnight.", + "schedule_time_optional": "Due at time (optional, HH:MM)", + "schedule_time_help": "Empty = midnight (default). HA timezone.", + "at_time": "at", + "notes_optional": "Notes (optional)", + "cost_optional": "Cost (optional)", + "duration_minutes": "Duration in minutes (optional)", + "days": "days", + "day": "day", + "today": "Today", + "d_overdue": "d overdue", + "no_tasks": "No maintenance tasks yet. Create an object to get started.", + "no_tasks_short": "No tasks", + "no_history": "No history entries yet.", + "show_all": "Show all", + "cost_duration_chart": "Cost & Duration", + "installed": "Installed", + "confirm_delete_object": "Delete this object and all its tasks?", + "confirm_delete_task": "Delete this task?", + "min": "Min", + "max": "Max", + "save": "Save", + "saving": "Saving…", + "edit_task": "Edit Task", + "new_task": "New Maintenance Task", + "task_name": "Task name", + "maintenance_type": "Maintenance type", + "priority": "Priority", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Low", + "priority_normal": "Normal", + "priority_high": "High", + "schedule_type": "Schedule type", + "interval_days": "Interval (days)", + "warning_days": "Warning days", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Last performed (optional)", + "interval_anchor": "Interval anchor", + "anchor_completion": "From completion date", + "anchor_planned": "From planned date (no drift)", + "edit_object": "Edit Object", + "name": "Name", + "manufacturer_optional": "Manufacturer (optional)", + "model_optional": "Model (optional)", + "serial_number_optional": "Serial number (optional)", + "serial_number_label": "S/N", + "documentation_url_label": "Manual", + "object_notes_label": "Notes", + "sort_due_date": "Due date", + "sort_object": "Object name", + "sort_type": "Type", + "sort_task_name": "Task name", + "all_objects": "All objects", + "tasks_lower": "tasks", + "no_tasks_yet": "No tasks yet", + "add_first_task": "Add first task", + "trigger_configuration": "Trigger Configuration", + "entity_id": "Entity ID", + "comma_separated": "comma-separated", + "entity_logic": "Entity logic", + "entity_logic_any": "Any entity triggers", + "entity_logic_all": "All entities must trigger", + "entities": "entities", + "attribute_optional": "Attribute (optional, blank = state)", + "use_entity_state": "Use entity state (no attribute)", + "trigger_above": "Trigger above", + "trigger_below": "Trigger below", + "for_at_least_minutes": "For at least (minutes)", + "safety_interval_days": "Safety interval (days, optional)", + "safety_interval": "Safety interval (optional)", + "delta_mode": "Delta mode", + "from_state_optional": "From state (optional)", + "to_state_optional": "To state (optional)", + "documentation_url_optional": "Documentation URL (optional)", + "object_notes_optional": "Notes (optional)", + "nfc_tag_id_optional": "NFC Tag ID (optional)", + "nfc_tags_empty_help": "No NFC tags registered in Home Assistant yet.", + "nfc_tags_open_settings": "Open Tags settings", + "nfc_tags_refresh": "Refresh", + "environmental_entity_optional": "Environmental sensor (optional)", + "environmental_entity_helper": "e.g. sensor.outdoor_temperature — adjusts the interval based on environmental conditions", + "environmental_attribute_optional": "Environmental attribute (optional)", + "nfc_tag_id": "NFC Tag ID", + "nfc_linked": "NFC tag linked", + "nfc_link_hint": "Click to link NFC tag", + "responsible_user": "Responsible User", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(No user assigned)", + "all_users": "All Users", + "my_tasks": "My Tasks", + "tab_calendar": "Calendar", + "cal_no_events": "No maintenance", + "cal_window_7": "7 days", + "cal_window_14": "14 days", + "cal_window_30": "30 days", + "cal_window_365": "1 year", + "cal_every_n_days": "every {n} days", + "cal_source_time": "Time-based", + "cal_source_time_adaptive": "Time-based (adaptive)", + "cal_source_sensor": "Sensor-based", + "cal_predicted": "predicted", + "cal_confidence_high": "high confidence", + "cal_confidence_medium": "medium confidence", + "cal_confidence_low": "low confidence", + "budget_monthly": "Monthly budget", + "budget_yearly": "Yearly budget", + "groups": "Groups", + "new_group": "New group", + "edit_group": "Edit group", + "no_groups": "No groups yet", + "delete_group": "Delete group", + "delete_group_confirm": "Delete group '{name}'?", + "group_select_tasks": "Select tasks", + "group_name_required": "Name is required", + "description_optional": "Description (optional)", + "selected": "Selected", + "loading_chart": "Loading chart data...", + "hide_outliers": "Hide outliers (sensor glitches)", + "was_maintenance_needed": "Was this maintenance needed?", + "feedback_needed": "Needed", + "feedback_not_needed": "Not needed", + "feedback_not_sure": "Not sure", + "suggested_interval": "Suggested interval", + "apply_suggestion": "Apply", + "reanalyze": "Re-analyze", + "reanalyze_result": "New analysis", + "reanalyze_insufficient_data": "Not enough data to produce a recommendation", + "data_points": "data points", + "dismiss_suggestion": "Dismiss", + "confidence_low": "Low", + "confidence_medium": "Medium", + "confidence_high": "High", + "recommended": "recommended", + "seasonal_awareness": "Seasonal Awareness", + "edit_seasonal_overrides": "Edit seasonal factors", + "seasonal_overrides_title": "Seasonal factors (override)", + "seasonal_overrides_hint": "Factor per month (0.1–5.0). Empty = learned automatically.", + "seasonal_override_invalid": "Invalid value", + "seasonal_override_range": "Factor must be between 0.1 and 5.0", + "clear_all": "Clear all", + "seasonal_chart_title": "Seasonal Factors", + "seasonal_learned": "Learned", + "seasonal_manual": "Manual", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "May", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Oct", + "month_nov": "Nov", + "month_dec": "Dec", + "sensor_prediction": "Sensor Prediction", + "degradation_trend": "Trend", + "trend_rising": "Rising", + "trend_falling": "Falling", + "trend_stable": "Stable", + "trend_insufficient_data": "Insufficient data", + "days_until_threshold": "Days until threshold", + "threshold_exceeded": "Threshold exceeded", + "environmental_adjustment": "Environmental factor", + "sensor_prediction_urgency": "Sensor predicts threshold in ~{days} days", + "day_short": "day", + "weibull_reliability_curve": "Reliability Curve", + "weibull_failure_probability": "Failure Probability", + "weibull_r_squared": "Fit R²", + "beta_early_failures": "Early Failures", + "beta_random_failures": "Random Failures", + "beta_wear_out": "Wear-out", + "beta_highly_predictable": "Highly Predictable", + "confidence_interval": "Confidence Interval", + "confidence_conservative": "Conservative", + "confidence_aggressive": "Optimistic", + "current_interval_marker": "Current interval", + "recommended_marker": "Recommended", + "characteristic_life": "Characteristic life", + "chart_mini_sparkline": "Trend sparkline", + "chart_history": "Cost and duration history", + "chart_seasonal": "Seasonal factors, 12 months", + "chart_weibull": "Weibull reliability curve", + "chart_sparkline": "Sensor trigger value chart", + "days_progress": "Days progress", + "qr_code": "QR Code", + "qr_generating": "Generating QR code…", + "qr_error": "Failed to generate QR code.", + "qr_error_no_url": "No HA URL configured. Please set an external or internal URL in Settings → System → Network.", + "save_error": "Failed to save. Please try again.", + "qr_print": "Print", + "qr_download": "Download SVG", + "qr_action": "Action on scan", + "qr_action_view": "View maintenance info", + "qr_action_complete": "Mark maintenance as complete", + "qr_url_mode": "Link type", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Local (mDNS)", + "qr_mode_server": "Server URL", + "overview": "Overview", + "analysis": "Analysis", + "recent_activities": "Recent Activities", + "search_notes": "Search notes", + "avg_cost": "Avg Cost", + "no_advanced_features": "No advanced features enabled", + "no_advanced_features_hint": "Enable “Adaptive Intervals” or “Seasonal Patterns” in the integration settings to see analysis data here.", + "analysis_not_enough_data": "Not enough data for analysis yet.", + "analysis_not_enough_data_hint": "Weibull analysis requires at least 5 completed maintenances; seasonal patterns become visible after 6+ data points per month.", + "analysis_manual_task_hint": "Manual tasks without an interval do not generate analysis data.", + "completions": "completions", + "current": "Current", + "shorter": "Shorter", + "longer": "Longer", + "normal": "Normal", + "disabled": "Disabled", + "compound_logic": "Compound logic", + "compound": "Compound (multiple conditions)", + "compound_logic_and": "AND — all conditions must trigger", + "compound_logic_or": "OR — any condition triggers", + "compound_help": "Combine several sensor conditions into one trigger.", + "compound_no_conditions": "No conditions yet — add at least one.", + "compound_add_condition": "Add condition", + "compound_condition": "Condition", + "compound_remove_condition": "Remove condition", + "card_title": "Title", + "card_show_header": "Show header with statistics", + "card_show_actions": "Show action buttons", + "card_compact": "Compact mode", + "card_max_items": "Max items (0 = all)", + "card_filter_status": "Filter by status", + "card_filter_status_help": "Empty = show all statuses.", + "card_filter_objects": "Filter by objects", + "card_filter_objects_help": "Empty = show all objects.", + "card_filter_entities": "Filter by entities (entity_ids)", + "card_filter_entities_help": "Pick sensor / binary_sensor entities from this integration. Empty = all.", + "card_loading_objects": "Loading objects…", + "card_load_error": "Could not load objects — check the WebSocket connection.", + "card_no_tasks_title": "No maintenance tasks yet", + "card_no_tasks_cta": "→ Create one in the Maintenance panel", + "no_objects": "No objects yet.", + "action_error": "Action failed. Please try again.", + "area_id_optional": "Area (optional)", + "installation_date_optional": "Installation date (optional)", + "warranty_expiry_optional": "Warranty expiry (optional)", + "warranty": "Warranty", + "warranty_valid_until": "valid until {date}", + "warranty_expires_in": "expires in {days} days", + "warranty_expired": "expired", + "cal_past_windows": "Past windows", + "cal_forward_windows": "Forward windows", + "history_edit_title": "Edit history entry", + "history_edit_timestamp": "Timestamp", + "manufacturer": "Manufacturer", + "model": "Model", + "area": "Area", + "actions": "Actions", + "view_mode_label": "View", + "view_cards": "Card view", + "view_table": "Table view", + "objects_table_columns_label": "Objects table columns", + "objects_table_columns_hint": "Choose which columns appear in the objects table view.", + "custom_icon_optional": "Icon (optional, e.g. mdi:wrench)", + "task_enabled": "Task enabled", + "skip_reason_prompt": "Skip this task?", + "reason_optional": "Reason (optional)", + "reset_date_prompt": "Mark task as performed?", + "reset_date_optional": "Last performed date (optional, defaults to today)", + "notes_label": "Notes", + "documentation_label": "Documentation", + "no_nfc_tag": "— No tag —", + "dashboard": "Dashboard", + "tab_today": "Today", + "palette_placeholder": "Search objects and tasks…", + "palette_no_results": "No matches", + "palette_hint": "↑↓ to navigate · Enter to open · Esc to close", + "today_all_caught_up": "All caught up! Nothing due this week.", + "today_overdue": "Overdue", + "today_due_today": "Due today", + "today_this_week": "This week", + "settings": "Settings", + "settings_features": "Advanced Features", + "settings_features_desc": "Enable or disable advanced features. Disabling hides them from the UI but does not delete data.", + "feat_adaptive": "Adaptive Scheduling", + "feat_adaptive_desc": "Learn optimal intervals from maintenance history", + "feat_predictions": "Sensor Predictions", + "feat_predictions_desc": "Predict trigger dates from sensor degradation", + "feat_seasonal": "Seasonal Adjustments", + "feat_seasonal_desc": "Adjust intervals based on seasonal patterns", + "feat_environmental": "Environmental Correlation", + "feat_environmental_desc": "Correlate intervals with temperature/humidity", + "feat_budget": "Budget Tracking", + "feat_budget_desc": "Track monthly and yearly maintenance spending", + "feat_groups": "Task Groups", + "feat_groups_desc": "Organize tasks into logical groups", + "feat_checklists": "Checklists", + "feat_checklists_desc": "Multi-step procedures for task completion", + "settings_general": "General", + "settings_default_warning": "Default warning days", + "settings_panel_enabled": "Sidebar panel", + "settings_panel_title": "Sidebar panel title", + "settings_notifications": "Notifications", + "settings_notify_service": "Notification service", + "test_notification": "Test notification", + "send_test": "Send test", + "testing": "Sending…", + "test_notification_success": "Test notification sent", + "test_notification_failed": "Test notification failed", + "settings_notify_due_soon": "Notify when due soon", + "settings_notify_overdue": "Notify when overdue", + "settings_notify_triggered": "Notify when triggered", + "settings_interval_hours": "Repeat interval (hours, 0 = once)", + "settings_quiet_hours": "Quiet hours", + "settings_quiet_start": "Start", + "settings_quiet_end": "End", + "settings_max_per_day": "Max notifications per day (0 = unlimited)", + "settings_bundling": "Bundle notifications", + "settings_bundle_threshold": "Bundle threshold", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobile Action Buttons", + "settings_action_complete": "Show 'Complete' button", + "settings_action_skip": "Show 'Skip' button", + "settings_action_snooze": "Show 'Snooze' button", + "settings_weekly_digest": "Weekly digest", + "settings_weekly_digest_hint": "A single summary notification on Monday morning when tasks are due.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Snooze duration (hours)", + "settings_budget": "Budget", + "settings_currency": "Currency", + "settings_budget_monthly": "Monthly budget", + "settings_budget_yearly": "Yearly budget", + "settings_budget_alerts": "Budget alerts", + "settings_budget_threshold": "Alert threshold (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Export JSON", + "settings_export_yaml": "Export YAML", + "settings_export_csv": "Export CSV", + "settings_import_csv": "Import CSV", + "settings_import_placeholder": "Paste JSON or CSV content here…", + "settings_import_btn": "Import", + "settings_import_success": "{count} objects imported successfully.", + "settings_export_success": "Export downloaded.", + "settings_saved": "Setting saved.", + "settings_include_history": "Include history", + "sort_alphabetical": "Alphabetical", + "sort_due_soonest": "Due soonest", + "sort_task_count": "Task count", + "sort_area": "Area", + "sort_assigned_user": "Assigned user", + "sort_group": "Group", + "groupby_none": "No grouping", + "groupby_area": "By area", + "groupby_group": "By group", + "groupby_user": "By user", + "filter_label": "Filter", + "user_label": "User", + "sort_label": "Sort", + "group_by_label": "Group by", + "state_value_help": "Use the HA state value (usually lowercase, e.g. \"on\"/\"off\"). Case is normalised on save.", + "target_changes_help": "Number of matching transitions before the trigger fires (default: 1).", + "qr_print_title": "Print QR codes", + "qr_print_desc": "Generate a printable page of QR codes to cut out and stick on your equipment.", + "qr_print_load": "Load objects", + "qr_print_filter": "Filter", + "qr_print_objects": "Objects", + "qr_print_actions": "Actions", + "qr_print_url_mode": "Link type", + "qr_print_estimate": "Estimated QR codes", + "qr_print_over_limit": "cap is 200, narrow the filter", + "qr_print_generate": "Generate QR codes", + "qr_print_generating": "Generating…", + "qr_print_ready": "QR codes ready", + "qr_print_print_button": "Print", + "qr_print_empty": "Nothing to generate", + "qr_action_skip": "Skip", + "vacation_title": "Vacation mode", + "vacation_active": "active", + "vacation_ended": "ended", + "vacation_desc": "Plan a vacation: notifications are paused during the period plus a buffer of days. You can opt specific tasks back in.", + "vacation_enable": "Enable vacation mode", + "vacation_start": "Start", + "vacation_end": "End", + "vacation_buffer": "Buffer (days)", + "vacation_exempt_title": "Notify anyway during vacation", + "vacation_exempt_desc": "Pick tasks that should still notify during vacation (e.g. critical pool chemistry).", + "vacation_load_tasks": "Load tasks", + "vacation_preview_btn": "Show preview", + "vacation_preview_affected": "tasks affected", + "vacation_event_due_soon": "becomes due soon", + "vacation_event_overdue": "becomes overdue", + "vacation_event_triggered_est": "sensor trigger possible", + "vacation_sensor_based": "(sensor-based)", + "vacation_action_notify": "Notify anyway", + "vacation_action_unsilence": "Silence again", + "vacation_marked_complete": "Marked complete", + "vacation_marked_skip": "Skipped", + "vacation_end_now": "End vacation now", + "add": "Add", + "show_stats": "Show stats + graphs", + "hide_stats": "Hide stats", + "adaptive_no_data": "Not enough completion history yet for adaptive analysis. Complete this task a few more times to unlock interval recommendations and reliability charts.", + "suggestion_applied": "Suggested interval applied", + "vacation_mode": "Vacation mode", + "vacation_status_active": "Active now", + "vacation_status_scheduled": "Scheduled", + "vacation_status_inactive": "Inactive", + "vacation_end_now_confirm": "End vacation immediately?", + "vacation_exempt_count": "exempt", + "vacation_advanced": "Advanced…", + "vacation_open_panel": "Open in panel", + "enable": "Enable", + "saved": "Saved", + "budget_monthly_set": "Set monthly", + "budget_yearly_set": "Set yearly", + "budget_advanced": "Currency, alerts…", + "budget_open_panel": "Open in panel", + "groups_empty": "No groups yet.", + "group_new_placeholder": "Add group…", + "group_delete_confirm": "Delete group \"{name}\"?", + "groups_manage_tasks": "Manage task assignments…", + "groups_open_panel": "Open in panel", + "unassigned": "Unassigned", + "no_area": "No area", + "has_overdue": "Has overdue tasks", + "object": "Object", + "settings_panel_access": "Panel access", + "settings_panel_access_desc": "Admins always have full access. To delegate create, edit and delete to specific non-admins, switch this on and pick them below — everyone else sees only Complete and Skip.", + "settings_operator_write": "Allow selected users to create, edit & delete", + "settings_operator_write_desc": "Off: only admins can change content. On: the selected users below get full access too.", + "no_non_admin_users": "No non-admin users found. Add some in Settings → People.", + "owner_label": "Owner", + "feat_completion_actions": "Completion actions", + "feat_completion_actions_desc": "Per-task HA action on complete + quick-complete QR with pre-set values.", + "on_complete_action_title": "On complete: trigger HA action (optional)", + "on_complete_action_desc": "Calls an HA service when the task is completed — e.g. reset a counter on the device.", + "on_complete_action_service": "Service", + "on_complete_action_target": "Target entity", + "on_complete_action_target_hint": "Note: the entity domain must match the service — e.g. 'button.press' only works on button.*, 'counter.increment' only on counter.*, 'input_button.press' only on input_button.* etc. On a mismatch the action will silently fail (HA logs 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Data (JSON, optional)", + "on_complete_action_test": "Validate configuration", + "on_complete_action_test_success": "✓ Configuration valid (action will fire only on task completion)", + "on_complete_action_test_failed": "Failed", + "quick_complete_defaults_title": "Quick-complete defaults (for QR scans, optional)", + "quick_complete_defaults_desc": "Pre-set values for quick-complete QR scans. Without these, the QR opens the complete dialog.", + "quick_complete_defaults_notes": "Notes", + "quick_complete_defaults_cost": "Cost", + "quick_complete_defaults_duration": "Duration (minutes)", + "quick_complete_defaults_feedback_none": "No feedback", + "quick_complete_defaults_feedback_needed": "Was needed", + "quick_complete_defaults_feedback_not_needed": "Not needed", + "quick_complete_success": "Quickly marked complete", + "show_all_objects": "Show all objects", + "show_all_tasks": "Clear filter — show all tasks", + "filter_to_overdue": "Filter task list to overdue only", + "filter_to_due_soon": "Filter task list to due-soon only", + "filter_to_triggered": "Filter task list to triggered only", + "open_task": "Open task", + "show_details": "Show history + stats", + "hide_details": "Hide details", + "history_empty": "No history yet.", + "history_edit_button": "Edit entry", + "total_cost": "Total cost", + "times_performed": "Performed", + "older_entries": "older", + "open_in_panel": "Open in Maintenance panel", + "skip_reason": "Skip reason (optional)", + "reset_to_date": "Reset last_performed to", + "delete_task_confirm": "Delete this task and its history?", + "delete_object_confirm": "Delete this object and all its tasks?", + "loading": "Loading…", + "archive": "Archive", + "undo": "Undo", + "task_archived": "Task archived", + "object_archived": "Object archived", + "unarchive": "Unarchive", + "archived": "Archived", + "show_archived": "Show archived", + "hide_archived": "Hide archived", + "confirm_archive_object": "Archive this object and its tasks? They keep their history and can be unarchived later.", + "settings_archive": "Archive & Retention", + "settings_archive_desc": "Retire completed one-off tasks without deleting them. Archived items are hidden and inert but keep their history and cost.", + "settings_archive_oneoff_days": "Auto-archive completed one-off tasks after (days, 0 = off)", + "settings_delete_archived_oneoff_days": "Auto-delete archived one-off tasks after (days, 0 = never)", + "archive_object": "Archive object", + "unarchive_object": "Unarchive object", + "documents": "Documents", + "documents_empty": "No documents yet.", + "doc_upload": "Upload file", + "doc_uploading": "Uploading…", + "doc_add_link": "Add link", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Title (optional)", + "doc_open": "Open", + "doc_delete_confirm": "Delete \"{name}\"?", + "doc_too_large": "File is too large (max 25 MB).", + "doc_upload_failed": "Upload failed.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Already stored elsewhere — shared, no extra space used.", + "doc_dup_in_object": "This file is already attached to this object.", + "doc_link_invalid": "Only http/https links are allowed.", + "doc_cat_manual": "Manual", + "doc_cat_warranty": "Warranty", + "doc_cat_invoice": "Invoice", + "doc_cat_spare_parts": "Spare parts", + "doc_cat_photo": "Photo", + "doc_cat_other": "Other", + "doc_link_badge": "Link", + "doc_storage_title": "Document storage", + "doc_storage_saved": "Saved via deduplication", + "doc_storage_refresh": "Refresh", + "doc_download": "Download", + "doc_close": "Close", + "doc_camera": "Take photo", + "doc_drop_hint": "Drop files here", + "doc_task_none": "No documents linked to this task.", + "doc_link_existing": "Link a document…", + "doc_attach": "Link", + "doc_unlink": "Unlink", + "doc_page": "Page", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1y", + "chart_since_service": "since last service", + "chart_no_stats": "No long-term statistics for this entity — showing maintenance-event values only", + "auto_complete_on_recovery": "Auto-complete when the sensor recovers", + "auto_complete_on_recovery_help": "Records a completion (sets last performed) when the trigger clears itself — e.g. salt refilled, filter replaced.", + "doc_search": "Search documents…", + "doc_search_none": "No matching documents", + "link_device_optional": "Link to existing device (optional)", + "parent_object_optional": "Parent object (optional)", + "parent_none": "(No parent)", + "paused": "Paused", + "pause_object": "Pause", + "resume_object": "Resume", + "pause_until_prompt": "Freeze this object's schedules — nothing becomes due and nothing notifies until it is resumed. Optionally set an auto-resume date.", + "pause_until_label": "Resume on (optional)", + "object_paused": "Object paused", + "object_resumed": "Object resumed — schedules restarted", + "object_paused_badge": "Paused", + "paused_until_label": "until", + "replace_object": "Replace…", + "replace_object_prompt": "Retire this object and create a successor. History and costs stay archived on the old one; tasks and documents carry over to the new one, counters start fresh.", + "replace_name_label": "Successor name", + "object_replaced": "Object replaced — successor created", + "reading_unit_label": "Reading unit (e.g. kWh, m³)", + "reading_unit_help": "Shown next to the recorded value when completing this task.", + "reading_value_label": "Reading value", + "reading_label": "Reading", + "settings_templates_label": "Template gallery", + "settings_templates_hint": "Untick templates you'll never need — they disappear from the \"From template\" pickers (panel and config flow). Nothing else changes; you can re-enable them any time.", + "worksheet": "Work sheet", + "worksheet_scan_view": "Scan to open the task", + "worksheet_scan_complete": "Scan to complete", + "worksheet_manual_excerpt": "Manual excerpt", + "worksheet_pages": "pages", + "worksheet_printed": "Printed", + "worksheet_never": "Never" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/es.json b/custom_components/maintenance_supporter/frontend-src/locales/es.json new file mode 100644 index 0000000..bd44e4b --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/es.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Mantenimiento", + "objects": "Objetos", + "tasks": "Tareas", + "overdue": "Vencida", + "due_soon": "Próxima", + "triggered": "Activada", + "ok": "OK", + "all": "Todos", + "new_object": "+ Nuevo objeto", + "templates_from": "Desde plantilla", + "templates_title": "Empezar desde una plantilla", + "templates_task_count": "{n} tareas", + "template_created": "Creado desde plantilla", + "onboard_hint": "Añade tu primer objeto para empezar a seguir el mantenimiento.", + "edit": "Editar", + "duplicate": "Duplicar", + "task_duplicated": "Tarea duplicada", + "object_duplicated": "Objeto duplicado", + "delete": "Eliminar", + "add_task": "+ Tarea", + "complete": "Completada", + "completed": "Completada", + "skip": "Omitir", + "skipped": "Omitida", + "missed": "Missed", + "reset": "Restablecer", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Cancelar", + "bulk_select": "Seleccionar", + "bulk_select_all": "Seleccionar todo", + "bulk_n_selected": "{n} seleccionados", + "bulk_completed": "{n} tareas completadas", + "bulk_archived": "{n} tareas archivadas", + "completing": "Completando…", + "interval": "Intervalo", + "warning": "Aviso", + "last_performed": "Última ejecución", + "next_due": "Próximo vencimiento", + "days_until_due": "Días hasta vencimiento", + "avg_duration": "Ø Duración", + "trigger": "Disparador", + "trigger_type": "Tipo de disparador", + "threshold_above": "Límite superior", + "threshold_below": "Límite inferior", + "threshold": "Umbral", + "counter": "Contador", + "state_change": "Cambio de estado", + "runtime": "Tiempo de funcionamiento", + "runtime_hours": "Duración objetivo (horas)", + "target_value": "Valor objetivo", + "baseline": "Línea base", + "target_changes": "Cambios objetivo", + "for_minutes": "Durante (minutos)", + "time_based": "Temporal", + "sensor_based": "Sensor", + "manual": "Manual", + "one_time": "Una vez", + "weekdays": "Días de la semana", + "nth_weekday": "N-ésimo día de la semana del mes", + "day_of_month": "Día del mes", + "recurrence_on_days": "Repetir los", + "recurrence_occurrence": "Aparición", + "recurrence_weekday": "Día de la semana", + "recurrence_day": "Día del mes (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.º", + "ord_2": "2.º", + "ord_3": "3.º", + "ord_4": "4.º", + "ord_5": "5.º", + "ord_last": "Último", + "day_word": "Día", + "interval_value": "Intervalo", + "interval_unit": "Unidad", + "unit_days": "Días", + "unit_weeks": "Semanas", + "unit_months": "Meses", + "unit_years": "Años", + "due_date": "Fecha de vencimiento", + "cleaning": "Limpieza", + "inspection": "Inspección", + "replacement": "Sustitución", + "calibration": "Calibración", + "service": "Servicio", + "reading": "Lectura", + "custom": "Personalizado", + "history": "Historial", + "cost": "Coste", + "report_button": "Informe", + "report_title": "Informe de mantenimiento", + "report_generated": "Generado", + "report_times_done": "Hecho", + "report_total_cost": "Coste total", + "report_every": "cada {n} {unit}", + "report_notes": "Notas", + "report_col_type": "Tipo", + "report_col_status": "Estado", + "report_col_schedule": "Programación", + "duration": "Duración", + "both": "Ambos", + "trigger_val": "Valor del disparador", + "complete_title": "Completada: ", + "checklist": "Lista de verificación", + "checklist_steps_optional": "Pasos de la lista de verificación (opcional)", + "checklist_placeholder": "Limpiar filtro\nReemplazar junta\nProbar presión", + "checklist_help": "Un paso por línea. Máx. 100 elementos.", + "err_too_long": "{field}: demasiado largo (máx. {n} caracteres)", + "err_too_short": "{field}: demasiado corto (mín. {n} caracteres)", + "err_value_too_high": "{field}: demasiado grande (máx. {n})", + "err_value_too_low": "{field}: demasiado pequeño (mín. {n})", + "err_required": "{field}: campo obligatorio", + "err_wrong_type": "{field}: tipo incorrecto (esperado: {type})", + "err_invalid_choice": "{field}: valor no permitido", + "err_invalid_value": "{field}: valor inválido", + "feat_schedule_time": "Programación por hora", + "feat_schedule_time_desc": "Las tareas vencen a una hora específica en lugar de medianoche.", + "schedule_time_optional": "Vence a las (opcional, HH:MM)", + "schedule_time_help": "Vacío = medianoche (predeterminado). Zona horaria HA.", + "at_time": "a las", + "notes_optional": "Notas (opcional)", + "cost_optional": "Coste (opcional)", + "duration_minutes": "Duración en minutos (opcional)", + "days": "días", + "day": "día", + "today": "Hoy", + "d_overdue": "d vencida", + "no_tasks": "No hay tareas de mantenimiento. Cree un objeto para empezar.", + "no_tasks_short": "Sin tareas", + "no_history": "Sin entradas en el historial.", + "show_all": "Mostrar todo", + "cost_duration_chart": "Costes & Duración", + "installed": "Instalado", + "confirm_delete_object": "¿Eliminar este objeto y todas sus tareas?", + "confirm_delete_task": "¿Eliminar esta tarea?", + "min": "Mín", + "max": "Máx", + "save": "Guardar", + "saving": "Guardando…", + "edit_task": "Editar tarea", + "new_task": "Nueva tarea de mantenimiento", + "task_name": "Nombre de la tarea", + "maintenance_type": "Tipo de mantenimiento", + "priority": "Prioridad", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Baja", + "priority_normal": "Normal", + "priority_high": "Alta", + "schedule_type": "Tipo de planificación", + "interval_days": "Intervalo (días)", + "warning_days": "Días de aviso", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Última ejecución (opcional)", + "interval_anchor": "Anclaje del intervalo", + "anchor_completion": "Desde la fecha de finalización", + "anchor_planned": "Desde la fecha planificada (sin desviación)", + "edit_object": "Editar objeto", + "name": "Nombre", + "manufacturer_optional": "Fabricante (opcional)", + "model_optional": "Modelo (opcional)", + "serial_number_optional": "Número de serie (opcional)", + "serial_number_label": "N/S", + "documentation_url_label": "Manual", + "object_notes_label": "Notas", + "sort_due_date": "Vencimiento", + "sort_object": "Nombre del objeto", + "sort_type": "Tipo", + "sort_task_name": "Nombre de la tarea", + "all_objects": "Todos los objetos", + "tasks_lower": "tareas", + "no_tasks_yet": "Aún no hay tareas", + "add_first_task": "Agregar primera tarea", + "trigger_configuration": "Configuración del disparador", + "entity_id": "ID de entidad", + "comma_separated": "separados por comas", + "entity_logic": "Lógica de entidad", + "entity_logic_any": "Cualquier entidad activa", + "entity_logic_all": "Todas las entidades deben activar", + "entities": "entidades", + "attribute_optional": "Atributo (opcional, vacío = estado)", + "use_entity_state": "Usar estado de la entidad (sin atributo)", + "trigger_above": "Activar por encima de", + "trigger_below": "Activar por debajo de", + "for_at_least_minutes": "Durante al menos (minutos)", + "safety_interval_days": "Intervalo de seguridad (días, opcional)", + "safety_interval": "Intervalo de seguridad (opcional)", + "delta_mode": "Modo delta", + "from_state_optional": "Desde estado (opcional)", + "to_state_optional": "Hasta estado (opcional)", + "documentation_url_optional": "URL de documentación (opcional)", + "object_notes_optional": "Notas (opcional)", + "nfc_tag_id_optional": "ID de etiqueta NFC (opcional)", + "nfc_tags_empty_help": "Aún no hay tags NFC registrados en Home Assistant.", + "nfc_tags_open_settings": "Abrir configuración de tags", + "nfc_tags_refresh": "Actualizar", + "environmental_entity_optional": "Sensor ambiental (opcional)", + "environmental_entity_helper": "p.ej. sensor.temperatura_exterior — ajusta el intervalo según las condiciones ambientales", + "environmental_attribute_optional": "Atributo ambiental (opcional)", + "nfc_tag_id": "ID de etiqueta NFC", + "nfc_linked": "Etiqueta NFC vinculada", + "nfc_link_hint": "Clic para vincular etiqueta NFC", + "responsible_user": "Usuario responsable", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ningún usuario asignado)", + "all_users": "Todos los usuarios", + "my_tasks": "Mis tareas", + "tab_calendar": "Calendario", + "cal_no_events": "Sin mantenimiento", + "cal_window_7": "7 días", + "cal_window_14": "14 días", + "cal_window_30": "30 días", + "cal_window_365": "1 año", + "cal_every_n_days": "cada {n} días", + "cal_source_time": "Basado en tiempo", + "cal_source_time_adaptive": "Basado en tiempo (adaptativo)", + "cal_source_sensor": "Basado en sensor", + "cal_predicted": "predicho", + "cal_confidence_high": "alta confianza", + "cal_confidence_medium": "confianza media", + "cal_confidence_low": "baja confianza", + "budget_monthly": "Presupuesto mensual", + "budget_yearly": "Presupuesto anual", + "groups": "Grupos", + "new_group": "Nuevo grupo", + "edit_group": "Editar grupo", + "no_groups": "Sin grupos todavía", + "delete_group": "Eliminar grupo", + "delete_group_confirm": "¿Eliminar el grupo '{name}'?", + "group_select_tasks": "Seleccionar tareas", + "group_name_required": "Nombre requerido", + "description_optional": "Descripción (opcional)", + "selected": "Seleccionado", + "loading_chart": "Cargando datos...", + "hide_outliers": "Ocultar valores atípicos (fallos del sensor)", + "was_maintenance_needed": "¿Era necesario este mantenimiento?", + "feedback_needed": "Necesario", + "feedback_not_needed": "No necesario", + "feedback_not_sure": "No seguro", + "suggested_interval": "Intervalo sugerido", + "apply_suggestion": "Aplicar", + "reanalyze": "Reanalizar", + "reanalyze_result": "Nuevo análisis", + "reanalyze_insufficient_data": "Datos insuficientes para una recomendación", + "data_points": "puntos de datos", + "dismiss_suggestion": "Descartar", + "confidence_low": "Baja", + "confidence_medium": "Media", + "confidence_high": "Alta", + "recommended": "recomendado", + "seasonal_awareness": "Conciencia estacional", + "edit_seasonal_overrides": "Editar factores estacionales", + "seasonal_overrides_title": "Factores estacionales (override)", + "seasonal_overrides_hint": "Factor por mes (0.1–5.0). Vacío = aprendido automáticamente.", + "seasonal_override_invalid": "Valor no válido", + "seasonal_override_range": "El factor debe estar entre 0.1 y 5.0", + "clear_all": "Borrar todo", + "seasonal_chart_title": "Factores estacionales", + "seasonal_learned": "Aprendido", + "seasonal_manual": "Manual", + "month_jan": "Ene", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Abr", + "month_may": "May", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Ago", + "month_sep": "Sep", + "month_oct": "Oct", + "month_nov": "Nov", + "month_dec": "Dic", + "sensor_prediction": "Predicción del sensor", + "degradation_trend": "Tendencia", + "trend_rising": "En aumento", + "trend_falling": "En descenso", + "trend_stable": "Estable", + "trend_insufficient_data": "Datos insuficientes", + "days_until_threshold": "Días hasta el umbral", + "threshold_exceeded": "Umbral superado", + "environmental_adjustment": "Factor ambiental", + "sensor_prediction_urgency": "El sensor predice el umbral en ~{days} días", + "day_short": "día", + "weibull_reliability_curve": "Curva de fiabilidad", + "weibull_failure_probability": "Probabilidad de fallo", + "weibull_r_squared": "Ajuste R²", + "beta_early_failures": "Fallos tempranos", + "beta_random_failures": "Fallos aleatorios", + "beta_wear_out": "Desgaste", + "beta_highly_predictable": "Altamente predecible", + "confidence_interval": "Intervalo de confianza", + "confidence_conservative": "Conservador", + "confidence_aggressive": "Optimista", + "current_interval_marker": "Intervalo actual", + "recommended_marker": "Recomendado", + "characteristic_life": "Vida característica", + "chart_mini_sparkline": "Sparkline de tendencia", + "chart_history": "Historial de costes y duración", + "chart_seasonal": "Factores estacionales, 12 meses", + "chart_weibull": "Curva de fiabilidad Weibull", + "chart_sparkline": "Gráfico de valor del disparador", + "days_progress": "Progreso en días", + "qr_code": "Código QR", + "qr_generating": "Generando código QR…", + "qr_error": "No se pudo generar el código QR.", + "qr_error_no_url": "No hay URL de HA configurada. Establezca una URL externa o interna en Ajustes → Sistema → Red.", + "save_error": "Error al guardar. Inténtelo de nuevo.", + "qr_print": "Imprimir", + "qr_download": "Descargar SVG", + "qr_action": "Acción al escanear", + "qr_action_view": "Ver info de mantenimiento", + "qr_action_complete": "Marcar mantenimiento como completado", + "qr_url_mode": "Tipo de enlace", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Local (mDNS)", + "qr_mode_server": "URL del servidor", + "overview": "Resumen", + "analysis": "Análisis", + "recent_activities": "Actividades recientes", + "search_notes": "Buscar en notas", + "avg_cost": "Ø Coste", + "no_advanced_features": "Sin funciones avanzadas activadas", + "no_advanced_features_hint": "Active “Intervalos Adaptativos” o “Patrones Estacionales” en la configuración de la integración para ver datos de análisis aquí.", + "analysis_not_enough_data": "Aún no hay suficientes datos para el análisis.", + "analysis_not_enough_data_hint": "El análisis Weibull requiere al menos 5 mantenimientos completados; los patrones estacionales son visibles tras 6+ puntos de datos por mes.", + "analysis_manual_task_hint": "Las tareas manuales sin intervalo no generan datos de análisis.", + "completions": "finalizaciones", + "current": "Actual", + "shorter": "Más corto", + "longer": "Más largo", + "normal": "Normal", + "disabled": "Desactivado", + "compound_logic": "Lógica compuesta", + "compound": "Compuesto (varias condiciones)", + "compound_logic_and": "Y — todas las condiciones deben activarse", + "compound_logic_or": "O — basta con una condición", + "compound_help": "Combina varias condiciones de sensores en un disparador.", + "compound_no_conditions": "Aún no hay condiciones — añade al menos una.", + "compound_add_condition": "Añadir condición", + "compound_condition": "Condición", + "compound_remove_condition": "Eliminar condición", + "card_title": "Título", + "card_show_header": "Mostrar encabezado con estadísticas", + "card_show_actions": "Mostrar botones de acción", + "card_compact": "Modo compacto", + "card_max_items": "Máx. elementos (0 = todos)", + "card_filter_status": "Filtrar por estado", + "card_filter_status_help": "Vacío = mostrar todos los estados.", + "card_filter_objects": "Filtrar por objetos", + "card_filter_objects_help": "Vacío = mostrar todos los objetos.", + "card_filter_entities": "Filtrar por entidades (entity_ids)", + "card_filter_entities_help": "Selecciona entidades sensor / binary_sensor de esta integración. Vacío = todas.", + "card_loading_objects": "Cargando objetos…", + "card_load_error": "No se pudieron cargar los objetos — verifica la conexión WebSocket.", + "card_no_tasks_title": "Aún no hay tareas de mantenimiento", + "card_no_tasks_cta": "→ Crea una en el panel Mantenimiento", + "no_objects": "Aún no hay objetos.", + "action_error": "Acción fallida. Inténtelo de nuevo.", + "area_id_optional": "Área (opcional)", + "installation_date_optional": "Fecha de instalación (opcional)", + "warranty_expiry_optional": "Vencimiento de garantía (opcional)", + "warranty": "Garantía", + "warranty_valid_until": "válida hasta {date}", + "warranty_expires_in": "vence en {days} días", + "warranty_expired": "vencida", + "cal_past_windows": "Ventanas pasadas", + "cal_forward_windows": "Ventanas futuras", + "history_edit_title": "Editar entrada del historial", + "history_edit_timestamp": "Marca de tiempo", + "manufacturer": "Fabricante", + "model": "Modelo", + "area": "Área", + "actions": "Acciones", + "view_mode_label": "Vista", + "view_cards": "Vista de tarjetas", + "view_table": "Vista de tabla", + "objects_table_columns_label": "Columnas de la tabla de objetos", + "objects_table_columns_hint": "Elige qué columnas aparecen en la vista de tabla de objetos.", + "custom_icon_optional": "Icono (opcional, ej. mdi:wrench)", + "task_enabled": "Tarea habilitada", + "skip_reason_prompt": "¿Omitir esta tarea?", + "reason_optional": "Motivo (opcional)", + "reset_date_prompt": "¿Marcar la tarea como realizada?", + "reset_date_optional": "Fecha de última ejecución (opcional, por defecto: hoy)", + "notes_label": "Notas", + "documentation_label": "Documentación", + "no_nfc_tag": "— Sin etiqueta —", + "dashboard": "Panel", + "tab_today": "Hoy", + "palette_placeholder": "Buscar objetos y tareas…", + "palette_no_results": "Sin resultados", + "palette_hint": "↑↓ navegar · Enter abrir · Esc cerrar", + "today_all_caught_up": "¡Todo al día! Nada pendiente esta semana.", + "today_overdue": "Vencidas", + "today_due_today": "Vence hoy", + "today_this_week": "Esta semana", + "settings": "Ajustes", + "settings_features": "Funciones avanzadas", + "settings_features_desc": "Active o desactive funciones avanzadas. Desactivar las oculta de la interfaz pero no elimina datos.", + "feat_adaptive": "Planificación adaptativa", + "feat_adaptive_desc": "Aprender intervalos óptimos del historial de mantenimiento", + "feat_predictions": "Predicciones de sensor", + "feat_predictions_desc": "Predecir fechas de activación por degradación del sensor", + "feat_seasonal": "Ajustes estacionales", + "feat_seasonal_desc": "Ajustar intervalos según patrones estacionales", + "feat_environmental": "Correlación ambiental", + "feat_environmental_desc": "Correlacionar intervalos con temperatura/humedad", + "feat_budget": "Seguimiento de presupuesto", + "feat_budget_desc": "Seguir los gastos de mantenimiento mensuales y anuales", + "feat_groups": "Grupos de tareas", + "feat_groups_desc": "Organizar tareas en grupos lógicos", + "feat_checklists": "Listas de verificación", + "feat_checklists_desc": "Procedimientos de varios pasos para completar tareas", + "settings_general": "General", + "settings_default_warning": "Días de aviso predeterminados", + "settings_panel_enabled": "Panel lateral", + "settings_panel_title": "Título del panel lateral", + "settings_notifications": "Notificaciones", + "settings_notify_service": "Servicio de notificación", + "test_notification": "Notificación de prueba", + "send_test": "Enviar prueba", + "testing": "Enviando…", + "test_notification_success": "Notificación de prueba enviada", + "test_notification_failed": "La notificación de prueba falló", + "settings_notify_due_soon": "Notificar cuando esté próxima", + "settings_notify_overdue": "Notificar cuando esté vencida", + "settings_notify_triggered": "Notificar cuando se active", + "settings_interval_hours": "Intervalo de repetición (horas, 0 = una vez)", + "settings_quiet_hours": "Horas de silencio", + "settings_quiet_start": "Inicio", + "settings_quiet_end": "Fin", + "settings_max_per_day": "Máx. notificaciones por día (0 = ilimitado)", + "settings_bundling": "Agrupar notificaciones", + "settings_bundle_threshold": "Umbral de agrupación", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Botones de acción móviles", + "settings_action_complete": "Mostrar botón 'Completada'", + "settings_action_skip": "Mostrar botón 'Omitir'", + "settings_action_snooze": "Mostrar botón 'Posponer'", + "settings_weekly_digest": "Resumen semanal", + "settings_weekly_digest_hint": "Una notificación de resumen el lunes por la mañana cuando hay tareas.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Duración de posposición (horas)", + "settings_budget": "Presupuesto", + "settings_currency": "Moneda", + "settings_budget_monthly": "Presupuesto mensual", + "settings_budget_yearly": "Presupuesto anual", + "settings_budget_alerts": "Alertas de presupuesto", + "settings_budget_threshold": "Umbral de alerta (%)", + "settings_import_export": "Importar / Exportar", + "settings_export_json": "Exportar JSON", + "settings_export_yaml": "Exportar YAML", + "settings_export_csv": "Exportar CSV", + "settings_import_csv": "Importar CSV", + "settings_import_placeholder": "Pegue el contenido JSON o CSV aquí…", + "settings_import_btn": "Importar", + "settings_import_success": "{count} objetos importados correctamente.", + "settings_export_success": "Exportación descargada.", + "settings_saved": "Ajuste guardado.", + "settings_include_history": "Incluir historial", + "sort_alphabetical": "Alfabético", + "sort_due_soonest": "Próximo a vencer", + "sort_task_count": "Cantidad de tareas", + "sort_area": "Área", + "sort_assigned_user": "Usuario asignado", + "sort_group": "Grupo", + "groupby_none": "Sin agrupación", + "groupby_area": "Por área", + "groupby_group": "Por grupo", + "groupby_user": "Por usuario", + "filter_label": "Filtro", + "user_label": "Usuario", + "sort_label": "Ordenar", + "group_by_label": "Agrupar por", + "state_value_help": "Usa el valor de estado de HA (normalmente en minúsculas, p. ej. \"on\"/\"off\"). Las mayúsculas se normalizan al guardar.", + "target_changes_help": "Número de transiciones coincidentes antes de que se dispare el trigger (predeterminado: 1).", + "qr_print_title": "Imprimir códigos QR", + "qr_print_desc": "Genera una página imprimible de códigos QR para recortar y pegar en tus equipos.", + "qr_print_load": "Cargar objetos", + "qr_print_filter": "Filtro", + "qr_print_objects": "Objetos", + "qr_print_actions": "Acciones", + "qr_print_url_mode": "Tipo de enlace", + "qr_print_estimate": "Códigos QR estimados", + "qr_print_over_limit": "máximo 200, reduce el filtro", + "qr_print_generate": "Generar códigos QR", + "qr_print_generating": "Generando…", + "qr_print_ready": "Códigos QR listos", + "qr_print_print_button": "Imprimir", + "qr_print_empty": "Nada que generar", + "qr_action_skip": "Omitir", + "vacation_title": "Modo vacaciones", + "vacation_active": "activo", + "vacation_ended": "terminado", + "vacation_desc": "Planifica tus vacaciones: las notificaciones se pausan durante el período más unos días de margen. Puedes excluir tareas concretas.", + "vacation_enable": "Activar modo vacaciones", + "vacation_start": "Inicio", + "vacation_end": "Fin", + "vacation_buffer": "Margen (días)", + "vacation_exempt_title": "Notificar igual durante vacaciones", + "vacation_exempt_desc": "Selecciona tareas que deben notificar también en vacaciones (p. ej. química crítica de piscina).", + "vacation_load_tasks": "Cargar tareas", + "vacation_preview_btn": "Mostrar vista previa", + "vacation_preview_affected": "tareas afectadas", + "vacation_event_due_soon": "vencerá pronto", + "vacation_event_overdue": "se volverá vencida", + "vacation_event_triggered_est": "posible activación de sensor", + "vacation_sensor_based": "(basado en sensor)", + "vacation_action_notify": "Notificar igual", + "vacation_action_unsilence": "Silenciar de nuevo", + "vacation_marked_complete": "Marcado como completado", + "vacation_marked_skip": "Omitido", + "vacation_end_now": "Terminar vacaciones ahora", + "unassigned": "Sin asignar", + "no_area": "Sin área", + "has_overdue": "Tareas vencidas", + "object": "Objeto", + "settings_panel_access": "Acceso al panel", + "settings_panel_access_desc": "Los administradores siempre tienen acceso completo. Para delegar crear, editar y eliminar a no administradores específicos, activa esto y selecciónalos abajo — los demás solo ven Completar y Omitir.", + "settings_operator_write": "Permitir a los usuarios seleccionados crear, editar y eliminar", + "settings_operator_write_desc": "Desactivado: solo los administradores pueden cambiar el contenido. Activado: los usuarios seleccionados abajo también tienen acceso completo.", + "no_non_admin_users": "No se encontraron usuarios no administradores. Añade alguno en Ajustes → Personas.", + "owner_label": "Propietario", + "feat_completion_actions": "Acciones de finalización", + "feat_completion_actions_desc": "Acción HA por tarea al completar + QR de finalización rápida con valores prefijados.", + "on_complete_action_title": "Al completar: ejecutar acción HA (opcional)", + "on_complete_action_desc": "Llama un servicio HA cuando se completa la tarea — p. ej. reiniciar un contador del dispositivo.", + "on_complete_action_service": "Servicio", + "on_complete_action_target": "Entidad objetivo", + "on_complete_action_data": "Datos (JSON, opcional)", + "on_complete_action_test": "Probar acción", + "on_complete_action_test_success": "Éxito", + "on_complete_action_test_failed": "Fallido", + "quick_complete_defaults_title": "Valores predeterminados de finalización rápida (para escaneos QR, opcional)", + "quick_complete_defaults_desc": "Valores prefijados para QR de finalización rápida. Sin ellos, el QR abre el diálogo de completar.", + "quick_complete_defaults_notes": "Notas", + "quick_complete_defaults_cost": "Coste", + "quick_complete_defaults_duration": "Duración (minutos)", + "quick_complete_defaults_feedback_none": "Sin feedback", + "quick_complete_defaults_feedback_needed": "Era necesario", + "quick_complete_defaults_feedback_not_needed": "No era necesario", + "quick_complete_success": "Completado rápido", + "trigger_replaced": "Disparador reemplazado", + "add": "Añadir", + "show_stats": "Mostrar estadísticas + gráficos", + "hide_stats": "Ocultar estadísticas", + "adaptive_no_data": "Aún no hay suficiente historial de finalizaciones para el análisis adaptativo. Completa esta tarea unas cuantas veces más para desbloquear las recomendaciones de intervalo y los gráficos de fiabilidad.", + "suggestion_applied": "Intervalo sugerido aplicado", + "vacation_mode": "Modo vacaciones", + "vacation_status_active": "Activo ahora", + "vacation_status_scheduled": "Programado", + "vacation_status_inactive": "Inactivo", + "vacation_end_now_confirm": "¿Finalizar las vacaciones de inmediato?", + "vacation_exempt_count": "exentas", + "vacation_advanced": "Avanzado…", + "vacation_open_panel": "Abrir en el panel", + "enable": "Activar", + "saved": "Guardado", + "budget_monthly_set": "Definir mensual", + "budget_yearly_set": "Definir anual", + "budget_advanced": "Moneda, alertas…", + "budget_open_panel": "Abrir en el panel", + "groups_empty": "Aún no hay grupos.", + "group_new_placeholder": "Añadir grupo…", + "group_delete_confirm": "¿Eliminar el grupo \"{name}\"?", + "groups_manage_tasks": "Gestionar asignaciones de tareas…", + "groups_open_panel": "Abrir en el panel", + "on_complete_action_target_hint": "Nota: el dominio de la entidad debe coincidir con el servicio — p. ej. 'button.press' solo funciona en button.*, 'counter.increment' solo en counter.*, 'input_button.press' solo en input_button.* etc. Si no coinciden, la acción falla silenciosamente (HA registra 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Mostrar todos los objetos", + "show_all_tasks": "Quitar filtro — mostrar todas las tareas", + "filter_to_overdue": "Filtrar la lista solo a vencidas", + "filter_to_due_soon": "Filtrar la lista solo a próximas a vencer", + "filter_to_triggered": "Filtrar la lista solo a activadas", + "open_task": "Abrir tarea", + "show_details": "Mostrar historial + estadísticas", + "hide_details": "Ocultar detalles", + "history_empty": "Aún no hay historial.", + "history_edit_button": "Editar entrada", + "total_cost": "Coste total", + "times_performed": "Realizada", + "older_entries": "anteriores", + "open_in_panel": "Abrir en el panel de Mantenimiento", + "skip_reason": "Motivo de omisión (opcional)", + "reset_to_date": "Restablecer última ejecución a", + "delete_task_confirm": "¿Eliminar esta tarea y su historial?", + "delete_object_confirm": "¿Eliminar este objeto y todas sus tareas?", + "loading": "Cargando…", + "archive": "Archivar", + "undo": "Deshacer", + "task_archived": "Tarea archivada", + "object_archived": "Objeto archivado", + "unarchive": "Desarchivar", + "archived": "Archivado", + "show_archived": "Mostrar archivados", + "hide_archived": "Ocultar archivados", + "confirm_archive_object": "¿Archivar este objeto y sus tareas? Se conserva el historial y se puede restaurar más tarde.", + "settings_archive": "Archivado y retención", + "settings_archive_desc": "Retira las tareas únicas completadas sin eliminarlas. Los elementos archivados se ocultan y quedan inactivos, pero conservan su historial y coste.", + "settings_archive_oneoff_days": "Archivar automáticamente las tareas únicas completadas tras (días, 0 = desactivado)", + "settings_delete_archived_oneoff_days": "Eliminar automáticamente las tareas únicas archivadas tras (días, 0 = nunca)", + "archive_object": "Archivar objeto", + "unarchive_object": "Desarchivar objeto", + "documents": "Documentos", + "documents_empty": "Aún no hay documentos.", + "doc_upload": "Subir archivo", + "doc_uploading": "Subiendo…", + "doc_add_link": "Añadir enlace", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Título (opcional)", + "doc_open": "Abrir", + "doc_delete_confirm": "¿Eliminar «{name}»?", + "doc_too_large": "El archivo es demasiado grande (máx. 25 MB).", + "doc_upload_failed": "Error al subir.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Ya almacenado en otro lugar — compartido, sin espacio adicional.", + "doc_dup_in_object": "Este archivo ya está adjunto a este objeto.", + "doc_link_invalid": "Solo se permiten enlaces http/https.", + "doc_cat_manual": "Manual", + "doc_cat_warranty": "Garantía", + "doc_cat_invoice": "Factura", + "doc_cat_spare_parts": "Repuestos", + "doc_cat_photo": "Foto", + "doc_cat_other": "Otro", + "doc_link_badge": "Enlace", + "doc_storage_title": "Almacenamiento de documentos", + "doc_storage_saved": "Ahorrado por deduplicación", + "doc_storage_refresh": "Actualizar", + "doc_download": "Descargar", + "doc_close": "Cerrar", + "doc_camera": "Tomar foto", + "doc_drop_hint": "Suelta los archivos aquí", + "doc_task_none": "Ningún documento vinculado a esta tarea.", + "doc_link_existing": "Vincular un documento…", + "doc_attach": "Vincular", + "doc_unlink": "Desvincular", + "doc_page": "Página", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1a", + "chart_since_service": "desde el último mantenimiento", + "chart_no_stats": "No hay estadísticas a largo plazo para esta entidad — solo se muestran valores de eventos de mantenimiento", + "auto_complete_on_recovery": "Completar automáticamente cuando el sensor se recupere", + "auto_complete_on_recovery_help": "Registra una finalización (fija la última realización) cuando el disparador se resuelve solo — p. ej., sal rellenada, filtro cambiado.", + "doc_search": "Buscar documentos…", + "doc_search_none": "Ningún documento coincidente", + "link_device_optional": "Vincular a dispositivo existente (opcional)", + "parent_object_optional": "Objeto principal (opcional)", + "parent_none": "(Sin objeto principal)", + "paused": "Pausado", + "pause_object": "Pausar", + "resume_object": "Reanudar", + "pause_until_prompt": "Congela los calendarios de este objeto: nada vence ni notifica hasta reanudarlo. Opcionalmente, fija una fecha de reanudación automática.", + "pause_until_label": "Reanudar el (opcional)", + "object_paused": "Objeto pausado", + "object_resumed": "Objeto reanudado — calendarios reiniciados", + "object_paused_badge": "Pausado", + "paused_until_label": "hasta", + "replace_object": "Sustituir…", + "replace_object_prompt": "Retira este objeto y crea un sucesor. El historial y los costes quedan archivados en el antiguo; las tareas y los documentos pasan al nuevo, los contadores empiezan de cero.", + "replace_name_label": "Nombre del sucesor", + "object_replaced": "Objeto sustituido — sucesor creado", + "reading_unit_label": "Unidad de lectura (p. ej. kWh, m³)", + "reading_unit_help": "Se muestra junto al valor registrado al completar esta tarea.", + "reading_value_label": "Valor de lectura", + "reading_label": "Lectura", + "settings_templates_label": "Galería de plantillas", + "settings_templates_hint": "Desmarca las plantillas que nunca necesitarás: desaparecen de los selectores \"Desde plantilla\" (panel y flujo de configuración). Nada más cambia; puedes reactivarlas cuando quieras.", + "worksheet": "Hoja de trabajo", + "worksheet_scan_view": "Escanea para abrir la tarea", + "worksheet_scan_complete": "Escanea para completar", + "worksheet_manual_excerpt": "Extracto del manual", + "worksheet_pages": "páginas", + "worksheet_printed": "Impreso", + "worksheet_never": "Nunca" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/fi.json b/custom_components/maintenance_supporter/frontend-src/locales/fi.json new file mode 100644 index 0000000..e641728 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/fi.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Huolto", + "objects": "Kohteet", + "tasks": "Tehtävät", + "overdue": "Myöhässä", + "due_soon": "Pian erääntyy", + "triggered": "Laukaistu", + "trigger_replaced": "Laukaisin korvattu", + "ok": "OK", + "all": "Kaikki", + "new_object": "+ Uusi kohde", + "templates_from": "Mallista", + "templates_title": "Aloita mallista", + "templates_task_count": "{n} tehtävää", + "template_created": "Luotu mallista", + "onboard_hint": "Lisää ensimmäinen kohde ja aloita huollon seuranta.", + "edit": "Muokkaa", + "duplicate": "Monista", + "task_duplicated": "Tehtävä monistettu", + "object_duplicated": "Kohde monistettu", + "delete": "Poista", + "add_task": "+ Lisää tehtävä", + "complete": "Valmis", + "completed": "Suoritettu", + "skip": "Ohita", + "skipped": "Ohitettu", + "missed": "Missed", + "reset": "Nollaa", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Peruuta", + "bulk_select": "Valitse", + "bulk_select_all": "Valitse kaikki", + "bulk_n_selected": "{n} valittu", + "bulk_completed": "{n} tehtävää valmiina", + "bulk_archived": "{n} tehtävää arkistoitu", + "completing": "Suoritetaan…", + "interval": "Väli", + "warning": "Varoitus", + "last_performed": "Viimeksi suoritettu", + "next_due": "Seuraava eräpäivä", + "days_until_due": "Päiviä eräpäivään", + "avg_duration": "Keskim. kesto", + "trigger": "Laukaisin", + "trigger_type": "Laukaisimen tyyppi", + "threshold_above": "Yläraja", + "threshold_below": "Alaraja", + "threshold": "Kynnysarvo", + "counter": "Laskuri", + "state_change": "Tilan muutos", + "runtime": "Käyttöaika", + "runtime_hours": "Tavoitekäyttöaika (tuntia)", + "target_value": "Tavoitearvo", + "baseline": "Lähtötaso", + "target_changes": "Tavoitemuutokset", + "for_minutes": "Vähintään (minuuttia)", + "time_based": "Aikaperusteinen", + "sensor_based": "Anturiperusteinen", + "manual": "Manuaalinen", + "one_time": "Kertaluonteinen", + "weekdays": "Viikonpäivät", + "nth_weekday": "Kuukauden n:s viikonpäivä", + "day_of_month": "Kuukauden päivä", + "recurrence_on_days": "Toista päivinä", + "recurrence_occurrence": "Esiintymä", + "recurrence_weekday": "Viikonpäivä", + "recurrence_day": "Kuukauden päivä (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Viimeinen", + "day_word": "Päivä", + "interval_value": "Väli", + "interval_unit": "Yksikkö", + "unit_days": "Päivää", + "unit_weeks": "Viikkoa", + "unit_months": "Kuukautta", + "unit_years": "Vuotta", + "due_date": "Eräpäivä", + "cleaning": "Puhdistus", + "inspection": "Tarkastus", + "replacement": "Vaihto", + "calibration": "Kalibrointi", + "service": "Huolto", + "reading": "Lukema", + "custom": "Mukautettu", + "history": "Historia", + "cost": "Kustannus", + "report_button": "Raportti", + "report_title": "Huoltoraportti", + "report_generated": "Luotu", + "report_times_done": "Tehty", + "report_total_cost": "Kokonaiskustannus", + "report_every": "joka {n}. {unit}", + "report_notes": "Muistiinpanot", + "report_col_type": "Tyyppi", + "report_col_status": "Tila", + "report_col_schedule": "Aikataulu", + "duration": "Kesto", + "both": "Molemmat", + "trigger_val": "Laukaisuarvo", + "complete_title": "Suorita: ", + "checklist": "Tarkistuslista", + "checklist_steps_optional": "Tarkistuslistan vaiheet (valinnainen)", + "checklist_placeholder": "Puhdista suodatin\nVaihda tiiviste\nTarkista paine", + "checklist_help": "Yksi vaihe riviä kohden. Enintään 100 kohdetta.", + "err_too_long": "{field}: liian pitkä (enint. {n} merkkiä)", + "err_too_short": "{field}: liian lyhyt (väh. {n} merkkiä)", + "err_value_too_high": "{field}: liian suuri (enint. {n})", + "err_value_too_low": "{field}: liian pieni (väh. {n})", + "err_required": "{field}: pakollinen", + "err_wrong_type": "{field}: väärä tyyppi (odotettiin: {type})", + "err_invalid_choice": "{field}: ei sallittu arvo", + "err_invalid_value": "{field}: virheellinen arvo", + "feat_schedule_time": "Kellonajan ajoitus", + "feat_schedule_time_desc": "Tehtävät erääntyvät tiettyyn kellonaikaan keskiyön sijaan.", + "schedule_time_optional": "Erääntyy klo (valinnainen, HH:MM)", + "schedule_time_help": "Tyhjä = keskiyö (oletus). HA:n aikavyöhyke.", + "at_time": "klo", + "notes_optional": "Muistiinpanot (valinnainen)", + "cost_optional": "Kustannus (valinnainen)", + "duration_minutes": "Kesto minuutteina (valinnainen)", + "days": "päivää", + "day": "päivä", + "today": "Tänään", + "d_overdue": "pv myöhässä", + "no_tasks": "Ei vielä huoltotehtäviä. Luo kohde aloittaaksesi.", + "no_tasks_short": "Ei tehtäviä", + "no_history": "Ei vielä historiatietoja.", + "show_all": "Näytä kaikki", + "cost_duration_chart": "Kustannus ja kesto", + "installed": "Asennettu", + "confirm_delete_object": "Poistetaanko tämä kohde ja kaikki sen tehtävät?", + "confirm_delete_task": "Poistetaanko tämä tehtävä?", + "min": "Min", + "max": "Maks", + "save": "Tallenna", + "saving": "Tallennetaan…", + "edit_task": "Muokkaa tehtävää", + "new_task": "Uusi huoltotehtävä", + "task_name": "Tehtävän nimi", + "maintenance_type": "Huoltotyyppi", + "priority": "Prioriteetti", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Matala", + "priority_normal": "Normaali", + "priority_high": "Korkea", + "schedule_type": "Ajoitustyyppi", + "interval_days": "Väli (päivää)", + "warning_days": "Varoituspäivät", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Viimeksi suoritettu (valinnainen)", + "interval_anchor": "Välin ankkuri", + "anchor_completion": "Suorituspäivästä", + "anchor_planned": "Suunnitellusta päivästä (ei poikkeamaa)", + "edit_object": "Muokkaa kohdetta", + "name": "Nimi", + "manufacturer_optional": "Valmistaja (valinnainen)", + "model_optional": "Malli (valinnainen)", + "serial_number_optional": "Sarjanumero (valinnainen)", + "serial_number_label": "S/N", + "documentation_url_label": "Käyttöohje", + "object_notes_label": "Muistiinpanot", + "sort_due_date": "Eräpäivä", + "sort_object": "Kohteen nimi", + "sort_type": "Tyyppi", + "sort_task_name": "Tehtävän nimi", + "all_objects": "Kaikki kohteet", + "tasks_lower": "tehtävää", + "no_tasks_yet": "Ei vielä tehtäviä", + "add_first_task": "Lisää ensimmäinen tehtävä", + "trigger_configuration": "Laukaisimen asetukset", + "entity_id": "Entiteetin tunnus", + "comma_separated": "pilkuilla eroteltu", + "entity_logic": "Entiteettilogiikka", + "entity_logic_any": "Mikä tahansa entiteetti laukaisee", + "entity_logic_all": "Kaikkien entiteettien on laukaistava", + "entities": "entiteettiä", + "attribute_optional": "Attribuutti (valinnainen, tyhjä = tila)", + "use_entity_state": "Käytä entiteetin tilaa (ei attribuuttia)", + "trigger_above": "Laukaise yli", + "trigger_below": "Laukaise alle", + "for_at_least_minutes": "Vähintään (minuuttia)", + "safety_interval_days": "Turvaväli (päivää, valinnainen)", + "safety_interval": "Turvaväli (valinnainen)", + "delta_mode": "Delta-tila", + "from_state_optional": "Lähtötilasta (valinnainen)", + "to_state_optional": "Kohdetilaan (valinnainen)", + "documentation_url_optional": "Dokumentaation URL (valinnainen)", + "object_notes_optional": "Muistiinpanot (valinnainen)", + "nfc_tag_id_optional": "NFC-tunnisteen tunnus (valinnainen)", + "nfc_tags_empty_help": "Home Assistantiin ei ole vielä rekisteröity NFC-tunnisteita.", + "nfc_tags_open_settings": "Avaa tunnisteiden asetukset", + "nfc_tags_refresh": "Päivitä", + "environmental_entity_optional": "Ympäristöanturi (valinnainen)", + "environmental_entity_helper": "esim. sensor.outdoor_temperature — säätää väliä ympäristöolosuhteiden mukaan", + "environmental_attribute_optional": "Ympäristöattribuutti (valinnainen)", + "nfc_tag_id": "NFC-tunnisteen tunnus", + "nfc_linked": "NFC-tunniste linkitetty", + "nfc_link_hint": "Linkitä NFC-tunniste napsauttamalla", + "responsible_user": "Vastuukäyttäjä", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ei käyttäjää määritetty)", + "all_users": "Kaikki käyttäjät", + "my_tasks": "Omat tehtävät", + "tab_calendar": "Kalenteri", + "cal_no_events": "Ei huoltoja", + "cal_window_7": "7 päivää", + "cal_window_14": "14 päivää", + "cal_window_30": "30 päivää", + "cal_window_365": "1 vuosi", + "cal_every_n_days": "{n} päivän välein", + "cal_source_time": "Aikaperusteinen", + "cal_source_time_adaptive": "Aikaperusteinen (mukautuva)", + "cal_source_sensor": "Anturiperusteinen", + "cal_predicted": "ennustettu", + "cal_confidence_high": "korkea luottamus", + "cal_confidence_medium": "keskitason luottamus", + "cal_confidence_low": "matala luottamus", + "budget_monthly": "Kuukausibudjetti", + "budget_yearly": "Vuosibudjetti", + "groups": "Ryhmät", + "new_group": "Uusi ryhmä", + "edit_group": "Muokkaa ryhmää", + "no_groups": "Ei vielä ryhmiä", + "delete_group": "Poista ryhmä", + "delete_group_confirm": "Poistetaanko ryhmä '{name}'?", + "group_select_tasks": "Valitse tehtävät", + "group_name_required": "Nimi on pakollinen", + "description_optional": "Kuvaus (valinnainen)", + "selected": "Valittu", + "loading_chart": "Ladataan kaaviotietoja...", + "hide_outliers": "Piilota poikkeamat (anturivirheet)", + "was_maintenance_needed": "Oliko tämä huolto tarpeen?", + "feedback_needed": "Tarpeen", + "feedback_not_needed": "Ei tarpeen", + "feedback_not_sure": "Ei varma", + "suggested_interval": "Ehdotettu väli", + "apply_suggestion": "Käytä", + "reanalyze": "Analysoi uudelleen", + "reanalyze_result": "Uusi analyysi", + "reanalyze_insufficient_data": "Ei tarpeeksi tietoja suosituksen tekemiseen", + "data_points": "tietopistettä", + "dismiss_suggestion": "Hylkää", + "confidence_low": "Matala", + "confidence_medium": "Keskitaso", + "confidence_high": "Korkea", + "recommended": "suositeltu", + "seasonal_awareness": "Kausivaihtelun huomiointi", + "edit_seasonal_overrides": "Muokkaa kausikertoimia", + "seasonal_overrides_title": "Kausikertoimet (ohitus)", + "seasonal_overrides_hint": "Kerroin kuukautta kohden (0,1–5,0). Tyhjä = opitaan automaattisesti.", + "seasonal_override_invalid": "Virheellinen arvo", + "seasonal_override_range": "Kertoimen on oltava välillä 0,1–5,0", + "clear_all": "Tyhjennä kaikki", + "seasonal_chart_title": "Kausikertoimet", + "seasonal_learned": "Opittu", + "seasonal_manual": "Manuaalinen", + "month_jan": "Tammi", + "month_feb": "Helmi", + "month_mar": "Maalis", + "month_apr": "Huhti", + "month_may": "Touko", + "month_jun": "Kesä", + "month_jul": "Heinä", + "month_aug": "Elo", + "month_sep": "Syys", + "month_oct": "Loka", + "month_nov": "Marras", + "month_dec": "Joulu", + "sensor_prediction": "Anturiennuste", + "degradation_trend": "Trendi", + "trend_rising": "Nouseva", + "trend_falling": "Laskeva", + "trend_stable": "Vakaa", + "trend_insufficient_data": "Riittämättömät tiedot", + "days_until_threshold": "Päiviä kynnysarvoon", + "threshold_exceeded": "Kynnysarvo ylitetty", + "environmental_adjustment": "Ympäristökerroin", + "sensor_prediction_urgency": "Anturi ennustaa kynnysarvon noin {days} päivässä", + "day_short": "pv", + "weibull_reliability_curve": "Luotettavuuskäyrä", + "weibull_failure_probability": "Vikaantumistodennäköisyys", + "weibull_r_squared": "Sovitus R²", + "beta_early_failures": "Varhaiset viat", + "beta_random_failures": "Satunnaiset viat", + "beta_wear_out": "Kuluminen", + "beta_highly_predictable": "Erittäin ennustettava", + "confidence_interval": "Luottamusväli", + "confidence_conservative": "Varovainen", + "confidence_aggressive": "Optimistinen", + "current_interval_marker": "Nykyinen väli", + "recommended_marker": "Suositeltu", + "characteristic_life": "Ominaiselinikä", + "chart_mini_sparkline": "Trendikäyrä", + "chart_history": "Kustannus- ja kestohistoria", + "chart_seasonal": "Kausikertoimet, 12 kuukautta", + "chart_weibull": "Weibull-luotettavuuskäyrä", + "chart_sparkline": "Anturin laukaisuarvon kaavio", + "days_progress": "Päivien edistyminen", + "qr_code": "QR-koodi", + "qr_generating": "Luodaan QR-koodia…", + "qr_error": "QR-koodin luonti epäonnistui.", + "qr_error_no_url": "HA:n URL-osoitetta ei ole määritetty. Aseta ulkoinen tai sisäinen URL kohdassa Asetukset → Järjestelmä → Verkko.", + "save_error": "Tallennus epäonnistui. Yritä uudelleen.", + "qr_print": "Tulosta", + "qr_download": "Lataa SVG", + "qr_action": "Toiminto skannattaessa", + "qr_action_view": "Näytä huoltotiedot", + "qr_action_complete": "Merkitse huolto valmiiksi", + "qr_url_mode": "Linkin tyyppi", + "qr_mode_companion": "Companion-sovellus", + "qr_mode_local": "Paikallinen (mDNS)", + "qr_mode_server": "Palvelimen URL", + "overview": "Yleiskatsaus", + "analysis": "Analyysi", + "recent_activities": "Viimeaikaiset toimet", + "search_notes": "Hae muistiinpanoja", + "avg_cost": "Keskim. kustannus", + "no_advanced_features": "Lisäominaisuuksia ei ole otettu käyttöön", + "no_advanced_features_hint": "Ota käyttöön ”Mukautuvat välit” tai ”Kausivaihtelut” integraation asetuksissa nähdäksesi analyysitiedot täällä.", + "analysis_not_enough_data": "Ei vielä tarpeeksi tietoja analyysiin.", + "analysis_not_enough_data_hint": "Weibull-analyysi vaatii vähintään 5 suoritettua huoltoa; kausivaihtelut näkyvät kun kuukautta kohden on yli 6 tietopistettä.", + "analysis_manual_task_hint": "Manuaaliset tehtävät ilman väliä eivät tuota analyysitietoja.", + "completions": "suoritusta", + "current": "Nykyinen", + "shorter": "Lyhyempi", + "longer": "Pidempi", + "normal": "Normaali", + "disabled": "Pois käytöstä", + "compound_logic": "Yhdistelmälogiikka", + "compound": "Yhdistetty (useita ehtoja)", + "compound_logic_and": "JA — kaikkien ehtojen on täytyttävä", + "compound_logic_or": "TAI — mikä tahansa ehto riittää", + "compound_help": "Yhdistä useita anturiehtoja yhdeksi laukaisimeksi.", + "compound_no_conditions": "Ei vielä ehtoja — lisää vähintään yksi.", + "compound_add_condition": "Lisää ehto", + "compound_condition": "Ehto", + "compound_remove_condition": "Poista ehto", + "card_title": "Otsikko", + "card_show_header": "Näytä otsikko tilastoineen", + "card_show_actions": "Näytä toimintopainikkeet", + "card_compact": "Tiivis tila", + "card_max_items": "Enimmäismäärä (0 = kaikki)", + "card_filter_status": "Suodata tilan mukaan", + "card_filter_status_help": "Tyhjä = näytä kaikki tilat.", + "card_filter_objects": "Suodata kohteiden mukaan", + "card_filter_objects_help": "Tyhjä = näytä kaikki kohteet.", + "card_filter_entities": "Suodata entiteettien mukaan (entity_id)", + "card_filter_entities_help": "Valitse sensor- / binary_sensor-entiteetit tästä integraatiosta. Tyhjä = kaikki.", + "card_loading_objects": "Ladataan kohteita…", + "card_load_error": "Kohteita ei voitu ladata — tarkista WebSocket-yhteys.", + "card_no_tasks_title": "Ei vielä huoltotehtäviä", + "card_no_tasks_cta": "→ Luo sellainen Huolto-paneelissa", + "no_objects": "Ei vielä kohteita.", + "action_error": "Toiminto epäonnistui. Yritä uudelleen.", + "area_id_optional": "Alue (valinnainen)", + "installation_date_optional": "Asennuspäivä (valinnainen)", + "warranty_expiry_optional": "Takuun päättyminen (valinnainen)", + "warranty": "Takuu", + "warranty_valid_until": "voimassa {date} asti", + "warranty_expires_in": "päättyy {days} päivän kuluttua", + "warranty_expired": "päättynyt", + "cal_past_windows": "Menneet jaksot", + "cal_forward_windows": "Tulevat jaksot", + "history_edit_title": "Muokkaa historiamerkintää", + "history_edit_timestamp": "Aikaleima", + "manufacturer": "Valmistaja", + "model": "Malli", + "area": "Alue", + "actions": "Toiminnot", + "view_mode_label": "Näkymä", + "view_cards": "Korttinäkymä", + "view_table": "Taulukkonäkymä", + "objects_table_columns_label": "Kohdetaulukon sarakkeet", + "objects_table_columns_hint": "Valitse, mitkä sarakkeet näkyvät kohdetaulukkonäkymässä.", + "custom_icon_optional": "Kuvake (valinnainen, esim. mdi:wrench)", + "task_enabled": "Tehtävä käytössä", + "skip_reason_prompt": "Ohitetaanko tämä tehtävä?", + "reason_optional": "Syy (valinnainen)", + "reset_date_prompt": "Merkitäänkö tehtävä suoritetuksi?", + "reset_date_optional": "Viimeksi suoritettu -päivä (valinnainen, oletus tänään)", + "notes_label": "Muistiinpanot", + "documentation_label": "Dokumentaatio", + "no_nfc_tag": "— Ei tunnistetta —", + "dashboard": "Kojelauta", + "tab_today": "Tänään", + "palette_placeholder": "Hae kohteita ja tehtäviä…", + "palette_no_results": "Ei osumia", + "palette_hint": "↑↓ liiku · Enter avaa · Esc sulje", + "today_all_caught_up": "Kaikki hoidettu! Tällä viikolla ei ole mitään.", + "today_overdue": "Myöhässä", + "today_due_today": "Erääntyy tänään", + "today_this_week": "Tällä viikolla", + "settings": "Asetukset", + "settings_features": "Lisäominaisuudet", + "settings_features_desc": "Ota lisäominaisuudet käyttöön tai pois käytöstä. Poistaminen piilottaa ne käyttöliittymästä mutta ei poista tietoja.", + "feat_adaptive": "Mukautuva ajoitus", + "feat_adaptive_desc": "Opi optimaaliset välit huoltohistoriasta", + "feat_predictions": "Anturiennusteet", + "feat_predictions_desc": "Ennusta laukaisupäivät anturin kulumisesta", + "feat_seasonal": "Kausisäädöt", + "feat_seasonal_desc": "Säädä välejä kausivaihteluiden mukaan", + "feat_environmental": "Ympäristökorrelaatio", + "feat_environmental_desc": "Korreloi välit lämpötilan/kosteuden kanssa", + "feat_budget": "Budjettiseuranta", + "feat_budget_desc": "Seuraa kuukausittaisia ja vuosittaisia huoltokuluja", + "feat_groups": "Tehtäväryhmät", + "feat_groups_desc": "Järjestä tehtävät loogisiin ryhmiin", + "feat_checklists": "Tarkistuslistat", + "feat_checklists_desc": "Monivaiheiset menettelyt tehtävän suorittamiseen", + "settings_general": "Yleiset", + "settings_default_warning": "Oletusvaroituspäivät", + "settings_panel_enabled": "Sivupalkin paneeli", + "settings_panel_title": "Sivupalkin paneelin otsikko", + "settings_notifications": "Ilmoitukset", + "settings_notify_service": "Ilmoituspalvelu", + "test_notification": "Testi-ilmoitus", + "send_test": "Lähetä testi", + "testing": "Lähetetään…", + "test_notification_success": "Testi-ilmoitus lähetetty", + "test_notification_failed": "Testi-ilmoitus epäonnistui", + "settings_notify_due_soon": "Ilmoita, kun erääntyy pian", + "settings_notify_overdue": "Ilmoita, kun myöhässä", + "settings_notify_triggered": "Ilmoita, kun laukaistu", + "settings_interval_hours": "Toistoväli (tuntia, 0 = kerran)", + "settings_quiet_hours": "Hiljaiset tunnit", + "settings_quiet_start": "Alku", + "settings_quiet_end": "Loppu", + "settings_max_per_day": "Ilmoituksia enintään päivässä (0 = rajoittamaton)", + "settings_bundling": "Niputa ilmoitukset", + "settings_bundle_threshold": "Niputuksen kynnys", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobiilitoimintopainikkeet", + "settings_action_complete": "Näytä Valmis-painike", + "settings_action_skip": "Näytä Ohita-painike", + "settings_action_snooze": "Näytä Torkku-painike", + "settings_weekly_digest": "Viikkokooste", + "settings_weekly_digest_hint": "Yksi kooste-ilmoitus maanantaiaamuna, kun tehtäviä on.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Torkun kesto (tuntia)", + "settings_budget": "Budjetti", + "settings_currency": "Valuutta", + "settings_budget_monthly": "Kuukausibudjetti", + "settings_budget_yearly": "Vuosibudjetti", + "settings_budget_alerts": "Budjettihälytykset", + "settings_budget_threshold": "Hälytyskynnys (%)", + "settings_import_export": "Tuonti / Vienti", + "settings_export_json": "Vie JSON", + "settings_export_yaml": "Vie YAML", + "settings_export_csv": "Vie CSV", + "settings_import_csv": "Tuo CSV", + "settings_import_placeholder": "Liitä JSON- tai CSV-sisältö tähän…", + "settings_import_btn": "Tuo", + "settings_import_success": "{count} kohdetta tuotu onnistuneesti.", + "settings_export_success": "Vienti ladattu.", + "settings_saved": "Asetus tallennettu.", + "settings_include_history": "Sisällytä historia", + "sort_alphabetical": "Aakkosjärjestys", + "sort_due_soonest": "Erääntyy pian", + "sort_task_count": "Tehtävien määrä", + "sort_area": "Alue", + "sort_assigned_user": "Määritetty käyttäjä", + "sort_group": "Ryhmä", + "groupby_none": "Ei ryhmittelyä", + "groupby_area": "Alueen mukaan", + "groupby_group": "Ryhmän mukaan", + "groupby_user": "Käyttäjän mukaan", + "filter_label": "Suodata", + "user_label": "Käyttäjä", + "sort_label": "Lajittele", + "group_by_label": "Ryhmittele", + "state_value_help": "Käytä HA:n tila-arvoa (yleensä pienillä kirjaimilla, esim. \"on\"/\"off\"). Kirjainkoko normalisoidaan tallennettaessa.", + "target_changes_help": "Vastaavien siirtymien määrä ennen laukaisua (oletus: 1).", + "qr_print_title": "Tulosta QR-koodit", + "qr_print_desc": "Luo tulostettava sivu QR-koodeja, jotka voit leikata ja kiinnittää laitteisiin.", + "qr_print_load": "Lataa kohteet", + "qr_print_filter": "Suodata", + "qr_print_objects": "Kohteet", + "qr_print_actions": "Toiminnot", + "qr_print_url_mode": "Linkin tyyppi", + "qr_print_estimate": "Arvioitu QR-koodien määrä", + "qr_print_over_limit": "raja on 200, rajaa suodatinta", + "qr_print_generate": "Luo QR-koodit", + "qr_print_generating": "Luodaan…", + "qr_print_ready": "QR-koodit valmiina", + "qr_print_print_button": "Tulosta", + "qr_print_empty": "Ei mitään luotavaa", + "qr_action_skip": "Ohita", + "vacation_title": "Lomatila", + "vacation_active": "aktiivinen", + "vacation_ended": "päättynyt", + "vacation_desc": "Suunnittele loma: ilmoitukset keskeytetään jakson ajaksi plus puskuripäivien ajaksi. Voit ottaa tietyt tehtävät takaisin käyttöön.", + "vacation_enable": "Ota lomatila käyttöön", + "vacation_start": "Alku", + "vacation_end": "Loppu", + "vacation_buffer": "Puskuri (päivää)", + "vacation_exempt_title": "Ilmoita silti loman aikana", + "vacation_exempt_desc": "Valitse tehtävät, joista ilmoitetaan silti loman aikana (esim. kriittinen altaan kemia).", + "vacation_load_tasks": "Lataa tehtävät", + "vacation_preview_btn": "Näytä esikatselu", + "vacation_preview_affected": "tehtävää koskee", + "vacation_event_due_soon": "erääntyy pian", + "vacation_event_overdue": "menee myöhässä", + "vacation_event_triggered_est": "anturin laukaisu mahdollinen", + "vacation_sensor_based": "(anturiperusteinen)", + "vacation_action_notify": "Ilmoita silti", + "vacation_action_unsilence": "Vaimenna uudelleen", + "vacation_marked_complete": "Merkitty valmiiksi", + "vacation_marked_skip": "Ohitettu", + "vacation_end_now": "Lopeta loma nyt", + "add": "Lisää", + "show_stats": "Näytä tilastot ja kaaviot", + "hide_stats": "Piilota tilastot", + "adaptive_no_data": "Ei vielä tarpeeksi suoritushistoriaa mukautuvaan analyysiin. Suorita tämä tehtävä muutaman kerran lisää avataksesi väliehdotukset ja luotettavuuskaaviot.", + "suggestion_applied": "Ehdotettu väli otettu käyttöön", + "vacation_mode": "Lomatila", + "vacation_status_active": "Aktiivinen nyt", + "vacation_status_scheduled": "Ajoitettu", + "vacation_status_inactive": "Ei aktiivinen", + "vacation_end_now_confirm": "Lopetetaanko loma heti?", + "vacation_exempt_count": "poikkeusta", + "vacation_advanced": "Lisäasetukset…", + "vacation_open_panel": "Avaa paneelissa", + "enable": "Ota käyttöön", + "saved": "Tallennettu", + "budget_monthly_set": "Aseta kuukausittain", + "budget_yearly_set": "Aseta vuosittain", + "budget_advanced": "Valuutta, hälytykset…", + "budget_open_panel": "Avaa paneelissa", + "groups_empty": "Ei vielä ryhmiä.", + "group_new_placeholder": "Lisää ryhmä…", + "group_delete_confirm": "Poistetaanko ryhmä \"{name}\"?", + "groups_manage_tasks": "Hallinnoi tehtävien määrityksiä…", + "groups_open_panel": "Avaa paneelissa", + "unassigned": "Ei määritetty", + "no_area": "Ei aluetta", + "has_overdue": "Sisältää myöhässä olevia tehtäviä", + "object": "Kohde", + "settings_panel_access": "Paneelin käyttöoikeus", + "settings_panel_access_desc": "Ylläpitäjillä on aina täydet oikeudet. Delegoidaksesi luonnin, muokkauksen ja poiston tietyille ei-ylläpitäjille, kytke tämä päälle ja valitse heidät alta — muut näkevät vain Valmis ja Ohita.", + "settings_operator_write": "Salli valittujen käyttäjien luoda, muokata ja poistaa", + "settings_operator_write_desc": "Pois: vain ylläpitäjät voivat muuttaa sisältöä. Päällä: myös alla valitut käyttäjät saavat täydet oikeudet.", + "no_non_admin_users": "Ei-ylläpitäjäkäyttäjiä ei löytynyt. Lisää heitä kohdassa Asetukset → Henkilöt.", + "owner_label": "Omistaja", + "feat_completion_actions": "Suoritustoiminnot", + "feat_completion_actions_desc": "Tehtäväkohtainen HA-toiminto suorituksen yhteydessä + pikasuoritus-QR esiasetetuilla arvoilla.", + "on_complete_action_title": "Suorituksen yhteydessä: laukaise HA-toiminto (valinnainen)", + "on_complete_action_desc": "Kutsuu HA-palvelua, kun tehtävä suoritetaan — esim. nollaa laitteen laskuri.", + "on_complete_action_service": "Palvelu", + "on_complete_action_target": "Kohde-entiteetti", + "on_complete_action_target_hint": "Huom: entiteetin toimialueen on vastattava palvelua — esim. 'button.press' toimii vain button.*-entiteeteillä, 'counter.increment' vain counter.*-entiteeteillä, 'input_button.press' vain input_button.*-entiteeteillä jne. Epäsopivuuden sattuessa toiminto epäonnistuu hiljaisesti (HA kirjaa 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Tiedot (JSON, valinnainen)", + "on_complete_action_test": "Tarkista asetukset", + "on_complete_action_test_success": "✓ Asetukset kelvolliset (toiminto laukeaa vain tehtävän suorituksen yhteydessä)", + "on_complete_action_test_failed": "Epäonnistui", + "quick_complete_defaults_title": "Pikasuorituksen oletukset (QR-skannauksille, valinnainen)", + "quick_complete_defaults_desc": "Esiasetetut arvot pikasuoritus-QR-skannauksille. Ilman näitä QR avaa suoritusikkunan.", + "quick_complete_defaults_notes": "Muistiinpanot", + "quick_complete_defaults_cost": "Kustannus", + "quick_complete_defaults_duration": "Kesto (minuuttia)", + "quick_complete_defaults_feedback_none": "Ei palautetta", + "quick_complete_defaults_feedback_needed": "Oli tarpeen", + "quick_complete_defaults_feedback_not_needed": "Ei tarpeen", + "quick_complete_success": "Merkitty nopeasti valmiiksi", + "show_all_objects": "Näytä kaikki kohteet", + "show_all_tasks": "Tyhjennä suodatin — näytä kaikki tehtävät", + "filter_to_overdue": "Suodata tehtävälista vain myöhässä oleviin", + "filter_to_due_soon": "Suodata tehtävälista vain pian erääntyviin", + "filter_to_triggered": "Suodata tehtävälista vain laukaistuihin", + "open_task": "Avaa tehtävä", + "show_details": "Näytä historia ja tilastot", + "hide_details": "Piilota tiedot", + "history_empty": "Ei vielä historiaa.", + "history_edit_button": "Muokkaa merkintää", + "total_cost": "Kokonaiskustannus", + "times_performed": "Suoritettu", + "older_entries": "vanhempaa", + "open_in_panel": "Avaa Huolto-paneelissa", + "skip_reason": "Ohituksen syy (valinnainen)", + "reset_to_date": "Nollaa viimeksi suoritettu -päivä", + "delete_task_confirm": "Poistetaanko tämä tehtävä ja sen historia?", + "delete_object_confirm": "Poistetaanko tämä kohde ja kaikki sen tehtävät?", + "loading": "Ladataan…", + "archive": "Arkistoi", + "undo": "Kumoa", + "task_archived": "Tehtävä arkistoitu", + "object_archived": "Kohde arkistoitu", + "unarchive": "Palauta arkistosta", + "archived": "Arkistoitu", + "show_archived": "Näytä arkistoidut", + "hide_archived": "Piilota arkistoidut", + "confirm_archive_object": "Arkistoidaanko tämä kohde ja sen tehtävät? Historia säilyy ja sen voi palauttaa myöhemmin.", + "settings_archive": "Arkistointi ja säilytys", + "settings_archive_desc": "Siirrä valmiit kertatehtävät arkistoon poistamatta niitä. Arkistoidut kohteet ovat piilossa ja passiivisia, mutta säilyttävät historian ja kustannukset.", + "settings_archive_oneoff_days": "Arkistoi valmiit kertatehtävät automaattisesti (päivää, 0 = pois)", + "settings_delete_archived_oneoff_days": "Poista arkistoidut kertatehtävät automaattisesti (päivää, 0 = ei koskaan)", + "archive_object": "Arkistoi kohde", + "unarchive_object": "Palauta kohde arkistosta", + "documents": "Asiakirjat", + "documents_empty": "Ei vielä asiakirjoja.", + "doc_upload": "Lataa tiedosto", + "doc_uploading": "Ladataan…", + "doc_add_link": "Lisää linkki", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Otsikko (valinnainen)", + "doc_open": "Avaa", + "doc_delete_confirm": "Poistetaanko \"{name}\"?", + "doc_too_large": "Tiedosto on liian suuri (enint. 25 Mt).", + "doc_upload_failed": "Lataus epäonnistui.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Jo tallennettu muualle — jaettu, ei lisätilaa.", + "doc_dup_in_object": "Tämä tiedosto on jo liitetty tähän kohteeseen.", + "doc_link_invalid": "Vain http/https-linkit ovat sallittuja.", + "doc_cat_manual": "Käyttöohje", + "doc_cat_warranty": "Takuu", + "doc_cat_invoice": "Lasku", + "doc_cat_spare_parts": "Varaosat", + "doc_cat_photo": "Valokuva", + "doc_cat_other": "Muu", + "doc_link_badge": "Linkki", + "doc_storage_title": "Asiakirjojen tallennustila", + "doc_storage_saved": "Säästetty deduplikoinnilla", + "doc_storage_refresh": "Päivitä", + "doc_download": "Lataa", + "doc_close": "Sulje", + "doc_camera": "Ota kuva", + "doc_drop_hint": "Pudota tiedostot tähän", + "doc_task_none": "Tähän tehtävään ei ole liitetty asiakirjoja.", + "doc_link_existing": "Liitä asiakirja…", + "doc_attach": "Liitä", + "doc_unlink": "Poista liitos", + "doc_page": "Sivu", + "chart_range_7d": "7pv", + "chart_range_30d": "30pv", + "chart_range_90d": "90pv", + "chart_range_1y": "1v", + "chart_since_service": "edellisen huollon jälkeen", + "chart_no_stats": "Tälle entiteetille ei ole pitkän aikavälin tilastoja — näytetään vain huoltotapahtumien arvot", + "auto_complete_on_recovery": "Suorita automaattisesti, kun anturi palautuu", + "auto_complete_on_recovery_help": "Kirjaa suorituksen (asettaa viimeksi tehdyn), kun laukaisin poistuu itsestään — esim. suola lisätty, suodatin vaihdettu.", + "doc_search": "Hae asiakirjoja…", + "doc_search_none": "Ei vastaavia asiakirjoja", + "link_device_optional": "Liitä olemassa olevaan laitteeseen (valinnainen)", + "parent_object_optional": "Yläobjekti (valinnainen)", + "parent_none": "(Ei yläobjektia)", + "paused": "Keskeytetty", + "pause_object": "Keskeytä", + "resume_object": "Jatka", + "pause_until_prompt": "Jäädytä tämän kohteen aikataulut — mikään ei eräänny eikä ilmoita ennen jatkamista. Aseta halutessasi automaattisen jatkamisen päivämäärä.", + "pause_until_label": "Jatka (valinnainen)", + "object_paused": "Kohde keskeytetty", + "object_resumed": "Kohde jatkettu — aikataulut käynnistetty uudelleen", + "object_paused_badge": "Keskeytetty", + "paused_until_label": "asti", + "replace_object": "Korvaa…", + "replace_object_prompt": "Poista tämä kohde käytöstä ja luo seuraaja. Historia ja kustannukset pysyvät arkistoituina vanhassa; tehtävät ja asiakirjat siirtyvät uuteen, laskurit alkavat alusta.", + "replace_name_label": "Seuraajan nimi", + "object_replaced": "Kohde korvattu — seuraaja luotu", + "reading_unit_label": "Lukeman yksikkö (esim. kWh, m³)", + "reading_unit_help": "Näytetään kirjatun arvon vieressä tehtävää suoritettaessa.", + "reading_value_label": "Lukema", + "reading_label": "Lukema", + "settings_templates_label": "Mallipohjagalleria", + "settings_templates_hint": "Poista valinta mallipohjista, joita et koskaan tarvitse — ne katoavat \"Mallipohjasta\"-valitsimista (paneeli ja config flow). Mikään muu ei muutu; voi ottaa käyttöön uudelleen milloin tahansa.", + "worksheet": "Työlomake", + "worksheet_scan_view": "Skannaa avataksesi tehtävän", + "worksheet_scan_complete": "Skannaa suorittaaksesi", + "worksheet_manual_excerpt": "Ote käyttöohjeesta", + "worksheet_pages": "sivut", + "worksheet_printed": "Tulostettu", + "worksheet_never": "Ei koskaan" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/fr.json b/custom_components/maintenance_supporter/frontend-src/locales/fr.json new file mode 100644 index 0000000..46cfd17 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/fr.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Maintenance", + "objects": "Objets", + "tasks": "Tâches", + "overdue": "En retard", + "due_soon": "Bientôt dû", + "triggered": "Déclenché", + "ok": "OK", + "all": "Tous", + "new_object": "+ Nouvel objet", + "templates_from": "Depuis un modèle", + "templates_title": "Partir d'un modèle", + "templates_task_count": "{n} tâches", + "template_created": "Créé depuis un modèle", + "onboard_hint": "Ajoutez votre premier objet pour suivre l'entretien.", + "edit": "Modifier", + "duplicate": "Dupliquer", + "task_duplicated": "Tâche dupliquée", + "object_duplicated": "Objet dupliqué", + "delete": "Supprimer", + "add_task": "+ Tâche", + "complete": "Terminé", + "completed": "Terminé", + "skip": "Passer", + "skipped": "Ignoré", + "missed": "Missed", + "reset": "Réinitialiser", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Annuler", + "bulk_select": "Sélectionner", + "bulk_select_all": "Tout sélectionner", + "bulk_n_selected": "{n} sélectionné(s)", + "bulk_completed": "{n} tâches terminées", + "bulk_archived": "{n} tâches archivées", + "completing": "En cours…", + "interval": "Intervalle", + "warning": "Avertissement", + "last_performed": "Dernière exécution", + "next_due": "Prochaine échéance", + "days_until_due": "Jours restants", + "avg_duration": "Ø Durée", + "trigger": "Déclencheur", + "trigger_type": "Type de déclencheur", + "threshold_above": "Limite supérieure", + "threshold_below": "Limite inférieure", + "threshold": "Seuil", + "counter": "Compteur", + "state_change": "Changement d'état", + "runtime": "Durée de fonctionnement", + "runtime_hours": "Durée cible (heures)", + "target_value": "Valeur cible", + "baseline": "Ligne de base", + "target_changes": "Changements cibles", + "for_minutes": "Pendant (minutes)", + "time_based": "Temporel", + "sensor_based": "Capteur", + "manual": "Manuel", + "one_time": "Unique", + "weekdays": "Jours de semaine", + "nth_weekday": "N-ième jour de la semaine du mois", + "day_of_month": "Jour du mois", + "recurrence_on_days": "Répéter le", + "recurrence_occurrence": "Occurrence", + "recurrence_weekday": "Jour de semaine", + "recurrence_day": "Jour du mois (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1er", + "ord_2": "2e", + "ord_3": "3e", + "ord_4": "4e", + "ord_5": "5e", + "ord_last": "Dernier", + "day_word": "Jour", + "interval_value": "Intervalle", + "interval_unit": "Unité", + "unit_days": "Jours", + "unit_weeks": "Semaines", + "unit_months": "Mois", + "unit_years": "Années", + "due_date": "Date d'échéance", + "cleaning": "Nettoyage", + "inspection": "Inspection", + "replacement": "Remplacement", + "calibration": "Étalonnage", + "service": "Service", + "reading": "Relevé", + "custom": "Personnalisé", + "history": "Historique", + "cost": "Coût", + "report_button": "Rapport", + "report_title": "Rapport d'entretien", + "report_generated": "Généré", + "report_times_done": "Effectué", + "report_total_cost": "Coût total", + "report_every": "tous les {n} {unit}", + "report_notes": "Notes", + "report_col_type": "Type", + "report_col_status": "Statut", + "report_col_schedule": "Planning", + "duration": "Durée", + "both": "Les deux", + "trigger_val": "Valeur du déclencheur", + "complete_title": "Terminé : ", + "checklist": "Checklist", + "checklist_steps_optional": "Étapes de la checklist (optionnel)", + "checklist_placeholder": "Nettoyer le filtre\nRemplacer le joint\nTester la pression", + "checklist_help": "Une étape par ligne. Max 100 éléments.", + "err_too_long": "{field} : trop long (max {n} caractères)", + "err_too_short": "{field} : trop court (min {n} caractères)", + "err_value_too_high": "{field} : trop grand (max {n})", + "err_value_too_low": "{field} : trop petit (min {n})", + "err_required": "{field} : champ obligatoire", + "err_wrong_type": "{field} : mauvais type (attendu : {type})", + "err_invalid_choice": "{field} : valeur non autorisée", + "err_invalid_value": "{field} : valeur invalide", + "feat_schedule_time": "Planification à l'heure", + "feat_schedule_time_desc": "Les tâches arrivent à échéance à une heure précise plutôt qu'à minuit.", + "schedule_time_optional": "Échéance à l'heure (optionnel, HH:MM)", + "schedule_time_help": "Vide = minuit (défaut). Fuseau horaire HA.", + "at_time": "à", + "notes_optional": "Notes (optionnel)", + "cost_optional": "Coût (optionnel)", + "duration_minutes": "Durée en minutes (optionnel)", + "days": "jours", + "day": "jour", + "today": "Aujourd'hui", + "d_overdue": "j en retard", + "no_tasks": "Aucune tâche de maintenance. Créez un objet pour commencer.", + "no_tasks_short": "Aucune tâche", + "no_history": "Aucun historique.", + "show_all": "Tout afficher", + "cost_duration_chart": "Coûts & Durée", + "installed": "Installé", + "confirm_delete_object": "Supprimer cet objet et toutes ses tâches ?", + "confirm_delete_task": "Supprimer cette tâche ?", + "min": "Min", + "max": "Max", + "save": "Enregistrer", + "saving": "Enregistrement…", + "edit_task": "Modifier la tâche", + "new_task": "Nouvelle tâche de maintenance", + "task_name": "Nom de la tâche", + "maintenance_type": "Type de maintenance", + "priority": "Priorité", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Basse", + "priority_normal": "Normale", + "priority_high": "Haute", + "schedule_type": "Type de planification", + "interval_days": "Intervalle (jours)", + "warning_days": "Jours d'avertissement", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Dernière exécution (optionnel)", + "interval_anchor": "Ancrage de l'intervalle", + "anchor_completion": "Depuis la date de réalisation", + "anchor_planned": "Depuis la date prévue (sans dérive)", + "edit_object": "Modifier l'objet", + "name": "Nom", + "manufacturer_optional": "Fabricant (optionnel)", + "model_optional": "Modèle (optionnel)", + "serial_number_optional": "Numéro de série (optionnel)", + "serial_number_label": "N/S", + "documentation_url_label": "Manuel", + "object_notes_label": "Notes", + "sort_due_date": "Échéance", + "sort_object": "Nom de l'objet", + "sort_type": "Type", + "sort_task_name": "Nom de la tâche", + "all_objects": "Tous les objets", + "tasks_lower": "tâches", + "no_tasks_yet": "Pas encore de tâches", + "add_first_task": "Ajouter la première tâche", + "trigger_configuration": "Configuration du déclencheur", + "entity_id": "ID d'entité", + "comma_separated": "séparé par des virgules", + "entity_logic": "Logique d'entité", + "entity_logic_any": "N'importe quelle entité déclenche", + "entity_logic_all": "Toutes les entités doivent déclencher", + "entities": "entités", + "attribute_optional": "Attribut (optionnel, vide = état)", + "use_entity_state": "Utiliser l'état de l'entité (pas d'attribut)", + "trigger_above": "Déclencher au-dessus de", + "trigger_below": "Déclencher en dessous de", + "for_at_least_minutes": "Pendant au moins (minutes)", + "safety_interval_days": "Intervalle de sécurité (jours, optionnel)", + "safety_interval": "Intervalle de sécurité (optionnel)", + "delta_mode": "Mode delta", + "from_state_optional": "État source (optionnel)", + "to_state_optional": "État cible (optionnel)", + "documentation_url_optional": "URL de documentation (optionnel)", + "object_notes_optional": "Notes (facultatif)", + "nfc_tag_id_optional": "ID tag NFC (optionnel)", + "nfc_tags_empty_help": "Aucun tag NFC enregistré dans Home Assistant pour le moment.", + "nfc_tags_open_settings": "Ouvrir les réglages des tags", + "nfc_tags_refresh": "Actualiser", + "environmental_entity_optional": "Capteur d'environnement (optionnel)", + "environmental_entity_helper": "ex. sensor.temperature_exterieure — ajuste l'intervalle selon les conditions environnementales", + "environmental_attribute_optional": "Attribut d'environnement (optionnel)", + "nfc_tag_id": "ID tag NFC", + "nfc_linked": "Tag NFC lié", + "nfc_link_hint": "Cliquer pour associer un tag NFC", + "responsible_user": "Utilisateur responsable", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Aucun utilisateur assigné)", + "all_users": "Tous les utilisateurs", + "my_tasks": "Mes tâches", + "tab_calendar": "Calendrier", + "cal_no_events": "Aucun entretien", + "cal_window_7": "7 jours", + "cal_window_14": "14 jours", + "cal_window_30": "30 jours", + "cal_window_365": "1 an", + "cal_every_n_days": "tous les {n} jours", + "cal_source_time": "Basé sur le temps", + "cal_source_time_adaptive": "Basé sur le temps (adaptatif)", + "cal_source_sensor": "Basé sur capteur", + "cal_predicted": "prédit", + "cal_confidence_high": "haute confiance", + "cal_confidence_medium": "confiance moyenne", + "cal_confidence_low": "faible confiance", + "budget_monthly": "Budget mensuel", + "budget_yearly": "Budget annuel", + "groups": "Groupes", + "new_group": "Nouveau groupe", + "edit_group": "Modifier le groupe", + "no_groups": "Aucun groupe pour l'instant", + "delete_group": "Supprimer le groupe", + "delete_group_confirm": "Supprimer le groupe '{name}' ?", + "group_select_tasks": "Sélectionner les tâches", + "group_name_required": "Nom requis", + "description_optional": "Description (optionnel)", + "selected": "Sélectionné", + "loading_chart": "Chargement des données...", + "hide_outliers": "Masquer les valeurs aberrantes (erreurs capteur)", + "was_maintenance_needed": "Cette maintenance était-elle nécessaire ?", + "feedback_needed": "Nécessaire", + "feedback_not_needed": "Pas nécessaire", + "feedback_not_sure": "Pas sûr", + "suggested_interval": "Intervalle suggéré", + "apply_suggestion": "Appliquer", + "reanalyze": "Réanalyser", + "reanalyze_result": "Nouvelle analyse", + "reanalyze_insufficient_data": "Données insuffisantes pour une recommandation", + "data_points": "points de données", + "dismiss_suggestion": "Ignorer", + "confidence_low": "Faible", + "confidence_medium": "Moyen", + "confidence_high": "Élevé", + "recommended": "recommandé", + "seasonal_awareness": "Conscience saisonnière", + "edit_seasonal_overrides": "Modifier les facteurs saisonniers", + "seasonal_overrides_title": "Facteurs saisonniers (override)", + "seasonal_overrides_hint": "Facteur par mois (0.1–5.0). Vide = appris automatiquement.", + "seasonal_override_invalid": "Valeur invalide", + "seasonal_override_range": "Le facteur doit être entre 0.1 et 5.0", + "clear_all": "Tout effacer", + "seasonal_chart_title": "Facteurs saisonniers", + "seasonal_learned": "Appris", + "seasonal_manual": "Manuel", + "month_jan": "Jan", + "month_feb": "Fév", + "month_mar": "Mar", + "month_apr": "Avr", + "month_may": "Mai", + "month_jun": "Juin", + "month_jul": "Juil", + "month_aug": "Août", + "month_sep": "Sep", + "month_oct": "Oct", + "month_nov": "Nov", + "month_dec": "Déc", + "sensor_prediction": "Prédiction capteur", + "degradation_trend": "Tendance", + "trend_rising": "En hausse", + "trend_falling": "En baisse", + "trend_stable": "Stable", + "trend_insufficient_data": "Données insuffisantes", + "days_until_threshold": "Jours avant le seuil", + "threshold_exceeded": "Seuil dépassé", + "environmental_adjustment": "Facteur environnemental", + "sensor_prediction_urgency": "Le capteur prévoit le seuil dans ~{days} jours", + "day_short": "jour", + "weibull_reliability_curve": "Courbe de fiabilité", + "weibull_failure_probability": "Probabilité de défaillance", + "weibull_r_squared": "Ajustement R²", + "beta_early_failures": "Défaillances précoces", + "beta_random_failures": "Défaillances aléatoires", + "beta_wear_out": "Usure", + "beta_highly_predictable": "Très prévisible", + "confidence_interval": "Intervalle de confiance", + "confidence_conservative": "Conservateur", + "confidence_aggressive": "Optimiste", + "current_interval_marker": "Intervalle actuel", + "recommended_marker": "Recommandé", + "characteristic_life": "Durée de vie caractéristique", + "chart_mini_sparkline": "Sparkline de tendance", + "chart_history": "Historique coûts et durée", + "chart_seasonal": "Facteurs saisonniers, 12 mois", + "chart_weibull": "Courbe de fiabilité Weibull", + "chart_sparkline": "Graphique valeur déclencheur", + "days_progress": "Progression en jours", + "qr_code": "QR Code", + "qr_generating": "Génération du QR code…", + "qr_error": "Impossible de générer le QR code.", + "qr_error_no_url": "Aucune URL HA configurée. Veuillez définir une URL externe ou interne dans Paramètres → Système → Réseau.", + "save_error": "Échec de l'enregistrement. Veuillez réessayer.", + "qr_print": "Imprimer", + "qr_download": "Télécharger SVG", + "qr_action": "Action au scan", + "qr_action_view": "Afficher les infos de maintenance", + "qr_action_complete": "Marquer la maintenance comme terminée", + "qr_url_mode": "Type de lien", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Local (mDNS)", + "qr_mode_server": "URL serveur", + "overview": "Aperçu", + "analysis": "Analyse", + "recent_activities": "Activités récentes", + "search_notes": "Rechercher dans les notes", + "avg_cost": "Ø Coût", + "no_advanced_features": "Aucune fonction avancée activée", + "no_advanced_features_hint": "Activez « Intervalles adaptatifs » ou « Tendances saisonnières » dans les paramètres de l'intégration pour voir les données d'analyse ici.", + "analysis_not_enough_data": "Pas encore assez de données pour l'analyse.", + "analysis_not_enough_data_hint": "L'analyse Weibull nécessite au moins 5 maintenances terminées ; les tendances saisonnières apparaissent après 6+ points par mois.", + "analysis_manual_task_hint": "Les tâches manuelles sans intervalle ne génèrent pas de données d'analyse.", + "completions": "réalisations", + "current": "Actuel", + "shorter": "Plus court", + "longer": "Plus long", + "normal": "Normal", + "disabled": "Désactivé", + "compound_logic": "Logique composée", + "compound": "Composé (plusieurs conditions)", + "compound_logic_and": "ET — toutes les conditions doivent se déclencher", + "compound_logic_or": "OU — une seule condition suffit", + "compound_help": "Combinez plusieurs conditions de capteurs en un déclencheur.", + "compound_no_conditions": "Aucune condition — ajoutez-en au moins une.", + "compound_add_condition": "Ajouter une condition", + "compound_condition": "Condition", + "compound_remove_condition": "Supprimer la condition", + "card_title": "Titre", + "card_show_header": "Afficher l'en-tête avec statistiques", + "card_show_actions": "Afficher les boutons d'action", + "card_compact": "Mode compact", + "card_max_items": "Nombre max (0 = tous)", + "card_filter_status": "Filtrer par statut", + "card_filter_status_help": "Vide = afficher tous les statuts.", + "card_filter_objects": "Filtrer par objets", + "card_filter_objects_help": "Vide = afficher tous les objets.", + "card_filter_entities": "Filtrer par entités (entity_ids)", + "card_filter_entities_help": "Choisissez des entités sensor / binary_sensor de cette intégration. Vide = toutes.", + "card_loading_objects": "Chargement des objets…", + "card_load_error": "Impossible de charger les objets — vérifiez la connexion WebSocket.", + "card_no_tasks_title": "Aucune tâche de maintenance pour l'instant", + "card_no_tasks_cta": "→ Créez-en une dans le panneau Maintenance", + "no_objects": "Aucun objet pour l'instant.", + "action_error": "Action échouée. Veuillez réessayer.", + "area_id_optional": "Zone (optionnel)", + "installation_date_optional": "Date d'installation (optionnel)", + "warranty_expiry_optional": "Fin de garantie (optionnel)", + "warranty": "Garantie", + "warranty_valid_until": "valable jusqu'au {date}", + "warranty_expires_in": "expire dans {days} jours", + "warranty_expired": "expirée", + "cal_past_windows": "Fenêtres passées", + "cal_forward_windows": "Fenêtres à venir", + "history_edit_title": "Modifier l'entrée d'historique", + "history_edit_timestamp": "Horodatage", + "manufacturer": "Fabricant", + "model": "Modèle", + "area": "Zone", + "actions": "Actions", + "view_mode_label": "Vue", + "view_cards": "Vue cartes", + "view_table": "Vue tableau", + "objects_table_columns_label": "Colonnes du tableau d'objets", + "objects_table_columns_hint": "Choisissez les colonnes affichées dans la vue tableau des objets.", + "custom_icon_optional": "Icône (optionnel, ex. mdi:wrench)", + "task_enabled": "Tâche activée", + "skip_reason_prompt": "Ignorer cette tâche ?", + "reason_optional": "Raison (optionnel)", + "reset_date_prompt": "Marquer la tâche comme effectuée ?", + "reset_date_optional": "Date de dernière exécution (optionnel, défaut : aujourd'hui)", + "notes_label": "Notes", + "documentation_label": "Documentation", + "no_nfc_tag": "— Aucun tag —", + "dashboard": "Tableau de bord", + "tab_today": "Aujourd'hui", + "palette_placeholder": "Rechercher objets et tâches…", + "palette_no_results": "Aucun résultat", + "palette_hint": "↑↓ naviguer · Entrée ouvrir · Échap fermer", + "today_all_caught_up": "Tout est à jour ! Rien cette semaine.", + "today_overdue": "En retard", + "today_due_today": "À faire aujourd'hui", + "today_this_week": "Cette semaine", + "settings": "Paramètres", + "settings_features": "Fonctions avancées", + "settings_features_desc": "Activez ou désactivez les fonctions avancées. La désactivation les masque dans l'interface mais ne supprime pas les données.", + "feat_adaptive": "Planification adaptative", + "feat_adaptive_desc": "Apprendre les intervalles optimaux à partir de l'historique", + "feat_predictions": "Prédictions capteurs", + "feat_predictions_desc": "Prédire les dates de déclenchement par dégradation des capteurs", + "feat_seasonal": "Ajustements saisonniers", + "feat_seasonal_desc": "Ajuster les intervalles selon les tendances saisonnières", + "feat_environmental": "Corrélation environnementale", + "feat_environmental_desc": "Corréler les intervalles avec la température/humidité", + "feat_budget": "Suivi budgétaire", + "feat_budget_desc": "Suivre les dépenses de maintenance mensuelles et annuelles", + "feat_groups": "Groupes de tâches", + "feat_groups_desc": "Organiser les tâches en groupes logiques", + "feat_checklists": "Checklists", + "feat_checklists_desc": "Procédures multi-étapes pour la réalisation des tâches", + "settings_general": "Général", + "settings_default_warning": "Jours d'avertissement par défaut", + "settings_panel_enabled": "Panneau latéral", + "settings_panel_title": "Titre du panneau latéral", + "settings_notifications": "Notifications", + "settings_notify_service": "Service de notification", + "test_notification": "Notification de test", + "send_test": "Envoyer le test", + "testing": "Envoi en cours…", + "test_notification_success": "Notification de test envoyée", + "test_notification_failed": "Échec de la notification de test", + "settings_notify_due_soon": "Notifier quand bientôt dû", + "settings_notify_overdue": "Notifier quand en retard", + "settings_notify_triggered": "Notifier quand déclenché", + "settings_interval_hours": "Intervalle de répétition (heures, 0 = une fois)", + "settings_quiet_hours": "Heures de silence", + "settings_quiet_start": "Début", + "settings_quiet_end": "Fin", + "settings_max_per_day": "Max notifications par jour (0 = illimité)", + "settings_bundling": "Regrouper les notifications", + "settings_bundle_threshold": "Seuil de regroupement", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Boutons d'action mobiles", + "settings_action_complete": "Afficher le bouton 'Terminé'", + "settings_action_skip": "Afficher le bouton 'Passer'", + "settings_action_snooze": "Afficher le bouton 'Reporter'", + "settings_weekly_digest": "Récapitulatif hebdomadaire", + "settings_weekly_digest_hint": "Une notification récapitulative le lundi matin quand des tâches sont dues.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Durée de report (heures)", + "settings_budget": "Budget", + "settings_currency": "Devise", + "settings_budget_monthly": "Budget mensuel", + "settings_budget_yearly": "Budget annuel", + "settings_budget_alerts": "Alertes budgétaires", + "settings_budget_threshold": "Seuil d'alerte (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Exporter JSON", + "settings_export_yaml": "Exporter YAML", + "settings_export_csv": "Exporter CSV", + "settings_import_csv": "Importer CSV", + "settings_import_placeholder": "Collez le contenu JSON ou CSV ici…", + "settings_import_btn": "Importer", + "settings_import_success": "{count} objets importés avec succès.", + "settings_export_success": "Export téléchargé.", + "settings_saved": "Paramètre enregistré.", + "settings_include_history": "Inclure l'historique", + "sort_alphabetical": "Alphabétique", + "sort_due_soonest": "Échéance la plus proche", + "sort_task_count": "Nombre de tâches", + "sort_area": "Zone", + "sort_assigned_user": "Utilisateur affecté", + "sort_group": "Groupe", + "groupby_none": "Aucun groupement", + "groupby_area": "Par zone", + "groupby_group": "Par groupe", + "groupby_user": "Par utilisateur", + "filter_label": "Filtre", + "user_label": "Utilisateur", + "sort_label": "Tri", + "group_by_label": "Grouper par", + "state_value_help": "Utilisez la valeur d'état HA (généralement en minuscules, p. ex. \"on\"/\"off\"). La casse est normalisée à l'enregistrement.", + "target_changes_help": "Nombre de transitions correspondantes avant le déclenchement (par défaut : 1).", + "qr_print_title": "Imprimer les QR codes", + "qr_print_desc": "Générer une page imprimable de QR codes à découper et coller sur votre équipement.", + "qr_print_load": "Charger les objets", + "qr_print_filter": "Filtre", + "qr_print_objects": "Objets", + "qr_print_actions": "Actions", + "qr_print_url_mode": "Type de lien", + "qr_print_estimate": "QR codes estimés", + "qr_print_over_limit": "limite à 200, affinez le filtre", + "qr_print_generate": "Générer les QR codes", + "qr_print_generating": "Génération…", + "qr_print_ready": "QR codes prêts", + "qr_print_print_button": "Imprimer", + "qr_print_empty": "Rien à générer", + "qr_action_skip": "Passer", + "vacation_title": "Mode vacances", + "vacation_active": "actif", + "vacation_ended": "terminé", + "vacation_desc": "Planifiez vos vacances : les notifications sont mises en pause pendant la période plus quelques jours tampon. Vous pouvez réactiver certaines tâches.", + "vacation_enable": "Activer le mode vacances", + "vacation_start": "Début", + "vacation_end": "Fin", + "vacation_buffer": "Tampon (jours)", + "vacation_exempt_title": "Notifier malgré les vacances", + "vacation_exempt_desc": "Choisissez les tâches qui doivent quand même notifier pendant les vacances (p. ex. chimie de piscine critique).", + "vacation_load_tasks": "Charger les tâches", + "vacation_preview_btn": "Afficher l'aperçu", + "vacation_preview_affected": "tâches concernées", + "vacation_event_due_soon": "bientôt dû", + "vacation_event_overdue": "deviendra en retard", + "vacation_event_triggered_est": "déclencheur capteur possible", + "vacation_sensor_based": "(basé sur capteur)", + "vacation_action_notify": "Notifier quand même", + "vacation_action_unsilence": "Mettre en silence", + "vacation_marked_complete": "Marqué comme terminé", + "vacation_marked_skip": "Passé", + "vacation_end_now": "Terminer les vacances maintenant", + "unassigned": "Non assigné", + "no_area": "Aucune zone", + "has_overdue": "Tâches en retard", + "object": "Objet", + "settings_panel_access": "Accès au panneau", + "settings_panel_access_desc": "Les administrateurs ont toujours un accès complet. Pour déléguer la création, la modification et la suppression à des non-administrateurs spécifiques, activez ceci et sélectionnez-les ci-dessous — les autres ne voient que Terminer et Ignorer.", + "settings_operator_write": "Autoriser les utilisateurs sélectionnés à créer, modifier et supprimer", + "settings_operator_write_desc": "Désactivé : seuls les administrateurs peuvent modifier le contenu. Activé : les utilisateurs ci-dessous ont aussi un accès complet.", + "no_non_admin_users": "Aucun utilisateur non administrateur trouvé. Ajoutez-en dans Paramètres → Personnes.", + "owner_label": "Propriétaire", + "feat_completion_actions": "Actions de finalisation", + "feat_completion_actions_desc": "Action HA par tâche lors de la finalisation + QR de finalisation rapide avec valeurs pré-définies.", + "on_complete_action_title": "À la finalisation : déclencher une action HA (optionnel)", + "on_complete_action_desc": "Appelle un service HA quand la tâche est terminée — p. ex. réinitialiser un compteur sur l'appareil.", + "on_complete_action_service": "Service", + "on_complete_action_target": "Entité cible", + "on_complete_action_data": "Données (JSON, optionnel)", + "on_complete_action_test": "Tester l'action", + "on_complete_action_test_success": "Réussi", + "on_complete_action_test_failed": "Échoué", + "quick_complete_defaults_title": "Valeurs par défaut pour finalisation rapide (scans QR, optionnel)", + "quick_complete_defaults_desc": "Valeurs pré-définies pour les scans QR de finalisation rapide. Sans ces valeurs, le QR ouvre la boîte de dialogue.", + "quick_complete_defaults_notes": "Notes", + "quick_complete_defaults_cost": "Coût", + "quick_complete_defaults_duration": "Durée (minutes)", + "quick_complete_defaults_feedback_none": "Aucun feedback", + "quick_complete_defaults_feedback_needed": "Était nécessaire", + "quick_complete_defaults_feedback_not_needed": "Non nécessaire", + "quick_complete_success": "Terminé rapidement", + "trigger_replaced": "Déclencheur remplacé", + "add": "Ajouter", + "show_stats": "Afficher les statistiques + graphiques", + "hide_stats": "Masquer les statistiques", + "adaptive_no_data": "Pas encore assez d'historique de réalisations pour l'analyse adaptative. Réalisez cette tâche encore quelques fois pour débloquer les recommandations d'intervalle et les courbes de fiabilité.", + "suggestion_applied": "Intervalle suggéré appliqué", + "vacation_mode": "Mode vacances", + "vacation_status_active": "Actif maintenant", + "vacation_status_scheduled": "Planifié", + "vacation_status_inactive": "Inactif", + "vacation_end_now_confirm": "Terminer les vacances immédiatement ?", + "vacation_exempt_count": "exemptées", + "vacation_advanced": "Avancé…", + "vacation_open_panel": "Ouvrir dans le panneau", + "enable": "Activer", + "saved": "Enregistré", + "budget_monthly_set": "Définir mensuel", + "budget_yearly_set": "Définir annuel", + "budget_advanced": "Devise, alertes…", + "budget_open_panel": "Ouvrir dans le panneau", + "groups_empty": "Aucun groupe pour le moment.", + "group_new_placeholder": "Ajouter un groupe…", + "group_delete_confirm": "Supprimer le groupe « {name} » ?", + "groups_manage_tasks": "Gérer les affectations de tâches…", + "groups_open_panel": "Ouvrir dans le panneau", + "on_complete_action_target_hint": "Note : le domaine de l'entité doit correspondre au service — p. ex. 'button.press' ne fonctionne que sur button.*, 'counter.increment' seulement sur counter.*, 'input_button.press' seulement sur input_button.* etc. En cas de non-correspondance, l'action échoue silencieusement (HA journalise 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Afficher tous les objets", + "show_all_tasks": "Effacer le filtre — afficher toutes les tâches", + "filter_to_overdue": "Filtrer la liste sur les tâches en retard uniquement", + "filter_to_due_soon": "Filtrer la liste sur les tâches bientôt dues uniquement", + "filter_to_triggered": "Filtrer la liste sur les tâches déclenchées uniquement", + "open_task": "Ouvrir la tâche", + "show_details": "Afficher l'historique + statistiques", + "hide_details": "Masquer les détails", + "history_empty": "Aucun historique pour le moment.", + "history_edit_button": "Modifier l'entrée", + "total_cost": "Coût total", + "times_performed": "Réalisée", + "older_entries": "plus anciennes", + "open_in_panel": "Ouvrir dans le panneau Maintenance", + "skip_reason": "Raison du saut (facultatif)", + "reset_to_date": "Réinitialiser la dernière réalisation au", + "delete_task_confirm": "Supprimer cette tâche et son historique ?", + "delete_object_confirm": "Supprimer cet objet et toutes ses tâches ?", + "loading": "Chargement…", + "archive": "Archiver", + "undo": "Annuler", + "task_archived": "Tâche archivée", + "object_archived": "Objet archivé", + "unarchive": "Désarchiver", + "archived": "Archivé", + "show_archived": "Afficher les archivés", + "hide_archived": "Masquer les archivés", + "confirm_archive_object": "Archiver cet objet et ses tâches ? L'historique est conservé et peut être restauré plus tard.", + "settings_archive": "Archivage et conservation", + "settings_archive_desc": "Mettez de côté les tâches ponctuelles terminées sans les supprimer. Les éléments archivés sont masqués et inactifs, mais conservent leur historique et leurs coûts.", + "settings_archive_oneoff_days": "Archiver automatiquement les tâches ponctuelles terminées après (jours, 0 = désactivé)", + "settings_delete_archived_oneoff_days": "Supprimer automatiquement les tâches ponctuelles archivées après (jours, 0 = jamais)", + "archive_object": "Archiver l'objet", + "unarchive_object": "Désarchiver l'objet", + "documents": "Documents", + "documents_empty": "Aucun document pour l'instant.", + "doc_upload": "Téléverser un fichier", + "doc_uploading": "Téléversement…", + "doc_add_link": "Ajouter un lien", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titre (facultatif)", + "doc_open": "Ouvrir", + "doc_delete_confirm": "Supprimer « {name} » ?", + "doc_too_large": "Fichier trop volumineux (max. 25 Mo).", + "doc_upload_failed": "Échec du téléversement.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Déjà stocké ailleurs — partagé, aucun espace supplémentaire.", + "doc_dup_in_object": "Ce fichier est déjà attaché à cet objet.", + "doc_link_invalid": "Seuls les liens http/https sont autorisés.", + "doc_cat_manual": "Manuel", + "doc_cat_warranty": "Garantie", + "doc_cat_invoice": "Facture", + "doc_cat_spare_parts": "Pièces détachées", + "doc_cat_photo": "Photo", + "doc_cat_other": "Autre", + "doc_link_badge": "Lien", + "doc_storage_title": "Stockage des documents", + "doc_storage_saved": "Économisé par déduplication", + "doc_storage_refresh": "Actualiser", + "doc_download": "Télécharger", + "doc_close": "Fermer", + "doc_camera": "Prendre une photo", + "doc_drop_hint": "Déposez les fichiers ici", + "doc_task_none": "Aucun document lié à cette tâche.", + "doc_link_existing": "Lier un document…", + "doc_attach": "Lier", + "doc_unlink": "Dissocier", + "doc_page": "Page", + "chart_range_7d": "7j", + "chart_range_30d": "30j", + "chart_range_90d": "90j", + "chart_range_1y": "1a", + "chart_since_service": "depuis le dernier entretien", + "chart_no_stats": "Pas de statistiques à long terme pour cette entité — seules les valeurs des entretiens sont affichées", + "auto_complete_on_recovery": "Terminer automatiquement quand le capteur se rétablit", + "auto_complete_on_recovery_help": "Enregistre une exécution (met à jour la dernière réalisation) lorsque le déclencheur se résout de lui-même — p. ex. sel rechargé, filtre remplacé.", + "doc_search": "Rechercher des documents…", + "doc_search_none": "Aucun document correspondant", + "link_device_optional": "Lier à un appareil existant (optionnel)", + "parent_object_optional": "Objet parent (optionnel)", + "parent_none": "(Aucun parent)", + "paused": "En pause", + "pause_object": "Mettre en pause", + "resume_object": "Reprendre", + "pause_until_prompt": "Gèle les calendriers de cet objet — rien n'arrive à échéance et rien ne notifie jusqu'à la reprise. Optionnellement, fixez une date de reprise automatique.", + "pause_until_label": "Reprendre le (optionnel)", + "object_paused": "Objet mis en pause", + "object_resumed": "Objet repris — calendriers relancés", + "object_paused_badge": "En pause", + "paused_until_label": "jusqu'au", + "replace_object": "Remplacer…", + "replace_object_prompt": "Retire cet objet et crée un successeur. L'historique et les coûts restent archivés sur l'ancien ; les tâches et documents passent au nouveau, les compteurs repartent de zéro.", + "replace_name_label": "Nom du successeur", + "object_replaced": "Objet remplacé — successeur créé", + "reading_unit_label": "Unité de relevé (p. ex. kWh, m³)", + "reading_unit_help": "Affichée à côté de la valeur enregistrée lors de l'exécution de cette tâche.", + "reading_value_label": "Valeur relevée", + "reading_label": "Relevé", + "settings_templates_label": "Galerie de modèles", + "settings_templates_hint": "Décochez les modèles dont vous n'aurez jamais besoin — ils disparaissent des sélecteurs « Depuis un modèle » (panneau et config flow). Rien d'autre ne change ; réactivables à tout moment.", + "worksheet": "Fiche de travail", + "worksheet_scan_view": "Scanner pour ouvrir la tâche", + "worksheet_scan_complete": "Scanner pour terminer", + "worksheet_manual_excerpt": "Extrait du manuel", + "worksheet_pages": "pages", + "worksheet_printed": "Imprimé", + "worksheet_never": "Jamais" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/hi.json b/custom_components/maintenance_supporter/frontend-src/locales/hi.json new file mode 100644 index 0000000..0b0ca9d --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/hi.json @@ -0,0 +1,696 @@ +{ + "maintenance": "रखरखाव", + "objects": "वस्तुएँ", + "tasks": "कार्य", + "overdue": "अतिदेय", + "due_soon": "जल्द देय", + "triggered": "ट्रिगर हुआ", + "trigger_replaced": "ट्रिगर बदला गया", + "ok": "ठीक", + "all": "सभी", + "new_object": "+ नई वस्तु", + "templates_from": "टेम्पलेट से", + "templates_title": "टेम्पलेट से शुरू करें", + "templates_task_count": "{n} कार्य", + "template_created": "टेम्पलेट से बनाया गया", + "onboard_hint": "रखरखाव ट्रैक करने के लिए अपना पहला ऑब्जेक्ट जोड़ें।", + "edit": "संपादित करें", + "duplicate": "डुप्लिकेट करें", + "task_duplicated": "कार्य डुप्लिकेट किया गया", + "object_duplicated": "ऑब्जेक्ट डुप्लिकेट किया गया", + "delete": "हटाएँ", + "add_task": "+ कार्य जोड़ें", + "complete": "पूर्ण करें", + "completed": "पूर्ण हुआ", + "skip": "छोड़ें", + "skipped": "छोड़ा गया", + "missed": "Missed", + "reset": "रीसेट करें", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "रद्द करें", + "bulk_select": "चुनें", + "bulk_select_all": "सभी चुनें", + "bulk_n_selected": "{n} चयनित", + "bulk_completed": "{n} कार्य पूर्ण", + "bulk_archived": "{n} कार्य संग्रहित", + "completing": "पूर्ण किया जा रहा है…", + "interval": "अंतराल", + "warning": "चेतावनी", + "last_performed": "अंतिम बार किया गया", + "next_due": "अगली देय", + "days_until_due": "देय तक दिन", + "avg_duration": "औसत अवधि", + "trigger": "ट्रिगर", + "trigger_type": "ट्रिगर प्रकार", + "threshold_above": "ऊपरी सीमा", + "threshold_below": "निचली सीमा", + "threshold": "सीमा", + "counter": "काउंटर", + "state_change": "स्थिति परिवर्तन", + "runtime": "रनटाइम", + "runtime_hours": "लक्ष्य रनटाइम (घंटे)", + "target_value": "लक्ष्य मान", + "baseline": "आधार रेखा", + "target_changes": "लक्ष्य परिवर्तन", + "for_minutes": "के लिए (मिनट)", + "time_based": "समय-आधारित", + "sensor_based": "सेंसर-आधारित", + "manual": "मैनुअल", + "one_time": "एक बार", + "weekdays": "सप्ताह के दिन", + "nth_weekday": "महीने का N-वाँ सप्ताह-दिन", + "day_of_month": "महीने का दिन", + "recurrence_on_days": "दोहराएँ", + "recurrence_occurrence": "घटना", + "recurrence_weekday": "सप्ताह-दिन", + "recurrence_day": "महीने का दिन (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "पहला", + "ord_2": "दूसरा", + "ord_3": "तीसरा", + "ord_4": "चौथा", + "ord_5": "पाँचवाँ", + "ord_last": "अंतिम", + "day_word": "दिन", + "interval_value": "अंतराल", + "interval_unit": "इकाई", + "unit_days": "दिन", + "unit_weeks": "सप्ताह", + "unit_months": "महीने", + "unit_years": "वर्ष", + "due_date": "देय तिथि", + "cleaning": "सफ़ाई", + "inspection": "निरीक्षण", + "replacement": "प्रतिस्थापन", + "calibration": "अंशांकन", + "service": "सर्विस", + "reading": "रीडिंग", + "custom": "कस्टम", + "history": "इतिहास", + "cost": "लागत", + "report_button": "रिपोर्ट", + "report_title": "रखरखाव रिपोर्ट", + "report_generated": "जनरेट किया गया", + "report_times_done": "पूर्ण", + "report_total_cost": "कुल लागत", + "report_every": "हर {n} {unit}", + "report_notes": "नोट्स", + "report_col_type": "प्रकार", + "report_col_status": "स्थिति", + "report_col_schedule": "शेड्यूल", + "duration": "अवधि", + "both": "दोनों", + "trigger_val": "ट्रिगर मान", + "complete_title": "पूर्ण करें: ", + "checklist": "चेकलिस्ट", + "checklist_steps_optional": "चेकलिस्ट चरण (वैकल्पिक)", + "checklist_placeholder": "फ़िल्टर साफ़ करें\nसील बदलें\nदबाव जाँचें", + "checklist_help": "प्रति पंक्ति एक चरण। अधिकतम 100 आइटम।", + "err_too_long": "{field}: बहुत लंबा (अधिकतम {n} अक्षर)", + "err_too_short": "{field}: बहुत छोटा (न्यूनतम {n} अक्षर)", + "err_value_too_high": "{field}: बहुत बड़ा (अधिकतम {n})", + "err_value_too_low": "{field}: बहुत छोटा (न्यूनतम {n})", + "err_required": "{field}: आवश्यक", + "err_wrong_type": "{field}: गलत प्रकार (अपेक्षित: {type})", + "err_invalid_choice": "{field}: अनुमत मान नहीं है", + "err_invalid_value": "{field}: अमान्य मान", + "feat_schedule_time": "दिन का समय निर्धारण", + "feat_schedule_time_desc": "कार्य मध्यरात्रि के बजाय दिन के एक विशिष्ट समय पर अतिदेय हो जाते हैं।", + "schedule_time_optional": "इस समय देय (वैकल्पिक, HH:MM)", + "schedule_time_help": "खाली = मध्यरात्रि (डिफ़ॉल्ट)। HA समय क्षेत्र।", + "at_time": "बजे", + "notes_optional": "टिप्पणियाँ (वैकल्पिक)", + "cost_optional": "लागत (वैकल्पिक)", + "duration_minutes": "मिनटों में अवधि (वैकल्पिक)", + "days": "दिन", + "day": "दिन", + "today": "आज", + "d_overdue": "दिन अतिदेय", + "no_tasks": "अभी कोई रखरखाव कार्य नहीं। शुरू करने के लिए एक वस्तु बनाएँ।", + "no_tasks_short": "कोई कार्य नहीं", + "no_history": "अभी कोई इतिहास प्रविष्टि नहीं।", + "show_all": "सभी दिखाएँ", + "cost_duration_chart": "लागत और अवधि", + "installed": "स्थापित", + "confirm_delete_object": "इस वस्तु और इसके सभी कार्यों को हटाएँ?", + "confirm_delete_task": "इस कार्य को हटाएँ?", + "min": "न्यूनतम", + "max": "अधिकतम", + "save": "सहेजें", + "saving": "सहेजा जा रहा है…", + "edit_task": "कार्य संपादित करें", + "new_task": "नया रखरखाव कार्य", + "task_name": "कार्य का नाम", + "maintenance_type": "रखरखाव प्रकार", + "priority": "प्राथमिकता", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "कम", + "priority_normal": "सामान्य", + "priority_high": "उच्च", + "schedule_type": "अनुसूची प्रकार", + "interval_days": "अंतराल (दिन)", + "warning_days": "चेतावनी दिन", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "अंतिम बार किया गया (वैकल्पिक)", + "interval_anchor": "अंतराल आधार", + "anchor_completion": "पूर्ण तिथि से", + "anchor_planned": "नियोजित तिथि से (कोई विचलन नहीं)", + "edit_object": "वस्तु संपादित करें", + "name": "नाम", + "manufacturer_optional": "निर्माता (वैकल्पिक)", + "model_optional": "मॉडल (वैकल्पिक)", + "serial_number_optional": "सीरियल नंबर (वैकल्पिक)", + "serial_number_label": "S/N", + "documentation_url_label": "मैनुअल", + "object_notes_label": "टिप्पणियाँ", + "sort_due_date": "देय तिथि", + "sort_object": "वस्तु का नाम", + "sort_type": "प्रकार", + "sort_task_name": "कार्य का नाम", + "all_objects": "सभी वस्तुएँ", + "tasks_lower": "कार्य", + "no_tasks_yet": "अभी कोई कार्य नहीं", + "add_first_task": "पहला कार्य जोड़ें", + "trigger_configuration": "ट्रिगर कॉन्फ़िगरेशन", + "entity_id": "एंटिटी ID", + "comma_separated": "अल्पविराम से अलग", + "entity_logic": "एंटिटी लॉजिक", + "entity_logic_any": "कोई भी एंटिटी ट्रिगर करती है", + "entity_logic_all": "सभी एंटिटी ट्रिगर होनी चाहिए", + "entities": "एंटिटी", + "attribute_optional": "विशेषता (वैकल्पिक, खाली = स्थिति)", + "use_entity_state": "एंटिटी स्थिति का उपयोग करें (कोई विशेषता नहीं)", + "trigger_above": "इससे ऊपर ट्रिगर करें", + "trigger_below": "इससे नीचे ट्रिगर करें", + "for_at_least_minutes": "कम से कम (मिनट)", + "safety_interval_days": "सुरक्षा अंतराल (दिन, वैकल्पिक)", + "safety_interval": "सुरक्षा अंतराल (वैकल्पिक)", + "delta_mode": "डेल्टा मोड", + "from_state_optional": "किस स्थिति से (वैकल्पिक)", + "to_state_optional": "किस स्थिति तक (वैकल्पिक)", + "documentation_url_optional": "दस्तावेज़ URL (वैकल्पिक)", + "object_notes_optional": "टिप्पणियाँ (वैकल्पिक)", + "nfc_tag_id_optional": "NFC टैग ID (वैकल्पिक)", + "nfc_tags_empty_help": "Home Assistant में अभी कोई NFC टैग पंजीकृत नहीं है।", + "nfc_tags_open_settings": "टैग सेटिंग्स खोलें", + "nfc_tags_refresh": "रिफ़्रेश करें", + "environmental_entity_optional": "पर्यावरण सेंसर (वैकल्पिक)", + "environmental_entity_helper": "उदा. sensor.outdoor_temperature — पर्यावरणीय स्थितियों के आधार पर अंतराल समायोजित करता है", + "environmental_attribute_optional": "पर्यावरण विशेषता (वैकल्पिक)", + "nfc_tag_id": "NFC टैग ID", + "nfc_linked": "NFC टैग लिंक किया गया", + "nfc_link_hint": "NFC टैग लिंक करने के लिए क्लिक करें", + "responsible_user": "जिम्मेदार उपयोगकर्ता", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(कोई उपयोगकर्ता असाइन नहीं)", + "all_users": "सभी उपयोगकर्ता", + "my_tasks": "मेरे कार्य", + "tab_calendar": "कैलेंडर", + "cal_no_events": "कोई रखरखाव नहीं", + "cal_window_7": "7 दिन", + "cal_window_14": "14 दिन", + "cal_window_30": "30 दिन", + "cal_window_365": "1 वर्ष", + "cal_every_n_days": "हर {n} दिन", + "cal_source_time": "समय-आधारित", + "cal_source_time_adaptive": "समय-आधारित (अनुकूली)", + "cal_source_sensor": "सेंसर-आधारित", + "cal_predicted": "अनुमानित", + "cal_confidence_high": "उच्च विश्वास", + "cal_confidence_medium": "मध्यम विश्वास", + "cal_confidence_low": "निम्न विश्वास", + "budget_monthly": "मासिक बजट", + "budget_yearly": "वार्षिक बजट", + "groups": "समूह", + "new_group": "नया समूह", + "edit_group": "समूह संपादित करें", + "no_groups": "अभी कोई समूह नहीं", + "delete_group": "समूह हटाएँ", + "delete_group_confirm": "समूह '{name}' हटाएँ?", + "group_select_tasks": "कार्य चुनें", + "group_name_required": "नाम आवश्यक है", + "description_optional": "विवरण (वैकल्पिक)", + "selected": "चयनित", + "loading_chart": "चार्ट डेटा लोड हो रहा है...", + "hide_outliers": "आउटलायर छिपाएँ (सेंसर गड़बड़)", + "was_maintenance_needed": "क्या यह रखरखाव आवश्यक था?", + "feedback_needed": "आवश्यक था", + "feedback_not_needed": "आवश्यक नहीं", + "feedback_not_sure": "निश्चित नहीं", + "suggested_interval": "सुझाया गया अंतराल", + "apply_suggestion": "लागू करें", + "reanalyze": "पुनः विश्लेषण करें", + "reanalyze_result": "नया विश्लेषण", + "reanalyze_insufficient_data": "अनुशंसा देने के लिए पर्याप्त डेटा नहीं", + "data_points": "डेटा बिंदु", + "dismiss_suggestion": "खारिज करें", + "confidence_low": "निम्न", + "confidence_medium": "मध्यम", + "confidence_high": "उच्च", + "recommended": "अनुशंसित", + "seasonal_awareness": "मौसमी जागरूकता", + "edit_seasonal_overrides": "मौसमी कारक संपादित करें", + "seasonal_overrides_title": "मौसमी कारक (ओवरराइड)", + "seasonal_overrides_hint": "प्रति माह कारक (0.1–5.0)। खाली = स्वचालित रूप से सीखा गया।", + "seasonal_override_invalid": "अमान्य मान", + "seasonal_override_range": "कारक 0.1 और 5.0 के बीच होना चाहिए", + "clear_all": "सभी साफ़ करें", + "seasonal_chart_title": "मौसमी कारक", + "seasonal_learned": "सीखा गया", + "seasonal_manual": "मैनुअल", + "month_jan": "जन", + "month_feb": "फ़र", + "month_mar": "मार्च", + "month_apr": "अप्रैल", + "month_may": "मई", + "month_jun": "जून", + "month_jul": "जुल", + "month_aug": "अग", + "month_sep": "सित", + "month_oct": "अक्टू", + "month_nov": "नव", + "month_dec": "दिस", + "sensor_prediction": "सेंसर पूर्वानुमान", + "degradation_trend": "रुझान", + "trend_rising": "बढ़ता हुआ", + "trend_falling": "घटता हुआ", + "trend_stable": "स्थिर", + "trend_insufficient_data": "अपर्याप्त डेटा", + "days_until_threshold": "सीमा तक दिन", + "threshold_exceeded": "सीमा पार हुई", + "environmental_adjustment": "पर्यावरण कारक", + "sensor_prediction_urgency": "सेंसर ~{days} दिनों में सीमा का अनुमान लगाता है", + "day_short": "दिन", + "weibull_reliability_curve": "विश्वसनीयता वक्र", + "weibull_failure_probability": "विफलता संभावना", + "weibull_r_squared": "फ़िट R²", + "beta_early_failures": "प्रारंभिक विफलताएँ", + "beta_random_failures": "यादृच्छिक विफलताएँ", + "beta_wear_out": "घिसाव", + "beta_highly_predictable": "अत्यधिक पूर्वानुमेय", + "confidence_interval": "विश्वास अंतराल", + "confidence_conservative": "रूढ़िवादी", + "confidence_aggressive": "आशावादी", + "current_interval_marker": "वर्तमान अंतराल", + "recommended_marker": "अनुशंसित", + "characteristic_life": "विशेषता आयु", + "chart_mini_sparkline": "रुझान स्पार्कलाइन", + "chart_history": "लागत और अवधि इतिहास", + "chart_seasonal": "मौसमी कारक, 12 महीने", + "chart_weibull": "वीबुल विश्वसनीयता वक्र", + "chart_sparkline": "सेंसर ट्रिगर मान चार्ट", + "days_progress": "दिनों की प्रगति", + "qr_code": "QR कोड", + "qr_generating": "QR कोड बनाया जा रहा है…", + "qr_error": "QR कोड बनाने में विफल।", + "qr_error_no_url": "कोई HA URL कॉन्फ़िगर नहीं है। कृपया सेटिंग्स → सिस्टम → नेटवर्क में एक बाहरी या आंतरिक URL सेट करें।", + "save_error": "सहेजने में विफल। कृपया पुनः प्रयास करें।", + "qr_print": "प्रिंट करें", + "qr_download": "SVG डाउनलोड करें", + "qr_action": "स्कैन पर क्रिया", + "qr_action_view": "रखरखाव जानकारी देखें", + "qr_action_complete": "रखरखाव को पूर्ण के रूप में चिह्नित करें", + "qr_url_mode": "लिंक प्रकार", + "qr_mode_companion": "Companion ऐप", + "qr_mode_local": "लोकल (mDNS)", + "qr_mode_server": "सर्वर URL", + "overview": "अवलोकन", + "analysis": "विश्लेषण", + "recent_activities": "हाल की गतिविधियाँ", + "search_notes": "टिप्पणियाँ खोजें", + "avg_cost": "औसत लागत", + "no_advanced_features": "कोई उन्नत सुविधा सक्षम नहीं", + "no_advanced_features_hint": "यहाँ विश्लेषण डेटा देखने के लिए इंटीग्रेशन सेटिंग्स में “अनुकूली अंतराल” या “मौसमी पैटर्न” सक्षम करें।", + "analysis_not_enough_data": "अभी विश्लेषण के लिए पर्याप्त डेटा नहीं।", + "analysis_not_enough_data_hint": "वीबुल विश्लेषण के लिए कम से कम 5 पूर्ण रखरखाव आवश्यक हैं; मौसमी पैटर्न प्रति माह 6+ डेटा बिंदुओं के बाद दिखाई देते हैं।", + "analysis_manual_task_hint": "अंतराल के बिना मैनुअल कार्य विश्लेषण डेटा उत्पन्न नहीं करते।", + "completions": "पूर्णताएँ", + "current": "वर्तमान", + "shorter": "छोटा", + "longer": "लंबा", + "normal": "सामान्य", + "disabled": "अक्षम", + "compound_logic": "संयुक्त लॉजिक", + "compound": "संयुक्त (कई शर्तें)", + "compound_logic_and": "और — सभी शर्तें पूरी हों", + "compound_logic_or": "या — कोई एक शर्त पर्याप्त", + "compound_help": "कई सेंसर शर्तों को एक ट्रिगर में मिलाएँ।", + "compound_no_conditions": "अभी कोई शर्त नहीं — कम से कम एक जोड़ें।", + "compound_add_condition": "शर्त जोड़ें", + "compound_condition": "शर्त", + "compound_remove_condition": "शर्त हटाएँ", + "card_title": "शीर्षक", + "card_show_header": "सांख्यिकी के साथ हेडर दिखाएँ", + "card_show_actions": "क्रिया बटन दिखाएँ", + "card_compact": "संक्षिप्त मोड", + "card_max_items": "अधिकतम आइटम (0 = सभी)", + "card_filter_status": "स्थिति के अनुसार फ़िल्टर करें", + "card_filter_status_help": "खाली = सभी स्थितियाँ दिखाएँ।", + "card_filter_objects": "वस्तुओं के अनुसार फ़िल्टर करें", + "card_filter_objects_help": "खाली = सभी वस्तुएँ दिखाएँ।", + "card_filter_entities": "एंटिटी के अनुसार फ़िल्टर करें (entity_ids)", + "card_filter_entities_help": "इस इंटीग्रेशन से sensor / binary_sensor एंटिटी चुनें। खाली = सभी।", + "card_loading_objects": "वस्तुएँ लोड हो रही हैं…", + "card_load_error": "वस्तुएँ लोड नहीं हो सकीं — WebSocket कनेक्शन जाँचें।", + "card_no_tasks_title": "अभी कोई रखरखाव कार्य नहीं", + "card_no_tasks_cta": "→ रखरखाव पैनल में एक बनाएँ", + "no_objects": "अभी कोई वस्तु नहीं।", + "action_error": "क्रिया विफल। कृपया पुनः प्रयास करें।", + "area_id_optional": "क्षेत्र (वैकल्पिक)", + "installation_date_optional": "स्थापना तिथि (वैकल्पिक)", + "warranty_expiry_optional": "वारंटी समाप्ति (वैकल्पिक)", + "warranty": "वारंटी", + "warranty_valid_until": "{date} तक वैध", + "warranty_expires_in": "{days} दिनों में समाप्त", + "warranty_expired": "समाप्त", + "cal_past_windows": "पिछली अवधियाँ", + "cal_forward_windows": "आगामी अवधियाँ", + "history_edit_title": "इतिहास प्रविष्टि संपादित करें", + "history_edit_timestamp": "टाइमस्टैम्प", + "manufacturer": "निर्माता", + "model": "मॉडल", + "area": "क्षेत्र", + "actions": "क्रियाएँ", + "view_mode_label": "दृश्य", + "view_cards": "कार्ड दृश्य", + "view_table": "तालिका दृश्य", + "objects_table_columns_label": "वस्तु तालिका कॉलम", + "objects_table_columns_hint": "चुनें कि वस्तु तालिका दृश्य में कौन से कॉलम दिखाई दें।", + "custom_icon_optional": "आइकन (वैकल्पिक, उदा. mdi:wrench)", + "task_enabled": "कार्य सक्षम", + "skip_reason_prompt": "इस कार्य को छोड़ें?", + "reason_optional": "कारण (वैकल्पिक)", + "reset_date_prompt": "कार्य को किया गया के रूप में चिह्नित करें?", + "reset_date_optional": "अंतिम बार किया गया तिथि (वैकल्पिक, डिफ़ॉल्ट आज)", + "notes_label": "टिप्पणियाँ", + "documentation_label": "दस्तावेज़", + "no_nfc_tag": "— कोई टैग नहीं —", + "dashboard": "डैशबोर्ड", + "tab_today": "आज", + "palette_placeholder": "ऑब्जेक्ट और कार्य खोजें…", + "palette_no_results": "कोई मिलान नहीं", + "palette_hint": "↑↓ नेविगेट · Enter खोलें · Esc बंद", + "today_all_caught_up": "सब पूरा! इस सप्ताह कुछ भी बाकी नहीं।", + "today_overdue": "अतिदेय", + "today_due_today": "आज देय", + "today_this_week": "इस सप्ताह", + "settings": "सेटिंग्स", + "settings_features": "उन्नत सुविधाएँ", + "settings_features_desc": "उन्नत सुविधाओं को सक्षम या अक्षम करें। अक्षम करने से वे UI से छिप जाती हैं पर डेटा नहीं हटता।", + "feat_adaptive": "अनुकूली निर्धारण", + "feat_adaptive_desc": "रखरखाव इतिहास से इष्टतम अंतराल सीखें", + "feat_predictions": "सेंसर पूर्वानुमान", + "feat_predictions_desc": "सेंसर क्षरण से ट्रिगर तिथियों का अनुमान लगाएँ", + "feat_seasonal": "मौसमी समायोजन", + "feat_seasonal_desc": "मौसमी पैटर्न के आधार पर अंतराल समायोजित करें", + "feat_environmental": "पर्यावरणीय सहसंबंध", + "feat_environmental_desc": "तापमान/आर्द्रता के साथ अंतराल का सहसंबंध करें", + "feat_budget": "बजट ट्रैकिंग", + "feat_budget_desc": "मासिक और वार्षिक रखरखाव खर्च ट्रैक करें", + "feat_groups": "कार्य समूह", + "feat_groups_desc": "कार्यों को तार्किक समूहों में व्यवस्थित करें", + "feat_checklists": "चेकलिस्ट", + "feat_checklists_desc": "कार्य पूर्णता के लिए बहु-चरणीय प्रक्रियाएँ", + "settings_general": "सामान्य", + "settings_default_warning": "डिफ़ॉल्ट चेतावनी दिन", + "settings_panel_enabled": "साइडबार पैनल", + "settings_panel_title": "साइडबार पैनल शीर्षक", + "settings_notifications": "सूचनाएँ", + "settings_notify_service": "सूचना सेवा", + "test_notification": "परीक्षण सूचना", + "send_test": "परीक्षण भेजें", + "testing": "भेजा जा रहा है…", + "test_notification_success": "परीक्षण सूचना भेजी गई", + "test_notification_failed": "परीक्षण सूचना विफल", + "settings_notify_due_soon": "जल्द देय होने पर सूचित करें", + "settings_notify_overdue": "अतिदेय होने पर सूचित करें", + "settings_notify_triggered": "ट्रिगर होने पर सूचित करें", + "settings_interval_hours": "दोहराव अंतराल (घंटे, 0 = एक बार)", + "settings_quiet_hours": "शांत घंटे", + "settings_quiet_start": "प्रारंभ", + "settings_quiet_end": "समाप्त", + "settings_max_per_day": "प्रति दिन अधिकतम सूचनाएँ (0 = असीमित)", + "settings_bundling": "सूचनाएँ बंडल करें", + "settings_bundle_threshold": "बंडल सीमा", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "मोबाइल क्रिया बटन", + "settings_action_complete": "'पूर्ण करें' बटन दिखाएँ", + "settings_action_skip": "'छोड़ें' बटन दिखाएँ", + "settings_action_snooze": "'स्नूज़' बटन दिखाएँ", + "settings_weekly_digest": "साप्ताहिक सारांश", + "settings_weekly_digest_hint": "सोमवार सुबह एक सारांश सूचना जब कार्य देय हों।", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "स्नूज़ अवधि (घंटे)", + "settings_budget": "बजट", + "settings_currency": "मुद्रा", + "settings_budget_monthly": "मासिक बजट", + "settings_budget_yearly": "वार्षिक बजट", + "settings_budget_alerts": "बजट अलर्ट", + "settings_budget_threshold": "अलर्ट सीमा (%)", + "settings_import_export": "आयात / निर्यात", + "settings_export_json": "JSON निर्यात करें", + "settings_export_yaml": "YAML निर्यात करें", + "settings_export_csv": "CSV निर्यात करें", + "settings_import_csv": "CSV आयात करें", + "settings_import_placeholder": "JSON या CSV सामग्री यहाँ चिपकाएँ…", + "settings_import_btn": "आयात करें", + "settings_import_success": "{count} वस्तुएँ सफलतापूर्वक आयात की गईं।", + "settings_export_success": "निर्यात डाउनलोड किया गया।", + "settings_saved": "सेटिंग सहेजी गई।", + "settings_include_history": "इतिहास शामिल करें", + "sort_alphabetical": "वर्णानुक्रम", + "sort_due_soonest": "सबसे जल्द देय", + "sort_task_count": "कार्य संख्या", + "sort_area": "क्षेत्र", + "sort_assigned_user": "असाइन किया गया उपयोगकर्ता", + "sort_group": "समूह", + "groupby_none": "कोई समूहन नहीं", + "groupby_area": "क्षेत्र के अनुसार", + "groupby_group": "समूह के अनुसार", + "groupby_user": "उपयोगकर्ता के अनुसार", + "filter_label": "फ़िल्टर", + "user_label": "उपयोगकर्ता", + "sort_label": "क्रमबद्ध करें", + "group_by_label": "इसके अनुसार समूहित करें", + "state_value_help": "HA स्थिति मान का उपयोग करें (आमतौर पर लोअरकेस, उदा. \"on\"/\"off\")। सहेजने पर केस सामान्यीकृत हो जाता है।", + "target_changes_help": "ट्रिगर होने से पहले मिलान वाले संक्रमणों की संख्या (डिफ़ॉल्ट: 1)।", + "qr_print_title": "QR कोड प्रिंट करें", + "qr_print_desc": "अपने उपकरण पर काटकर चिपकाने के लिए QR कोड का प्रिंट-योग्य पृष्ठ बनाएँ।", + "qr_print_load": "वस्तुएँ लोड करें", + "qr_print_filter": "फ़िल्टर", + "qr_print_objects": "वस्तुएँ", + "qr_print_actions": "क्रियाएँ", + "qr_print_url_mode": "लिंक प्रकार", + "qr_print_estimate": "अनुमानित QR कोड", + "qr_print_over_limit": "सीमा 200 है, फ़िल्टर को संकीर्ण करें", + "qr_print_generate": "QR कोड बनाएँ", + "qr_print_generating": "बनाया जा रहा है…", + "qr_print_ready": "QR कोड तैयार", + "qr_print_print_button": "प्रिंट करें", + "qr_print_empty": "बनाने के लिए कुछ नहीं", + "qr_action_skip": "छोड़ें", + "vacation_title": "अवकाश मोड", + "vacation_active": "सक्रिय", + "vacation_ended": "समाप्त", + "vacation_desc": "अवकाश की योजना बनाएँ: इस अवधि और कुछ बफ़र दिनों के दौरान सूचनाएँ रुकी रहती हैं। आप विशिष्ट कार्यों को वापस शामिल कर सकते हैं।", + "vacation_enable": "अवकाश मोड सक्षम करें", + "vacation_start": "प्रारंभ", + "vacation_end": "समाप्त", + "vacation_buffer": "बफ़र (दिन)", + "vacation_exempt_title": "अवकाश के दौरान फिर भी सूचित करें", + "vacation_exempt_desc": "ऐसे कार्य चुनें जिनकी अवकाश के दौरान भी सूचना मिलनी चाहिए (उदा. महत्वपूर्ण पूल रसायन)।", + "vacation_load_tasks": "कार्य लोड करें", + "vacation_preview_btn": "पूर्वावलोकन दिखाएँ", + "vacation_preview_affected": "प्रभावित कार्य", + "vacation_event_due_soon": "जल्द देय हो जाता है", + "vacation_event_overdue": "अतिदेय हो जाता है", + "vacation_event_triggered_est": "सेंसर ट्रिगर संभव", + "vacation_sensor_based": "(सेंसर-आधारित)", + "vacation_action_notify": "फिर भी सूचित करें", + "vacation_action_unsilence": "फिर से चुप करें", + "vacation_marked_complete": "पूर्ण के रूप में चिह्नित", + "vacation_marked_skip": "छोड़ा गया", + "vacation_end_now": "अवकाश अभी समाप्त करें", + "add": "जोड़ें", + "show_stats": "सांख्यिकी + ग्राफ़ दिखाएँ", + "hide_stats": "सांख्यिकी छिपाएँ", + "adaptive_no_data": "अनुकूली विश्लेषण के लिए अभी पर्याप्त पूर्णता इतिहास नहीं। अंतराल अनुशंसाएँ और विश्वसनीयता चार्ट अनलॉक करने के लिए इस कार्य को कुछ और बार पूर्ण करें।", + "suggestion_applied": "सुझाया गया अंतराल लागू किया गया", + "vacation_mode": "अवकाश मोड", + "vacation_status_active": "अभी सक्रिय", + "vacation_status_scheduled": "निर्धारित", + "vacation_status_inactive": "निष्क्रिय", + "vacation_end_now_confirm": "अवकाश तुरंत समाप्त करें?", + "vacation_exempt_count": "छूट प्राप्त", + "vacation_advanced": "उन्नत…", + "vacation_open_panel": "पैनल में खोलें", + "enable": "सक्षम करें", + "saved": "सहेजा गया", + "budget_monthly_set": "मासिक सेट करें", + "budget_yearly_set": "वार्षिक सेट करें", + "budget_advanced": "मुद्रा, अलर्ट…", + "budget_open_panel": "पैनल में खोलें", + "groups_empty": "अभी कोई समूह नहीं।", + "group_new_placeholder": "समूह जोड़ें…", + "group_delete_confirm": "समूह \"{name}\" हटाएँ?", + "groups_manage_tasks": "कार्य असाइनमेंट प्रबंधित करें…", + "groups_open_panel": "पैनल में खोलें", + "unassigned": "असाइन नहीं", + "no_area": "कोई क्षेत्र नहीं", + "has_overdue": "अतिदेय कार्य हैं", + "object": "वस्तु", + "settings_panel_access": "पैनल पहुँच", + "settings_panel_access_desc": "व्यवस्थापकों को हमेशा पूर्ण पहुँच होती है। विशिष्ट गैर-व्यवस्थापकों को बनाना, संपादित करना और हटाना सौंपने के लिए, इसे चालू करें और नीचे उन्हें चुनें — बाकी सभी को केवल पूर्ण करें और छोड़ें दिखता है।", + "settings_operator_write": "चयनित उपयोगकर्ताओं को बनाने, संपादित करने और हटाने की अनुमति दें", + "settings_operator_write_desc": "बंद: केवल व्यवस्थापक सामग्री बदल सकते हैं। चालू: नीचे चयनित उपयोगकर्ताओं को भी पूर्ण पहुँच मिलती है।", + "no_non_admin_users": "कोई गैर-व्यवस्थापक उपयोगकर्ता नहीं मिला। सेटिंग्स → लोग में कुछ जोड़ें।", + "owner_label": "स्वामी", + "feat_completion_actions": "पूर्णता क्रियाएँ", + "feat_completion_actions_desc": "पूर्ण होने पर प्रति-कार्य HA क्रिया + पूर्व-निर्धारित मानों के साथ त्वरित-पूर्ण QR।", + "on_complete_action_title": "पूर्ण होने पर: HA क्रिया ट्रिगर करें (वैकल्पिक)", + "on_complete_action_desc": "कार्य पूर्ण होने पर एक HA सेवा कॉल करता है — उदा. डिवाइस पर काउंटर रीसेट करना।", + "on_complete_action_service": "सेवा", + "on_complete_action_target": "लक्ष्य एंटिटी", + "on_complete_action_target_hint": "ध्यान दें: एंटिटी डोमेन सेवा से मेल खाना चाहिए — उदा. 'button.press' केवल button.* पर काम करता है, 'counter.increment' केवल counter.* पर, 'input_button.press' केवल input_button.* पर आदि। मेल न खाने पर क्रिया चुपचाप विफल हो जाएगी (HA लॉग करता है 'Referenced entities ... missing or not currently available')।", + "on_complete_action_data": "डेटा (JSON, वैकल्पिक)", + "on_complete_action_test": "कॉन्फ़िगरेशन सत्यापित करें", + "on_complete_action_test_success": "✓ कॉन्फ़िगरेशन वैध (क्रिया केवल कार्य पूर्ण होने पर चलेगी)", + "on_complete_action_test_failed": "विफल", + "quick_complete_defaults_title": "त्वरित-पूर्ण डिफ़ॉल्ट (QR स्कैन के लिए, वैकल्पिक)", + "quick_complete_defaults_desc": "त्वरित-पूर्ण QR स्कैन के लिए पूर्व-निर्धारित मान। इनके बिना, QR पूर्ण करें संवाद खोलता है।", + "quick_complete_defaults_notes": "टिप्पणियाँ", + "quick_complete_defaults_cost": "लागत", + "quick_complete_defaults_duration": "अवधि (मिनट)", + "quick_complete_defaults_feedback_none": "कोई प्रतिक्रिया नहीं", + "quick_complete_defaults_feedback_needed": "आवश्यक था", + "quick_complete_defaults_feedback_not_needed": "आवश्यक नहीं", + "quick_complete_success": "त्वरित रूप से पूर्ण के रूप में चिह्नित", + "show_all_objects": "सभी वस्तुएँ दिखाएँ", + "show_all_tasks": "फ़िल्टर हटाएँ — सभी कार्य दिखाएँ", + "filter_to_overdue": "कार्य सूची को केवल अतिदेय तक फ़िल्टर करें", + "filter_to_due_soon": "कार्य सूची को केवल जल्द-देय तक फ़िल्टर करें", + "filter_to_triggered": "कार्य सूची को केवल ट्रिगर हुए तक फ़िल्टर करें", + "open_task": "कार्य खोलें", + "show_details": "इतिहास + सांख्यिकी दिखाएँ", + "hide_details": "विवरण छिपाएँ", + "history_empty": "अभी कोई इतिहास नहीं।", + "history_edit_button": "प्रविष्टि संपादित करें", + "total_cost": "कुल लागत", + "times_performed": "किया गया", + "older_entries": "पुराने", + "open_in_panel": "रखरखाव पैनल में खोलें", + "skip_reason": "छोड़ने का कारण (वैकल्पिक)", + "reset_to_date": "अंतिम बार किया गया को रीसेट करें", + "delete_task_confirm": "इस कार्य और इसके इतिहास को हटाएँ?", + "delete_object_confirm": "इस वस्तु और इसके सभी कार्यों को हटाएँ?", + "loading": "लोड हो रहा है…", + "archive": "संग्रह करें", + "undo": "पूर्ववत करें", + "task_archived": "कार्य संग्रहित", + "object_archived": "ऑब्जेक्ट संग्रहित", + "unarchive": "संग्रह से हटाएँ", + "archived": "संग्रहीत", + "show_archived": "संग्रहीत दिखाएँ", + "hide_archived": "संग्रहीत छिपाएँ", + "confirm_archive_object": "इस ऑब्जेक्ट और इसके कार्यों को संग्रहित करें? इतिहास सुरक्षित रहता है और बाद में पुनर्स्थापित किया जा सकता है।", + "settings_archive": "संग्रह और प्रतिधारण", + "settings_archive_desc": "पूर्ण एक-बार के कार्यों को हटाए बिना अलग रखें। संग्रहीत आइटम छिपे और निष्क्रिय रहते हैं, पर इतिहास और लागत बनाए रखते हैं।", + "settings_archive_oneoff_days": "पूर्ण एक-बार के कार्य स्वतः संग्रहित करें (दिन, 0 = बंद)", + "settings_delete_archived_oneoff_days": "संग्रहीत एक-बार के कार्य स्वतः हटाएँ (दिन, 0 = कभी नहीं)", + "archive_object": "ऑब्जेक्ट संग्रहित करें", + "unarchive_object": "ऑब्जेक्ट संग्रह से हटाएँ", + "documents": "दस्तावेज़", + "documents_empty": "अभी तक कोई दस्तावेज़ नहीं।", + "doc_upload": "फ़ाइल अपलोड करें", + "doc_uploading": "अपलोड हो रहा है…", + "doc_add_link": "लिंक जोड़ें", + "doc_link_url": "URL (https://…)", + "doc_link_title": "शीर्षक (वैकल्पिक)", + "doc_open": "खोलें", + "doc_delete_confirm": "\"{name}\" हटाएँ?", + "doc_too_large": "फ़ाइल बहुत बड़ी है (अधिकतम 25 MB)।", + "doc_upload_failed": "अपलोड विफल रहा।", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "पहले से कहीं और संग्रहीत — साझा, कोई अतिरिक्त स्थान नहीं।", + "doc_dup_in_object": "यह फ़ाइल पहले से इस ऑब्जेक्ट से जुड़ी है।", + "doc_link_invalid": "केवल http/https लिंक की अनुमति है।", + "doc_cat_manual": "मैनुअल", + "doc_cat_warranty": "वारंटी", + "doc_cat_invoice": "चालान", + "doc_cat_spare_parts": "स्पेयर पार्ट्स", + "doc_cat_photo": "फ़ोटो", + "doc_cat_other": "अन्य", + "doc_link_badge": "लिंक", + "doc_storage_title": "दस्तावेज़ संग्रहण", + "doc_storage_saved": "डीडुप्लिकेशन से बचत", + "doc_storage_refresh": "ताज़ा करें", + "doc_download": "डाउनलोड करें", + "doc_close": "बंद करें", + "doc_camera": "फ़ोटो लें", + "doc_drop_hint": "फ़ाइलें यहाँ छोड़ें", + "doc_task_none": "इस कार्य से कोई दस्तावेज़ संबद्ध नहीं है।", + "doc_link_existing": "दस्तावेज़ जोड़ें…", + "doc_attach": "जोड़ें", + "doc_unlink": "अलग करें", + "doc_page": "पृष्ठ", + "chart_range_7d": "7दिन", + "chart_range_30d": "30दिन", + "chart_range_90d": "90दिन", + "chart_range_1y": "1वर्ष", + "chart_since_service": "पिछले रखरखाव के बाद से", + "chart_no_stats": "इस एंटिटी के लिए दीर्घकालिक आँकड़े नहीं हैं — केवल रखरखाव इवेंट मान दिखाए जा रहे हैं", + "auto_complete_on_recovery": "सेंसर ठीक होने पर स्वतः पूर्ण करें", + "auto_complete_on_recovery_help": "जब ट्रिगर स्वयं हल हो जाता है तो एक पूर्णता दर्ज करता है (अंतिम निष्पादन सेट करता है) — जैसे नमक भरा गया, फ़िल्टर बदला गया।", + "doc_search": "दस्तावेज़ खोजें…", + "doc_search_none": "कोई मेल खाता दस्तावेज़ नहीं", + "link_device_optional": "मौजूदा डिवाइस से लिंक करें (वैकल्पिक)", + "parent_object_optional": "मूल ऑब्जेक्ट (वैकल्पिक)", + "parent_none": "(कोई मूल नहीं)", + "paused": "रोका गया", + "pause_object": "रोकें", + "resume_object": "फिर से शुरू करें", + "pause_until_prompt": "इस ऑब्जेक्ट के शेड्यूल फ़्रीज़ करें — फिर से शुरू करने तक कुछ भी देय नहीं होगा और कोई सूचना नहीं आएगी। वैकल्पिक रूप से स्वतः पुनरारंभ की तारीख़ सेट करें।", + "pause_until_label": "फिर से शुरू करें (वैकल्पिक)", + "object_paused": "ऑब्जेक्ट रोका गया", + "object_resumed": "ऑब्जेक्ट फिर से शुरू — शेड्यूल पुनः आरंभ", + "object_paused_badge": "रोका गया", + "paused_until_label": "तक", + "replace_object": "बदलें…", + "replace_object_prompt": "इस ऑब्जेक्ट को सेवानिवृत्त करें और उत्तराधिकारी बनाएं। इतिहास और लागत पुराने पर संग्रहीत रहती है; कार्य और दस्तावेज़ नए में चले जाते हैं, काउंटर नए सिरे से शुरू होते हैं।", + "replace_name_label": "उत्तराधिकारी का नाम", + "object_replaced": "ऑब्जेक्ट बदला गया — उत्तराधिकारी बनाया गया", + "reading_unit_label": "रीडिंग इकाई (जैसे kWh, m³)", + "reading_unit_help": "इस कार्य को पूरा करते समय दर्ज मान के बगल में दिखाई जाती है।", + "reading_value_label": "रीडिंग मान", + "reading_label": "रीडिंग", + "settings_templates_label": "टेम्पलेट गैलरी", + "settings_templates_hint": "जिन टेम्पलेट की कभी ज़रूरत नहीं होगी उन्हें अनचेक करें — वे \"टेम्पलेट से\" चयनकर्ताओं (पैनल और कॉन्फ़िग फ़्लो) से गायब हो जाएंगे। और कुछ नहीं बदलता; कभी भी फिर से सक्षम कर सकते हैं।", + "worksheet": "कार्य पत्रक", + "worksheet_scan_view": "कार्य खोलने के लिए स्कैन करें", + "worksheet_scan_complete": "पूरा करने के लिए स्कैन करें", + "worksheet_manual_excerpt": "मैनुअल अंश", + "worksheet_pages": "पृष्ठ", + "worksheet_printed": "मुद्रित", + "worksheet_never": "कभी नहीं" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/it.json b/custom_components/maintenance_supporter/frontend-src/locales/it.json new file mode 100644 index 0000000..8397385 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/it.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Manutenzione", + "objects": "Oggetti", + "tasks": "Attività", + "overdue": "Scaduto", + "due_soon": "In scadenza", + "triggered": "Attivato", + "ok": "OK", + "all": "Tutti", + "new_object": "+ Nuovo oggetto", + "templates_from": "Da modello", + "templates_title": "Inizia da un modello", + "templates_task_count": "{n} attività", + "template_created": "Creato da modello", + "onboard_hint": "Aggiungi il primo oggetto per iniziare a tracciare la manutenzione.", + "edit": "Modifica", + "duplicate": "Duplica", + "task_duplicated": "Attività duplicata", + "object_duplicated": "Oggetto duplicato", + "delete": "Elimina", + "add_task": "+ Attività", + "complete": "Completato", + "completed": "Completato", + "skip": "Salta", + "skipped": "Saltato", + "missed": "Missed", + "reset": "Reimposta", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Annulla", + "bulk_select": "Seleziona", + "bulk_select_all": "Seleziona tutto", + "bulk_n_selected": "{n} selezionati", + "bulk_completed": "{n} attività completate", + "bulk_archived": "{n} attività archiviate", + "completing": "Completamento…", + "interval": "Intervallo", + "warning": "Avviso", + "last_performed": "Ultima esecuzione", + "next_due": "Prossima scadenza", + "days_until_due": "Giorni alla scadenza", + "avg_duration": "Ø Durata", + "trigger": "Trigger", + "trigger_type": "Tipo di trigger", + "threshold_above": "Limite superiore", + "threshold_below": "Limite inferiore", + "threshold": "Soglia", + "counter": "Contatore", + "state_change": "Cambio di stato", + "runtime": "Tempo di funzionamento", + "runtime_hours": "Durata obiettivo (ore)", + "target_value": "Valore obiettivo", + "baseline": "Linea di base", + "target_changes": "Modifiche obiettivo", + "for_minutes": "Per (minuti)", + "time_based": "Temporale", + "sensor_based": "Sensore", + "manual": "Manuale", + "one_time": "Una tantum", + "weekdays": "Giorni della settimana", + "nth_weekday": "N-esimo giorno della settimana del mese", + "day_of_month": "Giorno del mese", + "recurrence_on_days": "Ripeti il", + "recurrence_occurrence": "Occorrenza", + "recurrence_weekday": "Giorno della settimana", + "recurrence_day": "Giorno del mese (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1º", + "ord_2": "2º", + "ord_3": "3º", + "ord_4": "4º", + "ord_5": "5º", + "ord_last": "Ultimo", + "day_word": "Giorno", + "interval_value": "Intervallo", + "interval_unit": "Unità", + "unit_days": "Giorni", + "unit_weeks": "Settimane", + "unit_months": "Mesi", + "unit_years": "Anni", + "due_date": "Data di scadenza", + "cleaning": "Pulizia", + "inspection": "Ispezione", + "replacement": "Sostituzione", + "calibration": "Calibrazione", + "service": "Servizio", + "reading": "Lettura", + "custom": "Personalizzato", + "history": "Cronologia", + "cost": "Costo", + "report_button": "Report", + "report_title": "Report di manutenzione", + "report_generated": "Generato", + "report_times_done": "Fatto", + "report_total_cost": "Costo totale", + "report_every": "ogni {n} {unit}", + "report_notes": "Note", + "report_col_type": "Tipo", + "report_col_status": "Stato", + "report_col_schedule": "Pianificazione", + "duration": "Durata", + "both": "Entrambi", + "trigger_val": "Valore trigger", + "complete_title": "Completato: ", + "checklist": "Checklist", + "checklist_steps_optional": "Passaggi della checklist (opzionale)", + "checklist_placeholder": "Pulire il filtro\nSostituire la guarnizione\nTestare la pressione", + "checklist_help": "Un passaggio per riga. Max 100 elementi.", + "err_too_long": "{field}: troppo lungo (max {n} caratteri)", + "err_too_short": "{field}: troppo corto (min {n} caratteri)", + "err_value_too_high": "{field}: troppo grande (max {n})", + "err_value_too_low": "{field}: troppo piccolo (min {n})", + "err_required": "{field}: campo obbligatorio", + "err_wrong_type": "{field}: tipo errato (atteso: {type})", + "err_invalid_choice": "{field}: valore non consentito", + "err_invalid_value": "{field}: valore non valido", + "feat_schedule_time": "Pianificazione oraria", + "feat_schedule_time_desc": "Le attività scadono a un'ora specifica anziché a mezzanotte.", + "schedule_time_optional": "Scadenza all'ora (opzionale, HH:MM)", + "schedule_time_help": "Vuoto = mezzanotte (default). Fuso orario HA.", + "at_time": "alle", + "notes_optional": "Note (opzionale)", + "cost_optional": "Costo (opzionale)", + "duration_minutes": "Durata in minuti (opzionale)", + "days": "giorni", + "day": "giorno", + "today": "Oggi", + "d_overdue": "g in ritardo", + "no_tasks": "Nessuna attività di manutenzione. Crea un oggetto per iniziare.", + "no_tasks_short": "Nessuna attività", + "no_history": "Nessuna voce nella cronologia.", + "show_all": "Mostra tutto", + "cost_duration_chart": "Costi & Durata", + "installed": "Installato", + "confirm_delete_object": "Eliminare questo oggetto e tutte le sue attività?", + "confirm_delete_task": "Eliminare questa attività?", + "min": "Min", + "max": "Max", + "save": "Salva", + "saving": "Salvataggio…", + "edit_task": "Modifica attività", + "new_task": "Nuova attività di manutenzione", + "task_name": "Nome attività", + "maintenance_type": "Tipo di manutenzione", + "priority": "Priorità", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Bassa", + "priority_normal": "Normale", + "priority_high": "Alta", + "schedule_type": "Tipo di pianificazione", + "interval_days": "Intervallo (giorni)", + "warning_days": "Giorni di avviso", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Ultima esecuzione (opzionale)", + "interval_anchor": "Ancoraggio intervallo", + "anchor_completion": "Dalla data di completamento", + "anchor_planned": "Dalla data pianificata (nessuna deriva)", + "edit_object": "Modifica oggetto", + "name": "Nome", + "manufacturer_optional": "Produttore (opzionale)", + "model_optional": "Modello (opzionale)", + "serial_number_optional": "Numero di serie (opzionale)", + "serial_number_label": "N/S", + "documentation_url_label": "Manuale", + "object_notes_label": "Note", + "sort_due_date": "Scadenza", + "sort_object": "Nome oggetto", + "sort_type": "Tipo", + "sort_task_name": "Nome attività", + "all_objects": "Tutti gli oggetti", + "tasks_lower": "attività", + "no_tasks_yet": "Nessuna attività", + "add_first_task": "Aggiungi prima attività", + "trigger_configuration": "Configurazione trigger", + "entity_id": "ID entità", + "comma_separated": "separati da virgola", + "entity_logic": "Logica entità", + "entity_logic_any": "Qualsiasi entità attiva", + "entity_logic_all": "Tutte le entità devono attivare", + "entities": "entità", + "attribute_optional": "Attributo (opzionale, vuoto = stato)", + "use_entity_state": "Usa stato dell'entità (nessun attributo)", + "trigger_above": "Attivare sopra", + "trigger_below": "Attivare sotto", + "for_at_least_minutes": "Per almeno (minuti)", + "safety_interval_days": "Intervallo di sicurezza (giorni, opzionale)", + "safety_interval": "Intervallo di sicurezza (opzionale)", + "delta_mode": "Modalità delta", + "from_state_optional": "Dallo stato (opzionale)", + "to_state_optional": "Allo stato (opzionale)", + "documentation_url_optional": "URL documentazione (opzionale)", + "object_notes_optional": "Note (opzionale)", + "nfc_tag_id_optional": "ID tag NFC (opzionale)", + "nfc_tags_empty_help": "Nessun tag NFC ancora registrato in Home Assistant.", + "nfc_tags_open_settings": "Apri impostazioni tag", + "nfc_tags_refresh": "Aggiorna", + "environmental_entity_optional": "Sensore ambientale (opzionale)", + "environmental_entity_helper": "es. sensor.temperatura_esterna — regola l'intervallo in base alle condizioni ambientali", + "environmental_attribute_optional": "Attributo ambientale (opzionale)", + "nfc_tag_id": "ID tag NFC", + "nfc_linked": "Tag NFC collegato", + "nfc_link_hint": "Clicca per collegare un tag NFC", + "responsible_user": "Utente responsabile", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Nessun utente assegnato)", + "all_users": "Tutti gli utenti", + "my_tasks": "Le mie attività", + "tab_calendar": "Calendario", + "cal_no_events": "Nessuna manutenzione", + "cal_window_7": "7 giorni", + "cal_window_14": "14 giorni", + "cal_window_30": "30 giorni", + "cal_window_365": "1 anno", + "cal_every_n_days": "ogni {n} giorni", + "cal_source_time": "Basato sul tempo", + "cal_source_time_adaptive": "Basato sul tempo (adattivo)", + "cal_source_sensor": "Basato su sensore", + "cal_predicted": "previsto", + "cal_confidence_high": "alta confidenza", + "cal_confidence_medium": "media confidenza", + "cal_confidence_low": "bassa confidenza", + "budget_monthly": "Budget mensile", + "budget_yearly": "Budget annuale", + "groups": "Gruppi", + "new_group": "Nuovo gruppo", + "edit_group": "Modifica gruppo", + "no_groups": "Nessun gruppo", + "delete_group": "Elimina gruppo", + "delete_group_confirm": "Eliminare il gruppo '{name}'?", + "group_select_tasks": "Seleziona attività", + "group_name_required": "Nome richiesto", + "description_optional": "Descrizione (opzionale)", + "selected": "Selezionato", + "loading_chart": "Caricamento dati...", + "hide_outliers": "Nascondi valori anomali (errori sensore)", + "was_maintenance_needed": "Questa manutenzione era necessaria?", + "feedback_needed": "Necessaria", + "feedback_not_needed": "Non necessaria", + "feedback_not_sure": "Non sicuro", + "suggested_interval": "Intervallo suggerito", + "apply_suggestion": "Applica", + "reanalyze": "Rianalizza", + "reanalyze_result": "Nuova analisi", + "reanalyze_insufficient_data": "Dati insufficienti per una raccomandazione", + "data_points": "punti dati", + "dismiss_suggestion": "Ignora", + "confidence_low": "Bassa", + "confidence_medium": "Media", + "confidence_high": "Alta", + "recommended": "consigliato", + "seasonal_awareness": "Consapevolezza stagionale", + "edit_seasonal_overrides": "Modifica fattori stagionali", + "seasonal_overrides_title": "Fattori stagionali (override)", + "seasonal_overrides_hint": "Fattore per mese (0.1–5.0). Vuoto = appreso automaticamente.", + "seasonal_override_invalid": "Valore non valido", + "seasonal_override_range": "Il fattore deve essere tra 0.1 e 5.0", + "clear_all": "Cancella tutto", + "seasonal_chart_title": "Fattori stagionali", + "seasonal_learned": "Appreso", + "seasonal_manual": "Manuale", + "month_jan": "Gen", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "Mag", + "month_jun": "Giu", + "month_jul": "Lug", + "month_aug": "Ago", + "month_sep": "Set", + "month_oct": "Ott", + "month_nov": "Nov", + "month_dec": "Dic", + "sensor_prediction": "Previsione sensore", + "degradation_trend": "Tendenza", + "trend_rising": "In aumento", + "trend_falling": "In calo", + "trend_stable": "Stabile", + "trend_insufficient_data": "Dati insufficienti", + "days_until_threshold": "Giorni alla soglia", + "threshold_exceeded": "Soglia superata", + "environmental_adjustment": "Fattore ambientale", + "sensor_prediction_urgency": "Il sensore prevede la soglia tra ~{days} giorni", + "day_short": "giorno", + "weibull_reliability_curve": "Curva di affidabilità", + "weibull_failure_probability": "Probabilità di guasto", + "weibull_r_squared": "Adattamento R²", + "beta_early_failures": "Guasti precoci", + "beta_random_failures": "Guasti casuali", + "beta_wear_out": "Usura", + "beta_highly_predictable": "Altamente prevedibile", + "confidence_interval": "Intervallo di confidenza", + "confidence_conservative": "Conservativo", + "confidence_aggressive": "Ottimistico", + "current_interval_marker": "Intervallo attuale", + "recommended_marker": "Consigliato", + "characteristic_life": "Vita caratteristica", + "chart_mini_sparkline": "Sparkline di tendenza", + "chart_history": "Cronologia costi e durata", + "chart_seasonal": "Fattori stagionali, 12 mesi", + "chart_weibull": "Curva di affidabilità Weibull", + "chart_sparkline": "Grafico valore trigger sensore", + "days_progress": "Avanzamento giorni", + "qr_code": "Codice QR", + "qr_generating": "Generazione codice QR…", + "qr_error": "Impossibile generare il codice QR.", + "qr_error_no_url": "Nessun URL HA configurato. Impostare un URL esterno o interno in Impostazioni → Sistema → Rete.", + "save_error": "Salvataggio non riuscito. Riprovare.", + "qr_print": "Stampa", + "qr_download": "Scarica SVG", + "qr_action": "Azione alla scansione", + "qr_action_view": "Visualizza info manutenzione", + "qr_action_complete": "Segna manutenzione come completata", + "qr_url_mode": "Tipo di link", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Locale (mDNS)", + "qr_mode_server": "URL server", + "overview": "Panoramica", + "analysis": "Analisi", + "recent_activities": "Attività recenti", + "search_notes": "Cerca nelle note", + "avg_cost": "Ø Costo", + "no_advanced_features": "Nessuna funzione avanzata attivata", + "no_advanced_features_hint": "Attiva “Intervalli Adattivi” o “Modelli Stagionali” nelle impostazioni dell'integrazione per vedere i dati di analisi qui.", + "analysis_not_enough_data": "Non ci sono ancora abbastanza dati per l'analisi.", + "analysis_not_enough_data_hint": "L'analisi Weibull richiede almeno 5 manutenzioni completate; i modelli stagionali diventano visibili dopo 6+ punti dati al mese.", + "analysis_manual_task_hint": "Le attività manuali senza intervallo non generano dati di analisi.", + "completions": "completamenti", + "current": "Attuale", + "shorter": "Più breve", + "longer": "Più lungo", + "normal": "Normale", + "disabled": "Disattivato", + "compound_logic": "Logica composta", + "compound": "Composto (più condizioni)", + "compound_logic_and": "E — tutte le condizioni devono attivarsi", + "compound_logic_or": "O — basta una condizione", + "compound_help": "Combina più condizioni dei sensori in un unico trigger.", + "compound_no_conditions": "Ancora nessuna condizione — aggiungine almeno una.", + "compound_add_condition": "Aggiungi condizione", + "compound_condition": "Condizione", + "compound_remove_condition": "Rimuovi condizione", + "card_title": "Titolo", + "card_show_header": "Mostra intestazione con statistiche", + "card_show_actions": "Mostra pulsanti azione", + "card_compact": "Modalità compatta", + "card_max_items": "Max elementi (0 = tutti)", + "card_filter_status": "Filtra per stato", + "card_filter_status_help": "Vuoto = mostra tutti gli stati.", + "card_filter_objects": "Filtra per oggetti", + "card_filter_objects_help": "Vuoto = mostra tutti gli oggetti.", + "card_filter_entities": "Filtra per entità (entity_ids)", + "card_filter_entities_help": "Seleziona entità sensor / binary_sensor da questa integrazione. Vuoto = tutte.", + "card_loading_objects": "Caricamento oggetti…", + "card_load_error": "Impossibile caricare gli oggetti — verifica la connessione WebSocket.", + "card_no_tasks_title": "Nessuna attività di manutenzione", + "card_no_tasks_cta": "→ Creane una nel pannello Manutenzione", + "no_objects": "Nessun oggetto ancora.", + "action_error": "Azione fallita. Riprova.", + "area_id_optional": "Area (opzionale)", + "installation_date_optional": "Data di installazione (opzionale)", + "warranty_expiry_optional": "Scadenza garanzia (opzionale)", + "warranty": "Garanzia", + "warranty_valid_until": "valida fino al {date}", + "warranty_expires_in": "scade tra {days} giorni", + "warranty_expired": "scaduta", + "cal_past_windows": "Finestre passate", + "cal_forward_windows": "Finestre future", + "history_edit_title": "Modifica voce dello storico", + "history_edit_timestamp": "Data e ora", + "manufacturer": "Produttore", + "model": "Modello", + "area": "Area", + "actions": "Azioni", + "view_mode_label": "Vista", + "view_cards": "Vista schede", + "view_table": "Vista tabella", + "objects_table_columns_label": "Colonne tabella oggetti", + "objects_table_columns_hint": "Scegli quali colonne mostrare nella vista tabella degli oggetti.", + "custom_icon_optional": "Icona (opzionale, es. mdi:wrench)", + "task_enabled": "Attività abilitata", + "skip_reason_prompt": "Saltare questa attività?", + "reason_optional": "Motivo (opzionale)", + "reset_date_prompt": "Segnare l'attività come eseguita?", + "reset_date_optional": "Data ultima esecuzione (opzionale, predefinito: oggi)", + "notes_label": "Note", + "documentation_label": "Documentazione", + "no_nfc_tag": "— Nessun tag —", + "dashboard": "Dashboard", + "tab_today": "Oggi", + "palette_placeholder": "Cerca oggetti e attività…", + "palette_no_results": "Nessun risultato", + "palette_hint": "↑↓ naviga · Invio apri · Esc chiudi", + "today_all_caught_up": "Tutto fatto! Niente in scadenza questa settimana.", + "today_overdue": "Scadute", + "today_due_today": "In scadenza oggi", + "today_this_week": "Questa settimana", + "settings": "Impostazioni", + "settings_features": "Funzioni avanzate", + "settings_features_desc": "Attiva o disattiva le funzioni avanzate. La disattivazione le nasconde dall'interfaccia ma non elimina i dati.", + "feat_adaptive": "Pianificazione adattiva", + "feat_adaptive_desc": "Impara intervalli ottimali dalla cronologia di manutenzione", + "feat_predictions": "Previsioni sensore", + "feat_predictions_desc": "Prevedi date di attivazione dalla degradazione dei sensori", + "feat_seasonal": "Adeguamenti stagionali", + "feat_seasonal_desc": "Adegua gli intervalli in base ai modelli stagionali", + "feat_environmental": "Correlazione ambientale", + "feat_environmental_desc": "Correla gli intervalli con temperatura/umidità", + "feat_budget": "Monitoraggio budget", + "feat_budget_desc": "Monitora le spese di manutenzione mensili e annuali", + "feat_groups": "Gruppi di attività", + "feat_groups_desc": "Organizza le attività in gruppi logici", + "feat_checklists": "Checklist", + "feat_checklists_desc": "Procedure multi-fase per il completamento delle attività", + "settings_general": "Generale", + "settings_default_warning": "Giorni di avviso predefiniti", + "settings_panel_enabled": "Pannello laterale", + "settings_panel_title": "Titolo pannello laterale", + "settings_notifications": "Notifiche", + "settings_notify_service": "Servizio di notifica", + "test_notification": "Notifica di test", + "send_test": "Invia test", + "testing": "Invio in corso…", + "test_notification_success": "Notifica di test inviata", + "test_notification_failed": "Notifica di test non riuscita", + "settings_notify_due_soon": "Notifica quando in scadenza", + "settings_notify_overdue": "Notifica quando scaduto", + "settings_notify_triggered": "Notifica quando attivato", + "settings_interval_hours": "Intervallo di ripetizione (ore, 0 = una volta)", + "settings_quiet_hours": "Ore di silenzio", + "settings_quiet_start": "Inizio", + "settings_quiet_end": "Fine", + "settings_max_per_day": "Max notifiche al giorno (0 = illimitato)", + "settings_bundling": "Raggruppare le notifiche", + "settings_bundle_threshold": "Soglia di raggruppamento", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Pulsanti azione mobili", + "settings_action_complete": "Mostra pulsante 'Completato'", + "settings_action_skip": "Mostra pulsante 'Salta'", + "settings_action_snooze": "Mostra pulsante 'Posticipa'", + "settings_weekly_digest": "Riepilogo settimanale", + "settings_weekly_digest_hint": "Una notifica di riepilogo il lunedì mattina quando ci sono attività.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Durata posticipo (ore)", + "settings_budget": "Budget", + "settings_currency": "Valuta", + "settings_budget_monthly": "Budget mensile", + "settings_budget_yearly": "Budget annuale", + "settings_budget_alerts": "Avvisi budget", + "settings_budget_threshold": "Soglia di avviso (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Esporta JSON", + "settings_export_yaml": "Esporta YAML", + "settings_export_csv": "Esporta CSV", + "settings_import_csv": "Importa CSV", + "settings_import_placeholder": "Incolla il contenuto JSON o CSV qui…", + "settings_import_btn": "Importa", + "settings_import_success": "{count} oggetti importati con successo.", + "settings_export_success": "Export scaricato.", + "settings_saved": "Impostazione salvata.", + "settings_include_history": "Includi cronologia", + "sort_alphabetical": "Alfabetico", + "sort_due_soonest": "Scadenza più vicina", + "sort_task_count": "Numero di attività", + "sort_area": "Area", + "sort_assigned_user": "Utente assegnato", + "sort_group": "Gruppo", + "groupby_none": "Nessun raggruppamento", + "groupby_area": "Per area", + "groupby_group": "Per gruppo", + "groupby_user": "Per utente", + "filter_label": "Filtro", + "user_label": "Utente", + "sort_label": "Ordinamento", + "group_by_label": "Raggruppa per", + "state_value_help": "Usa il valore di stato HA (di solito minuscolo, es. \"on\"/\"off\"). Il case viene normalizzato al salvataggio.", + "target_changes_help": "Numero di transizioni corrispondenti prima che il trigger si attivi (predefinito: 1).", + "qr_print_title": "Stampa codici QR", + "qr_print_desc": "Genera una pagina stampabile di codici QR da ritagliare e applicare sulle apparecchiature.", + "qr_print_load": "Carica oggetti", + "qr_print_filter": "Filtro", + "qr_print_objects": "Oggetti", + "qr_print_actions": "Azioni", + "qr_print_url_mode": "Tipo di link", + "qr_print_estimate": "Codici QR stimati", + "qr_print_over_limit": "limite 200, restringi il filtro", + "qr_print_generate": "Genera codici QR", + "qr_print_generating": "Generazione…", + "qr_print_ready": "Codici QR pronti", + "qr_print_print_button": "Stampa", + "qr_print_empty": "Niente da generare", + "qr_action_skip": "Salta", + "vacation_title": "Modalità vacanza", + "vacation_active": "attiva", + "vacation_ended": "terminata", + "vacation_desc": "Pianifica le tue vacanze: le notifiche vengono messe in pausa durante il periodo più giorni di buffer. Puoi escludere singole attività.", + "vacation_enable": "Attiva modalità vacanza", + "vacation_start": "Inizio", + "vacation_end": "Fine", + "vacation_buffer": "Buffer (giorni)", + "vacation_exempt_title": "Notifica comunque durante le vacanze", + "vacation_exempt_desc": "Scegli attività che devono notificare anche in vacanza (es. chimica della piscina critica).", + "vacation_load_tasks": "Carica attività", + "vacation_preview_btn": "Mostra anteprima", + "vacation_preview_affected": "attività interessate", + "vacation_event_due_soon": "sarà in scadenza", + "vacation_event_overdue": "diventerà scaduta", + "vacation_event_triggered_est": "trigger sensore possibile", + "vacation_sensor_based": "(basato su sensore)", + "vacation_action_notify": "Notifica comunque", + "vacation_action_unsilence": "Silenzia di nuovo", + "vacation_marked_complete": "Segnato come completato", + "vacation_marked_skip": "Saltato", + "vacation_end_now": "Termina vacanza ora", + "unassigned": "Non assegnato", + "no_area": "Nessuna area", + "has_overdue": "Attività scadute", + "object": "Oggetto", + "settings_panel_access": "Accesso al pannello", + "settings_panel_access_desc": "Gli amministratori hanno sempre accesso completo. Per delegare creazione, modifica ed eliminazione a non amministratori specifici, attiva questa opzione e selezionali qui sotto — gli altri vedono solo Completa e Salta.", + "settings_operator_write": "Consenti agli utenti selezionati di creare, modificare ed eliminare", + "settings_operator_write_desc": "Disattivato: solo gli amministratori possono modificare i contenuti. Attivato: anche gli utenti selezionati qui sotto hanno accesso completo.", + "no_non_admin_users": "Nessun utente non amministratore trovato. Aggiungili in Impostazioni → Persone.", + "owner_label": "Proprietario", + "feat_completion_actions": "Azioni di completamento", + "feat_completion_actions_desc": "Azione HA per attività al completamento + QR completamento rapido con valori predefiniti.", + "on_complete_action_title": "Al completamento: attiva azione HA (opzionale)", + "on_complete_action_desc": "Chiama un servizio HA quando l'attività viene completata — es. azzerare un contatore sul dispositivo.", + "on_complete_action_service": "Servizio", + "on_complete_action_target": "Entità target", + "on_complete_action_data": "Dati (JSON, opzionale)", + "on_complete_action_test": "Testa azione", + "on_complete_action_test_success": "Riuscito", + "on_complete_action_test_failed": "Fallito", + "quick_complete_defaults_title": "Valori predefiniti completamento rapido (per scansioni QR, opzionale)", + "quick_complete_defaults_desc": "Valori predefiniti per QR di completamento rapido. Senza, il QR apre la finestra di completamento.", + "quick_complete_defaults_notes": "Note", + "quick_complete_defaults_cost": "Costo", + "quick_complete_defaults_duration": "Durata (minuti)", + "quick_complete_defaults_feedback_none": "Nessun feedback", + "quick_complete_defaults_feedback_needed": "Era necessario", + "quick_complete_defaults_feedback_not_needed": "Non necessario", + "quick_complete_success": "Completato rapidamente", + "trigger_replaced": "Trigger sostituito", + "add": "Aggiungi", + "show_stats": "Mostra statistiche + grafici", + "hide_stats": "Nascondi statistiche", + "adaptive_no_data": "Storico dei completamenti ancora insufficiente per l'analisi adattiva. Completa questa attività ancora qualche volta per sbloccare i suggerimenti sull'intervallo e i grafici di affidabilità.", + "suggestion_applied": "Intervallo suggerito applicato", + "vacation_mode": "Modalità vacanza", + "vacation_status_active": "Attiva ora", + "vacation_status_scheduled": "Pianificata", + "vacation_status_inactive": "Inattiva", + "vacation_end_now_confirm": "Terminare subito la vacanza?", + "vacation_exempt_count": "escluse", + "vacation_advanced": "Avanzate…", + "vacation_open_panel": "Apri nel pannello", + "enable": "Attiva", + "saved": "Salvato", + "budget_monthly_set": "Imposta mensile", + "budget_yearly_set": "Imposta annuale", + "budget_advanced": "Valuta, avvisi…", + "budget_open_panel": "Apri nel pannello", + "groups_empty": "Nessun gruppo ancora.", + "group_new_placeholder": "Aggiungi gruppo…", + "group_delete_confirm": "Eliminare il gruppo \"{name}\"?", + "groups_manage_tasks": "Gestisci assegnazioni attività…", + "groups_open_panel": "Apri nel pannello", + "on_complete_action_target_hint": "Nota: il dominio dell'entità deve corrispondere al servizio — es. 'button.press' funziona solo su button.*, 'counter.increment' solo su counter.*, 'input_button.press' solo su input_button.* ecc. In caso di mancata corrispondenza l'azione fallisce silenziosamente (HA registra 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Mostra tutti gli oggetti", + "show_all_tasks": "Azzera filtro — mostra tutte le attività", + "filter_to_overdue": "Filtra l'elenco solo sulle attività in ritardo", + "filter_to_due_soon": "Filtra l'elenco solo sulle attività in scadenza", + "filter_to_triggered": "Filtra l'elenco solo sulle attività attivate", + "open_task": "Apri attività", + "show_details": "Mostra storico + statistiche", + "hide_details": "Nascondi dettagli", + "history_empty": "Nessuno storico ancora.", + "history_edit_button": "Modifica voce", + "total_cost": "Costo totale", + "times_performed": "Eseguita", + "older_entries": "precedenti", + "open_in_panel": "Apri nel pannello Manutenzione", + "skip_reason": "Motivo del salto (facoltativo)", + "reset_to_date": "Reimposta ultima esecuzione al", + "delete_task_confirm": "Eliminare questa attività e il suo storico?", + "delete_object_confirm": "Eliminare questo oggetto e tutte le sue attività?", + "loading": "Caricamento…", + "archive": "Archivia", + "undo": "Annulla", + "task_archived": "Attività archiviata", + "object_archived": "Oggetto archiviato", + "unarchive": "Annulla archiviazione", + "archived": "Archiviato", + "show_archived": "Mostra archiviati", + "hide_archived": "Nascondi archiviati", + "confirm_archive_object": "Archiviare questo oggetto e le sue attività? La cronologia viene conservata e può essere ripristinata in seguito.", + "settings_archive": "Archiviazione e conservazione", + "settings_archive_desc": "Metti da parte le attività una tantum completate senza eliminarle. Gli elementi archiviati sono nascosti e inattivi, ma conservano cronologia e costi.", + "settings_archive_oneoff_days": "Archivia automaticamente le attività una tantum completate dopo (giorni, 0 = off)", + "settings_delete_archived_oneoff_days": "Elimina automaticamente le attività una tantum archiviate dopo (giorni, 0 = mai)", + "archive_object": "Archivia oggetto", + "unarchive_object": "Annulla archiviazione oggetto", + "documents": "Documenti", + "documents_empty": "Ancora nessun documento.", + "doc_upload": "Carica file", + "doc_uploading": "Caricamento…", + "doc_add_link": "Aggiungi link", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titolo (facoltativo)", + "doc_open": "Apri", + "doc_delete_confirm": "Eliminare «{name}»?", + "doc_too_large": "File troppo grande (max 25 MB).", + "doc_upload_failed": "Caricamento non riuscito.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Già archiviato altrove — condiviso, nessuno spazio aggiuntivo.", + "doc_dup_in_object": "Questo file è già allegato a questo oggetto.", + "doc_link_invalid": "Sono consentiti solo link http/https.", + "doc_cat_manual": "Manuale", + "doc_cat_warranty": "Garanzia", + "doc_cat_invoice": "Fattura", + "doc_cat_spare_parts": "Ricambi", + "doc_cat_photo": "Foto", + "doc_cat_other": "Altro", + "doc_link_badge": "Link", + "doc_storage_title": "Archiviazione documenti", + "doc_storage_saved": "Risparmiato con deduplicazione", + "doc_storage_refresh": "Aggiorna", + "doc_download": "Scarica", + "doc_close": "Chiudi", + "doc_camera": "Scatta foto", + "doc_drop_hint": "Trascina qui i file", + "doc_task_none": "Nessun documento collegato a questa attività.", + "doc_link_existing": "Collega un documento…", + "doc_attach": "Collega", + "doc_unlink": "Scollega", + "doc_page": "Pagina", + "chart_range_7d": "7g", + "chart_range_30d": "30g", + "chart_range_90d": "90g", + "chart_range_1y": "1a", + "chart_since_service": "dall'ultima manutenzione", + "chart_no_stats": "Nessuna statistica a lungo termine per questa entità — vengono mostrati solo i valori degli interventi", + "auto_complete_on_recovery": "Completa automaticamente quando il sensore si ripristina", + "auto_complete_on_recovery_help": "Registra un completamento (imposta l'ultima esecuzione) quando il trigger si risolve da solo — ad es. sale ricaricato, filtro sostituito.", + "doc_search": "Cerca documenti…", + "doc_search_none": "Nessun documento corrispondente", + "link_device_optional": "Collega a dispositivo esistente (opzionale)", + "parent_object_optional": "Oggetto principale (opzionale)", + "parent_none": "(Nessun principale)", + "paused": "In pausa", + "pause_object": "Metti in pausa", + "resume_object": "Riprendi", + "pause_until_prompt": "Congela i calendari di questo oggetto: nulla scade e nulla notifica finché non viene ripreso. Facoltativamente imposta una data di ripresa automatica.", + "pause_until_label": "Riprendi il (facoltativo)", + "object_paused": "Oggetto in pausa", + "object_resumed": "Oggetto ripreso — calendari riavviati", + "object_paused_badge": "In pausa", + "paused_until_label": "fino al", + "replace_object": "Sostituisci…", + "replace_object_prompt": "Ritira questo oggetto e crea un successore. Storico e costi restano archiviati sul vecchio; attività e documenti passano al nuovo, i contatori ripartono da zero.", + "replace_name_label": "Nome del successore", + "object_replaced": "Oggetto sostituito — successore creato", + "reading_unit_label": "Unità di lettura (es. kWh, m³)", + "reading_unit_help": "Mostrata accanto al valore registrato al completamento di questa attività.", + "reading_value_label": "Valore letto", + "reading_label": "Lettura", + "settings_templates_label": "Galleria modelli", + "settings_templates_hint": "Deseleziona i modelli che non ti serviranno mai: spariscono dai selettori \"Da modello\" (pannello e config flow). Nient'altro cambia; riattivabili in qualsiasi momento.", + "worksheet": "Scheda di lavoro", + "worksheet_scan_view": "Scansiona per aprire l'attività", + "worksheet_scan_complete": "Scansiona per completare", + "worksheet_manual_excerpt": "Estratto del manuale", + "worksheet_pages": "pagine", + "worksheet_printed": "Stampato", + "worksheet_never": "Mai" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/ja.json b/custom_components/maintenance_supporter/frontend-src/locales/ja.json new file mode 100644 index 0000000..6c87564 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/ja.json @@ -0,0 +1,696 @@ +{ + "maintenance": "メンテナンス", + "objects": "対象", + "tasks": "タスク", + "overdue": "期限超過", + "due_soon": "期限間近", + "triggered": "トリガー発生", + "trigger_replaced": "トリガー解除済み", + "ok": "OK", + "all": "すべて", + "new_object": "+ 新規対象", + "templates_from": "テンプレートから", + "templates_title": "テンプレートから始める", + "templates_task_count": "{n} 件のタスク", + "template_created": "テンプレートから作成しました", + "onboard_hint": "最初のオブジェクトを追加してメンテナンスの記録を始めましょう。", + "edit": "編集", + "duplicate": "複製", + "task_duplicated": "タスクを複製しました", + "object_duplicated": "オブジェクトを複製しました", + "delete": "削除", + "add_task": "+ タスク追加", + "complete": "完了", + "completed": "完了済み", + "skip": "スキップ", + "skipped": "スキップ済み", + "missed": "Missed", + "reset": "リセット", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "キャンセル", + "bulk_select": "選択", + "bulk_select_all": "すべて選択", + "bulk_n_selected": "{n} 件選択", + "bulk_completed": "{n} 件のタスクを完了", + "bulk_archived": "{n} 件のタスクをアーカイブ", + "completing": "完了処理中…", + "interval": "間隔", + "warning": "警告", + "last_performed": "最終実施", + "next_due": "次回期限", + "days_until_due": "期限までの日数", + "avg_duration": "平均所要時間", + "trigger": "トリガー", + "trigger_type": "トリガー種別", + "threshold_above": "上限", + "threshold_below": "下限", + "threshold": "しきい値", + "counter": "カウンター", + "state_change": "状態変化", + "runtime": "稼働時間", + "runtime_hours": "目標稼働時間(時間)", + "target_value": "目標値", + "baseline": "基準値", + "target_changes": "変化回数", + "for_minutes": "継続(分)", + "time_based": "時間ベース", + "sensor_based": "センサーベース", + "manual": "手動", + "one_time": "1回のみ", + "weekdays": "曜日", + "nth_weekday": "毎月第N曜日", + "day_of_month": "毎月の日", + "recurrence_on_days": "繰り返す曜日", + "recurrence_occurrence": "出現週", + "recurrence_weekday": "曜日", + "recurrence_day": "日(1〜31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "第1", + "ord_2": "第2", + "ord_3": "第3", + "ord_4": "第4", + "ord_5": "第5", + "ord_last": "最終", + "day_word": "日", + "interval_value": "間隔", + "interval_unit": "単位", + "unit_days": "日", + "unit_weeks": "週", + "unit_months": "か月", + "unit_years": "年", + "due_date": "期限日", + "cleaning": "清掃", + "inspection": "点検", + "replacement": "交換", + "calibration": "校正", + "service": "保守", + "reading": "読み取り", + "custom": "カスタム", + "history": "履歴", + "cost": "費用", + "report_button": "レポート", + "report_title": "メンテナンスレポート", + "report_generated": "作成日", + "report_times_done": "実施回数", + "report_total_cost": "合計費用", + "report_every": "{n} {unit} ごと", + "report_notes": "メモ", + "report_col_type": "種類", + "report_col_status": "状態", + "report_col_schedule": "スケジュール", + "duration": "所要時間", + "both": "両方", + "trigger_val": "トリガー値", + "complete_title": "完了: ", + "checklist": "チェックリスト", + "checklist_steps_optional": "チェックリストの手順(任意)", + "checklist_placeholder": "フィルター清掃\nシール交換\n圧力テスト", + "checklist_help": "1行に1手順。最大100項目。", + "err_too_long": "{field}: 長すぎます(最大{n}文字)", + "err_too_short": "{field}: 短すぎます(最小{n}文字)", + "err_value_too_high": "{field}: 大きすぎます(最大{n})", + "err_value_too_low": "{field}: 小さすぎます(最小{n})", + "err_required": "{field}: 必須です", + "err_wrong_type": "{field}: 型が違います(期待値: {type})", + "err_invalid_choice": "{field}: 許可されていない値です", + "err_invalid_value": "{field}: 無効な値です", + "feat_schedule_time": "時刻指定スケジュール", + "feat_schedule_time_desc": "タスクが深夜0時ではなく指定した時刻に期限超過となります。", + "schedule_time_optional": "期限の時刻(任意、HH:MM)", + "schedule_time_help": "空欄=深夜0時(既定)。HAのタイムゾーン。", + "at_time": "時刻", + "notes_optional": "メモ(任意)", + "cost_optional": "費用(任意)", + "duration_minutes": "所要時間(分、任意)", + "days": "日", + "day": "日", + "today": "今日", + "d_overdue": "日超過", + "no_tasks": "メンテナンスタスクがまだありません。対象を作成して始めましょう。", + "no_tasks_short": "タスクなし", + "no_history": "履歴がまだありません。", + "show_all": "すべて表示", + "cost_duration_chart": "費用と所要時間", + "installed": "設置済み", + "confirm_delete_object": "この対象とすべてのタスクを削除しますか?", + "confirm_delete_task": "このタスクを削除しますか?", + "min": "最小", + "max": "最大", + "save": "保存", + "saving": "保存中…", + "edit_task": "タスクを編集", + "new_task": "新規メンテナンスタスク", + "task_name": "タスク名", + "maintenance_type": "メンテナンス種別", + "priority": "優先度", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "低", + "priority_normal": "標準", + "priority_high": "高", + "schedule_type": "スケジュール種別", + "interval_days": "間隔(日)", + "warning_days": "警告日数", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "最終実施(任意)", + "interval_anchor": "間隔の起点", + "anchor_completion": "完了日から", + "anchor_planned": "予定日から(ずれなし)", + "edit_object": "対象を編集", + "name": "名前", + "manufacturer_optional": "メーカー(任意)", + "model_optional": "型番(任意)", + "serial_number_optional": "シリアル番号(任意)", + "serial_number_label": "S/N", + "documentation_url_label": "取扱説明書", + "object_notes_label": "メモ", + "sort_due_date": "期限日", + "sort_object": "対象名", + "sort_type": "種別", + "sort_task_name": "タスク名", + "all_objects": "すべての対象", + "tasks_lower": "タスク", + "no_tasks_yet": "タスクがまだありません", + "add_first_task": "最初のタスクを追加", + "trigger_configuration": "トリガー設定", + "entity_id": "エンティティID", + "comma_separated": "カンマ区切り", + "entity_logic": "エンティティ条件", + "entity_logic_any": "いずれかのエンティティでトリガー", + "entity_logic_all": "すべてのエンティティが条件を満たす必要あり", + "entities": "エンティティ", + "attribute_optional": "属性(任意、空欄=状態)", + "use_entity_state": "エンティティの状態を使用(属性なし)", + "trigger_above": "この値を超えたらトリガー", + "trigger_below": "この値を下回ったらトリガー", + "for_at_least_minutes": "最低継続時間(分)", + "safety_interval_days": "安全間隔(日、任意)", + "safety_interval": "安全間隔(任意)", + "delta_mode": "差分モード", + "from_state_optional": "変化前の状態(任意)", + "to_state_optional": "変化後の状態(任意)", + "documentation_url_optional": "ドキュメントURL(任意)", + "object_notes_optional": "メモ(任意)", + "nfc_tag_id_optional": "NFCタグID(任意)", + "nfc_tags_empty_help": "Home AssistantにNFCタグがまだ登録されていません。", + "nfc_tags_open_settings": "タグ設定を開く", + "nfc_tags_refresh": "更新", + "environmental_entity_optional": "環境センサー(任意)", + "environmental_entity_helper": "例: sensor.outdoor_temperature — 環境条件に応じて間隔を調整します", + "environmental_attribute_optional": "環境属性(任意)", + "nfc_tag_id": "NFCタグID", + "nfc_linked": "NFCタグ連携済み", + "nfc_link_hint": "クリックしてNFCタグを連携", + "responsible_user": "担当ユーザー", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(担当者なし)", + "all_users": "すべてのユーザー", + "my_tasks": "自分のタスク", + "tab_calendar": "カレンダー", + "cal_no_events": "メンテナンスなし", + "cal_window_7": "7日間", + "cal_window_14": "14日間", + "cal_window_30": "30日間", + "cal_window_365": "1年間", + "cal_every_n_days": "{n}日ごと", + "cal_source_time": "時間ベース", + "cal_source_time_adaptive": "時間ベース(適応)", + "cal_source_sensor": "センサーベース", + "cal_predicted": "予測", + "cal_confidence_high": "信頼度:高", + "cal_confidence_medium": "信頼度:中", + "cal_confidence_low": "信頼度:低", + "budget_monthly": "月間予算", + "budget_yearly": "年間予算", + "groups": "グループ", + "new_group": "新規グループ", + "edit_group": "グループを編集", + "no_groups": "グループがまだありません", + "delete_group": "グループを削除", + "delete_group_confirm": "グループ「{name}」を削除しますか?", + "group_select_tasks": "タスクを選択", + "group_name_required": "名前は必須です", + "description_optional": "説明(任意)", + "selected": "選択中", + "loading_chart": "グラフデータを読み込み中...", + "hide_outliers": "外れ値を隠す(センサー誤差)", + "was_maintenance_needed": "このメンテナンスは必要でしたか?", + "feedback_needed": "必要だった", + "feedback_not_needed": "不要だった", + "feedback_not_sure": "わからない", + "suggested_interval": "推奨間隔", + "apply_suggestion": "適用", + "reanalyze": "再分析", + "reanalyze_result": "新しい分析結果", + "reanalyze_insufficient_data": "推奨を出すにはデータが不足しています", + "data_points": "データポイント", + "dismiss_suggestion": "閉じる", + "confidence_low": "低", + "confidence_medium": "中", + "confidence_high": "高", + "recommended": "推奨", + "seasonal_awareness": "季節対応", + "edit_seasonal_overrides": "季節係数を編集", + "seasonal_overrides_title": "季節係数(上書き)", + "seasonal_overrides_hint": "月ごとの係数(0.1〜5.0)。空欄=自動学習。", + "seasonal_override_invalid": "無効な値です", + "seasonal_override_range": "係数は0.1〜5.0の範囲で指定してください", + "clear_all": "すべてクリア", + "seasonal_chart_title": "季節係数", + "seasonal_learned": "学習値", + "seasonal_manual": "手動", + "month_jan": "1月", + "month_feb": "2月", + "month_mar": "3月", + "month_apr": "4月", + "month_may": "5月", + "month_jun": "6月", + "month_jul": "7月", + "month_aug": "8月", + "month_sep": "9月", + "month_oct": "10月", + "month_nov": "11月", + "month_dec": "12月", + "sensor_prediction": "センサー予測", + "degradation_trend": "傾向", + "trend_rising": "上昇", + "trend_falling": "下降", + "trend_stable": "安定", + "trend_insufficient_data": "データ不足", + "days_until_threshold": "しきい値までの日数", + "threshold_exceeded": "しきい値超過", + "environmental_adjustment": "環境係数", + "sensor_prediction_urgency": "センサー予測:約{days}日でしきい値到達", + "day_short": "日", + "weibull_reliability_curve": "信頼性曲線", + "weibull_failure_probability": "故障確率", + "weibull_r_squared": "適合度 R²", + "beta_early_failures": "初期故障", + "beta_random_failures": "偶発故障", + "beta_wear_out": "摩耗故障", + "beta_highly_predictable": "高い予測可能性", + "confidence_interval": "信頼区間", + "confidence_conservative": "保守的", + "confidence_aggressive": "楽観的", + "current_interval_marker": "現在の間隔", + "recommended_marker": "推奨", + "characteristic_life": "特性寿命", + "chart_mini_sparkline": "傾向スパークライン", + "chart_history": "費用と所要時間の履歴", + "chart_seasonal": "季節係数(12か月)", + "chart_weibull": "ワイブル信頼性曲線", + "chart_sparkline": "センサートリガー値のグラフ", + "days_progress": "経過日数", + "qr_code": "QRコード", + "qr_generating": "QRコードを生成中…", + "qr_error": "QRコードの生成に失敗しました。", + "qr_error_no_url": "HAのURLが設定されていません。設定 → システム → ネットワークで外部または内部URLを設定してください。", + "save_error": "保存に失敗しました。もう一度お試しください。", + "qr_print": "印刷", + "qr_download": "SVGをダウンロード", + "qr_action": "スキャン時の動作", + "qr_action_view": "メンテナンス情報を表示", + "qr_action_complete": "メンテナンスを完了にする", + "qr_url_mode": "リンク種別", + "qr_mode_companion": "コンパニオンアプリ", + "qr_mode_local": "ローカル(mDNS)", + "qr_mode_server": "サーバーURL", + "overview": "概要", + "analysis": "分析", + "recent_activities": "最近のアクティビティ", + "search_notes": "メモを検索", + "avg_cost": "平均費用", + "no_advanced_features": "高度な機能が有効になっていません", + "no_advanced_features_hint": "統合設定で「適応間隔」または「季節パターン」を有効にすると、ここに分析データが表示されます。", + "analysis_not_enough_data": "分析に必要なデータがまだ不足しています。", + "analysis_not_enough_data_hint": "ワイブル分析には完了済みメンテナンスが最低5件必要です。季節パターンは月あたり6件以上のデータポイントで表示されるようになります。", + "analysis_manual_task_hint": "間隔のない手動タスクは分析データを生成しません。", + "completions": "完了回数", + "current": "現在", + "shorter": "短く", + "longer": "長く", + "normal": "通常", + "disabled": "無効", + "compound_logic": "複合条件", + "compound": "複合(複数条件)", + "compound_logic_and": "AND — すべての条件が必要", + "compound_logic_or": "OR — いずれかの条件で発火", + "compound_help": "複数のセンサー条件を1つのトリガーにまとめます。", + "compound_no_conditions": "条件がありません — 少なくとも1つ追加してください。", + "compound_add_condition": "条件を追加", + "compound_condition": "条件", + "compound_remove_condition": "条件を削除", + "card_title": "タイトル", + "card_show_header": "統計付きヘッダーを表示", + "card_show_actions": "操作ボタンを表示", + "card_compact": "コンパクトモード", + "card_max_items": "最大表示件数(0=すべて)", + "card_filter_status": "状態で絞り込み", + "card_filter_status_help": "空欄=すべての状態を表示。", + "card_filter_objects": "対象で絞り込み", + "card_filter_objects_help": "空欄=すべての対象を表示。", + "card_filter_entities": "エンティティで絞り込み(entity_ids)", + "card_filter_entities_help": "この統合のsensor/binary_sensorエンティティを選択。空欄=すべて。", + "card_loading_objects": "対象を読み込み中…", + "card_load_error": "対象を読み込めませんでした — WebSocket接続を確認してください。", + "card_no_tasks_title": "メンテナンスタスクがまだありません", + "card_no_tasks_cta": "→ メンテナンスパネルで作成しましょう", + "no_objects": "対象がまだありません。", + "action_error": "操作に失敗しました。もう一度お試しください。", + "area_id_optional": "エリア(任意)", + "installation_date_optional": "設置日(任意)", + "warranty_expiry_optional": "保証期限(任意)", + "warranty": "保証", + "warranty_valid_until": "{date}まで有効", + "warranty_expires_in": "あと{days}日で期限切れ", + "warranty_expired": "期限切れ", + "cal_past_windows": "過去の期間", + "cal_forward_windows": "今後の期間", + "history_edit_title": "履歴項目を編集", + "history_edit_timestamp": "日時", + "manufacturer": "メーカー", + "model": "型番", + "area": "エリア", + "actions": "操作", + "view_mode_label": "表示", + "view_cards": "カード表示", + "view_table": "テーブル表示", + "objects_table_columns_label": "対象テーブルの列", + "objects_table_columns_hint": "対象のテーブル表示に表示する列を選択します。", + "custom_icon_optional": "アイコン(任意、例: mdi:wrench)", + "task_enabled": "タスク有効", + "skip_reason_prompt": "このタスクをスキップしますか?", + "reason_optional": "理由(任意)", + "reset_date_prompt": "タスクを実施済みにしますか?", + "reset_date_optional": "最終実施日(任意、既定は今日)", + "notes_label": "メモ", + "documentation_label": "ドキュメント", + "no_nfc_tag": "— タグなし —", + "dashboard": "ダッシュボード", + "tab_today": "今日", + "palette_placeholder": "オブジェクトとタスクを検索…", + "palette_no_results": "一致なし", + "palette_hint": "↑↓ 移動 · Enter 開く · Esc 閉じる", + "today_all_caught_up": "すべて完了!今週の予定はありません。", + "today_overdue": "期限切れ", + "today_due_today": "今日期限", + "today_this_week": "今週", + "settings": "設定", + "settings_features": "高度な機能", + "settings_features_desc": "高度な機能の有効/無効を切り替えます。無効にするとUIから非表示になりますが、データは削除されません。", + "feat_adaptive": "適応スケジューリング", + "feat_adaptive_desc": "メンテナンス履歴から最適な間隔を学習します", + "feat_predictions": "センサー予測", + "feat_predictions_desc": "センサーの劣化からトリガー日を予測します", + "feat_seasonal": "季節調整", + "feat_seasonal_desc": "季節パターンに基づいて間隔を調整します", + "feat_environmental": "環境相関", + "feat_environmental_desc": "温度・湿度と間隔を相関させます", + "feat_budget": "予算管理", + "feat_budget_desc": "月間・年間のメンテナンス費用を管理します", + "feat_groups": "タスクグループ", + "feat_groups_desc": "タスクを論理的なグループに整理します", + "feat_checklists": "チェックリスト", + "feat_checklists_desc": "タスク完了のための複数手順の手続き", + "settings_general": "一般", + "settings_default_warning": "既定の警告日数", + "settings_panel_enabled": "サイドバーパネル", + "settings_panel_title": "サイドバーパネルのタイトル", + "settings_notifications": "通知", + "settings_notify_service": "通知サービス", + "test_notification": "テスト通知", + "send_test": "テスト送信", + "testing": "送信中…", + "test_notification_success": "テスト通知を送信しました", + "test_notification_failed": "テスト通知に失敗しました", + "settings_notify_due_soon": "期限間近で通知", + "settings_notify_overdue": "期限超過で通知", + "settings_notify_triggered": "トリガー発生で通知", + "settings_interval_hours": "繰り返し間隔(時間、0=1回のみ)", + "settings_quiet_hours": "通知停止時間", + "settings_quiet_start": "開始", + "settings_quiet_end": "終了", + "settings_max_per_day": "1日あたりの最大通知数(0=無制限)", + "settings_bundling": "通知をまとめる", + "settings_bundle_threshold": "まとめるしきい値", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "モバイル操作ボタン", + "settings_action_complete": "「完了」ボタンを表示", + "settings_action_skip": "「スキップ」ボタンを表示", + "settings_action_snooze": "「スヌーズ」ボタンを表示", + "settings_weekly_digest": "週間まとめ", + "settings_weekly_digest_hint": "タスクがある月曜の朝に1件のまとめ通知。", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "スヌーズ時間(時間)", + "settings_budget": "予算", + "settings_currency": "通貨", + "settings_budget_monthly": "月間予算", + "settings_budget_yearly": "年間予算", + "settings_budget_alerts": "予算アラート", + "settings_budget_threshold": "アラートしきい値(%)", + "settings_import_export": "インポート/エクスポート", + "settings_export_json": "JSONをエクスポート", + "settings_export_yaml": "YAMLをエクスポート", + "settings_export_csv": "CSVをエクスポート", + "settings_import_csv": "CSVをインポート", + "settings_import_placeholder": "JSONまたはCSVの内容をここに貼り付け…", + "settings_import_btn": "インポート", + "settings_import_success": "{count}件の対象を正常にインポートしました。", + "settings_export_success": "エクスポートをダウンロードしました。", + "settings_saved": "設定を保存しました。", + "settings_include_history": "履歴を含める", + "sort_alphabetical": "アルファベット順", + "sort_due_soonest": "期限が近い順", + "sort_task_count": "タスク数", + "sort_area": "エリア", + "sort_assigned_user": "担当ユーザー", + "sort_group": "グループ", + "groupby_none": "グループ化なし", + "groupby_area": "エリア別", + "groupby_group": "グループ別", + "groupby_user": "ユーザー別", + "filter_label": "絞り込み", + "user_label": "ユーザー", + "sort_label": "並べ替え", + "group_by_label": "グループ化", + "state_value_help": "HAの状態値を使用してください(通常は小文字、例: \"on\"/\"off\")。保存時に大文字小文字は正規化されます。", + "target_changes_help": "トリガーが発生するまでに一致する遷移の回数(既定: 1)。", + "qr_print_title": "QRコードを印刷", + "qr_print_desc": "切り取って機器に貼れるQRコードの印刷ページを生成します。", + "qr_print_load": "対象を読み込む", + "qr_print_filter": "絞り込み", + "qr_print_objects": "対象", + "qr_print_actions": "操作", + "qr_print_url_mode": "リンク種別", + "qr_print_estimate": "QRコードの推定数", + "qr_print_over_limit": "上限は200件です。絞り込んでください", + "qr_print_generate": "QRコードを生成", + "qr_print_generating": "生成中…", + "qr_print_ready": "QRコードの準備完了", + "qr_print_print_button": "印刷", + "qr_print_empty": "生成する対象がありません", + "qr_action_skip": "スキップ", + "vacation_title": "休暇モード", + "vacation_active": "有効", + "vacation_ended": "終了済み", + "vacation_desc": "休暇を計画します。期間中とその前後のバッファ日数の間、通知が一時停止されます。特定のタスクは通知対象に戻せます。", + "vacation_enable": "休暇モードを有効にする", + "vacation_start": "開始", + "vacation_end": "終了", + "vacation_buffer": "バッファ(日)", + "vacation_exempt_title": "休暇中でも通知する", + "vacation_exempt_desc": "休暇中でも通知すべきタスクを選択します(例: 重要なプールの水質管理)。", + "vacation_load_tasks": "タスクを読み込む", + "vacation_preview_btn": "プレビューを表示", + "vacation_preview_affected": "件のタスクが影響を受けます", + "vacation_event_due_soon": "期限間近になります", + "vacation_event_overdue": "期限超過になります", + "vacation_event_triggered_est": "センサートリガーの可能性あり", + "vacation_sensor_based": "(センサーベース)", + "vacation_action_notify": "通知する", + "vacation_action_unsilence": "再び停止する", + "vacation_marked_complete": "完了にしました", + "vacation_marked_skip": "スキップしました", + "vacation_end_now": "今すぐ休暇を終了", + "add": "追加", + "show_stats": "統計とグラフを表示", + "hide_stats": "統計を非表示", + "adaptive_no_data": "適応分析に必要な完了履歴がまだ不足しています。このタスクをあと数回完了すると、間隔の推奨と信頼性グラフが利用できるようになります。", + "suggestion_applied": "推奨間隔を適用しました", + "vacation_mode": "休暇モード", + "vacation_status_active": "現在有効", + "vacation_status_scheduled": "予定済み", + "vacation_status_inactive": "無効", + "vacation_end_now_confirm": "今すぐ休暇を終了しますか?", + "vacation_exempt_count": "件 除外", + "vacation_advanced": "詳細…", + "vacation_open_panel": "パネルで開く", + "enable": "有効にする", + "saved": "保存しました", + "budget_monthly_set": "月間を設定", + "budget_yearly_set": "年間を設定", + "budget_advanced": "通貨、アラート…", + "budget_open_panel": "パネルで開く", + "groups_empty": "グループがまだありません。", + "group_new_placeholder": "グループを追加…", + "group_delete_confirm": "グループ「{name}」を削除しますか?", + "groups_manage_tasks": "タスクの割り当てを管理…", + "groups_open_panel": "パネルで開く", + "unassigned": "未割り当て", + "no_area": "エリアなし", + "has_overdue": "期限超過のタスクあり", + "object": "対象", + "settings_panel_access": "パネルアクセス", + "settings_panel_access_desc": "管理者は常にフルアクセスできます。作成・編集・削除を特定の非管理者に委任するには、これをオンにして下記で選択してください。それ以外のユーザーには完了とスキップのみが表示されます。", + "settings_operator_write": "選択したユーザーに作成・編集・削除を許可", + "settings_operator_write_desc": "オフ:管理者のみがコンテンツを変更できます。オン:下記で選択したユーザーもフルアクセスできます。", + "no_non_admin_users": "非管理者ユーザーが見つかりません。設定 → ユーザーで追加してください。", + "owner_label": "オーナー", + "feat_completion_actions": "完了時アクション", + "feat_completion_actions_desc": "タスクごとの完了時HAアクション+事前設定値付きのクイック完了QR。", + "on_complete_action_title": "完了時: HAアクションを実行(任意)", + "on_complete_action_desc": "タスク完了時にHAサービスを呼び出します — 例: デバイスのカウンターをリセット。", + "on_complete_action_service": "サービス", + "on_complete_action_target": "対象エンティティ", + "on_complete_action_target_hint": "注意: エンティティのドメインはサービスと一致する必要があります — 例: 'button.press'はbutton.*のみ、'counter.increment'はcounter.*のみ、'input_button.press'はinput_button.*のみで動作します。不一致の場合、アクションは何も表示せずに失敗します(HAのログに「Referenced entities ... missing or not currently available」と記録されます)。", + "on_complete_action_data": "データ(JSON、任意)", + "on_complete_action_test": "設定を検証", + "on_complete_action_test_success": "✓ 設定は有効です(アクションはタスク完了時のみ実行されます)", + "on_complete_action_test_failed": "失敗しました", + "quick_complete_defaults_title": "クイック完了の既定値(QRスキャン用、任意)", + "quick_complete_defaults_desc": "クイック完了QRスキャンの事前設定値。設定しない場合、QRは完了ダイアログを開きます。", + "quick_complete_defaults_notes": "メモ", + "quick_complete_defaults_cost": "費用", + "quick_complete_defaults_duration": "所要時間(分)", + "quick_complete_defaults_feedback_none": "フィードバックなし", + "quick_complete_defaults_feedback_needed": "必要だった", + "quick_complete_defaults_feedback_not_needed": "不要だった", + "quick_complete_success": "クイック完了にしました", + "show_all_objects": "すべての対象を表示", + "show_all_tasks": "絞り込みを解除 — すべてのタスクを表示", + "filter_to_overdue": "タスク一覧を期限超過のみに絞り込む", + "filter_to_due_soon": "タスク一覧を期限間近のみに絞り込む", + "filter_to_triggered": "タスク一覧をトリガー発生のみに絞り込む", + "open_task": "タスクを開く", + "show_details": "履歴と統計を表示", + "hide_details": "詳細を非表示", + "history_empty": "履歴がまだありません。", + "history_edit_button": "項目を編集", + "total_cost": "費用合計", + "times_performed": "実施回数", + "older_entries": "以前", + "open_in_panel": "メンテナンスパネルで開く", + "skip_reason": "スキップ理由(任意)", + "reset_to_date": "最終実施日を次の日付にリセット", + "delete_task_confirm": "このタスクと履歴を削除しますか?", + "delete_object_confirm": "この対象とすべてのタスクを削除しますか?", + "loading": "読み込み中…", + "archive": "アーカイブ", + "undo": "元に戻す", + "task_archived": "タスクをアーカイブしました", + "object_archived": "オブジェクトをアーカイブしました", + "unarchive": "アーカイブ解除", + "archived": "アーカイブ済み", + "show_archived": "アーカイブを表示", + "hide_archived": "アーカイブを非表示", + "confirm_archive_object": "このオブジェクトとそのタスクをアーカイブしますか?履歴は保持され、後で元に戻せます。", + "settings_archive": "アーカイブと保持", + "settings_archive_desc": "完了した単発タスクを削除せずに片付けます。アーカイブされた項目は非表示で動作しませんが、履歴とコストは保持されます。", + "settings_archive_oneoff_days": "完了した単発タスクを自動アーカイブする日数(0 = 無効)", + "settings_delete_archived_oneoff_days": "アーカイブ済み単発タスクを自動削除する日数(0 = しない)", + "archive_object": "オブジェクトをアーカイブ", + "unarchive_object": "オブジェクトのアーカイブを解除", + "documents": "ドキュメント", + "documents_empty": "まだドキュメントがありません。", + "doc_upload": "ファイルをアップロード", + "doc_uploading": "アップロード中…", + "doc_add_link": "リンクを追加", + "doc_link_url": "URL (https://…)", + "doc_link_title": "タイトル(任意)", + "doc_open": "開く", + "doc_delete_confirm": "「{name}」を削除しますか?", + "doc_too_large": "ファイルが大きすぎます(最大 25 MB)。", + "doc_upload_failed": "アップロードに失敗しました。", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "既に別の場所に保存済み — 共有され、追加容量はかかりません。", + "doc_dup_in_object": "このファイルは既にこのオブジェクトに添付されています。", + "doc_link_invalid": "http/https のリンクのみ許可されています。", + "doc_cat_manual": "取扱説明書", + "doc_cat_warranty": "保証書", + "doc_cat_invoice": "請求書", + "doc_cat_spare_parts": "交換部品", + "doc_cat_photo": "写真", + "doc_cat_other": "その他", + "doc_link_badge": "リンク", + "doc_storage_title": "ドキュメントストレージ", + "doc_storage_saved": "重複排除による節約", + "doc_storage_refresh": "更新", + "doc_download": "ダウンロード", + "doc_close": "閉じる", + "doc_camera": "写真を撮る", + "doc_drop_hint": "ここにファイルをドロップ", + "doc_task_none": "このタスクに紐付いたドキュメントはありません。", + "doc_link_existing": "ドキュメントを紐付け…", + "doc_attach": "紐付け", + "doc_unlink": "紐付け解除", + "doc_page": "ページ", + "chart_range_7d": "7日", + "chart_range_30d": "30日", + "chart_range_90d": "90日", + "chart_range_1y": "1年", + "chart_since_service": "前回のメンテナンス以降", + "chart_no_stats": "このエンティティには長期統計がありません — メンテナンス記録の値のみ表示しています", + "auto_complete_on_recovery": "センサー回復時に自動完了", + "auto_complete_on_recovery_help": "トリガーが自然に解消されたとき(塩の補充、フィルター交換など)に完了を記録し、最終実施日を更新します。", + "doc_search": "ドキュメントを検索…", + "doc_search_none": "一致するドキュメントはありません", + "link_device_optional": "既存のデバイスにリンク(任意)", + "parent_object_optional": "親オブジェクト(任意)", + "parent_none": "(親なし)", + "paused": "一時停止中", + "pause_object": "一時停止", + "resume_object": "再開", + "pause_until_prompt": "このオブジェクトのスケジュールを凍結します。再開するまで期限も通知も発生しません。必要に応じて自動再開日を設定できます。", + "pause_until_label": "再開日(任意)", + "object_paused": "オブジェクトを一時停止しました", + "object_resumed": "オブジェクトを再開しました — スケジュールを再スタート", + "object_paused_badge": "一時停止中", + "paused_until_label": "まで", + "replace_object": "置き換え…", + "replace_object_prompt": "このオブジェクトを退役させ、後継を作成します。履歴とコストは旧オブジェクトにアーカイブされたまま残り、タスクと文書は新しいオブジェクトへ引き継がれ、カウンターはリセットされます。", + "replace_name_label": "後継の名前", + "object_replaced": "オブジェクトを置き換えました — 後継を作成", + "reading_unit_label": "検針単位(例: kWh、m³)", + "reading_unit_help": "このタスクの完了時に記録値の横に表示されます。", + "reading_value_label": "検針値", + "reading_label": "検針", + "settings_templates_label": "テンプレートギャラリー", + "settings_templates_hint": "不要なテンプレートのチェックを外すと、「テンプレートから」の選択肢(パネルと設定フロー)から消えます。それ以外は何も変わらず、いつでも再有効化できます。", + "worksheet": "作業シート", + "worksheet_scan_view": "スキャンしてタスクを開く", + "worksheet_scan_complete": "スキャンして完了", + "worksheet_manual_excerpt": "マニュアル抜粋", + "worksheet_pages": "ページ", + "worksheet_printed": "印刷日", + "worksheet_never": "未実施" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/nb.json b/custom_components/maintenance_supporter/frontend-src/locales/nb.json new file mode 100644 index 0000000..dacefde --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/nb.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Vedlikehold", + "objects": "Objekter", + "tasks": "Oppgaver", + "overdue": "Forfalt", + "due_soon": "Forfaller snart", + "triggered": "Utløst", + "trigger_replaced": "Utløser erstattet", + "ok": "OK", + "all": "Alle", + "new_object": "+ Nytt objekt", + "templates_from": "Fra mal", + "templates_title": "Start fra en mal", + "templates_task_count": "{n} oppgaver", + "template_created": "Opprettet fra mal", + "onboard_hint": "Legg til ditt første objekt for å spore vedlikehold.", + "edit": "Rediger", + "duplicate": "Dupliser", + "task_duplicated": "Oppgave duplisert", + "object_duplicated": "Objekt duplisert", + "delete": "Slett", + "add_task": "+ Legg til oppgave", + "complete": "Fullfør", + "completed": "Fullført", + "skip": "Hopp over", + "skipped": "Hoppet over", + "missed": "Missed", + "reset": "Tilbakestill", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Avbryt", + "bulk_select": "Velg", + "bulk_select_all": "Velg alle", + "bulk_n_selected": "{n} valgt", + "bulk_completed": "{n} oppgaver fullført", + "bulk_archived": "{n} oppgaver arkivert", + "completing": "Fullfører…", + "interval": "Intervall", + "warning": "Advarsel", + "last_performed": "Sist utført", + "next_due": "Neste forfall", + "days_until_due": "Dager til forfall", + "avg_duration": "Gj.snittlig varighet", + "trigger": "Utløser", + "trigger_type": "Utløsertype", + "threshold_above": "Øvre grense", + "threshold_below": "Nedre grense", + "threshold": "Terskel", + "counter": "Teller", + "state_change": "Tilstandsendring", + "runtime": "Driftstid", + "runtime_hours": "Måldriftstid (timer)", + "target_value": "Målverdi", + "baseline": "Utgangspunkt", + "target_changes": "Antall endringer", + "for_minutes": "I (minutter)", + "time_based": "Tidsbasert", + "sensor_based": "Sensorbasert", + "manual": "Manuell", + "one_time": "Engangs", + "weekdays": "Ukedager", + "nth_weekday": "N-te ukedag i måneden", + "day_of_month": "Dag i måneden", + "recurrence_on_days": "Gjenta på", + "recurrence_occurrence": "Forekomst", + "recurrence_weekday": "Ukedag", + "recurrence_day": "Dag i måneden (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Siste", + "day_word": "Dag", + "interval_value": "Intervall", + "interval_unit": "Enhet", + "unit_days": "Dager", + "unit_weeks": "Uker", + "unit_months": "Måneder", + "unit_years": "År", + "due_date": "Forfallsdato", + "cleaning": "Rengjøring", + "inspection": "Inspeksjon", + "replacement": "Utskifting", + "calibration": "Kalibrering", + "service": "Service", + "reading": "Avlesning", + "custom": "Egendefinert", + "history": "Historikk", + "cost": "Kostnad", + "report_button": "Rapport", + "report_title": "Vedlikeholdsrapport", + "report_generated": "Generert", + "report_times_done": "Utført", + "report_total_cost": "Totalkostnad", + "report_every": "hver {n}. {unit}", + "report_notes": "Notater", + "report_col_type": "Type", + "report_col_status": "Status", + "report_col_schedule": "Tidsplan", + "duration": "Varighet", + "both": "Begge", + "trigger_val": "Utløserverdi", + "complete_title": "Fullfør: ", + "checklist": "Sjekkliste", + "checklist_steps_optional": "Sjekklistetrinn (valgfritt)", + "checklist_placeholder": "Rengjør filter\nBytt pakning\nTest trykk", + "checklist_help": "Ett trinn per linje. Maks 100 elementer.", + "err_too_long": "{field}: for lang (maks {n} tegn)", + "err_too_short": "{field}: for kort (min {n} tegn)", + "err_value_too_high": "{field}: for stor (maks {n})", + "err_value_too_low": "{field}: for liten (min {n})", + "err_required": "{field}: påkrevd", + "err_wrong_type": "{field}: feil type (forventet: {type})", + "err_invalid_choice": "{field}: ikke en tillatt verdi", + "err_invalid_value": "{field}: ugyldig verdi", + "feat_schedule_time": "Planlegging med klokkeslett", + "feat_schedule_time_desc": "Oppgaver forfaller på et bestemt klokkeslett i stedet for ved midnatt.", + "schedule_time_optional": "Forfaller kl. (valgfritt, TT:MM)", + "schedule_time_help": "Tom = midnatt (standard). HA-tidssone.", + "at_time": "kl.", + "notes_optional": "Notater (valgfritt)", + "cost_optional": "Kostnad (valgfritt)", + "duration_minutes": "Varighet i minutter (valgfritt)", + "days": "dager", + "day": "dag", + "today": "I dag", + "d_overdue": "d forfalt", + "no_tasks": "Ingen vedlikeholdsoppgaver ennå. Opprett et objekt for å komme i gang.", + "no_tasks_short": "Ingen oppgaver", + "no_history": "Ingen historikkoppføringer ennå.", + "show_all": "Vis alle", + "cost_duration_chart": "Kostnad og varighet", + "installed": "Installert", + "confirm_delete_object": "Slette dette objektet og alle dets oppgaver?", + "confirm_delete_task": "Slette denne oppgaven?", + "min": "Min", + "max": "Maks", + "save": "Lagre", + "saving": "Lagrer…", + "edit_task": "Rediger oppgave", + "new_task": "Ny vedlikeholdsoppgave", + "task_name": "Oppgavenavn", + "maintenance_type": "Vedlikeholdstype", + "priority": "Prioritet", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Lav", + "priority_normal": "Normal", + "priority_high": "Høy", + "schedule_type": "Planleggingstype", + "interval_days": "Intervall (dager)", + "warning_days": "Varslingsdager", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Sist utført (valgfritt)", + "interval_anchor": "Intervallforankring", + "anchor_completion": "Fra fullføringsdato", + "anchor_planned": "Fra planlagt dato (ingen avdrift)", + "edit_object": "Rediger objekt", + "name": "Navn", + "manufacturer_optional": "Produsent (valgfritt)", + "model_optional": "Modell (valgfritt)", + "serial_number_optional": "Serienummer (valgfritt)", + "serial_number_label": "S/N", + "documentation_url_label": "Bruksanvisning", + "object_notes_label": "Notater", + "sort_due_date": "Forfallsdato", + "sort_object": "Objektnavn", + "sort_type": "Type", + "sort_task_name": "Oppgavenavn", + "all_objects": "Alle objekter", + "tasks_lower": "oppgaver", + "no_tasks_yet": "Ingen oppgaver ennå", + "add_first_task": "Legg til første oppgave", + "trigger_configuration": "Utløserkonfigurasjon", + "entity_id": "Entitets-ID", + "comma_separated": "kommaseparert", + "entity_logic": "Entitetslogikk", + "entity_logic_any": "Hvilken som helst entitet utløser", + "entity_logic_all": "Alle entiteter må utløse", + "entities": "entiteter", + "attribute_optional": "Attributt (valgfritt, tomt = tilstand)", + "use_entity_state": "Bruk entitetstilstand (ingen attributt)", + "trigger_above": "Utløs over", + "trigger_below": "Utløs under", + "for_at_least_minutes": "I minst (minutter)", + "safety_interval_days": "Sikkerhetsintervall (dager, valgfritt)", + "safety_interval": "Sikkerhetsintervall (valgfritt)", + "delta_mode": "Deltamodus", + "from_state_optional": "Fra tilstand (valgfritt)", + "to_state_optional": "Til tilstand (valgfritt)", + "documentation_url_optional": "Dokumentasjons-URL (valgfritt)", + "object_notes_optional": "Notater (valgfritt)", + "nfc_tag_id_optional": "NFC-brikke-ID (valgfritt)", + "nfc_tags_empty_help": "Ingen NFC-brikker registrert i Home Assistant ennå.", + "nfc_tags_open_settings": "Åpne brikkeinnstillinger", + "nfc_tags_refresh": "Oppdater", + "environmental_entity_optional": "Miljøsensor (valgfritt)", + "environmental_entity_helper": "f.eks. sensor.outdoor_temperature — justerer intervallet basert på miljøforhold", + "environmental_attribute_optional": "Miljøattributt (valgfritt)", + "nfc_tag_id": "NFC-brikke-ID", + "nfc_linked": "NFC-brikke koblet", + "nfc_link_hint": "Klikk for å koble NFC-brikke", + "responsible_user": "Ansvarlig bruker", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ingen bruker tildelt)", + "all_users": "Alle brukere", + "my_tasks": "Mine oppgaver", + "tab_calendar": "Kalender", + "cal_no_events": "Ingen vedlikehold", + "cal_window_7": "7 dager", + "cal_window_14": "14 dager", + "cal_window_30": "30 dager", + "cal_window_365": "1 år", + "cal_every_n_days": "hver {n}. dag", + "cal_source_time": "Tidsbasert", + "cal_source_time_adaptive": "Tidsbasert (adaptiv)", + "cal_source_sensor": "Sensorbasert", + "cal_predicted": "forutsagt", + "cal_confidence_high": "høy sikkerhet", + "cal_confidence_medium": "middels sikkerhet", + "cal_confidence_low": "lav sikkerhet", + "budget_monthly": "Månedsbudsjett", + "budget_yearly": "Årsbudsjett", + "groups": "Grupper", + "new_group": "Ny gruppe", + "edit_group": "Rediger gruppe", + "no_groups": "Ingen grupper ennå", + "delete_group": "Slett gruppe", + "delete_group_confirm": "Slette gruppen '{name}'?", + "group_select_tasks": "Velg oppgaver", + "group_name_required": "Navn er påkrevd", + "description_optional": "Beskrivelse (valgfritt)", + "selected": "Valgt", + "loading_chart": "Laster diagramdata...", + "hide_outliers": "Skjul avvik (sensorfeil)", + "was_maintenance_needed": "Var dette vedlikeholdet nødvendig?", + "feedback_needed": "Nødvendig", + "feedback_not_needed": "Ikke nødvendig", + "feedback_not_sure": "Usikker", + "suggested_interval": "Foreslått intervall", + "apply_suggestion": "Bruk", + "reanalyze": "Analyser på nytt", + "reanalyze_result": "Ny analyse", + "reanalyze_insufficient_data": "Ikke nok data til å gi en anbefaling", + "data_points": "datapunkter", + "dismiss_suggestion": "Avvis", + "confidence_low": "Lav", + "confidence_medium": "Middels", + "confidence_high": "Høy", + "recommended": "anbefalt", + "seasonal_awareness": "Sesongbevissthet", + "edit_seasonal_overrides": "Rediger sesongfaktorer", + "seasonal_overrides_title": "Sesongfaktorer (overstyring)", + "seasonal_overrides_hint": "Faktor per måned (0,1–5,0). Tom = læres automatisk.", + "seasonal_override_invalid": "Ugyldig verdi", + "seasonal_override_range": "Faktor må være mellom 0,1 og 5,0", + "clear_all": "Fjern alle", + "seasonal_chart_title": "Sesongfaktorer", + "seasonal_learned": "Lært", + "seasonal_manual": "Manuell", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "Mai", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Des", + "sensor_prediction": "Sensorprediksjon", + "degradation_trend": "Trend", + "trend_rising": "Stigende", + "trend_falling": "Synkende", + "trend_stable": "Stabil", + "trend_insufficient_data": "Utilstrekkelige data", + "days_until_threshold": "Dager til terskel", + "threshold_exceeded": "Terskel overskredet", + "environmental_adjustment": "Miljøfaktor", + "sensor_prediction_urgency": "Sensoren forutsier terskel om ~{days} dager", + "day_short": "dag", + "weibull_reliability_curve": "Pålitelighetskurve", + "weibull_failure_probability": "Feilsannsynlighet", + "weibull_r_squared": "Tilpasning R²", + "beta_early_failures": "Tidlige feil", + "beta_random_failures": "Tilfeldige feil", + "beta_wear_out": "Slitasje", + "beta_highly_predictable": "Svært forutsigbar", + "confidence_interval": "Konfidensintervall", + "confidence_conservative": "Konservativ", + "confidence_aggressive": "Optimistisk", + "current_interval_marker": "Nåværende intervall", + "recommended_marker": "Anbefalt", + "characteristic_life": "Karakteristisk levetid", + "chart_mini_sparkline": "Trend-minigraf", + "chart_history": "Kostnads- og varighetshistorikk", + "chart_seasonal": "Sesongfaktorer, 12 måneder", + "chart_weibull": "Weibull-pålitelighetskurve", + "chart_sparkline": "Diagram over sensorutløserverdi", + "days_progress": "Dagsfremdrift", + "qr_code": "QR-kode", + "qr_generating": "Genererer QR-kode…", + "qr_error": "Kunne ikke generere QR-kode.", + "qr_error_no_url": "Ingen HA-URL konfigurert. Angi en ekstern eller intern URL under Innstillinger → System → Nettverk.", + "save_error": "Kunne ikke lagre. Prøv igjen.", + "qr_print": "Skriv ut", + "qr_download": "Last ned SVG", + "qr_action": "Handling ved skanning", + "qr_action_view": "Vis vedlikeholdsinfo", + "qr_action_complete": "Merk vedlikehold som fullført", + "qr_url_mode": "Lenketype", + "qr_mode_companion": "Companion-app", + "qr_mode_local": "Lokal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Oversikt", + "analysis": "Analyse", + "recent_activities": "Nylige aktiviteter", + "search_notes": "Søk i notater", + "avg_cost": "Gj.snittlig kostnad", + "no_advanced_features": "Ingen avanserte funksjoner aktivert", + "no_advanced_features_hint": "Aktiver «Adaptive intervaller» eller «Sesongmønstre» i integrasjonsinnstillingene for å se analysedata her.", + "analysis_not_enough_data": "Ikke nok data for analyse ennå.", + "analysis_not_enough_data_hint": "Weibull-analyse krever minst 5 fullførte vedlikehold; sesongmønstre blir synlige etter 6+ datapunkter per måned.", + "analysis_manual_task_hint": "Manuelle oppgaver uten intervall genererer ikke analysedata.", + "completions": "fullføringer", + "current": "Nåværende", + "shorter": "Kortere", + "longer": "Lengre", + "normal": "Normal", + "disabled": "Deaktivert", + "compound_logic": "Sammensatt logikk", + "compound": "Sammensatt (flere betingelser)", + "compound_logic_and": "OG — alle betingelser må utløses", + "compound_logic_or": "ELLER — én betingelse er nok", + "compound_help": "Kombiner flere sensorbetingelser til én utløser.", + "compound_no_conditions": "Ingen betingelser ennå — legg til minst én.", + "compound_add_condition": "Legg til betingelse", + "compound_condition": "Betingelse", + "compound_remove_condition": "Fjern betingelse", + "card_title": "Tittel", + "card_show_header": "Vis topptekst med statistikk", + "card_show_actions": "Vis handlingsknapper", + "card_compact": "Kompakt modus", + "card_max_items": "Maks elementer (0 = alle)", + "card_filter_status": "Filtrer etter status", + "card_filter_status_help": "Tom = vis alle statuser.", + "card_filter_objects": "Filtrer etter objekter", + "card_filter_objects_help": "Tom = vis alle objekter.", + "card_filter_entities": "Filtrer etter entiteter (entity_ids)", + "card_filter_entities_help": "Velg sensor-/binary_sensor-entiteter fra denne integrasjonen. Tom = alle.", + "card_loading_objects": "Laster objekter…", + "card_load_error": "Kunne ikke laste objekter — kontroller WebSocket-tilkoblingen.", + "card_no_tasks_title": "Ingen vedlikeholdsoppgaver ennå", + "card_no_tasks_cta": "→ Opprett en i vedlikeholdspanelet", + "no_objects": "Ingen objekter ennå.", + "action_error": "Handlingen mislyktes. Prøv igjen.", + "area_id_optional": "Område (valgfritt)", + "installation_date_optional": "Installasjonsdato (valgfritt)", + "warranty_expiry_optional": "Garanti utløper (valgfritt)", + "warranty": "Garanti", + "warranty_valid_until": "gyldig til {date}", + "warranty_expires_in": "utløper om {days} dager", + "warranty_expired": "utløpt", + "cal_past_windows": "Tidligere vinduer", + "cal_forward_windows": "Fremtidige vinduer", + "history_edit_title": "Rediger historikkoppføring", + "history_edit_timestamp": "Tidsstempel", + "manufacturer": "Produsent", + "model": "Modell", + "area": "Område", + "actions": "Handlinger", + "view_mode_label": "Visning", + "view_cards": "Kortvisning", + "view_table": "Tabellvisning", + "objects_table_columns_label": "Kolonner i objekttabell", + "objects_table_columns_hint": "Velg hvilke kolonner som vises i tabellvisningen for objekter.", + "custom_icon_optional": "Ikon (valgfritt, f.eks. mdi:wrench)", + "task_enabled": "Oppgave aktivert", + "skip_reason_prompt": "Hoppe over denne oppgaven?", + "reason_optional": "Årsak (valgfritt)", + "reset_date_prompt": "Merke oppgaven som utført?", + "reset_date_optional": "Sist utført-dato (valgfritt, standard er i dag)", + "notes_label": "Notater", + "documentation_label": "Dokumentasjon", + "no_nfc_tag": "— Ingen brikke —", + "dashboard": "Dashbord", + "tab_today": "I dag", + "palette_placeholder": "Søk objekter og oppgaver…", + "palette_no_results": "Ingen treff", + "palette_hint": "↑↓ naviger · Enter åpne · Esc lukk", + "today_all_caught_up": "Alt er ajour! Ingenting forfaller denne uken.", + "today_overdue": "Forfalt", + "today_due_today": "Forfaller i dag", + "today_this_week": "Denne uken", + "settings": "Innstillinger", + "settings_features": "Avanserte funksjoner", + "settings_features_desc": "Aktiver eller deaktiver avanserte funksjoner. Deaktivering skjuler dem fra grensesnittet, men sletter ikke data.", + "feat_adaptive": "Adaptiv planlegging", + "feat_adaptive_desc": "Lær optimale intervaller fra vedlikeholdshistorikken", + "feat_predictions": "Sensorprediksjoner", + "feat_predictions_desc": "Forutsi utløserdatoer fra sensorforringelse", + "feat_seasonal": "Sesongjusteringer", + "feat_seasonal_desc": "Juster intervaller basert på sesongmønstre", + "feat_environmental": "Miljøkorrelasjon", + "feat_environmental_desc": "Korreler intervaller med temperatur/fuktighet", + "feat_budget": "Budsjettsporing", + "feat_budget_desc": "Spor månedlige og årlige vedlikeholdsutgifter", + "feat_groups": "Oppgavegrupper", + "feat_groups_desc": "Organiser oppgaver i logiske grupper", + "feat_checklists": "Sjekklister", + "feat_checklists_desc": "Flertrinnsprosedyrer for fullføring av oppgaver", + "settings_general": "Generelt", + "settings_default_warning": "Standard varslingsdager", + "settings_panel_enabled": "Sidepanel", + "settings_panel_title": "Tittel på sidepanel", + "settings_notifications": "Varsler", + "settings_notify_service": "Varslingstjeneste", + "test_notification": "Testvarsel", + "send_test": "Send test", + "testing": "Sender…", + "test_notification_success": "Testvarsel sendt", + "test_notification_failed": "Testvarsel mislyktes", + "settings_notify_due_soon": "Varsle når noe forfaller snart", + "settings_notify_overdue": "Varsle når noe er forfalt", + "settings_notify_triggered": "Varsle når noe utløses", + "settings_interval_hours": "Gjentaksintervall (timer, 0 = én gang)", + "settings_quiet_hours": "Stilletimer", + "settings_quiet_start": "Start", + "settings_quiet_end": "Slutt", + "settings_max_per_day": "Maks varsler per dag (0 = ubegrenset)", + "settings_bundling": "Samle varsler", + "settings_bundle_threshold": "Samlingsterskel", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Handlingsknapper på mobil", + "settings_action_complete": "Vis «Fullfør»-knapp", + "settings_action_skip": "Vis «Hopp over»-knapp", + "settings_action_snooze": "Vis «Slumre»-knapp", + "settings_weekly_digest": "Ukentlig oversikt", + "settings_weekly_digest_hint": "Ett sammendragsvarsel mandag morgen når oppgaver forfaller.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Slumrevarighet (timer)", + "settings_budget": "Budsjett", + "settings_currency": "Valuta", + "settings_budget_monthly": "Månedsbudsjett", + "settings_budget_yearly": "Årsbudsjett", + "settings_budget_alerts": "Budsjettvarsler", + "settings_budget_threshold": "Varslingsterskel (%)", + "settings_import_export": "Import / eksport", + "settings_export_json": "Eksporter JSON", + "settings_export_yaml": "Eksporter YAML", + "settings_export_csv": "Eksporter CSV", + "settings_import_csv": "Importer CSV", + "settings_import_placeholder": "Lim inn JSON- eller CSV-innhold her…", + "settings_import_btn": "Importer", + "settings_import_success": "{count} objekter importert.", + "settings_export_success": "Eksport lastet ned.", + "settings_saved": "Innstilling lagret.", + "settings_include_history": "Inkluder historikk", + "sort_alphabetical": "Alfabetisk", + "sort_due_soonest": "Forfaller først", + "sort_task_count": "Antall oppgaver", + "sort_area": "Område", + "sort_assigned_user": "Tildelt bruker", + "sort_group": "Gruppe", + "groupby_none": "Ingen gruppering", + "groupby_area": "Etter område", + "groupby_group": "Etter gruppe", + "groupby_user": "Etter bruker", + "filter_label": "Filter", + "user_label": "Bruker", + "sort_label": "Sorter", + "group_by_label": "Grupper etter", + "state_value_help": "Bruk HA-tilstandsverdien (vanligvis små bokstaver, f.eks. \"on\"/\"off\"). Store/små bokstaver normaliseres ved lagring.", + "target_changes_help": "Antall samsvarende overganger før utløseren aktiveres (standard: 1).", + "qr_print_title": "Skriv ut QR-koder", + "qr_print_desc": "Generer en utskrivbar side med QR-koder som kan klippes ut og festes på utstyret ditt.", + "qr_print_load": "Last objekter", + "qr_print_filter": "Filter", + "qr_print_objects": "Objekter", + "qr_print_actions": "Handlinger", + "qr_print_url_mode": "Lenketype", + "qr_print_estimate": "Estimerte QR-koder", + "qr_print_over_limit": "grensen er 200, snevre inn filteret", + "qr_print_generate": "Generer QR-koder", + "qr_print_generating": "Genererer…", + "qr_print_ready": "QR-koder klare", + "qr_print_print_button": "Skriv ut", + "qr_print_empty": "Ingenting å generere", + "qr_action_skip": "Hopp over", + "vacation_title": "Feriemodus", + "vacation_active": "aktiv", + "vacation_ended": "avsluttet", + "vacation_desc": "Planlegg en ferie: varsler settes på pause i perioden pluss en buffer på noen dager. Du kan inkludere bestemte oppgaver igjen.", + "vacation_enable": "Aktiver feriemodus", + "vacation_start": "Start", + "vacation_end": "Slutt", + "vacation_buffer": "Buffer (dager)", + "vacation_exempt_title": "Varsle likevel under ferie", + "vacation_exempt_desc": "Velg oppgaver som fortsatt skal varsle under ferie (f.eks. kritisk bassengkjemi).", + "vacation_load_tasks": "Last oppgaver", + "vacation_preview_btn": "Vis forhåndsvisning", + "vacation_preview_affected": "oppgaver berørt", + "vacation_event_due_soon": "forfaller snart", + "vacation_event_overdue": "blir forfalt", + "vacation_event_triggered_est": "sensorutløsning mulig", + "vacation_sensor_based": "(sensorbasert)", + "vacation_action_notify": "Varsle likevel", + "vacation_action_unsilence": "Demp igjen", + "vacation_marked_complete": "Merket som fullført", + "vacation_marked_skip": "Hoppet over", + "vacation_end_now": "Avslutt ferie nå", + "add": "Legg til", + "show_stats": "Vis statistikk + grafer", + "hide_stats": "Skjul statistikk", + "adaptive_no_data": "Ikke nok fullføringshistorikk ennå for adaptiv analyse. Fullfør denne oppgaven noen flere ganger for å låse opp intervallanbefalinger og pålitelighetsdiagrammer.", + "suggestion_applied": "Foreslått intervall tatt i bruk", + "vacation_mode": "Feriemodus", + "vacation_status_active": "Aktiv nå", + "vacation_status_scheduled": "Planlagt", + "vacation_status_inactive": "Inaktiv", + "vacation_end_now_confirm": "Avslutte ferien umiddelbart?", + "vacation_exempt_count": "unntatt", + "vacation_advanced": "Avansert…", + "vacation_open_panel": "Åpne i panel", + "enable": "Aktiver", + "saved": "Lagret", + "budget_monthly_set": "Angi månedlig", + "budget_yearly_set": "Angi årlig", + "budget_advanced": "Valuta, varsler…", + "budget_open_panel": "Åpne i panel", + "groups_empty": "Ingen grupper ennå.", + "group_new_placeholder": "Legg til gruppe…", + "group_delete_confirm": "Slette gruppen \"{name}\"?", + "groups_manage_tasks": "Administrer oppgavetildelinger…", + "groups_open_panel": "Åpne i panel", + "unassigned": "Ikke tildelt", + "no_area": "Intet område", + "has_overdue": "Har forfalte oppgaver", + "object": "Objekt", + "settings_panel_access": "Paneltilgang", + "settings_panel_access_desc": "Administratorer har alltid full tilgang. For å delegere opprettelse, redigering og sletting til bestemte ikke-administratorer, slå dette på og velg dem nedenfor — alle andre ser kun Fullfør og Hopp over.", + "settings_operator_write": "Tillat valgte brukere å opprette, redigere og slette", + "settings_operator_write_desc": "Av: bare administratorer kan endre innhold. På: de valgte brukerne nedenfor får også full tilgang.", + "no_non_admin_users": "Ingen ikke-administratorbrukere funnet. Legg til noen under Innstillinger → Personer.", + "owner_label": "Eier", + "feat_completion_actions": "Fullføringshandlinger", + "feat_completion_actions_desc": "HA-handling per oppgave ved fullføring + hurtigfullføring med QR og forhåndsinnstilte verdier.", + "on_complete_action_title": "Ved fullføring: utløs HA-handling (valgfritt)", + "on_complete_action_desc": "Kaller en HA-tjeneste når oppgaven fullføres — f.eks. tilbakestille en teller på enheten.", + "on_complete_action_service": "Tjeneste", + "on_complete_action_target": "Målentitet", + "on_complete_action_target_hint": "Merk: entitetsdomenet må samsvare med tjenesten — f.eks. fungerer 'button.press' kun på button.*, 'counter.increment' kun på counter.*, 'input_button.press' kun på input_button.* osv. Ved manglende samsvar mislykkes handlingen i det stille (HA logger 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Data (JSON, valgfritt)", + "on_complete_action_test": "Valider konfigurasjon", + "on_complete_action_test_success": "✓ Konfigurasjon gyldig (handlingen utløses kun ved fullføring av oppgaven)", + "on_complete_action_test_failed": "Mislyktes", + "quick_complete_defaults_title": "Standardverdier for hurtigfullføring (for QR-skanninger, valgfritt)", + "quick_complete_defaults_desc": "Forhåndsinnstilte verdier for hurtigfullføring via QR-skanning. Uten disse åpner QR-koden fullføringsdialogen.", + "quick_complete_defaults_notes": "Notater", + "quick_complete_defaults_cost": "Kostnad", + "quick_complete_defaults_duration": "Varighet (minutter)", + "quick_complete_defaults_feedback_none": "Ingen tilbakemelding", + "quick_complete_defaults_feedback_needed": "Var nødvendig", + "quick_complete_defaults_feedback_not_needed": "Ikke nødvendig", + "quick_complete_success": "Raskt merket som fullført", + "show_all_objects": "Vis alle objekter", + "show_all_tasks": "Fjern filter — vis alle oppgaver", + "filter_to_overdue": "Filtrer oppgavelisten til kun forfalte", + "filter_to_due_soon": "Filtrer oppgavelisten til kun forfaller snart", + "filter_to_triggered": "Filtrer oppgavelisten til kun utløste", + "open_task": "Åpne oppgave", + "show_details": "Vis historikk + statistikk", + "hide_details": "Skjul detaljer", + "history_empty": "Ingen historikk ennå.", + "history_edit_button": "Rediger oppføring", + "total_cost": "Total kostnad", + "times_performed": "Utført", + "older_entries": "eldre", + "open_in_panel": "Åpne i vedlikeholdspanelet", + "skip_reason": "Årsak for å hoppe over (valgfritt)", + "reset_to_date": "Tilbakestill sist utført til", + "delete_task_confirm": "Slette denne oppgaven og dens historikk?", + "delete_object_confirm": "Slette dette objektet og alle dets oppgaver?", + "loading": "Laster…", + "archive": "Arkivér", + "undo": "Angre", + "task_archived": "Oppgave arkivert", + "object_archived": "Objekt arkivert", + "unarchive": "Gjenopprett", + "archived": "Arkivert", + "show_archived": "Vis arkiverte", + "hide_archived": "Skjul arkiverte", + "confirm_archive_object": "Arkivere dette objektet og oppgavene? Historikken beholdes og kan gjenopprettes senere.", + "settings_archive": "Arkiv og oppbevaring", + "settings_archive_desc": "Legg bort fullførte engangsoppgaver uten å slette dem. Arkiverte elementer er skjult og inaktive, men beholder historikk og kostnad.", + "settings_archive_oneoff_days": "Arkivér fullførte engangsoppgaver automatisk etter (dager, 0 = av)", + "settings_delete_archived_oneoff_days": "Slett arkiverte engangsoppgaver automatisk etter (dager, 0 = aldri)", + "archive_object": "Arkivér objekt", + "unarchive_object": "Gjenopprett objekt", + "documents": "Dokumenter", + "documents_empty": "Ingen dokumenter ennå.", + "doc_upload": "Last opp fil", + "doc_uploading": "Laster opp…", + "doc_add_link": "Legg til lenke", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Tittel (valgfritt)", + "doc_open": "Åpne", + "doc_delete_confirm": "Slette \"{name}\"?", + "doc_too_large": "Filen er for stor (maks. 25 MB).", + "doc_upload_failed": "Opplasting mislyktes.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Allerede lagret et annet sted — delt, ingen ekstra plass.", + "doc_dup_in_object": "Denne filen er allerede knyttet til dette objektet.", + "doc_link_invalid": "Kun http/https-lenker er tillatt.", + "doc_cat_manual": "Bruksanvisning", + "doc_cat_warranty": "Garanti", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Reservedeler", + "doc_cat_photo": "Foto", + "doc_cat_other": "Annet", + "doc_link_badge": "Lenke", + "doc_storage_title": "Dokumentlagring", + "doc_storage_saved": "Spart ved deduplisering", + "doc_storage_refresh": "Oppdater", + "doc_download": "Last ned", + "doc_close": "Lukk", + "doc_camera": "Ta bilde", + "doc_drop_hint": "Slipp filer her", + "doc_task_none": "Ingen dokumenter knyttet til denne oppgaven.", + "doc_link_existing": "Knytt et dokument…", + "doc_attach": "Knytt", + "doc_unlink": "Fjern kobling", + "doc_page": "Side", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1å", + "chart_since_service": "siden siste vedlikehold", + "chart_no_stats": "Ingen langtidsstatistikk for denne enheten — viser bare verdier fra vedlikeholdsoppføringer", + "auto_complete_on_recovery": "Fullfør automatisk når sensoren normaliseres", + "auto_complete_on_recovery_help": "Registrerer en fullføring (setter sist utført) når utløseren løser seg selv — f.eks. salt etterfylt, filter byttet.", + "doc_search": "Søk i dokumenter…", + "doc_search_none": "Ingen samsvarende dokumenter", + "link_device_optional": "Koble til eksisterende enhet (valgfritt)", + "parent_object_optional": "Overordnet objekt (valgfritt)", + "parent_none": "(Ingen overordnet)", + "paused": "Satt på pause", + "pause_object": "Sett på pause", + "resume_object": "Gjenoppta", + "pause_until_prompt": "Frys objektets planer — ingenting forfaller og ingenting varsler før det gjenopptas. Angi eventuelt en dato for automatisk gjenopptakelse.", + "pause_until_label": "Gjenoppta den (valgfritt)", + "object_paused": "Objekt satt på pause", + "object_resumed": "Objekt gjenopptatt — planer startet på nytt", + "object_paused_badge": "På pause", + "paused_until_label": "til", + "replace_object": "Erstatt…", + "replace_object_prompt": "Pensjoner dette objektet og opprett en etterfølger. Historikk og kostnader forblir arkivert på det gamle; oppgaver og dokumenter følger med til det nye, tellere starter på nytt.", + "replace_name_label": "Etterfølgerens navn", + "object_replaced": "Objekt erstattet — etterfølger opprettet", + "reading_unit_label": "Avlesningsenhet (f.eks. kWh, m³)", + "reading_unit_help": "Vises ved siden av den registrerte verdien når oppgaven fullføres.", + "reading_value_label": "Avlest verdi", + "reading_label": "Avlesning", + "settings_templates_label": "Malgalleri", + "settings_templates_hint": "Fjern haken for maler du aldri trenger — de forsvinner fra \"Fra mal\"-velgerne (panel og config flow). Ellers endres ingenting; kan aktiveres igjen når som helst.", + "worksheet": "Arbeidsark", + "worksheet_scan_view": "Skann for å åpne oppgaven", + "worksheet_scan_complete": "Skann for å fullføre", + "worksheet_manual_excerpt": "Utdrag fra manualen", + "worksheet_pages": "sider", + "worksheet_printed": "Skrevet ut", + "worksheet_never": "Aldri" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/nl.json b/custom_components/maintenance_supporter/frontend-src/locales/nl.json new file mode 100644 index 0000000..ed43fbd --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/nl.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Onderhoud", + "objects": "Objecten", + "tasks": "Taken", + "overdue": "Achterstallig", + "due_soon": "Binnenkort", + "triggered": "Geactiveerd", + "ok": "OK", + "all": "Alle", + "new_object": "+ Nieuw object", + "templates_from": "Uit sjabloon", + "templates_title": "Begin met een sjabloon", + "templates_task_count": "{n} taken", + "template_created": "Aangemaakt uit sjabloon", + "onboard_hint": "Voeg je eerste object toe om onderhoud te volgen.", + "edit": "Bewerken", + "duplicate": "Dupliceren", + "task_duplicated": "Taak gedupliceerd", + "object_duplicated": "Object gedupliceerd", + "delete": "Verwijderen", + "add_task": "+ Taak", + "complete": "Voltooid", + "completed": "Voltooid", + "skip": "Overslaan", + "skipped": "Overgeslagen", + "missed": "Missed", + "reset": "Resetten", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Annuleren", + "bulk_select": "Selecteren", + "bulk_select_all": "Alles selecteren", + "bulk_n_selected": "{n} geselecteerd", + "bulk_completed": "{n} taken voltooid", + "bulk_archived": "{n} taken gearchiveerd", + "completing": "Wordt voltooid…", + "interval": "Interval", + "warning": "Waarschuwing", + "last_performed": "Laatst uitgevoerd", + "next_due": "Volgende keer", + "days_until_due": "Dagen tot vervaldatum", + "avg_duration": "Ø Duur", + "trigger": "Trigger", + "trigger_type": "Triggertype", + "threshold_above": "Bovengrens", + "threshold_below": "Ondergrens", + "threshold": "Drempelwaarde", + "counter": "Teller", + "state_change": "Statuswijziging", + "runtime": "Looptijd", + "runtime_hours": "Doellooptijd (uren)", + "target_value": "Doelwaarde", + "baseline": "Basislijn", + "target_changes": "Doelwijzigingen", + "for_minutes": "Voor (minuten)", + "time_based": "Tijdgebaseerd", + "sensor_based": "Sensorgebaseerd", + "manual": "Handmatig", + "one_time": "Eenmalig", + "weekdays": "Weekdagen", + "nth_weekday": "Zoveelste weekdag van de maand", + "day_of_month": "Dag van de maand", + "recurrence_on_days": "Herhalen op", + "recurrence_occurrence": "Voorkomen", + "recurrence_weekday": "Weekdag", + "recurrence_day": "Dag van de maand (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1e", + "ord_2": "2e", + "ord_3": "3e", + "ord_4": "4e", + "ord_5": "5e", + "ord_last": "Laatste", + "day_word": "Dag", + "interval_value": "Interval", + "interval_unit": "Eenheid", + "unit_days": "Dagen", + "unit_weeks": "Weken", + "unit_months": "Maanden", + "unit_years": "Jaren", + "due_date": "Vervaldatum", + "cleaning": "Reiniging", + "inspection": "Inspectie", + "replacement": "Vervanging", + "calibration": "Kalibratie", + "service": "Service", + "reading": "Aflezing", + "custom": "Aangepast", + "history": "Geschiedenis", + "cost": "Kosten", + "report_button": "Rapport", + "report_title": "Onderhoudsrapport", + "report_generated": "Gegenereerd", + "report_times_done": "Gedaan", + "report_total_cost": "Totale kosten", + "report_every": "elke {n} {unit}", + "report_notes": "Notities", + "report_col_type": "Type", + "report_col_status": "Status", + "report_col_schedule": "Schema", + "duration": "Duur", + "both": "Beide", + "trigger_val": "Triggerwaarde", + "complete_title": "Voltooid: ", + "checklist": "Checklist", + "checklist_steps_optional": "Checklist-stappen (optioneel)", + "checklist_placeholder": "Filter schoonmaken\nPakking vervangen\nDruk testen", + "checklist_help": "Eén stap per regel. Max. 100 items.", + "err_too_long": "{field}: te lang (max. {n} tekens)", + "err_too_short": "{field}: te kort (min. {n} tekens)", + "err_value_too_high": "{field}: te groot (max. {n})", + "err_value_too_low": "{field}: te klein (min. {n})", + "err_required": "{field}: verplicht", + "err_wrong_type": "{field}: verkeerd type (verwacht: {type})", + "err_invalid_choice": "{field}: niet-toegestane waarde", + "err_invalid_value": "{field}: ongeldige waarde", + "feat_schedule_time": "Tijd-van-dag-planning", + "feat_schedule_time_desc": "Taken vervallen op een specifieke tijd in plaats van middernacht.", + "schedule_time_optional": "Vervaldagstijd (optioneel, HH:MM)", + "schedule_time_help": "Leeg = middernacht (standaard). HA-tijdzone.", + "at_time": "om", + "notes_optional": "Notities (optioneel)", + "cost_optional": "Kosten (optioneel)", + "duration_minutes": "Duur in minuten (optioneel)", + "days": "dagen", + "day": "dag", + "today": "Vandaag", + "d_overdue": "d achterstallig", + "no_tasks": "Geen onderhoudstaken. Maak een object aan om te beginnen.", + "no_tasks_short": "Geen taken", + "no_history": "Nog geen geschiedenisitems.", + "show_all": "Alles tonen", + "cost_duration_chart": "Kosten & Duur", + "installed": "Geïnstalleerd", + "confirm_delete_object": "Dit object en alle bijbehorende taken verwijderen?", + "confirm_delete_task": "Deze taak verwijderen?", + "min": "Min", + "max": "Max", + "save": "Opslaan", + "saving": "Opslaan…", + "edit_task": "Taak bewerken", + "new_task": "Nieuwe onderhoudstaak", + "task_name": "Taaknaam", + "maintenance_type": "Onderhoudstype", + "priority": "Prioriteit", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Laag", + "priority_normal": "Normaal", + "priority_high": "Hoog", + "schedule_type": "Planningstype", + "interval_days": "Interval (dagen)", + "warning_days": "Waarschuwingsdagen", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Laatst uitgevoerd (optioneel)", + "interval_anchor": "Interval-anker", + "anchor_completion": "Vanaf voltooiing", + "anchor_planned": "Vanaf geplande datum (geen drift)", + "edit_object": "Object bewerken", + "name": "Naam", + "manufacturer_optional": "Fabrikant (optioneel)", + "model_optional": "Model (optioneel)", + "serial_number_optional": "Serienummer (optioneel)", + "serial_number_label": "S/N", + "documentation_url_label": "Handleiding", + "object_notes_label": "Notities", + "sort_due_date": "Vervaldatum", + "sort_object": "Objectnaam", + "sort_type": "Type", + "sort_task_name": "Taaknaam", + "all_objects": "Alle objecten", + "tasks_lower": "taken", + "no_tasks_yet": "Nog geen taken", + "add_first_task": "Eerste taak toevoegen", + "trigger_configuration": "Triggerconfiguratie", + "entity_id": "Entiteits-ID", + "comma_separated": "kommagescheiden", + "entity_logic": "Entiteitslogica", + "entity_logic_any": "Elke entiteit triggert", + "entity_logic_all": "Alle entiteiten moeten triggeren", + "entities": "entiteiten", + "attribute_optional": "Attribuut (optioneel, leeg = status)", + "use_entity_state": "Entiteitsstatus gebruiken (geen attribuut)", + "trigger_above": "Activeren als boven", + "trigger_below": "Activeren als onder", + "for_at_least_minutes": "Voor minstens (minuten)", + "safety_interval_days": "Veiligheidsinterval (dagen, optioneel)", + "safety_interval": "Veiligheidsinterval (optioneel)", + "delta_mode": "Deltamodus", + "from_state_optional": "Van status (optioneel)", + "to_state_optional": "Naar status (optioneel)", + "documentation_url_optional": "Documentatie-URL (optioneel)", + "object_notes_optional": "Notities (optioneel)", + "nfc_tag_id_optional": "NFC-tag-ID (optioneel)", + "nfc_tags_empty_help": "Nog geen NFC-tags geregistreerd in Home Assistant.", + "nfc_tags_open_settings": "Tag-instellingen openen", + "nfc_tags_refresh": "Vernieuwen", + "environmental_entity_optional": "Omgevingssensor (optioneel)", + "environmental_entity_helper": "bv. sensor.buitentemperatuur — past het interval aan op basis van omgevingswaarden", + "environmental_attribute_optional": "Omgevingsattribuut (optioneel)", + "nfc_tag_id": "NFC-tag-ID", + "nfc_linked": "NFC-tag gekoppeld", + "nfc_link_hint": "Klik om NFC-tag te koppelen", + "responsible_user": "Verantwoordelijke gebruiker", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Geen gebruiker toegewezen)", + "all_users": "Alle gebruikers", + "my_tasks": "Mijn taken", + "tab_calendar": "Kalender", + "cal_no_events": "Geen onderhoud", + "cal_window_7": "7 dagen", + "cal_window_14": "14 dagen", + "cal_window_30": "30 dagen", + "cal_window_365": "1 jaar", + "cal_every_n_days": "elke {n} dagen", + "cal_source_time": "Tijd-gebaseerd", + "cal_source_time_adaptive": "Tijd-gebaseerd (adaptief)", + "cal_source_sensor": "Sensor-gebaseerd", + "cal_predicted": "voorspeld", + "cal_confidence_high": "hoge zekerheid", + "cal_confidence_medium": "gemiddelde zekerheid", + "cal_confidence_low": "lage zekerheid", + "budget_monthly": "Maandbudget", + "budget_yearly": "Jaarbudget", + "groups": "Groepen", + "new_group": "Nieuwe groep", + "edit_group": "Groep bewerken", + "no_groups": "Nog geen groepen", + "delete_group": "Groep verwijderen", + "delete_group_confirm": "Groep '{name}' verwijderen?", + "group_select_tasks": "Taken selecteren", + "group_name_required": "Naam vereist", + "description_optional": "Beschrijving (optioneel)", + "selected": "Geselecteerd", + "loading_chart": "Grafiekgegevens laden...", + "hide_outliers": "Uitschieters verbergen (sensorfouten)", + "was_maintenance_needed": "Was dit onderhoud nodig?", + "feedback_needed": "Nodig", + "feedback_not_needed": "Niet nodig", + "feedback_not_sure": "Niet zeker", + "suggested_interval": "Voorgesteld interval", + "apply_suggestion": "Toepassen", + "reanalyze": "Opnieuw analyseren", + "reanalyze_result": "Nieuwe analyse", + "reanalyze_insufficient_data": "Onvoldoende gegevens voor een aanbeveling", + "data_points": "datapunten", + "dismiss_suggestion": "Negeren", + "confidence_low": "Laag", + "confidence_medium": "Gemiddeld", + "confidence_high": "Hoog", + "recommended": "aanbevolen", + "seasonal_awareness": "Seizoensbewustzijn", + "edit_seasonal_overrides": "Seizoensfactoren bewerken", + "seasonal_overrides_title": "Seizoensfactoren (override)", + "seasonal_overrides_hint": "Factor per maand (0.1–5.0). Leeg = automatisch geleerd.", + "seasonal_override_invalid": "Ongeldige waarde", + "seasonal_override_range": "Factor moet tussen 0.1 en 5.0 liggen", + "clear_all": "Alles wissen", + "seasonal_chart_title": "Seizoensfactoren", + "seasonal_learned": "Geleerd", + "seasonal_manual": "Handmatig", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mrt", + "month_apr": "Apr", + "month_may": "Mei", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Dec", + "sensor_prediction": "Sensorvoorspelling", + "degradation_trend": "Trend", + "trend_rising": "Stijgend", + "trend_falling": "Dalend", + "trend_stable": "Stabiel", + "trend_insufficient_data": "Onvoldoende gegevens", + "days_until_threshold": "Dagen tot drempelwaarde", + "threshold_exceeded": "Drempelwaarde overschreden", + "environmental_adjustment": "Omgevingsfactor", + "sensor_prediction_urgency": "Sensor voorspelt drempelwaarde in ~{days} dagen", + "day_short": "dag", + "weibull_reliability_curve": "Betrouwbaarheidscurve", + "weibull_failure_probability": "Faalkans", + "weibull_r_squared": "Fit R²", + "beta_early_failures": "Vroege uitval", + "beta_random_failures": "Willekeurige uitval", + "beta_wear_out": "Slijtage", + "beta_highly_predictable": "Zeer voorspelbaar", + "confidence_interval": "Betrouwbaarheidsinterval", + "confidence_conservative": "Conservatief", + "confidence_aggressive": "Optimistisch", + "current_interval_marker": "Huidig interval", + "recommended_marker": "Aanbevolen", + "characteristic_life": "Karakteristieke levensduur", + "chart_mini_sparkline": "Trend-sparkline", + "chart_history": "Kosten- en duurgeschiedenis", + "chart_seasonal": "Seizoensfactoren, 12 maanden", + "chart_weibull": "Weibull-betrouwbaarheidscurve", + "chart_sparkline": "Sensor-triggerwaardegrafiek", + "days_progress": "Dagenvoortgang", + "qr_code": "QR-code", + "qr_generating": "QR-code genereren…", + "qr_error": "QR-code kon niet worden gegenereerd.", + "qr_error_no_url": "Geen HA-URL geconfigureerd. Stel een externe of interne URL in via Instellingen → Systeem → Netwerk.", + "save_error": "Opslaan mislukt. Probeer het opnieuw.", + "qr_print": "Afdrukken", + "qr_download": "SVG downloaden", + "qr_action": "Actie bij scannen", + "qr_action_view": "Onderhoudsinfo bekijken", + "qr_action_complete": "Onderhoud als voltooid markeren", + "qr_url_mode": "Linktype", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokaal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Overzicht", + "analysis": "Analyse", + "recent_activities": "Recente activiteiten", + "search_notes": "Notities doorzoeken", + "avg_cost": "Ø Kosten", + "no_advanced_features": "Geen geavanceerde functies ingeschakeld", + "no_advanced_features_hint": "Schakel „Adaptieve Intervallen” of „Seizoenpatronen” in via de integratie-instellingen om hier analysegegevens te zien.", + "analysis_not_enough_data": "Nog niet genoeg gegevens voor analyse.", + "analysis_not_enough_data_hint": "Weibull-analyse vereist minstens 5 voltooide onderhoudsbeurten; seizoenspatronen worden zichtbaar na 6+ datapunten per maand.", + "analysis_manual_task_hint": "Handmatige taken zonder interval genereren geen analysegegevens.", + "completions": "voltooiingen", + "current": "Huidig", + "shorter": "Korter", + "longer": "Langer", + "normal": "Normaal", + "disabled": "Uitgeschakeld", + "compound_logic": "Samengestelde logica", + "compound": "Samengesteld (meerdere voorwaarden)", + "compound_logic_and": "EN — alle voorwaarden moeten activeren", + "compound_logic_or": "OF — één voorwaarde is genoeg", + "compound_help": "Combineer meerdere sensorvoorwaarden tot één trigger.", + "compound_no_conditions": "Nog geen voorwaarden — voeg er ten minste één toe.", + "compound_add_condition": "Voorwaarde toevoegen", + "compound_condition": "Voorwaarde", + "compound_remove_condition": "Voorwaarde verwijderen", + "card_title": "Titel", + "card_show_header": "Koptekst met statistieken tonen", + "card_show_actions": "Actieknoppen tonen", + "card_compact": "Compacte modus", + "card_max_items": "Max items (0 = alle)", + "card_filter_status": "Filteren op status", + "card_filter_status_help": "Leeg = alle statussen tonen.", + "card_filter_objects": "Filteren op objecten", + "card_filter_objects_help": "Leeg = alle objecten tonen.", + "card_filter_entities": "Filteren op entiteiten (entity_ids)", + "card_filter_entities_help": "Kies sensor/binary_sensor entiteiten van deze integratie. Leeg = alle.", + "card_loading_objects": "Objecten laden…", + "card_load_error": "Objecten konden niet worden geladen — controleer de WebSocket-verbinding.", + "card_no_tasks_title": "Nog geen onderhoudstaken", + "card_no_tasks_cta": "→ Maak er een aan in het Maintenance-paneel", + "no_objects": "Nog geen objecten.", + "action_error": "Actie mislukt. Probeer het opnieuw.", + "area_id_optional": "Gebied (optioneel)", + "installation_date_optional": "Installatiedatum (optioneel)", + "warranty_expiry_optional": "Garantie verloopt (optioneel)", + "warranty": "Garantie", + "warranty_valid_until": "geldig tot {date}", + "warranty_expires_in": "verloopt over {days} dagen", + "warranty_expired": "verlopen", + "cal_past_windows": "Eerdere vensters", + "cal_forward_windows": "Toekomstige vensters", + "history_edit_title": "Geschiedenisitem bewerken", + "history_edit_timestamp": "Tijdstempel", + "manufacturer": "Fabrikant", + "model": "Model", + "area": "Gebied", + "actions": "Acties", + "view_mode_label": "Weergave", + "view_cards": "Kaartweergave", + "view_table": "Tabelweergave", + "objects_table_columns_label": "Kolommen objecttabel", + "objects_table_columns_hint": "Kies welke kolommen in de tabelweergave van objecten verschijnen.", + "custom_icon_optional": "Icoon (optioneel, bijv. mdi:wrench)", + "task_enabled": "Taak ingeschakeld", + "skip_reason_prompt": "Deze taak overslaan?", + "reason_optional": "Reden (optioneel)", + "reset_date_prompt": "Taak markeren als uitgevoerd?", + "reset_date_optional": "Laatste uitvoeringsdatum (optioneel, standaard vandaag)", + "notes_label": "Notities", + "documentation_label": "Documentatie", + "no_nfc_tag": "— Geen tag —", + "dashboard": "Dashboard", + "tab_today": "Vandaag", + "palette_placeholder": "Objecten en taken zoeken…", + "palette_no_results": "Geen resultaten", + "palette_hint": "↑↓ navigeren · Enter openen · Esc sluiten", + "today_all_caught_up": "Helemaal bij! Niets te doen deze week.", + "today_overdue": "Achterstallig", + "today_due_today": "Vandaag te doen", + "today_this_week": "Deze week", + "settings": "Instellingen", + "settings_features": "Geavanceerde functies", + "settings_features_desc": "Schakel geavanceerde functies in of uit. Uitschakelen verbergt ze in de interface maar verwijdert geen gegevens.", + "feat_adaptive": "Adaptieve planning", + "feat_adaptive_desc": "Leer optimale intervallen uit onderhoudsgeschiedenis", + "feat_predictions": "Sensorvoorspellingen", + "feat_predictions_desc": "Voorspel triggerdatums op basis van sensordegradatie", + "feat_seasonal": "Seizoensaanpassingen", + "feat_seasonal_desc": "Pas intervallen aan op seizoenspatronen", + "feat_environmental": "Omgevingscorrelatie", + "feat_environmental_desc": "Correleer intervallen met temperatuur/vochtigheid", + "feat_budget": "Budgetbeheer", + "feat_budget_desc": "Volg maandelijkse en jaarlijkse onderhoudsuitgaven", + "feat_groups": "Taakgroepen", + "feat_groups_desc": "Organiseer taken in logische groepen", + "feat_checklists": "Checklists", + "feat_checklists_desc": "Meerstaps procedures voor taakvoltooiing", + "settings_general": "Algemeen", + "settings_default_warning": "Standaard waarschuwingsdagen", + "settings_panel_enabled": "Zijbalkpaneel", + "settings_panel_title": "Titel zijbalkpaneel", + "settings_notifications": "Meldingen", + "settings_notify_service": "Meldingsservice", + "test_notification": "Testmelding", + "send_test": "Test versturen", + "testing": "Verzenden…", + "test_notification_success": "Testmelding verzonden", + "test_notification_failed": "Testmelding mislukt", + "settings_notify_due_soon": "Melding bij bijna verlopen", + "settings_notify_overdue": "Melding bij achterstallig", + "settings_notify_triggered": "Melding bij geactiveerd", + "settings_interval_hours": "Herhalingsinterval (uren, 0 = eenmalig)", + "settings_quiet_hours": "Stille uren", + "settings_quiet_start": "Start", + "settings_quiet_end": "Einde", + "settings_max_per_day": "Max meldingen per dag (0 = onbeperkt)", + "settings_bundling": "Meldingen bundelen", + "settings_bundle_threshold": "Bundeldrempel", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobiele actieknoppen", + "settings_action_complete": "Knop 'Voltooid' tonen", + "settings_action_skip": "Knop 'Overslaan' tonen", + "settings_action_snooze": "Knop 'Snooze' tonen", + "settings_weekly_digest": "Wekelijks overzicht", + "settings_weekly_digest_hint": "Eén samenvattingsmelding op maandagochtend als er taken zijn.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Snoozeduur (uren)", + "settings_budget": "Budget", + "settings_currency": "Valuta", + "settings_budget_monthly": "Maandbudget", + "settings_budget_yearly": "Jaarbudget", + "settings_budget_alerts": "Budgetwaarschuwingen", + "settings_budget_threshold": "Waarschuwingsdrempel (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "JSON exporteren", + "settings_export_yaml": "YAML exporteren", + "settings_export_csv": "CSV exporteren", + "settings_import_csv": "CSV importeren", + "settings_import_placeholder": "Plak JSON- of CSV-inhoud hier…", + "settings_import_btn": "Importeren", + "settings_import_success": "{count} objecten succesvol geïmporteerd.", + "settings_export_success": "Export gedownload.", + "settings_saved": "Instelling opgeslagen.", + "settings_include_history": "Geschiedenis meenemen", + "sort_alphabetical": "Alfabetisch", + "sort_due_soonest": "Eerst vervallend", + "sort_task_count": "Aantal taken", + "sort_area": "Gebied", + "sort_assigned_user": "Toegewezen gebruiker", + "sort_group": "Groep", + "groupby_none": "Geen groepering", + "groupby_area": "Per gebied", + "groupby_group": "Per groep", + "groupby_user": "Per gebruiker", + "filter_label": "Filter", + "user_label": "Gebruiker", + "sort_label": "Sorteren", + "group_by_label": "Groeperen op", + "state_value_help": "Gebruik de HA-statuswaarde (meestal in kleine letters, bv. \"on\"/\"off\"). Hoofdletters worden bij opslaan genormaliseerd.", + "target_changes_help": "Aantal overeenkomende overgangen voordat de trigger wordt geactiveerd (standaard: 1).", + "qr_print_title": "QR-codes afdrukken", + "qr_print_desc": "Genereer een afdrukpagina met QR-codes om uit te knippen en op je apparaten te plakken.", + "qr_print_load": "Objecten laden", + "qr_print_filter": "Filter", + "qr_print_objects": "Objecten", + "qr_print_actions": "Acties", + "qr_print_url_mode": "Linktype", + "qr_print_estimate": "Geschatte QR-codes", + "qr_print_over_limit": "max is 200, beperk de filter", + "qr_print_generate": "QR-codes genereren", + "qr_print_generating": "Genereren…", + "qr_print_ready": "QR-codes klaar", + "qr_print_print_button": "Afdrukken", + "qr_print_empty": "Niets te genereren", + "qr_action_skip": "Overslaan", + "vacation_title": "Vakantiemodus", + "vacation_active": "actief", + "vacation_ended": "beëindigd", + "vacation_desc": "Plan je vakantie: meldingen worden gepauzeerd tijdens de periode plus een buffer van dagen. Je kunt per taak uitzonderingen instellen.", + "vacation_enable": "Vakantiemodus inschakelen", + "vacation_start": "Begin", + "vacation_end": "Einde", + "vacation_buffer": "Buffer (dagen)", + "vacation_exempt_title": "Toch melden tijdens vakantie", + "vacation_exempt_desc": "Kies taken die ook tijdens vakantie meldingen moeten geven (bv. kritische zwembadchemie).", + "vacation_load_tasks": "Taken laden", + "vacation_preview_btn": "Voorvertoning", + "vacation_preview_affected": "taken betrokken", + "vacation_event_due_soon": "wordt binnenkort verschuldigd", + "vacation_event_overdue": "wordt achterstallig", + "vacation_event_triggered_est": "sensortrigger mogelijk", + "vacation_sensor_based": "(sensor-gebaseerd)", + "vacation_action_notify": "Toch melden", + "vacation_action_unsilence": "Weer dempen", + "vacation_marked_complete": "Als voltooid gemarkeerd", + "vacation_marked_skip": "Overgeslagen", + "vacation_end_now": "Vakantie nu beëindigen", + "unassigned": "Niet toegewezen", + "no_area": "Geen gebied", + "has_overdue": "Heeft achterstallige taken", + "object": "Object", + "settings_panel_access": "Paneel-toegang", + "settings_panel_access_desc": "Admins hebben altijd volledige toegang. Om aanmaken, bewerken en verwijderen aan specifieke niet-admins te delegeren, schakel dit in en selecteer hen hieronder — alle anderen zien alleen Voltooien en Overslaan.", + "settings_operator_write": "Geselecteerde gebruikers laten aanmaken, bewerken en verwijderen", + "settings_operator_write_desc": "Uit: alleen admins kunnen inhoud wijzigen. Aan: de geselecteerde gebruikers hieronder krijgen ook volledige toegang.", + "no_non_admin_users": "Geen niet-admin gebruikers gevonden. Voeg ze toe in Instellingen → Personen.", + "owner_label": "Eigenaar", + "feat_completion_actions": "Voltooiings-acties", + "feat_completion_actions_desc": "Per taak HA-actie bij voltooien + snel-voltooien-QR met vooraf ingestelde waarden.", + "on_complete_action_title": "Bij voltooien: HA-actie uitvoeren (optioneel)", + "on_complete_action_desc": "Roept een HA-service aan wanneer de taak is voltooid — bv. een teller op het apparaat resetten.", + "on_complete_action_service": "Service", + "on_complete_action_target": "Doel-entiteit", + "on_complete_action_data": "Data (JSON, optioneel)", + "on_complete_action_test": "Actie testen", + "on_complete_action_test_success": "Geslaagd", + "on_complete_action_test_failed": "Mislukt", + "quick_complete_defaults_title": "Snel-voltooien-standaardwaarden (voor QR-scans, optioneel)", + "quick_complete_defaults_desc": "Vooraf ingestelde waarden voor snel-voltooien-QR. Zonder deze opent de QR de voltooi-dialoog.", + "quick_complete_defaults_notes": "Notities", + "quick_complete_defaults_cost": "Kosten", + "quick_complete_defaults_duration": "Duur (minuten)", + "quick_complete_defaults_feedback_none": "Geen feedback", + "quick_complete_defaults_feedback_needed": "Was nodig", + "quick_complete_defaults_feedback_not_needed": "Niet nodig", + "quick_complete_success": "Snel als voltooid gemarkeerd", + "trigger_replaced": "Trigger vervangen", + "add": "Toevoegen", + "show_stats": "Statistieken + grafieken tonen", + "hide_stats": "Statistieken verbergen", + "adaptive_no_data": "Nog niet genoeg voltooiingsgeschiedenis voor adaptieve analyse. Voer deze taak nog een paar keer uit om intervaladviezen en betrouwbaarheidsgrafieken te ontgrendelen.", + "suggestion_applied": "Voorgesteld interval toegepast", + "vacation_mode": "Vakantiemodus", + "vacation_status_active": "Nu actief", + "vacation_status_scheduled": "Gepland", + "vacation_status_inactive": "Inactief", + "vacation_end_now_confirm": "Vakantie onmiddellijk beëindigen?", + "vacation_exempt_count": "uitgezonderd", + "vacation_advanced": "Geavanceerd…", + "vacation_open_panel": "Openen in paneel", + "enable": "Inschakelen", + "saved": "Opgeslagen", + "budget_monthly_set": "Maandelijks instellen", + "budget_yearly_set": "Jaarlijks instellen", + "budget_advanced": "Valuta, waarschuwingen…", + "budget_open_panel": "Openen in paneel", + "groups_empty": "Nog geen groepen.", + "group_new_placeholder": "Groep toevoegen…", + "group_delete_confirm": "Groep \"{name}\" verwijderen?", + "groups_manage_tasks": "Taaktoewijzingen beheren…", + "groups_open_panel": "Openen in paneel", + "on_complete_action_target_hint": "Let op: het domein van de entiteit moet bij de service passen — bijv. 'button.press' werkt alleen op button.*, 'counter.increment' alleen op counter.*, 'input_button.press' alleen op input_button.* enz. Bij een mismatch mislukt de actie stil (HA logt 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Alle objecten tonen", + "show_all_tasks": "Filter wissen — alle taken tonen", + "filter_to_overdue": "Takenlijst filteren op alleen achterstallig", + "filter_to_due_soon": "Takenlijst filteren op alleen binnenkort verschuldigd", + "filter_to_triggered": "Takenlijst filteren op alleen geactiveerd", + "open_task": "Taak openen", + "show_details": "Geschiedenis + statistieken tonen", + "hide_details": "Details verbergen", + "history_empty": "Nog geen geschiedenis.", + "history_edit_button": "Item bewerken", + "total_cost": "Totale kosten", + "times_performed": "Uitgevoerd", + "older_entries": "ouder", + "open_in_panel": "Openen in Onderhoudspaneel", + "skip_reason": "Reden voor overslaan (optioneel)", + "reset_to_date": "Laatst uitgevoerd terugzetten op", + "delete_task_confirm": "Deze taak en de geschiedenis verwijderen?", + "delete_object_confirm": "Dit object en alle taken verwijderen?", + "loading": "Laden…", + "archive": "Archiveren", + "undo": "Ongedaan maken", + "task_archived": "Taak gearchiveerd", + "object_archived": "Object gearchiveerd", + "unarchive": "Dearchiveren", + "archived": "Gearchiveerd", + "show_archived": "Gearchiveerde tonen", + "hide_archived": "Gearchiveerde verbergen", + "confirm_archive_object": "Dit object en zijn taken archiveren? De geschiedenis blijft behouden en kan later worden hersteld.", + "settings_archive": "Archief & bewaring", + "settings_archive_desc": "Voltooide eenmalige taken archiveren in plaats van verwijderen. Gearchiveerde items zijn verborgen en inactief, maar behouden geschiedenis en kosten.", + "settings_archive_oneoff_days": "Voltooide eenmalige taken automatisch archiveren na (dagen, 0 = uit)", + "settings_delete_archived_oneoff_days": "Gearchiveerde eenmalige taken automatisch verwijderen na (dagen, 0 = nooit)", + "archive_object": "Object archiveren", + "unarchive_object": "Object dearchiveren", + "documents": "Documenten", + "documents_empty": "Nog geen documenten.", + "doc_upload": "Bestand uploaden", + "doc_uploading": "Uploaden…", + "doc_add_link": "Link toevoegen", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titel (optioneel)", + "doc_open": "Openen", + "doc_delete_confirm": "\"{name}\" verwijderen?", + "doc_too_large": "Bestand is te groot (max. 25 MB).", + "doc_upload_failed": "Uploaden mislukt.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Al elders opgeslagen — gedeeld, geen extra ruimte.", + "doc_dup_in_object": "Dit bestand is al aan dit object gekoppeld.", + "doc_link_invalid": "Alleen http/https-links zijn toegestaan.", + "doc_cat_manual": "Handleiding", + "doc_cat_warranty": "Garantie", + "doc_cat_invoice": "Factuur", + "doc_cat_spare_parts": "Onderdelen", + "doc_cat_photo": "Foto", + "doc_cat_other": "Overig", + "doc_link_badge": "Link", + "doc_storage_title": "Documentopslag", + "doc_storage_saved": "Bespaard door deduplicatie", + "doc_storage_refresh": "Vernieuwen", + "doc_download": "Downloaden", + "doc_close": "Sluiten", + "doc_camera": "Foto maken", + "doc_drop_hint": "Sleep bestanden hierheen", + "doc_task_none": "Geen documenten aan deze taak gekoppeld.", + "doc_link_existing": "Document koppelen…", + "doc_attach": "Koppelen", + "doc_unlink": "Ontkoppelen", + "doc_page": "Pagina", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1j", + "chart_since_service": "sinds laatste onderhoud", + "chart_no_stats": "Geen langetermijnstatistieken voor deze entiteit — alleen waarden uit onderhoudsregels worden getoond", + "auto_complete_on_recovery": "Automatisch voltooien wanneer de sensor herstelt", + "auto_complete_on_recovery_help": "Registreert een voltooiing (stelt laatst uitgevoerd in) wanneer de trigger zichzelf oplost — bijv. zout bijgevuld, filter vervangen.", + "doc_search": "Documenten zoeken…", + "doc_search_none": "Geen overeenkomende documenten", + "link_device_optional": "Koppelen aan bestaand apparaat (optioneel)", + "parent_object_optional": "Bovenliggend object (optioneel)", + "parent_none": "(Geen bovenliggend object)", + "paused": "Gepauzeerd", + "pause_object": "Pauzeren", + "resume_object": "Hervatten", + "pause_until_prompt": "Bevries de schema's van dit object — niets wordt verwacht en niets meldt tot het wordt hervat. Stel optioneel een datum voor automatisch hervatten in.", + "pause_until_label": "Hervatten op (optioneel)", + "object_paused": "Object gepauzeerd", + "object_resumed": "Object hervat — schema's herstart", + "object_paused_badge": "Gepauzeerd", + "paused_until_label": "tot", + "replace_object": "Vervangen…", + "replace_object_prompt": "Dit object uitfaseren en een opvolger aanmaken. Geschiedenis en kosten blijven gearchiveerd bij het oude; taken en documenten gaan mee naar het nieuwe, tellers beginnen opnieuw.", + "replace_name_label": "Naam van de opvolger", + "object_replaced": "Object vervangen — opvolger aangemaakt", + "reading_unit_label": "Meeteenheid (bijv. kWh, m³)", + "reading_unit_help": "Wordt naast de vastgelegde waarde getoond bij het afronden van deze taak.", + "reading_value_label": "Meterstand", + "reading_label": "Meting", + "settings_templates_label": "Sjabloongalerij", + "settings_templates_hint": "Vink sjablonen uit die je nooit nodig hebt — ze verdwijnen uit de \"Vanuit sjabloon\"-keuzes (paneel en config flow). Verder verandert er niets; altijd weer in te schakelen.", + "worksheet": "Werkblad", + "worksheet_scan_view": "Scan om de taak te openen", + "worksheet_scan_complete": "Scan om af te ronden", + "worksheet_manual_excerpt": "Handleidingfragment", + "worksheet_pages": "pagina's", + "worksheet_printed": "Afgedrukt", + "worksheet_never": "Nooit" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/pl.json b/custom_components/maintenance_supporter/frontend-src/locales/pl.json new file mode 100644 index 0000000..e95f4e5 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/pl.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Konserwacja", + "objects": "Obiekty", + "tasks": "Zadania", + "overdue": "Zaległe", + "due_soon": "Wkrótce", + "triggered": "Wyzwolone", + "ok": "OK", + "all": "Wszystkie", + "new_object": "+ Nowy obiekt", + "templates_from": "Z szablonu", + "templates_title": "Zacznij od szablonu", + "templates_task_count": "{n} zadań", + "template_created": "Utworzono z szablonu", + "onboard_hint": "Dodaj pierwszy obiekt, aby śledzić konserwację.", + "edit": "Edytuj", + "duplicate": "Duplikuj", + "task_duplicated": "Zadanie zduplikowane", + "object_duplicated": "Obiekt zduplikowany", + "delete": "Usuń", + "add_task": "+ Dodaj zadanie", + "complete": "Wykonaj", + "completed": "Wykonano", + "skip": "Pomiń", + "skipped": "Pominięte", + "missed": "Missed", + "reset": "Resetuj", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Anuluj", + "bulk_select": "Zaznacz", + "bulk_select_all": "Zaznacz wszystko", + "bulk_n_selected": "Zaznaczono {n}", + "bulk_completed": "Ukończono {n} zadań", + "bulk_archived": "Zarchiwizowano {n} zadań", + "completing": "Wykonywanie…", + "interval": "Interwał", + "warning": "Ostrzeżenie", + "last_performed": "Ostatnio wykonane", + "next_due": "Następny termin", + "days_until_due": "Dni do terminu", + "avg_duration": "Śr. czas trwania", + "trigger": "Wyzwalacz", + "trigger_type": "Typ wyzwalacza", + "threshold_above": "Górny limit", + "threshold_below": "Dolny limit", + "threshold": "Próg", + "counter": "Licznik", + "state_change": "Zmiana stanu", + "runtime": "Czas pracy", + "runtime_hours": "Docelowy czas pracy (godziny)", + "target_value": "Wartość docelowa", + "baseline": "Wartość bazowa", + "target_changes": "Docelowa liczba zmian", + "for_minutes": "Przez (minuty)", + "time_based": "Czasowy", + "sensor_based": "Oparty na czujniku", + "manual": "Ręczny", + "one_time": "Jednorazowo", + "weekdays": "Dni tygodnia", + "nth_weekday": "N-ty dzień tygodnia w miesiącu", + "day_of_month": "Dzień miesiąca", + "recurrence_on_days": "Powtarzaj w", + "recurrence_occurrence": "Wystąpienie", + "recurrence_weekday": "Dzień tygodnia", + "recurrence_day": "Dzień miesiąca (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Ostatni", + "day_word": "Dzień", + "interval_value": "Interwał", + "interval_unit": "Jednostka", + "unit_days": "Dni", + "unit_weeks": "Tygodnie", + "unit_months": "Miesiące", + "unit_years": "Lata", + "due_date": "Termin", + "cleaning": "Czyszczenie", + "inspection": "Inspekcja", + "replacement": "Wymiana", + "calibration": "Kalibracja", + "service": "Serwis", + "reading": "Odczyt", + "custom": "Niestandardowy", + "history": "Historia", + "cost": "Koszt", + "report_button": "Raport", + "report_title": "Raport konserwacji", + "report_generated": "Wygenerowano", + "report_times_done": "Wykonano", + "report_total_cost": "Koszt całkowity", + "report_every": "co {n} {unit}", + "report_notes": "Notatki", + "report_col_type": "Typ", + "report_col_status": "Status", + "report_col_schedule": "Harmonogram", + "duration": "Czas trwania", + "both": "Oba", + "trigger_val": "Wartość wyzwalacza", + "complete_title": "Wykonaj: ", + "checklist": "Lista kontrolna", + "checklist_steps_optional": "Kroki listy kontrolnej (opcjonalne)", + "checklist_placeholder": "Wyczyść filtr\nWymień uszczelkę\nSprawdź ciśnienie", + "checklist_help": "Jeden krok na linię. Maks. 100 elementów.", + "err_too_long": "{field}: za długie (maks. {n} znaków)", + "err_too_short": "{field}: za krótkie (min. {n} znaków)", + "err_value_too_high": "{field}: za duże (maks. {n})", + "err_value_too_low": "{field}: za małe (min. {n})", + "err_required": "{field}: wymagane", + "err_wrong_type": "{field}: zły typ (oczekiwano: {type})", + "err_invalid_choice": "{field}: niedozwolona wartość", + "err_invalid_value": "{field}: nieprawidłowa wartość", + "feat_schedule_time": "Harmonogram według pory dnia", + "feat_schedule_time_desc": "Zadania stają się zaległe o określonej porze dnia zamiast o północy.", + "schedule_time_optional": "Termin o godzinie (opcjonalne, HH:MM)", + "schedule_time_help": "Puste = północ (domyślnie). Strefa czasowa HA.", + "at_time": "o", + "notes_optional": "Notatki (opcjonalne)", + "cost_optional": "Koszt (opcjonalne)", + "duration_minutes": "Czas trwania w minutach (opcjonalne)", + "days": "dni", + "day": "dzień", + "today": "Dzisiaj", + "d_overdue": "d zaległe", + "no_tasks": "Jeszcze brak zadań konserwacyjnych. Utwórz obiekt, aby zacząć.", + "no_tasks_short": "Brak zadań", + "no_history": "Jeszcze brak wpisów historii.", + "show_all": "Pokaż wszystko", + "cost_duration_chart": "Koszt i czas trwania", + "installed": "Zainstalowane", + "confirm_delete_object": "Usunąć ten obiekt i wszystkie jego zadania?", + "confirm_delete_task": "Usunąć to zadanie?", + "min": "Min", + "max": "Maks", + "save": "Zapisz", + "saving": "Zapisywanie…", + "edit_task": "Edytuj zadanie", + "new_task": "Nowe zadanie konserwacyjne", + "task_name": "Nazwa zadania", + "maintenance_type": "Typ konserwacji", + "priority": "Priorytet", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Niski", + "priority_normal": "Normalny", + "priority_high": "Wysoki", + "schedule_type": "Typ harmonogramu", + "interval_days": "Interwał (dni)", + "warning_days": "Dni ostrzeżenia", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Ostatnio wykonane (opcjonalne)", + "interval_anchor": "Punkt zaczepienia interwału", + "anchor_completion": "Od daty wykonania", + "anchor_planned": "Od daty planowanej (bez przesunięć)", + "edit_object": "Edytuj obiekt", + "name": "Nazwa", + "manufacturer_optional": "Producent (opcjonalne)", + "model_optional": "Model (opcjonalne)", + "serial_number_optional": "Numer seryjny (opcjonalne)", + "serial_number_label": "S/N", + "documentation_url_label": "Instrukcja", + "object_notes_label": "Notatki", + "sort_due_date": "Termin", + "sort_object": "Nazwa obiektu", + "sort_type": "Typ", + "sort_task_name": "Nazwa zadania", + "all_objects": "Wszystkie obiekty", + "tasks_lower": "zadań", + "no_tasks_yet": "Jeszcze brak zadań", + "add_first_task": "Dodaj pierwsze zadanie", + "trigger_configuration": "Konfiguracja wyzwalacza", + "entity_id": "ID encji", + "comma_separated": "oddzielone przecinkami", + "entity_logic": "Logika encji", + "entity_logic_any": "Wyzwala dowolna encja", + "entity_logic_all": "Wszystkie encje muszą wyzwolić", + "entities": "encje", + "attribute_optional": "Atrybut (opcjonalny, puste = stan)", + "use_entity_state": "Użyj stanu encji (bez atrybutu)", + "trigger_above": "Wyzwól powyżej", + "trigger_below": "Wyzwól poniżej", + "for_at_least_minutes": "Przez co najmniej (minuty)", + "safety_interval_days": "Interwał bezpieczeństwa (dni, opcjonalny)", + "safety_interval": "Interwał bezpieczeństwa (opcjonalny)", + "delta_mode": "Tryb delta", + "from_state_optional": "Ze stanu (opcjonalne)", + "to_state_optional": "Do stanu (opcjonalne)", + "documentation_url_optional": "URL dokumentacji (opcjonalne)", + "object_notes_optional": "Notatki (opcjonalne)", + "nfc_tag_id_optional": "ID tagu NFC (opcjonalne)", + "nfc_tags_empty_help": "W Home Assistant nie zarejestrowano jeszcze tagów NFC.", + "nfc_tags_open_settings": "Otwórz ustawienia tagów", + "nfc_tags_refresh": "Odśwież", + "environmental_entity_optional": "Czujnik środowiskowy (opcjonalne)", + "environmental_entity_helper": "np. sensor.outdoor_temperature — dostosowuje interwał na podstawie warunków środowiskowych", + "environmental_attribute_optional": "Atrybut środowiskowy (opcjonalne)", + "nfc_tag_id": "ID tagu NFC", + "nfc_linked": "Tag NFC powiązany", + "nfc_link_hint": "Kliknij, aby powiązać tag NFC", + "responsible_user": "Odpowiedzialny użytkownik", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Brak przypisanego użytkownika)", + "all_users": "Wszyscy użytkownicy", + "my_tasks": "Moje zadania", + "tab_calendar": "Kalendarz", + "cal_no_events": "Brak konserwacji", + "cal_window_7": "7 dni", + "cal_window_14": "14 dni", + "cal_window_30": "30 dni", + "cal_window_365": "1 rok", + "cal_every_n_days": "co {n} dni", + "cal_source_time": "Czas", + "cal_source_time_adaptive": "Czas (adaptacyjny)", + "cal_source_sensor": "Czujnik", + "cal_predicted": "przewidywane", + "cal_confidence_high": "wysoka pewność", + "cal_confidence_medium": "średnia pewność", + "cal_confidence_low": "niska pewność", + "budget_monthly": "Budżet miesięczny", + "budget_yearly": "Budżet roczny", + "groups": "Grupy", + "new_group": "Nowa grupa", + "edit_group": "Edytuj grupę", + "no_groups": "Jeszcze brak grup", + "delete_group": "Usuń grupę", + "delete_group_confirm": "Usunąć grupę '{name}'?", + "group_select_tasks": "Wybierz zadania", + "group_name_required": "Nazwa jest wymagana", + "description_optional": "Opis (opcjonalny)", + "selected": "Wybrane", + "loading_chart": "Ładowanie danych wykresu...", + "hide_outliers": "Ukryj wartości odstające (błędy czujnika)", + "was_maintenance_needed": "Czy ta konserwacja była potrzebna?", + "feedback_needed": "Potrzebna", + "feedback_not_needed": "Niepotrzebna", + "feedback_not_sure": "Nie jestem pewien", + "suggested_interval": "Sugerowany interwał", + "apply_suggestion": "Zastosuj", + "reanalyze": "Analizuj ponownie", + "reanalyze_result": "Nowa analiza", + "reanalyze_insufficient_data": "Za mało danych do wygenerowania rekomendacji", + "data_points": "punkty danych", + "dismiss_suggestion": "Odrzuć", + "confidence_low": "Niska", + "confidence_medium": "Średnia", + "confidence_high": "Wysoka", + "recommended": "rekomendowane", + "seasonal_awareness": "Świadomość sezonowa", + "edit_seasonal_overrides": "Edytuj czynniki sezonowe", + "seasonal_overrides_title": "Czynniki sezonowe (nadpisanie)", + "seasonal_overrides_hint": "Czynnik na miesiąc (0.1–5.0). Puste = uczone automatycznie.", + "seasonal_override_invalid": "Nieprawidłowa wartość", + "seasonal_override_range": "Czynnik musi być między 0.1 a 5.0", + "clear_all": "Wyczyść wszystko", + "seasonal_chart_title": "Czynniki sezonowe", + "seasonal_learned": "Wyuczone", + "seasonal_manual": "Ręczne", + "month_jan": "Sty", + "month_feb": "Lut", + "month_mar": "Mar", + "month_apr": "Kwi", + "month_may": "Maj", + "month_jun": "Cze", + "month_jul": "Lip", + "month_aug": "Sie", + "month_sep": "Wrz", + "month_oct": "Paź", + "month_nov": "Lis", + "month_dec": "Gru", + "sensor_prediction": "Predykcja czujnika", + "degradation_trend": "Trend", + "trend_rising": "Rosnący", + "trend_falling": "Malejący", + "trend_stable": "Stabilny", + "trend_insufficient_data": "Niewystarczające dane", + "days_until_threshold": "Dni do progu", + "threshold_exceeded": "Próg przekroczony", + "environmental_adjustment": "Czynnik środowiskowy", + "sensor_prediction_urgency": "Czujnik przewiduje próg za ~{days} dni", + "day_short": "d", + "weibull_reliability_curve": "Krzywa niezawodności", + "weibull_failure_probability": "Prawdopodobieństwo awarii", + "weibull_r_squared": "Dopasowanie R²", + "beta_early_failures": "Wczesne awarie", + "beta_random_failures": "Losowe awarie", + "beta_wear_out": "Zużycie", + "beta_highly_predictable": "Wysoce przewidywalne", + "confidence_interval": "Przedział ufności", + "confidence_conservative": "Konserwatywny", + "confidence_aggressive": "Optymistyczny", + "current_interval_marker": "Bieżący interwał", + "recommended_marker": "Rekomendowany", + "characteristic_life": "Charakterystyczna żywotność", + "chart_mini_sparkline": "Wykres trendu", + "chart_history": "Historia kosztów i czasu trwania", + "chart_seasonal": "Czynniki sezonowe, 12 miesięcy", + "chart_weibull": "Krzywa niezawodności Weibulla", + "chart_sparkline": "Wykres wartości wyzwalacza czujnika", + "days_progress": "Postęp dni", + "qr_code": "Kod QR", + "qr_generating": "Generowanie kodu QR…", + "qr_error": "Nie udało się wygenerować kodu QR.", + "qr_error_no_url": "Brak skonfigurowanego URL HA. Ustaw zewnętrzny lub wewnętrzny URL w Ustawienia → System → Sieć.", + "save_error": "Nie udało się zapisać. Spróbuj ponownie.", + "qr_print": "Drukuj", + "qr_download": "Pobierz SVG", + "qr_action": "Akcja przy skanowaniu", + "qr_action_view": "Wyświetl informacje o konserwacji", + "qr_action_complete": "Oznacz konserwację jako wykonaną", + "qr_url_mode": "Typ linku", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokalny (mDNS)", + "qr_mode_server": "URL serwera", + "overview": "Przegląd", + "analysis": "Analiza", + "recent_activities": "Ostatnie aktywności", + "search_notes": "Szukaj w notatkach", + "avg_cost": "Śr. koszt", + "no_advanced_features": "Brak włączonych funkcji zaawansowanych", + "no_advanced_features_hint": "Włącz „Adaptacyjne interwały” lub „Wzorce sezonowe” w ustawieniach integracji, aby zobaczyć tutaj dane analityczne.", + "analysis_not_enough_data": "Jeszcze za mało danych do analizy.", + "analysis_not_enough_data_hint": "Analiza Weibulla wymaga co najmniej 5 wykonanych konserwacji; wzorce sezonowe stają się widoczne po 6+ punktach danych na miesiąc.", + "analysis_manual_task_hint": "Zadania ręczne bez interwału nie generują danych analitycznych.", + "completions": "wykonania", + "current": "Bieżące", + "shorter": "Krótsze", + "longer": "Dłuższe", + "normal": "Normalne", + "disabled": "Wyłączone", + "compound_logic": "Logika złożona", + "compound": "Złożony (wiele warunków)", + "compound_logic_and": "I — wszystkie warunki muszą zadziałać", + "compound_logic_or": "LUB — wystarczy dowolny warunek", + "compound_help": "Połącz kilka warunków czujników w jeden wyzwalacz.", + "compound_no_conditions": "Brak warunków — dodaj co najmniej jeden.", + "compound_add_condition": "Dodaj warunek", + "compound_condition": "Warunek", + "compound_remove_condition": "Usuń warunek", + "card_title": "Tytuł", + "card_show_header": "Pokaż nagłówek ze statystykami", + "card_show_actions": "Pokaż przyciski akcji", + "card_compact": "Tryb kompaktowy", + "card_max_items": "Maks. elementów (0 = wszystkie)", + "card_filter_status": "Filtruj wg statusu", + "card_filter_status_help": "Puste = pokaż wszystkie statusy.", + "card_filter_objects": "Filtruj wg obiektów", + "card_filter_objects_help": "Puste = pokaż wszystkie obiekty.", + "card_filter_entities": "Filtruj wg encji (entity_ids)", + "card_filter_entities_help": "Wybierz encje sensor / binary_sensor z tej integracji. Puste = wszystkie.", + "card_loading_objects": "Ładowanie obiektów…", + "card_load_error": "Nie udało się załadować obiektów — sprawdź połączenie WebSocket.", + "card_no_tasks_title": "Brak zadań konserwacyjnych", + "card_no_tasks_cta": "→ Utwórz w panelu Maintenance", + "no_objects": "Brak obiektów.", + "action_error": "Akcja nie powiodła się. Spróbuj ponownie.", + "area_id_optional": "Obszar (opcjonalny)", + "installation_date_optional": "Data instalacji (opcjonalna)", + "warranty_expiry_optional": "Wygaśnięcie gwarancji (opcjonalnie)", + "warranty": "Gwarancja", + "warranty_valid_until": "ważna do {date}", + "warranty_expires_in": "wygasa za {days} dni", + "warranty_expired": "wygasła", + "cal_past_windows": "Minione okna", + "cal_forward_windows": "Przyszłe okna", + "history_edit_title": "Edytuj wpis historii", + "history_edit_timestamp": "Znacznik czasu", + "manufacturer": "Producent", + "model": "Model", + "area": "Obszar", + "actions": "Akcje", + "view_mode_label": "Widok", + "view_cards": "Widok kart", + "view_table": "Widok tabeli", + "objects_table_columns_label": "Kolumny tabeli obiektów", + "objects_table_columns_hint": "Wybierz, które kolumny mają być widoczne w widoku tabeli obiektów.", + "custom_icon_optional": "Ikona (opcjonalna, np. mdi:wrench)", + "task_enabled": "Zadanie włączone", + "skip_reason_prompt": "Pominąć to zadanie?", + "reason_optional": "Powód (opcjonalny)", + "reset_date_prompt": "Oznaczyć zadanie jako wykonane?", + "reset_date_optional": "Data ostatniego wykonania (opcjonalna, domyślnie dzisiaj)", + "notes_label": "Notatki", + "documentation_label": "Dokumentacja", + "no_nfc_tag": "— Brak tagu —", + "dashboard": "Pulpit", + "tab_today": "Dziś", + "palette_placeholder": "Szukaj obiektów i zadań…", + "palette_no_results": "Brak wyników", + "palette_hint": "↑↓ nawigacja · Enter otwórz · Esc zamknij", + "today_all_caught_up": "Wszystko zrobione! Nic w tym tygodniu.", + "today_overdue": "Zaległe", + "today_due_today": "Na dziś", + "today_this_week": "W tym tygodniu", + "settings": "Ustawienia", + "settings_features": "Funkcje zaawansowane", + "settings_features_desc": "Włącz lub wyłącz funkcje zaawansowane. Wyłączenie ukrywa je z UI, ale nie usuwa danych.", + "feat_adaptive": "Harmonogram adaptacyjny", + "feat_adaptive_desc": "Ucz się optymalnych interwałów z historii konserwacji", + "feat_predictions": "Predykcje czujników", + "feat_predictions_desc": "Przewiduj daty wyzwolenia z degradacji czujnika", + "feat_seasonal": "Korekty sezonowe", + "feat_seasonal_desc": "Dostosuj interwały do wzorców sezonowych", + "feat_environmental": "Korelacja środowiskowa", + "feat_environmental_desc": "Koreluj interwały z temperaturą/wilgotnością", + "feat_budget": "Śledzenie budżetu", + "feat_budget_desc": "Śledź miesięczne i roczne wydatki na konserwację", + "feat_groups": "Grupy zadań", + "feat_groups_desc": "Organizuj zadania w grupy logiczne", + "feat_checklists": "Listy kontrolne", + "feat_checklists_desc": "Wieloetapowe procedury wykonania zadania", + "settings_general": "Ogólne", + "settings_default_warning": "Domyślne dni ostrzeżenia", + "settings_panel_enabled": "Panel boczny", + "settings_panel_title": "Tytuł panelu bocznego", + "settings_notifications": "Powiadomienia", + "settings_notify_service": "Usługa powiadomień", + "test_notification": "Powiadomienie testowe", + "send_test": "Wyślij test", + "testing": "Wysyłanie…", + "test_notification_success": "Powiadomienie testowe wysłane", + "test_notification_failed": "Powiadomienie testowe nie powiodło się", + "settings_notify_due_soon": "Powiadom gdy wkrótce", + "settings_notify_overdue": "Powiadom gdy zaległe", + "settings_notify_triggered": "Powiadom gdy wyzwolone", + "settings_interval_hours": "Interwał powtarzania (godziny, 0 = raz)", + "settings_quiet_hours": "Godziny ciszy", + "settings_quiet_start": "Początek", + "settings_quiet_end": "Koniec", + "settings_max_per_day": "Maks. powiadomień dziennie (0 = bez limitu)", + "settings_bundling": "Grupowanie powiadomień", + "settings_bundle_threshold": "Próg grupowania", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobilne przyciski akcji", + "settings_action_complete": "Pokaż przycisk 'Wykonaj'", + "settings_action_skip": "Pokaż przycisk 'Pomiń'", + "settings_action_snooze": "Pokaż przycisk 'Drzemka'", + "settings_weekly_digest": "Cotygodniowe podsumowanie", + "settings_weekly_digest_hint": "Jedno powiadomienie podsumowujące w poniedziałek rano, gdy są zadania.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Czas drzemki (godziny)", + "settings_budget": "Budżet", + "settings_currency": "Waluta", + "settings_budget_monthly": "Budżet miesięczny", + "settings_budget_yearly": "Budżet roczny", + "settings_budget_alerts": "Alerty budżetowe", + "settings_budget_threshold": "Próg alertu (%)", + "settings_import_export": "Import / Eksport", + "settings_export_json": "Eksportuj JSON", + "settings_export_yaml": "Eksportuj YAML", + "settings_export_csv": "Eksportuj CSV", + "settings_import_csv": "Importuj CSV", + "settings_import_placeholder": "Wklej tutaj zawartość JSON lub CSV…", + "settings_import_btn": "Importuj", + "settings_import_success": "{count} obiektów zaimportowanych pomyślnie.", + "settings_export_success": "Eksport pobrany.", + "settings_saved": "Ustawienie zapisane.", + "settings_include_history": "Dołącz historię", + "sort_alphabetical": "Alfabetycznie", + "sort_due_soonest": "Najbliższy termin", + "sort_task_count": "Liczba zadań", + "sort_area": "Obszar", + "sort_assigned_user": "Przypisany użytkownik", + "sort_group": "Grupa", + "groupby_none": "Bez grupowania", + "groupby_area": "Wg obszaru", + "groupby_group": "Wg grupy", + "groupby_user": "Wg użytkownika", + "filter_label": "Filtr", + "user_label": "Użytkownik", + "sort_label": "Sortowanie", + "group_by_label": "Grupuj wg", + "state_value_help": "Użyj wartości stanu HA (zwykle małymi literami, np. \"on\"/\"off\"). Wielkość liter jest normalizowana przy zapisie.", + "target_changes_help": "Liczba pasujących przejść przed wyzwoleniem (domyślnie: 1).", + "qr_print_title": "Drukuj kody QR", + "qr_print_desc": "Wygeneruj stronę do druku z kodami QR do wycięcia i naklejenia na sprzęcie.", + "qr_print_load": "Załaduj obiekty", + "qr_print_filter": "Filtr", + "qr_print_objects": "Obiekty", + "qr_print_actions": "Akcje", + "qr_print_url_mode": "Typ linku", + "qr_print_estimate": "Szacowane kody QR", + "qr_print_over_limit": "limit 200, zawęź filtr", + "qr_print_generate": "Generuj kody QR", + "qr_print_generating": "Generowanie…", + "qr_print_ready": "Kody QR gotowe", + "qr_print_print_button": "Drukuj", + "qr_print_empty": "Nic do wygenerowania", + "qr_action_skip": "Pomiń", + "vacation_title": "Tryb urlopowy", + "vacation_active": "aktywny", + "vacation_ended": "zakończony", + "vacation_desc": "Zaplanuj urlop: powiadomienia są wstrzymane podczas okresu plus dni bufora. Możesz dodać wyjątki dla wybranych zadań.", + "vacation_enable": "Włącz tryb urlopowy", + "vacation_start": "Początek", + "vacation_end": "Koniec", + "vacation_buffer": "Bufor (dni)", + "vacation_exempt_title": "Powiadamiaj mimo urlopu", + "vacation_exempt_desc": "Wybierz zadania, które mają powiadamiać także w czasie urlopu (np. krytyczna chemia basenu).", + "vacation_load_tasks": "Wczytaj zadania", + "vacation_preview_btn": "Pokaż podgląd", + "vacation_preview_affected": "zadań dotyczy", + "vacation_event_due_soon": "wkrótce będzie wymagane", + "vacation_event_overdue": "stanie się zaległe", + "vacation_event_triggered_est": "możliwe wyzwolenie czujnika", + "vacation_sensor_based": "(czujnikowe)", + "vacation_action_notify": "Powiadamiaj mimo to", + "vacation_action_unsilence": "Ponownie wycisz", + "vacation_marked_complete": "Oznaczono jako wykonane", + "vacation_marked_skip": "Pominięto", + "vacation_end_now": "Zakończ urlop teraz", + "unassigned": "Nieprzypisane", + "no_area": "Brak obszaru", + "has_overdue": "Ma zaległe zadania", + "object": "Obiekt", + "settings_panel_access": "Dostęp do panelu", + "settings_panel_access_desc": "Administratorzy zawsze mają pełny dostęp. Aby delegować tworzenie, edytowanie i usuwanie wybranym użytkownikom bez uprawnień administratora, włącz tę opcję i zaznacz ich poniżej — pozostali widzą tylko Wykonaj i Pomiń.", + "settings_operator_write": "Zezwól wybranym użytkownikom na tworzenie, edycję i usuwanie", + "settings_operator_write_desc": "Wyłączone: tylko administratorzy mogą zmieniać treść. Włączone: wybrani użytkownicy poniżej również mają pełny dostęp.", + "no_non_admin_users": "Nie znaleziono użytkowników nie-admin. Dodaj ich w Ustawienia → Osoby.", + "owner_label": "Właściciel", + "feat_completion_actions": "Akcje po zakończeniu", + "feat_completion_actions_desc": "Akcja HA per zadanie po zakończeniu + QR szybkiego zakończenia z predefiniowanymi wartościami.", + "on_complete_action_title": "Po zakończeniu: wywołaj akcję HA (opcjonalnie)", + "on_complete_action_desc": "Wywołuje usługę HA po zakończeniu zadania — np. zresetuj licznik urządzenia.", + "on_complete_action_service": "Usługa", + "on_complete_action_target": "Encja docelowa", + "on_complete_action_data": "Dane (JSON, opcjonalnie)", + "on_complete_action_test": "Testuj akcję", + "on_complete_action_test_success": "Sukces", + "on_complete_action_test_failed": "Błąd", + "quick_complete_defaults_title": "Wartości domyślne szybkiego zakończenia (dla skanów QR, opcjonalnie)", + "quick_complete_defaults_desc": "Predefiniowane wartości dla QR szybkiego zakończenia. Bez nich QR otwiera okno dialogowe.", + "quick_complete_defaults_notes": "Notatki", + "quick_complete_defaults_cost": "Koszt", + "quick_complete_defaults_duration": "Czas trwania (minuty)", + "quick_complete_defaults_feedback_none": "Brak opinii", + "quick_complete_defaults_feedback_needed": "Było potrzebne", + "quick_complete_defaults_feedback_not_needed": "Nie było potrzebne", + "quick_complete_success": "Szybko oznaczono jako wykonane", + "trigger_replaced": "Wyzwalacz zastąpiony", + "add": "Dodaj", + "show_stats": "Pokaż statystyki + wykresy", + "hide_stats": "Ukryj statystyki", + "adaptive_no_data": "Za mało historii ukończeń do analizy adaptacyjnej. Wykonaj to zadanie jeszcze kilka razy, aby odblokować propozycje interwału i wykresy niezawodności.", + "suggestion_applied": "Zastosowano sugerowany interwał", + "vacation_mode": "Tryb urlopowy", + "vacation_status_active": "Aktywny teraz", + "vacation_status_scheduled": "Zaplanowany", + "vacation_status_inactive": "Nieaktywny", + "vacation_end_now_confirm": "Zakończyć urlop natychmiast?", + "vacation_exempt_count": "wykluczone", + "vacation_advanced": "Zaawansowane…", + "vacation_open_panel": "Otwórz w panelu", + "enable": "Włącz", + "saved": "Zapisano", + "budget_monthly_set": "Ustaw miesięczny", + "budget_yearly_set": "Ustaw roczny", + "budget_advanced": "Waluta, alerty…", + "budget_open_panel": "Otwórz w panelu", + "groups_empty": "Brak grup.", + "group_new_placeholder": "Dodaj grupę…", + "group_delete_confirm": "Usunąć grupę „{name}”?", + "groups_manage_tasks": "Zarządzaj przypisaniami zadań…", + "groups_open_panel": "Otwórz w panelu", + "on_complete_action_target_hint": "Uwaga: domena encji musi pasować do usługi — np. 'button.press' działa tylko na button.*, 'counter.increment' tylko na counter.*, 'input_button.press' tylko na input_button.* itd. W przypadku niezgodności akcja po cichu się nie wykona (HA zapisze w dzienniku 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Pokaż wszystkie obiekty", + "show_all_tasks": "Wyczyść filtr — pokaż wszystkie zadania", + "filter_to_overdue": "Filtruj listę tylko do zaległych", + "filter_to_due_soon": "Filtruj listę tylko do zbliżających się terminów", + "filter_to_triggered": "Filtruj listę tylko do wyzwolonych", + "open_task": "Otwórz zadanie", + "show_details": "Pokaż historię + statystyki", + "hide_details": "Ukryj szczegóły", + "history_empty": "Brak historii.", + "history_edit_button": "Edytuj wpis", + "total_cost": "Koszt całkowity", + "times_performed": "Wykonano", + "older_entries": "starsze", + "open_in_panel": "Otwórz w panelu Konserwacja", + "skip_reason": "Powód pominięcia (opcjonalnie)", + "reset_to_date": "Zresetuj ostatnie wykonanie na", + "delete_task_confirm": "Usunąć to zadanie i jego historię?", + "delete_object_confirm": "Usunąć ten obiekt i wszystkie jego zadania?", + "loading": "Ładowanie…", + "archive": "Archiwizuj", + "undo": "Cofnij", + "task_archived": "Zadanie zarchiwizowane", + "object_archived": "Obiekt zarchiwizowany", + "unarchive": "Przywróć z archiwum", + "archived": "Zarchiwizowane", + "show_archived": "Pokaż zarchiwizowane", + "hide_archived": "Ukryj zarchiwizowane", + "confirm_archive_object": "Zarchiwizować ten obiekt i jego zadania? Historia zostanie zachowana i można ją później przywrócić.", + "settings_archive": "Archiwum i przechowywanie", + "settings_archive_desc": "Odkładaj ukończone zadania jednorazowe bez usuwania. Zarchiwizowane elementy są ukryte i nieaktywne, ale zachowują historię i koszty.", + "settings_archive_oneoff_days": "Automatycznie archiwizuj ukończone zadania jednorazowe po (dni, 0 = wył.)", + "settings_delete_archived_oneoff_days": "Automatycznie usuwaj zarchiwizowane zadania jednorazowe po (dni, 0 = nigdy)", + "archive_object": "Archiwizuj obiekt", + "unarchive_object": "Przywróć obiekt z archiwum", + "documents": "Dokumenty", + "documents_empty": "Brak dokumentów.", + "doc_upload": "Prześlij plik", + "doc_uploading": "Przesyłanie…", + "doc_add_link": "Dodaj link", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Tytuł (opcjonalnie)", + "doc_open": "Otwórz", + "doc_delete_confirm": "Usunąć „{name}”?", + "doc_too_large": "Plik jest za duży (maks. 25 MB).", + "doc_upload_failed": "Przesyłanie nie powiodło się.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Już zapisany gdzie indziej — współdzielony, bez dodatkowego miejsca.", + "doc_dup_in_object": "Ten plik jest już dołączony do tego obiektu.", + "doc_link_invalid": "Dozwolone są tylko linki http/https.", + "doc_cat_manual": "Instrukcja", + "doc_cat_warranty": "Gwarancja", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Części zamienne", + "doc_cat_photo": "Zdjęcie", + "doc_cat_other": "Inne", + "doc_link_badge": "Link", + "doc_storage_title": "Magazyn dokumentów", + "doc_storage_saved": "Zaoszczędzone przez deduplikację", + "doc_storage_refresh": "Odśwież", + "doc_download": "Pobierz", + "doc_close": "Zamknij", + "doc_camera": "Zrób zdjęcie", + "doc_drop_hint": "Upuść pliki tutaj", + "doc_task_none": "Brak dokumentów powiązanych z tym zadaniem.", + "doc_link_existing": "Powiąż dokument…", + "doc_attach": "Powiąż", + "doc_unlink": "Odłącz", + "doc_page": "Strona", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1r", + "chart_since_service": "od ostatniej konserwacji", + "chart_no_stats": "Brak statystyk długoterminowych dla tej encji — wyświetlane są tylko wartości z wpisów konserwacji", + "auto_complete_on_recovery": "Zakończ automatycznie, gdy czujnik wróci do normy", + "auto_complete_on_recovery_help": "Rejestruje wykonanie (ustawia ostatnie wykonanie), gdy wyzwalacz sam się rozwiąże — np. uzupełniono sól, wymieniono filtr.", + "doc_search": "Szukaj dokumentów…", + "doc_search_none": "Brak pasujących dokumentów", + "link_device_optional": "Połącz z istniejącym urządzeniem (opcjonalnie)", + "parent_object_optional": "Obiekt nadrzędny (opcjonalnie)", + "parent_none": "(Brak nadrzędnego)", + "paused": "Wstrzymane", + "pause_object": "Wstrzymaj", + "resume_object": "Wznów", + "pause_until_prompt": "Zamroź harmonogramy tego obiektu — nic nie staje się wymagane i nic nie powiadamia do czasu wznowienia. Opcjonalnie ustaw datę automatycznego wznowienia.", + "pause_until_label": "Wznów dnia (opcjonalnie)", + "object_paused": "Obiekt wstrzymany", + "object_resumed": "Obiekt wznowiony — harmonogramy uruchomione od nowa", + "object_paused_badge": "Wstrzymane", + "paused_until_label": "do", + "replace_object": "Zastąp…", + "replace_object_prompt": "Wycofaj ten obiekt i utwórz następcę. Historia i koszty pozostają zarchiwizowane przy starym; zadania i dokumenty przechodzą do nowego, liczniki startują od zera.", + "replace_name_label": "Nazwa następcy", + "object_replaced": "Obiekt zastąpiony — utworzono następcę", + "reading_unit_label": "Jednostka odczytu (np. kWh, m³)", + "reading_unit_help": "Wyświetlana obok zarejestrowanej wartości przy ukończeniu tego zadania.", + "reading_value_label": "Wartość odczytu", + "reading_label": "Odczyt", + "settings_templates_label": "Galeria szablonów", + "settings_templates_hint": "Odznacz szablony, których nigdy nie użyjesz — znikną z wyboru „Z szablonu\" (panel i config flow). Nic więcej się nie zmienia; można je włączyć ponownie w każdej chwili.", + "worksheet": "Karta pracy", + "worksheet_scan_view": "Zeskanuj, aby otworzyć zadanie", + "worksheet_scan_complete": "Zeskanuj, aby ukończyć", + "worksheet_manual_excerpt": "Fragment instrukcji", + "worksheet_pages": "strony", + "worksheet_printed": "Wydrukowano", + "worksheet_never": "Nigdy" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/pt.json b/custom_components/maintenance_supporter/frontend-src/locales/pt.json new file mode 100644 index 0000000..233fdcc --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/pt.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Manutenção", + "objects": "Objetos", + "tasks": "Tarefas", + "overdue": "Atrasada", + "due_soon": "Próxima", + "triggered": "Acionada", + "ok": "OK", + "all": "Todos", + "new_object": "+ Novo objeto", + "templates_from": "De modelo", + "templates_title": "Começar a partir de um modelo", + "templates_task_count": "{n} tarefas", + "template_created": "Criado a partir de modelo", + "onboard_hint": "Adicione o seu primeiro objeto para acompanhar a manutenção.", + "edit": "Editar", + "duplicate": "Duplicar", + "task_duplicated": "Tarefa duplicada", + "object_duplicated": "Objeto duplicado", + "delete": "Eliminar", + "add_task": "+ Tarefa", + "complete": "Concluída", + "completed": "Concluída", + "skip": "Saltar", + "skipped": "Saltada", + "missed": "Missed", + "reset": "Repor", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Cancelar", + "bulk_select": "Selecionar", + "bulk_select_all": "Selecionar tudo", + "bulk_n_selected": "{n} selecionados", + "bulk_completed": "{n} tarefas concluídas", + "bulk_archived": "{n} tarefas arquivadas", + "completing": "A concluir…", + "interval": "Intervalo", + "warning": "Aviso", + "last_performed": "Última execução", + "next_due": "Próximo vencimento", + "days_until_due": "Dias até vencimento", + "avg_duration": "Ø Duração", + "trigger": "Acionador", + "trigger_type": "Tipo de acionador", + "threshold_above": "Limite superior", + "threshold_below": "Limite inferior", + "threshold": "Limiar", + "counter": "Contador", + "state_change": "Mudança de estado", + "runtime": "Tempo de funcionamento", + "runtime_hours": "Duração alvo (horas)", + "target_value": "Valor alvo", + "baseline": "Linha de base", + "target_changes": "Alterações alvo", + "for_minutes": "Durante (minutos)", + "time_based": "Temporal", + "sensor_based": "Sensor", + "manual": "Manual", + "one_time": "Única vez", + "weekdays": "Dias da semana", + "nth_weekday": "N-ésimo dia da semana do mês", + "day_of_month": "Dia do mês", + "recurrence_on_days": "Repetir em", + "recurrence_occurrence": "Ocorrência", + "recurrence_weekday": "Dia da semana", + "recurrence_day": "Dia do mês (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.º", + "ord_2": "2.º", + "ord_3": "3.º", + "ord_4": "4.º", + "ord_5": "5.º", + "ord_last": "Último", + "day_word": "Dia", + "interval_value": "Intervalo", + "interval_unit": "Unidade", + "unit_days": "Dias", + "unit_weeks": "Semanas", + "unit_months": "Meses", + "unit_years": "Anos", + "due_date": "Data de vencimento", + "cleaning": "Limpeza", + "inspection": "Inspeção", + "replacement": "Substituição", + "calibration": "Calibração", + "service": "Serviço", + "reading": "Leitura", + "custom": "Personalizado", + "history": "Histórico", + "cost": "Custo", + "report_button": "Relatório", + "report_title": "Relatório de manutenção", + "report_generated": "Gerado", + "report_times_done": "Feito", + "report_total_cost": "Custo total", + "report_every": "a cada {n} {unit}", + "report_notes": "Notas", + "report_col_type": "Tipo", + "report_col_status": "Estado", + "report_col_schedule": "Agenda", + "duration": "Duração", + "both": "Ambos", + "trigger_val": "Valor do acionador", + "complete_title": "Concluída: ", + "checklist": "Lista de verificação", + "checklist_steps_optional": "Passos da lista de verificação (opcional)", + "checklist_placeholder": "Limpar filtro\nSubstituir vedação\nTestar pressão", + "checklist_help": "Um passo por linha. Máx. 100 itens.", + "err_too_long": "{field}: demasiado longo (máx. {n} caracteres)", + "err_too_short": "{field}: demasiado curto (mín. {n} caracteres)", + "err_value_too_high": "{field}: demasiado grande (máx. {n})", + "err_value_too_low": "{field}: demasiado pequeno (mín. {n})", + "err_required": "{field}: campo obrigatório", + "err_wrong_type": "{field}: tipo incorreto (esperado: {type})", + "err_invalid_choice": "{field}: valor não permitido", + "err_invalid_value": "{field}: valor inválido", + "feat_schedule_time": "Agendamento por hora", + "feat_schedule_time_desc": "Tarefas vencem em um horário específico em vez de meia-noite.", + "schedule_time_optional": "Vence às (opcional, HH:MM)", + "schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário HA.", + "at_time": "às", + "notes_optional": "Notas (opcional)", + "cost_optional": "Custo (opcional)", + "duration_minutes": "Duração em minutos (opcional)", + "days": "dias", + "day": "dia", + "today": "Hoje", + "d_overdue": "d em atraso", + "no_tasks": "Sem tarefas de manutenção. Crie um objeto para começar.", + "no_tasks_short": "Sem tarefas", + "no_history": "Sem entradas no histórico.", + "show_all": "Mostrar tudo", + "cost_duration_chart": "Custos & Duração", + "installed": "Instalado", + "confirm_delete_object": "Eliminar este objeto e todas as suas tarefas?", + "confirm_delete_task": "Eliminar esta tarefa?", + "min": "Mín", + "max": "Máx", + "save": "Guardar", + "saving": "A guardar…", + "edit_task": "Editar tarefa", + "new_task": "Nova tarefa de manutenção", + "task_name": "Nome da tarefa", + "maintenance_type": "Tipo de manutenção", + "priority": "Prioridade", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Baixa", + "priority_normal": "Normal", + "priority_high": "Alta", + "schedule_type": "Tipo de agendamento", + "interval_days": "Intervalo (dias)", + "warning_days": "Dias de aviso", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Última execução (opcional)", + "interval_anchor": "Âncora do intervalo", + "anchor_completion": "A partir da data de conclusão", + "anchor_planned": "A partir da data planeada (sem desvio)", + "edit_object": "Editar objeto", + "name": "Nome", + "manufacturer_optional": "Fabricante (opcional)", + "model_optional": "Modelo (opcional)", + "serial_number_optional": "Número de série (opcional)", + "serial_number_label": "N/S", + "documentation_url_label": "Manual", + "object_notes_label": "Notas", + "sort_due_date": "Vencimento", + "sort_object": "Nome do objeto", + "sort_type": "Tipo", + "sort_task_name": "Nome da tarefa", + "all_objects": "Todos os objetos", + "tasks_lower": "tarefas", + "no_tasks_yet": "Ainda sem tarefas", + "add_first_task": "Adicionar primeira tarefa", + "trigger_configuration": "Configuração do acionador", + "entity_id": "ID da entidade", + "comma_separated": "separados por vírgulas", + "entity_logic": "Lógica da entidade", + "entity_logic_any": "Qualquer entidade aciona", + "entity_logic_all": "Todas as entidades devem acionar", + "entities": "entidades", + "attribute_optional": "Atributo (opcional, vazio = estado)", + "use_entity_state": "Usar estado da entidade (sem atributo)", + "trigger_above": "Acionar acima de", + "trigger_below": "Acionar abaixo de", + "for_at_least_minutes": "Durante pelo menos (minutos)", + "safety_interval_days": "Intervalo de segurança (dias, opcional)", + "safety_interval": "Intervalo de segurança (opcional)", + "delta_mode": "Modo delta", + "from_state_optional": "Do estado (opcional)", + "to_state_optional": "Para o estado (opcional)", + "documentation_url_optional": "URL de documentação (opcional)", + "object_notes_optional": "Notas (opcional)", + "nfc_tag_id_optional": "ID da etiqueta NFC (opcional)", + "nfc_tags_empty_help": "Ainda nenhuma tag NFC registada no Home Assistant.", + "nfc_tags_open_settings": "Abrir configurações de tags", + "nfc_tags_refresh": "Atualizar", + "environmental_entity_optional": "Sensor ambiental (opcional)", + "environmental_entity_helper": "ex. sensor.temperatura_exterior — ajusta o intervalo segundo as condições ambientais", + "environmental_attribute_optional": "Atributo ambiental (opcional)", + "nfc_tag_id": "ID da etiqueta NFC", + "nfc_linked": "Etiqueta NFC associada", + "nfc_link_hint": "Clique para associar etiqueta NFC", + "responsible_user": "Utilizador responsável", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Nenhum utilizador atribuído)", + "all_users": "Todos os utilizadores", + "my_tasks": "As minhas tarefas", + "tab_calendar": "Calendário", + "cal_no_events": "Sem manutenção", + "cal_window_7": "7 dias", + "cal_window_14": "14 dias", + "cal_window_30": "30 dias", + "cal_window_365": "1 ano", + "cal_every_n_days": "a cada {n} dias", + "cal_source_time": "Baseado em tempo", + "cal_source_time_adaptive": "Baseado em tempo (adaptativo)", + "cal_source_sensor": "Baseado em sensor", + "cal_predicted": "previsto", + "cal_confidence_high": "alta confiança", + "cal_confidence_medium": "confiança média", + "cal_confidence_low": "baixa confiança", + "budget_monthly": "Orçamento mensal", + "budget_yearly": "Orçamento anual", + "groups": "Grupos", + "new_group": "Novo grupo", + "edit_group": "Editar grupo", + "no_groups": "Ainda sem grupos", + "delete_group": "Eliminar grupo", + "delete_group_confirm": "Eliminar o grupo '{name}'?", + "group_select_tasks": "Selecionar tarefas", + "group_name_required": "Nome obrigatório", + "description_optional": "Descrição (opcional)", + "selected": "Selecionado", + "loading_chart": "A carregar dados...", + "hide_outliers": "Ocultar valores atípicos (erros do sensor)", + "was_maintenance_needed": "Esta manutenção era necessária?", + "feedback_needed": "Necessária", + "feedback_not_needed": "Não necessária", + "feedback_not_sure": "Não tenho a certeza", + "suggested_interval": "Intervalo sugerido", + "apply_suggestion": "Aplicar", + "reanalyze": "Reanalisar", + "reanalyze_result": "Nova análise", + "reanalyze_insufficient_data": "Dados insuficientes para uma recomendação", + "data_points": "pontos de dados", + "dismiss_suggestion": "Descartar", + "confidence_low": "Baixa", + "confidence_medium": "Média", + "confidence_high": "Alta", + "recommended": "recomendado", + "seasonal_awareness": "Consciência sazonal", + "edit_seasonal_overrides": "Editar fatores sazonais", + "seasonal_overrides_title": "Fatores sazonais (override)", + "seasonal_overrides_hint": "Fator por mês (0.1–5.0). Vazio = aprendido automaticamente.", + "seasonal_override_invalid": "Valor inválido", + "seasonal_override_range": "O fator deve estar entre 0.1 e 5.0", + "clear_all": "Limpar tudo", + "seasonal_chart_title": "Fatores sazonais", + "seasonal_learned": "Aprendido", + "seasonal_manual": "Manual", + "month_jan": "Jan", + "month_feb": "Fev", + "month_mar": "Mar", + "month_apr": "Abr", + "month_may": "Mai", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Ago", + "month_sep": "Set", + "month_oct": "Out", + "month_nov": "Nov", + "month_dec": "Dez", + "sensor_prediction": "Previsão do sensor", + "degradation_trend": "Tendência", + "trend_rising": "A subir", + "trend_falling": "A descer", + "trend_stable": "Estável", + "trend_insufficient_data": "Dados insuficientes", + "days_until_threshold": "Dias até ao limiar", + "threshold_exceeded": "Limiar ultrapassado", + "environmental_adjustment": "Fator ambiental", + "sensor_prediction_urgency": "O sensor prevê o limiar em ~{days} dias", + "day_short": "dia", + "weibull_reliability_curve": "Curva de fiabilidade", + "weibull_failure_probability": "Probabilidade de falha", + "weibull_r_squared": "Ajuste R²", + "beta_early_failures": "Falhas precoces", + "beta_random_failures": "Falhas aleatórias", + "beta_wear_out": "Desgaste", + "beta_highly_predictable": "Altamente previsível", + "confidence_interval": "Intervalo de confiança", + "confidence_conservative": "Conservador", + "confidence_aggressive": "Otimista", + "current_interval_marker": "Intervalo atual", + "recommended_marker": "Recomendado", + "characteristic_life": "Vida característica", + "chart_mini_sparkline": "Sparkline de tendência", + "chart_history": "Histórico de custos e duração", + "chart_seasonal": "Fatores sazonais, 12 meses", + "chart_weibull": "Curva de fiabilidade Weibull", + "chart_sparkline": "Gráfico de valor do acionador", + "days_progress": "Progresso em dias", + "qr_code": "Código QR", + "qr_generating": "A gerar código QR…", + "qr_error": "Não foi possível gerar o código QR.", + "qr_error_no_url": "Nenhum URL do HA configurado. Defina um URL externo ou interno em Definições → Sistema → Rede.", + "save_error": "Erro ao guardar. Tente novamente.", + "qr_print": "Imprimir", + "qr_download": "Transferir SVG", + "qr_action": "Ação ao digitalizar", + "qr_action_view": "Ver informações de manutenção", + "qr_action_complete": "Marcar manutenção como concluída", + "qr_url_mode": "Tipo de ligação", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Local (mDNS)", + "qr_mode_server": "URL do servidor", + "overview": "Visão geral", + "analysis": "Análise", + "recent_activities": "Atividades recentes", + "search_notes": "Pesquisar notas", + "avg_cost": "Ø Custo", + "no_advanced_features": "Sem funções avançadas ativadas", + "no_advanced_features_hint": "Ative “Intervalos Adaptativos” ou “Padrões Sazonais” nas definições da integração para ver dados de análise aqui.", + "analysis_not_enough_data": "Ainda não há dados suficientes para a análise.", + "analysis_not_enough_data_hint": "A análise Weibull requer pelo menos 5 manutenções concluídas; os padrões sazonais tornam-se visíveis após 6+ pontos de dados por mês.", + "analysis_manual_task_hint": "Tarefas manuais sem intervalo não geram dados de análise.", + "completions": "conclusões", + "current": "Atual", + "shorter": "Mais curto", + "longer": "Mais longo", + "normal": "Normal", + "disabled": "Desativado", + "compound_logic": "Lógica composta", + "compound": "Composto (várias condições)", + "compound_logic_and": "E — todas as condições devem disparar", + "compound_logic_or": "OU — basta uma condição", + "compound_help": "Combine várias condições de sensores num só disparador.", + "compound_no_conditions": "Ainda sem condições — adicione pelo menos uma.", + "compound_add_condition": "Adicionar condição", + "compound_condition": "Condição", + "compound_remove_condition": "Remover condição", + "card_title": "Título", + "card_show_header": "Mostrar cabeçalho com estatísticas", + "card_show_actions": "Mostrar botões de ação", + "card_compact": "Modo compacto", + "card_max_items": "Máx. itens (0 = todos)", + "card_filter_status": "Filtrar por estado", + "card_filter_status_help": "Vazio = mostrar todos os estados.", + "card_filter_objects": "Filtrar por objetos", + "card_filter_objects_help": "Vazio = mostrar todos os objetos.", + "card_filter_entities": "Filtrar por entidades (entity_ids)", + "card_filter_entities_help": "Selecione entidades sensor / binary_sensor desta integração. Vazio = todas.", + "card_loading_objects": "A carregar objetos…", + "card_load_error": "Não foi possível carregar os objetos — verifique a ligação WebSocket.", + "card_no_tasks_title": "Ainda sem tarefas de manutenção", + "card_no_tasks_cta": "→ Crie uma no painel Manutenção", + "no_objects": "Ainda sem objetos.", + "action_error": "Ação falhada. Tente novamente.", + "area_id_optional": "Área (opcional)", + "installation_date_optional": "Data de instalação (opcional)", + "warranty_expiry_optional": "Validade da garantia (opcional)", + "warranty": "Garantia", + "warranty_valid_until": "válida até {date}", + "warranty_expires_in": "expira em {days} dias", + "warranty_expired": "expirada", + "cal_past_windows": "Janelas passadas", + "cal_forward_windows": "Janelas futuras", + "history_edit_title": "Editar entrada do histórico", + "history_edit_timestamp": "Carimbo de data/hora", + "manufacturer": "Fabricante", + "model": "Modelo", + "area": "Área", + "actions": "Ações", + "view_mode_label": "Vista", + "view_cards": "Vista de cartões", + "view_table": "Vista de tabela", + "objects_table_columns_label": "Colunas da tabela de objetos", + "objects_table_columns_hint": "Escolha que colunas aparecem na vista de tabela de objetos.", + "custom_icon_optional": "Ícone (opcional, ex. mdi:wrench)", + "task_enabled": "Tarefa ativada", + "skip_reason_prompt": "Saltar esta tarefa?", + "reason_optional": "Motivo (opcional)", + "reset_date_prompt": "Marcar tarefa como executada?", + "reset_date_optional": "Data da última execução (opcional, padrão: hoje)", + "notes_label": "Notas", + "documentation_label": "Documentação", + "no_nfc_tag": "— Sem etiqueta —", + "dashboard": "Painel", + "tab_today": "Hoje", + "palette_placeholder": "Pesquisar objetos e tarefas…", + "palette_no_results": "Sem resultados", + "palette_hint": "↑↓ navegar · Enter abrir · Esc fechar", + "today_all_caught_up": "Tudo em dia! Nada para esta semana.", + "today_overdue": "Atrasadas", + "today_due_today": "Vence hoje", + "today_this_week": "Esta semana", + "settings": "Definições", + "settings_features": "Funções avançadas", + "settings_features_desc": "Ative ou desative funções avançadas. Desativar oculta-as da interface mas não elimina dados.", + "feat_adaptive": "Agendamento adaptativo", + "feat_adaptive_desc": "Aprender intervalos ideais a partir do histórico de manutenção", + "feat_predictions": "Previsões do sensor", + "feat_predictions_desc": "Prever datas de acionamento pela degradação do sensor", + "feat_seasonal": "Ajustes sazonais", + "feat_seasonal_desc": "Ajustar intervalos com base em padrões sazonais", + "feat_environmental": "Correlação ambiental", + "feat_environmental_desc": "Correlacionar intervalos com temperatura/humidade", + "feat_budget": "Controlo de orçamento", + "feat_budget_desc": "Acompanhar despesas de manutenção mensais e anuais", + "feat_groups": "Grupos de tarefas", + "feat_groups_desc": "Organizar tarefas em grupos lógicos", + "feat_checklists": "Listas de verificação", + "feat_checklists_desc": "Procedimentos com vários passos para conclusão de tarefas", + "settings_general": "Geral", + "settings_default_warning": "Dias de aviso predefinidos", + "settings_panel_enabled": "Painel lateral", + "settings_panel_title": "Título do painel lateral", + "settings_notifications": "Notificações", + "settings_notify_service": "Serviço de notificação", + "test_notification": "Notificação de teste", + "send_test": "Enviar teste", + "testing": "A enviar…", + "test_notification_success": "Notificação de teste enviada", + "test_notification_failed": "Falha na notificação de teste", + "settings_notify_due_soon": "Notificar quando próxima", + "settings_notify_overdue": "Notificar quando atrasada", + "settings_notify_triggered": "Notificar quando acionada", + "settings_interval_hours": "Intervalo de repetição (horas, 0 = uma vez)", + "settings_quiet_hours": "Horas de silêncio", + "settings_quiet_start": "Início", + "settings_quiet_end": "Fim", + "settings_max_per_day": "Máx. notificações por dia (0 = ilimitado)", + "settings_bundling": "Agrupar notificações", + "settings_bundle_threshold": "Limiar de agrupamento", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Botões de ação móveis", + "settings_action_complete": "Mostrar botão 'Concluída'", + "settings_action_skip": "Mostrar botão 'Saltar'", + "settings_action_snooze": "Mostrar botão 'Adiar'", + "settings_weekly_digest": "Resumo semanal", + "settings_weekly_digest_hint": "Uma notificação de resumo na segunda de manhã quando há tarefas.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Duração do adiamento (horas)", + "settings_budget": "Orçamento", + "settings_currency": "Moeda", + "settings_budget_monthly": "Orçamento mensal", + "settings_budget_yearly": "Orçamento anual", + "settings_budget_alerts": "Alertas de orçamento", + "settings_budget_threshold": "Limiar de alerta (%)", + "settings_import_export": "Importar / Exportar", + "settings_export_json": "Exportar JSON", + "settings_export_yaml": "Exportar YAML", + "settings_export_csv": "Exportar CSV", + "settings_import_csv": "Importar CSV", + "settings_import_placeholder": "Cole o conteúdo JSON ou CSV aqui…", + "settings_import_btn": "Importar", + "settings_import_success": "{count} objetos importados com sucesso.", + "settings_export_success": "Exportação transferida.", + "settings_saved": "Definição guardada.", + "settings_include_history": "Incluir histórico", + "sort_alphabetical": "Alfabético", + "sort_due_soonest": "Vencimento mais próximo", + "sort_task_count": "Quantidade de tarefas", + "sort_area": "Área", + "sort_assigned_user": "Usuário atribuído", + "sort_group": "Grupo", + "filter_label": "Filtro", + "user_label": "Utilizador", + "sort_label": "Ordenar", + "group_by_label": "Agrupar por", + "state_value_help": "Use o valor de estado HA (normalmente em minúsculas, p. ex. \"on\"/\"off\"). As maiúsculas são normalizadas ao guardar.", + "target_changes_help": "Número de transições correspondentes antes do trigger disparar (predefinido: 1).", + "qr_print_title": "Imprimir códigos QR", + "qr_print_desc": "Gera uma página imprimível de códigos QR para recortar e colar nos equipamentos.", + "qr_print_load": "Carregar objetos", + "qr_print_filter": "Filtro", + "qr_print_objects": "Objetos", + "qr_print_actions": "Ações", + "qr_print_url_mode": "Tipo de link", + "qr_print_estimate": "Códigos QR estimados", + "qr_print_over_limit": "máximo 200, restrinja o filtro", + "qr_print_generate": "Gerar códigos QR", + "qr_print_generating": "A gerar…", + "qr_print_ready": "Códigos QR prontos", + "qr_print_print_button": "Imprimir", + "qr_print_empty": "Nada a gerar", + "qr_action_skip": "Saltar", + "vacation_title": "Modo de férias", + "vacation_active": "ativo", + "vacation_ended": "terminado", + "vacation_desc": "Planeia as tuas férias: as notificações são pausadas durante o período mais dias de margem. Podes manter exceções por tarefa.", + "vacation_enable": "Ativar modo de férias", + "vacation_start": "Início", + "vacation_end": "Fim", + "vacation_buffer": "Margem (dias)", + "vacation_exempt_title": "Notificar mesmo em férias", + "vacation_exempt_desc": "Escolhe tarefas que devem notificar mesmo em férias (p. ex. química crítica de piscina).", + "vacation_load_tasks": "Carregar tarefas", + "vacation_preview_btn": "Mostrar pré-visualização", + "vacation_preview_affected": "tarefas afetadas", + "vacation_event_due_soon": "ficará próxima do prazo", + "vacation_event_overdue": "ficará em atraso", + "vacation_event_triggered_est": "possível disparo do sensor", + "vacation_sensor_based": "(baseado em sensor)", + "vacation_action_notify": "Notificar mesmo assim", + "vacation_action_unsilence": "Silenciar novamente", + "vacation_marked_complete": "Marcado como concluído", + "vacation_marked_skip": "Saltado", + "vacation_end_now": "Terminar férias agora", + "groupby_none": "Sem agrupamento", + "groupby_area": "Por área", + "groupby_group": "Por grupo", + "groupby_user": "Por usuário", + "unassigned": "Não atribuído", + "no_area": "Sem área", + "has_overdue": "Tarefas em atraso", + "object": "Objeto", + "settings_panel_access": "Acesso ao painel", + "settings_panel_access_desc": "Administradores sempre têm acesso completo. Para delegar criar, editar e excluir a não administradores específicos, ative isto e selecione-os abaixo — os demais só veem Concluir e Ignorar.", + "settings_operator_write": "Permitir que os usuários selecionados criem, editem e excluam", + "settings_operator_write_desc": "Desativado: apenas administradores podem alterar o conteúdo. Ativado: os usuários selecionados abaixo também têm acesso completo.", + "no_non_admin_users": "Nenhum usuário não administrador encontrado. Adicione em Configurações → Pessoas.", + "owner_label": "Proprietário", + "feat_completion_actions": "Ações de conclusão", + "feat_completion_actions_desc": "Ação HA por tarefa ao concluir + QR de conclusão rápida com valores predefinidos.", + "on_complete_action_title": "Na conclusão: acionar ação HA (opcional)", + "on_complete_action_desc": "Chama um serviço HA quando a tarefa é concluída — p. ex. reiniciar um contador no dispositivo.", + "on_complete_action_service": "Serviço", + "on_complete_action_target": "Entidade alvo", + "on_complete_action_data": "Dados (JSON, opcional)", + "on_complete_action_test": "Testar ação", + "on_complete_action_test_success": "Sucesso", + "on_complete_action_test_failed": "Falhou", + "quick_complete_defaults_title": "Valores predefinidos de conclusão rápida (para scans QR, opcional)", + "quick_complete_defaults_desc": "Valores predefinidos para QR de conclusão rápida. Sem eles, o QR abre o diálogo.", + "quick_complete_defaults_notes": "Notas", + "quick_complete_defaults_cost": "Custo", + "quick_complete_defaults_duration": "Duração (minutos)", + "quick_complete_defaults_feedback_none": "Sem feedback", + "quick_complete_defaults_feedback_needed": "Era necessário", + "quick_complete_defaults_feedback_not_needed": "Não era necessário", + "quick_complete_success": "Concluído rapidamente", + "trigger_replaced": "Acionador substituído", + "add": "Adicionar", + "show_stats": "Mostrar estatísticas + gráficos", + "hide_stats": "Ocultar estatísticas", + "adaptive_no_data": "Ainda não há histórico de conclusões suficiente para a análise adaptativa. Conclua esta tarefa mais algumas vezes para desbloquear as recomendações de intervalo e os gráficos de fiabilidade.", + "suggestion_applied": "Intervalo sugerido aplicado", + "vacation_mode": "Modo de férias", + "vacation_status_active": "Ativo agora", + "vacation_status_scheduled": "Agendado", + "vacation_status_inactive": "Inativo", + "vacation_end_now_confirm": "Terminar as férias imediatamente?", + "vacation_exempt_count": "isentas", + "vacation_advanced": "Avançado…", + "vacation_open_panel": "Abrir no painel", + "enable": "Ativar", + "saved": "Guardado", + "budget_monthly_set": "Definir mensal", + "budget_yearly_set": "Definir anual", + "budget_advanced": "Moeda, alertas…", + "budget_open_panel": "Abrir no painel", + "groups_empty": "Ainda sem grupos.", + "group_new_placeholder": "Adicionar grupo…", + "group_delete_confirm": "Eliminar o grupo \"{name}\"?", + "groups_manage_tasks": "Gerir atribuições de tarefas…", + "groups_open_panel": "Abrir no painel", + "on_complete_action_target_hint": "Nota: o domínio da entidade tem de corresponder ao serviço — p. ex. 'button.press' só funciona em button.*, 'counter.increment' só em counter.*, 'input_button.press' só em input_button.* etc. Em caso de incompatibilidade, a ação falha silenciosamente (o HA regista 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Mostrar todos os objetos", + "show_all_tasks": "Limpar filtro — mostrar todas as tarefas", + "filter_to_overdue": "Filtrar a lista apenas para em atraso", + "filter_to_due_soon": "Filtrar a lista apenas para a vencer em breve", + "filter_to_triggered": "Filtrar a lista apenas para acionadas", + "open_task": "Abrir tarefa", + "show_details": "Mostrar histórico + estatísticas", + "hide_details": "Ocultar detalhes", + "history_empty": "Ainda sem histórico.", + "history_edit_button": "Editar entrada", + "total_cost": "Custo total", + "times_performed": "Realizada", + "older_entries": "mais antigas", + "open_in_panel": "Abrir no painel Manutenção", + "skip_reason": "Motivo para saltar (opcional)", + "reset_to_date": "Repor última execução para", + "delete_task_confirm": "Eliminar esta tarefa e o seu histórico?", + "delete_object_confirm": "Eliminar este objeto e todas as suas tarefas?", + "loading": "A carregar…", + "archive": "Arquivar", + "undo": "Desfazer", + "task_archived": "Tarefa arquivada", + "object_archived": "Objeto arquivado", + "unarchive": "Desarquivar", + "archived": "Arquivado", + "show_archived": "Mostrar arquivados", + "hide_archived": "Ocultar arquivados", + "confirm_archive_object": "Arquivar este objeto e as suas tarefas? O histórico é mantido e pode ser restaurado mais tarde.", + "settings_archive": "Arquivo e retenção", + "settings_archive_desc": "Retire tarefas únicas concluídas sem as eliminar. Os itens arquivados ficam ocultos e inativos, mas mantêm histórico e custos.", + "settings_archive_oneoff_days": "Arquivar automaticamente tarefas únicas concluídas após (dias, 0 = desligado)", + "settings_delete_archived_oneoff_days": "Eliminar automaticamente tarefas únicas arquivadas após (dias, 0 = nunca)", + "archive_object": "Arquivar objeto", + "unarchive_object": "Desarquivar objeto", + "documents": "Documentos", + "documents_empty": "Ainda sem documentos.", + "doc_upload": "Carregar ficheiro", + "doc_uploading": "A carregar…", + "doc_add_link": "Adicionar ligação", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Título (opcional)", + "doc_open": "Abrir", + "doc_delete_confirm": "Eliminar «{name}»?", + "doc_too_large": "Ficheiro demasiado grande (máx. 25 MB).", + "doc_upload_failed": "Falha no carregamento.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Já armazenado noutro local — partilhado, sem espaço extra.", + "doc_dup_in_object": "Este ficheiro já está anexado a este objeto.", + "doc_link_invalid": "Apenas ligações http/https são permitidas.", + "doc_cat_manual": "Manual", + "doc_cat_warranty": "Garantia", + "doc_cat_invoice": "Fatura", + "doc_cat_spare_parts": "Peças", + "doc_cat_photo": "Foto", + "doc_cat_other": "Outro", + "doc_link_badge": "Ligação", + "doc_storage_title": "Armazenamento de documentos", + "doc_storage_saved": "Poupado por deduplicação", + "doc_storage_refresh": "Atualizar", + "doc_download": "Transferir", + "doc_close": "Fechar", + "doc_camera": "Tirar foto", + "doc_drop_hint": "Solte os ficheiros aqui", + "doc_task_none": "Nenhum documento associado a esta tarefa.", + "doc_link_existing": "Associar um documento…", + "doc_attach": "Associar", + "doc_unlink": "Desassociar", + "doc_page": "Página", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1a", + "chart_since_service": "desde a última manutenção", + "chart_no_stats": "Sem estatísticas de longo prazo para esta entidade — mostrando apenas valores de eventos de manutenção", + "auto_complete_on_recovery": "Concluir automaticamente quando o sensor recuperar", + "auto_complete_on_recovery_help": "Regista uma conclusão (define a última realização) quando o gatilho se resolve sozinho — p. ex., sal reabastecido, filtro substituído.", + "doc_search": "Pesquisar documentos…", + "doc_search_none": "Nenhum documento correspondente", + "link_device_optional": "Associar a dispositivo existente (opcional)", + "parent_object_optional": "Objeto principal (opcional)", + "parent_none": "(Sem objeto principal)", + "paused": "Pausado", + "pause_object": "Pausar", + "resume_object": "Retomar", + "pause_until_prompt": "Congela os agendamentos deste objeto — nada vence nem notifica até ser retomado. Opcionalmente, defina uma data de retoma automática.", + "pause_until_label": "Retomar em (opcional)", + "object_paused": "Objeto pausado", + "object_resumed": "Objeto retomado — agendamentos reiniciados", + "object_paused_badge": "Pausado", + "paused_until_label": "até", + "replace_object": "Substituir…", + "replace_object_prompt": "Retire este objeto e crie um sucessor. O histórico e os custos ficam arquivados no antigo; as tarefas e os documentos passam para o novo, os contadores começam do zero.", + "replace_name_label": "Nome do sucessor", + "object_replaced": "Objeto substituído — sucessor criado", + "reading_unit_label": "Unidade de leitura (p. ex. kWh, m³)", + "reading_unit_help": "Mostrada ao lado do valor registado ao concluir esta tarefa.", + "reading_value_label": "Valor de leitura", + "reading_label": "Leitura", + "settings_templates_label": "Galeria de modelos", + "settings_templates_hint": "Desmarque modelos de que nunca precisará — desaparecem dos seletores \"A partir de modelo\" (painel e config flow). Nada mais muda; reativáveis a qualquer momento.", + "worksheet": "Folha de trabalho", + "worksheet_scan_view": "Digitalize para abrir a tarefa", + "worksheet_scan_complete": "Digitalize para concluir", + "worksheet_manual_excerpt": "Excerto do manual", + "worksheet_pages": "páginas", + "worksheet_printed": "Impresso", + "worksheet_never": "Nunca" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/ru.json b/custom_components/maintenance_supporter/frontend-src/locales/ru.json new file mode 100644 index 0000000..cabacb7 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/ru.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Обслуживание", + "objects": "Объекты", + "tasks": "Задачи", + "overdue": "Просрочено", + "due_soon": "Скоро", + "triggered": "Сработало", + "ok": "OK", + "all": "Все", + "new_object": "+ Новый объект", + "templates_from": "Из шаблона", + "templates_title": "Начать с шаблона", + "templates_task_count": "{n} задач", + "template_created": "Создано из шаблона", + "onboard_hint": "Добавьте первый объект, чтобы отслеживать обслуживание.", + "edit": "Изменить", + "duplicate": "Дублировать", + "task_duplicated": "Задача продублирована", + "object_duplicated": "Объект продублирован", + "delete": "Удалить", + "add_task": "+ Добавить задачу", + "complete": "Выполнить", + "completed": "Выполнено", + "skip": "Пропустить", + "skipped": "Пропущено", + "missed": "Missed", + "reset": "Сбросить", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Отмена", + "bulk_select": "Выбрать", + "bulk_select_all": "Выбрать все", + "bulk_n_selected": "Выбрано: {n}", + "bulk_completed": "Выполнено задач: {n}", + "bulk_archived": "Архивировано задач: {n}", + "completing": "Выполнение…", + "interval": "Интервал", + "warning": "Предупреждение", + "last_performed": "Последнее выполнение", + "next_due": "Следующий срок", + "days_until_due": "Дней до срока", + "avg_duration": "Ср. длительность", + "trigger": "Триггер", + "trigger_type": "Тип триггера", + "threshold_above": "Верхний предел", + "threshold_below": "Нижний предел", + "threshold": "Порог", + "counter": "Счётчик", + "state_change": "Изменение состояния", + "runtime": "Время работы", + "runtime_hours": "Целевое время работы (часы)", + "target_value": "Целевое значение", + "baseline": "Базовое значение", + "target_changes": "Целевые изменения", + "for_minutes": "На (минут)", + "time_based": "По времени", + "sensor_based": "По датчику", + "manual": "Вручную", + "one_time": "Однократно", + "weekdays": "Дни недели", + "nth_weekday": "N-й день недели месяца", + "day_of_month": "День месяца", + "recurrence_on_days": "Повторять в", + "recurrence_occurrence": "Вхождение", + "recurrence_weekday": "День недели", + "recurrence_day": "День месяца (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1-й", + "ord_2": "2-й", + "ord_3": "3-й", + "ord_4": "4-й", + "ord_5": "5-й", + "ord_last": "Последний", + "day_word": "День", + "interval_value": "Интервал", + "interval_unit": "Единица", + "unit_days": "Дни", + "unit_weeks": "Недели", + "unit_months": "Месяцы", + "unit_years": "Годы", + "due_date": "Дата выполнения", + "cleaning": "Чистка", + "inspection": "Осмотр", + "replacement": "Замена", + "calibration": "Калибровка", + "service": "Сервис", + "reading": "Показания", + "custom": "Своё", + "history": "История", + "cost": "Стоимость", + "report_button": "Отчёт", + "report_title": "Отчёт об обслуживании", + "report_generated": "Создан", + "report_times_done": "Выполнено", + "report_total_cost": "Итого", + "report_every": "каждые {n} {unit}", + "report_notes": "Заметки", + "report_col_type": "Тип", + "report_col_status": "Статус", + "report_col_schedule": "Расписание", + "duration": "Длительность", + "both": "Оба", + "trigger_val": "Значение триггера", + "complete_title": "Выполнить: ", + "checklist": "Контрольный список", + "checklist_steps_optional": "Шаги контрольного списка (необязательно)", + "checklist_placeholder": "Очистить фильтр\nЗаменить уплотнитель\nПроверить давление", + "checklist_help": "Один шаг на строку. Макс. 100 элементов.", + "err_too_long": "{field}: слишком длинно (макс. {n} символов)", + "err_too_short": "{field}: слишком коротко (мин. {n} символов)", + "err_value_too_high": "{field}: слишком велико (макс. {n})", + "err_value_too_low": "{field}: слишком мало (мин. {n})", + "err_required": "{field}: обязательное поле", + "err_wrong_type": "{field}: неверный тип (ожидался: {type})", + "err_invalid_choice": "{field}: недопустимое значение", + "err_invalid_value": "{field}: неверное значение", + "feat_schedule_time": "Планирование по времени дня", + "feat_schedule_time_desc": "Задачи становятся просроченными в определённое время дня, а не в полночь.", + "schedule_time_optional": "Срок (необязательно, HH:MM)", + "schedule_time_help": "Пусто = полночь (по умолчанию). Часовой пояс HA.", + "at_time": "в", + "notes_optional": "Примечания (опционально)", + "cost_optional": "Стоимость (опционально)", + "duration_minutes": "Длительность в минутах (опционально)", + "days": "дней", + "day": "день", + "today": "Сегодня", + "d_overdue": "дн. просрочено", + "no_tasks": "Пока нет задач по обслуживанию. Создайте объект, чтобы начать.", + "no_tasks_short": "Нет задач", + "no_history": "Пока нет записей в истории.", + "show_all": "Показать все", + "cost_duration_chart": "Стоимость и длительность", + "installed": "Установлен", + "confirm_delete_object": "Удалить этот объект и все его задачи?", + "confirm_delete_task": "Удалить эту задачу?", + "min": "Мин", + "max": "Макс", + "save": "Сохранить", + "saving": "Сохранение…", + "edit_task": "Изменить задачу", + "new_task": "Новая задача обслуживания", + "task_name": "Название задачи", + "maintenance_type": "Тип обслуживания", + "priority": "Приоритет", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Низкий", + "priority_normal": "Обычный", + "priority_high": "Высокий", + "schedule_type": "Тип расписания", + "interval_days": "Интервал (дни)", + "warning_days": "Дни предупреждения", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "interval_anchor": "Якорь интервала", + "anchor_completion": "От даты выполнения", + "anchor_planned": "От плановой даты (без смещения)", + "edit_object": "Изменить объект", + "name": "Имя", + "manufacturer_optional": "Производитель (опционально)", + "model_optional": "Модель (опционально)", + "serial_number_optional": "Серийный номер (опционально)", + "serial_number_label": "С/Н", + "documentation_url_label": "Руководство", + "object_notes_label": "Заметки", + "sort_due_date": "Срок", + "sort_object": "Имя объекта", + "sort_type": "Тип", + "sort_task_name": "Имя задачи", + "all_objects": "Все объекты", + "tasks_lower": "задач", + "no_tasks_yet": "Пока нет задач", + "add_first_task": "Добавить первую задачу", + "last_performed_optional": "Последнее выполнение (опционально)", + "trigger_configuration": "Настройка триггера", + "entity_id": "ID сущности", + "comma_separated": "через запятую", + "entity_logic": "Логика сущностей", + "entity_logic_any": "Любая сущность срабатывает", + "entity_logic_all": "Все сущности должны сработать", + "entities": "сущности", + "attribute_optional": "Атрибут (опционально, пусто = состояние)", + "use_entity_state": "Использовать состояние сущности (без атрибута)", + "trigger_above": "Срабатывать выше", + "trigger_below": "Срабатывать ниже", + "for_at_least_minutes": "Не менее (минут)", + "safety_interval_days": "Интервал безопасности (дни, опционально)", + "safety_interval": "Интервал безопасности (опционально)", + "delta_mode": "Режим дельты", + "from_state_optional": "Из состояния (опционально)", + "to_state_optional": "В состояние (опционально)", + "documentation_url_optional": "URL документации (опционально)", + "object_notes_optional": "Заметки (опционально)", + "nfc_tag_id_optional": "ID NFC-метки (опционально)", + "nfc_tags_empty_help": "В Home Assistant ещё не зарегистрировано NFC-тегов.", + "nfc_tags_open_settings": "Открыть настройки тегов", + "nfc_tags_refresh": "Обновить", + "environmental_entity_optional": "Датчик окружающей среды (опционально)", + "environmental_entity_helper": "напр. sensor.outdoor_temperature — корректирует интервал в зависимости от условий", + "environmental_attribute_optional": "Атрибут среды (опционально)", + "nfc_tag_id": "ID NFC-метки", + "nfc_linked": "NFC-метка привязана", + "nfc_link_hint": "Нажмите, чтобы привязать NFC-метку", + "responsible_user": "Ответственный пользователь", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Не назначен)", + "all_users": "Все пользователи", + "my_tasks": "Мои задачи", + "tab_calendar": "Календарь", + "cal_no_events": "Без обслуживания", + "cal_window_7": "7 дней", + "cal_window_14": "14 дней", + "cal_window_30": "30 дней", + "cal_window_365": "1 год", + "cal_every_n_days": "каждые {n} дней", + "cal_source_time": "По времени", + "cal_source_time_adaptive": "По времени (адаптивно)", + "cal_source_sensor": "По датчику", + "cal_predicted": "прогноз", + "cal_confidence_high": "высокая надёжность", + "cal_confidence_medium": "средняя надёжность", + "cal_confidence_low": "низкая надёжность", + "budget_monthly": "Месячный бюджет", + "budget_yearly": "Годовой бюджет", + "groups": "Группы", + "new_group": "Новая группа", + "edit_group": "Редактировать группу", + "no_groups": "Групп пока нет", + "delete_group": "Удалить группу", + "delete_group_confirm": "Удалить группу '{name}'?", + "group_select_tasks": "Выбрать задачи", + "group_name_required": "Требуется имя", + "description_optional": "Описание (опционально)", + "selected": "Выбрано", + "loading_chart": "Загрузка данных графика...", + "hide_outliers": "Скрыть выбросы (сбои датчика)", + "was_maintenance_needed": "Требовалось ли это обслуживание?", + "feedback_needed": "Требовалось", + "feedback_not_needed": "Не требовалось", + "feedback_not_sure": "Не уверен", + "suggested_interval": "Рекомендуемый интервал", + "apply_suggestion": "Применить", + "reanalyze": "Повторный анализ", + "reanalyze_result": "Новый анализ", + "reanalyze_insufficient_data": "Недостаточно данных для рекомендации", + "data_points": "точек данных", + "dismiss_suggestion": "Отклонить", + "confidence_low": "Низкая", + "confidence_medium": "Средняя", + "confidence_high": "Высокая", + "recommended": "рекомендуется", + "seasonal_awareness": "Сезонная адаптация", + "edit_seasonal_overrides": "Редактировать сезонные коэффициенты", + "seasonal_overrides_title": "Сезонные коэффициенты (переопределение)", + "seasonal_overrides_hint": "Коэффициент на месяц (0.1–5.0). Пусто = автоматически.", + "seasonal_override_invalid": "Недопустимое значение", + "seasonal_override_range": "Коэффициент должен быть от 0.1 до 5.0", + "clear_all": "Очистить все", + "seasonal_chart_title": "Сезонные факторы", + "seasonal_learned": "Изученные", + "seasonal_manual": "Ручные", + "month_jan": "Янв", + "month_feb": "Фев", + "month_mar": "Мар", + "month_apr": "Апр", + "month_may": "Май", + "month_jun": "Июн", + "month_jul": "Июл", + "month_aug": "Авг", + "month_sep": "Сен", + "month_oct": "Окт", + "month_nov": "Ноя", + "month_dec": "Дек", + "sensor_prediction": "Предсказание по датчику", + "degradation_trend": "Тренд", + "trend_rising": "Растущий", + "trend_falling": "Падающий", + "trend_stable": "Стабильный", + "trend_insufficient_data": "Недостаточно данных", + "days_until_threshold": "Дней до порога", + "threshold_exceeded": "Порог превышен", + "environmental_adjustment": "Фактор среды", + "sensor_prediction_urgency": "Датчик предсказывает порог через ~{days} дней", + "day_short": "дн", + "weibull_reliability_curve": "Кривая надёжности", + "weibull_failure_probability": "Вероятность отказа", + "weibull_r_squared": "Качество аппроксимации R²", + "beta_early_failures": "Ранние отказы", + "beta_random_failures": "Случайные отказы", + "beta_wear_out": "Износ", + "beta_highly_predictable": "Высокая предсказуемость", + "confidence_interval": "Доверительный интервал", + "confidence_conservative": "Консервативный", + "confidence_aggressive": "Оптимистичный", + "current_interval_marker": "Текущий интервал", + "recommended_marker": "Рекомендуемый", + "characteristic_life": "Характеристический срок службы", + "chart_mini_sparkline": "Мини-график тренда", + "chart_history": "История стоимости и длительности", + "chart_seasonal": "Сезонные факторы, 12 месяцев", + "chart_weibull": "Кривая надёжности Вейбулла", + "chart_sparkline": "График значений триггера датчика", + "days_progress": "Прогресс по дням", + "qr_code": "QR-код", + "qr_generating": "Генерация QR-кода…", + "qr_error": "Не удалось сгенерировать QR-код.", + "qr_error_no_url": "URL HA не настроен. Установите внешний или внутренний URL в Настройках → Система → Сеть.", + "save_error": "Не удалось сохранить. Попробуйте ещё раз.", + "qr_print": "Печать", + "qr_download": "Скачать SVG", + "qr_action": "Действие при сканировании", + "qr_action_view": "Просмотр", + "qr_action_complete": "Отметить обслуживание как выполненное", + "qr_url_mode": "Тип ссылки", + "qr_mode_companion": "Приложение-компаньон", + "qr_mode_local": "Локальный (mDNS)", + "qr_mode_server": "URL сервера", + "overview": "Обзор", + "analysis": "Анализ", + "recent_activities": "Недавние действия", + "search_notes": "Поиск по заметкам", + "avg_cost": "Ср. стоимость", + "no_advanced_features": "Расширенные функции не включены", + "no_advanced_features_hint": "Включите «Адаптивные интервалы» или «Сезонные паттерны» в настройках интеграции, чтобы увидеть здесь аналитику.", + "analysis_not_enough_data": "Недостаточно данных для анализа.", + "analysis_not_enough_data_hint": "Для анализа Вейбулла требуется минимум 5 выполненных обслуживаний; сезонные паттерны становятся видны после 6+ точек данных в месяц.", + "analysis_manual_task_hint": "Ручные задачи без интервала не генерируют аналитику.", + "completions": "выполнений", + "current": "Текущий", + "shorter": "Короче", + "longer": "Длиннее", + "normal": "Нормальный", + "disabled": "Отключено", + "compound_logic": "Составная логика", + "compound": "Составной (несколько условий)", + "compound_logic_and": "И — должны сработать все условия", + "compound_logic_or": "ИЛИ — достаточно любого условия", + "compound_help": "Объедините несколько условий датчиков в один триггер.", + "compound_no_conditions": "Условий пока нет — добавьте хотя бы одно.", + "compound_add_condition": "Добавить условие", + "compound_condition": "Условие", + "compound_remove_condition": "Удалить условие", + "card_title": "Заголовок", + "card_show_header": "Показывать заголовок со статистикой", + "card_show_actions": "Показывать кнопки действий", + "card_compact": "Компактный режим", + "card_max_items": "Макс. элементов (0 = все)", + "card_filter_status": "Фильтровать по статусу", + "card_filter_status_help": "Пусто = показать все статусы.", + "card_filter_objects": "Фильтровать по объектам", + "card_filter_objects_help": "Пусто = показать все объекты.", + "card_filter_entities": "Фильтровать по сущностям (entity_ids)", + "card_filter_entities_help": "Выберите сущности sensor / binary_sensor из этой интеграции. Пусто = все.", + "card_loading_objects": "Загрузка объектов…", + "card_load_error": "Не удалось загрузить объекты — проверьте WebSocket-соединение.", + "card_no_tasks_title": "Пока нет задач обслуживания", + "card_no_tasks_cta": "→ Создайте в панели Maintenance", + "no_objects": "Пока нет объектов.", + "action_error": "Не удалось выполнить действие. Попробуйте ещё раз.", + "area_id_optional": "Зона (опционально)", + "installation_date_optional": "Дата установки (опционально)", + "warranty_expiry_optional": "Окончание гарантии (опционально)", + "warranty": "Гарантия", + "warranty_valid_until": "действует до {date}", + "warranty_expires_in": "истекает через {days} дн.", + "warranty_expired": "истекла", + "cal_past_windows": "Прошлые окна", + "cal_forward_windows": "Будущие окна", + "history_edit_title": "Редактировать запись истории", + "history_edit_timestamp": "Метка времени", + "manufacturer": "Производитель", + "model": "Модель", + "area": "Зона", + "actions": "Действия", + "view_mode_label": "Вид", + "view_cards": "Карточки", + "view_table": "Таблица", + "objects_table_columns_label": "Столбцы таблицы объектов", + "objects_table_columns_hint": "Выберите, какие столбцы показывать в табличном виде объектов.", + "custom_icon_optional": "Иконка (опционально, например mdi:wrench)", + "task_enabled": "Задача включена", + "skip_reason_prompt": "Пропустить эту задачу?", + "reason_optional": "Причина (опционально)", + "reset_date_prompt": "Отметить задачу как выполненную?", + "reset_date_optional": "Дата последнего выполнения (опционально, по умолчанию: сегодня)", + "notes_label": "Примечания", + "documentation_label": "Документация", + "no_nfc_tag": "— Нет метки —", + "dashboard": "Панель", + "tab_today": "Сегодня", + "palette_placeholder": "Поиск объектов и задач…", + "palette_no_results": "Нет совпадений", + "palette_hint": "↑↓ выбор · Enter открыть · Esc закрыть", + "today_all_caught_up": "Всё сделано! На этой неделе ничего нет.", + "today_overdue": "Просрочено", + "today_due_today": "Сегодня", + "today_this_week": "На этой неделе", + "settings": "Настройки", + "settings_features": "Расширенные функции", + "settings_features_desc": "Включите или отключите расширенные функции. Отключение скрывает их из интерфейса, но не удаляет данные.", + "feat_adaptive": "Адаптивное планирование", + "feat_adaptive_desc": "Изучать оптимальные интервалы из истории обслуживания", + "feat_predictions": "Предсказания по датчикам", + "feat_predictions_desc": "Предсказывать даты срабатывания по деградации датчика", + "feat_seasonal": "Сезонные корректировки", + "feat_seasonal_desc": "Корректировать интервалы на основе сезонных паттернов", + "feat_environmental": "Экологическая корреляция", + "feat_environmental_desc": "Связывать интервалы с температурой/влажностью", + "feat_budget": "Отслеживание бюджета", + "feat_budget_desc": "Отслеживать месячные и годовые расходы на обслуживание", + "feat_groups": "Группы задач", + "feat_groups_desc": "Организовывать задачи в логические группы", + "feat_checklists": "Контрольные списки", + "feat_checklists_desc": "Многошаговые процедуры для выполнения задачи", + "settings_general": "Основные", + "settings_default_warning": "Дни предупреждения по умолчанию", + "settings_panel_enabled": "Боковая панель", + "settings_panel_title": "Заголовок панели", + "settings_notifications": "Уведомления", + "settings_notify_service": "Сервис уведомлений", + "test_notification": "Тестовое уведомление", + "send_test": "Отправить тест", + "testing": "Отправка…", + "test_notification_success": "Тестовое уведомление отправлено", + "test_notification_failed": "Не удалось отправить тестовое уведомление", + "settings_notify_due_soon": "Уведомлять, когда срок скоро истекает", + "settings_notify_overdue": "Уведомлять при просрочке", + "settings_notify_triggered": "Уведомлять при срабатывании", + "settings_interval_hours": "Интервал повторения (часы, 0 = один раз)", + "settings_quiet_hours": "Часы тишины", + "settings_quiet_start": "Начало", + "settings_quiet_end": "Конец", + "settings_max_per_day": "Макс. уведомлений в день (0 = без ограничений)", + "settings_bundling": "Группировать уведомления", + "settings_bundle_threshold": "Порог группировки", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Кнопки действий в мобильном приложении", + "settings_action_complete": "Показывать кнопку «Выполнить»", + "settings_action_skip": "Показывать кнопку «Пропустить»", + "settings_action_snooze": "Показывать кнопку «Отложить»", + "settings_weekly_digest": "Еженедельная сводка", + "settings_weekly_digest_hint": "Одно сводное уведомление в понедельник утром, когда есть задачи.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Длительность откладывания (часы)", + "settings_budget": "Бюджет", + "settings_currency": "Валюта", + "settings_budget_monthly": "Месячный бюджет", + "settings_budget_yearly": "Годовой бюджет", + "settings_budget_alerts": "Оповещения о бюджете", + "settings_budget_threshold": "Порог оповещения (%)", + "settings_import_export": "Импорт / Экспорт", + "settings_export_json": "Экспорт JSON", + "settings_export_yaml": "Экспорт YAML", + "settings_export_csv": "Экспорт CSV", + "settings_import_csv": "Импорт CSV", + "settings_import_placeholder": "Вставьте содержимое JSON или CSV здесь…", + "settings_import_btn": "Импортировать", + "settings_import_success": "Импортировано объектов: {count}.", + "settings_export_success": "Экспорт загружен.", + "settings_saved": "Настройка сохранена.", + "settings_include_history": "Включать историю", + "sort_alphabetical": "По алфавиту", + "sort_due_soonest": "Ближайший срок", + "sort_task_count": "Количество задач", + "sort_area": "Область", + "sort_assigned_user": "Назначенный пользователь", + "sort_group": "Группа", + "groupby_none": "Без группировки", + "groupby_area": "По области", + "groupby_group": "По группе", + "groupby_user": "По пользователю", + "filter_label": "Фильтр", + "user_label": "Пользователь", + "sort_label": "Сортировка", + "group_by_label": "Группировать по", + "state_value_help": "Используйте значение состояния HA (обычно в нижнем регистре, напр. \"on\"/\"off\"). Регистр нормализуется при сохранении.", + "target_changes_help": "Количество совпадающих переходов, после которых срабатывает триггер (по умолчанию: 1).", + "qr_print_title": "Печать QR-кодов", + "qr_print_desc": "Создай страницу для печати с QR-кодами для вырезания и наклеивания на оборудование.", + "qr_print_load": "Загрузить объекты", + "qr_print_filter": "Фильтр", + "qr_print_objects": "Объекты", + "qr_print_actions": "Действия", + "qr_print_url_mode": "Тип ссылки", + "qr_print_estimate": "Оценка QR-кодов", + "qr_print_over_limit": "лимит 200, сузь фильтр", + "qr_print_generate": "Создать QR-коды", + "qr_print_generating": "Создание…", + "qr_print_ready": "QR-коды готовы", + "qr_print_print_button": "Печать", + "qr_print_empty": "Нечего создавать", + "qr_action_skip": "Пропустить", + "vacation_title": "Режим отпуска", + "vacation_active": "активен", + "vacation_ended": "завершён", + "vacation_desc": "Запланируй отпуск: уведомления приостанавливаются на период плюс несколько буферных дней. Можно задать исключения по задачам.", + "vacation_enable": "Включить режим отпуска", + "vacation_start": "Начало", + "vacation_end": "Конец", + "vacation_buffer": "Буфер (дней)", + "vacation_exempt_title": "Уведомлять несмотря на отпуск", + "vacation_exempt_desc": "Выбери задачи, по которым нужно уведомлять и в отпуске (например, критичная химия бассейна).", + "vacation_load_tasks": "Загрузить задачи", + "vacation_preview_btn": "Показать предпросмотр", + "vacation_preview_affected": "задач затронуто", + "vacation_event_due_soon": "скоро наступит срок", + "vacation_event_overdue": "станет просроченной", + "vacation_event_triggered_est": "возможно срабатывание сенсора", + "vacation_sensor_based": "(сенсорная)", + "vacation_action_notify": "Всё равно уведомлять", + "vacation_action_unsilence": "Снова заглушить", + "vacation_marked_complete": "Отмечено как выполнено", + "vacation_marked_skip": "Пропущено", + "vacation_end_now": "Завершить отпуск сейчас", + "unassigned": "Не назначено", + "no_area": "Без области", + "has_overdue": "Просроченные задачи", + "object": "Объект", + "settings_panel_access": "Доступ к панели", + "settings_panel_access_desc": "Администраторы всегда имеют полный доступ. Чтобы делегировать создание, редактирование и удаление определённым не-админ пользователям, включите это и выберите их ниже — остальные видят только Выполнить и Пропустить.", + "settings_operator_write": "Разрешить выбранным пользователям создавать, редактировать и удалять", + "settings_operator_write_desc": "Выключено: только администраторы могут изменять содержимое. Включено: выбранные пользователи ниже также получают полный доступ.", + "no_non_admin_users": "Не найдено не-админ пользователей. Добавьте их в Настройках → Люди.", + "owner_label": "Владелец", + "feat_completion_actions": "Действия при завершении", + "feat_completion_actions_desc": "HA-действие по задаче при завершении + QR быстрого завершения с предустановленными значениями.", + "on_complete_action_title": "При завершении: запустить HA-действие (опционально)", + "on_complete_action_desc": "Вызывает HA-сервис при завершении задачи — напр., сбросить счётчик на устройстве.", + "on_complete_action_service": "Сервис", + "on_complete_action_target": "Целевая сущность", + "on_complete_action_data": "Данные (JSON, опционально)", + "on_complete_action_test": "Тестировать действие", + "on_complete_action_test_success": "Успешно", + "on_complete_action_test_failed": "Ошибка", + "quick_complete_defaults_title": "Значения по умолчанию для быстрого завершения (для QR-сканов, опционально)", + "quick_complete_defaults_desc": "Предустановленные значения для QR быстрого завершения. Без них QR открывает диалог.", + "quick_complete_defaults_notes": "Заметки", + "quick_complete_defaults_cost": "Стоимость", + "quick_complete_defaults_duration": "Длительность (минут)", + "quick_complete_defaults_feedback_none": "Без отзыва", + "quick_complete_defaults_feedback_needed": "Было необходимо", + "quick_complete_defaults_feedback_not_needed": "Не было необходимо", + "quick_complete_success": "Быстро отмечено как выполнено", + "trigger_replaced": "Триггер заменён", + "add": "Добавить", + "show_stats": "Показать статистику + графики", + "hide_stats": "Скрыть статистику", + "adaptive_no_data": "Пока недостаточно истории выполнений для адаптивного анализа. Выполните эту задачу ещё несколько раз, чтобы разблокировать рекомендации по интервалу и графики надёжности.", + "suggestion_applied": "Предлагаемый интервал применён", + "vacation_mode": "Режим отпуска", + "vacation_status_active": "Активен сейчас", + "vacation_status_scheduled": "Запланирован", + "vacation_status_inactive": "Неактивен", + "vacation_end_now_confirm": "Завершить отпуск немедленно?", + "vacation_exempt_count": "исключено", + "vacation_advanced": "Дополнительно…", + "vacation_open_panel": "Открыть на панели", + "enable": "Включить", + "saved": "Сохранено", + "budget_monthly_set": "Задать месячный", + "budget_yearly_set": "Задать годовой", + "budget_advanced": "Валюта, оповещения…", + "budget_open_panel": "Открыть на панели", + "groups_empty": "Пока нет групп.", + "group_new_placeholder": "Добавить группу…", + "group_delete_confirm": "Удалить группу «{name}»?", + "groups_manage_tasks": "Управление назначениями задач…", + "groups_open_panel": "Открыть на панели", + "on_complete_action_target_hint": "Примечание: домен сущности должен соответствовать сервису — напр. 'button.press' работает только с button.*, 'counter.increment' только с counter.*, 'input_button.press' только с input_button.* и т. д. При несоответствии действие молча не выполнится (HA запишет в журнал 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Показать все объекты", + "show_all_tasks": "Сбросить фильтр — показать все задачи", + "filter_to_overdue": "Фильтровать список только по просроченным", + "filter_to_due_soon": "Фильтровать список только по скоро наступающим", + "filter_to_triggered": "Фильтровать список только по сработавшим", + "open_task": "Открыть задачу", + "show_details": "Показать историю + статистику", + "hide_details": "Скрыть детали", + "history_empty": "Пока нет истории.", + "history_edit_button": "Изменить запись", + "total_cost": "Общая стоимость", + "times_performed": "Выполнено", + "older_entries": "ранее", + "open_in_panel": "Открыть на панели обслуживания", + "skip_reason": "Причина пропуска (необязательно)", + "reset_to_date": "Сбросить последнее выполнение на", + "delete_task_confirm": "Удалить эту задачу и её историю?", + "delete_object_confirm": "Удалить этот объект и все его задачи?", + "loading": "Загрузка…", + "archive": "Архивировать", + "undo": "Отменить", + "task_archived": "Задача архивирована", + "object_archived": "Объект архивирован", + "unarchive": "Разархивировать", + "archived": "В архиве", + "show_archived": "Показать архив", + "hide_archived": "Скрыть архив", + "confirm_archive_object": "Архивировать этот объект и его задачи? История сохранится, позже можно восстановить.", + "settings_archive": "Архив и хранение", + "settings_archive_desc": "Убирайте выполненные разовые задачи в архив, не удаляя их. Архивные элементы скрыты и неактивны, но сохраняют историю и расходы.", + "settings_archive_oneoff_days": "Автоархивация выполненных разовых задач через (дней, 0 = выкл)", + "settings_delete_archived_oneoff_days": "Автоудаление архивных разовых задач через (дней, 0 = никогда)", + "archive_object": "Архивировать объект", + "unarchive_object": "Разархивировать объект", + "documents": "Документы", + "documents_empty": "Пока нет документов.", + "doc_upload": "Загрузить файл", + "doc_uploading": "Загрузка…", + "doc_add_link": "Добавить ссылку", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Название (необязательно)", + "doc_open": "Открыть", + "doc_delete_confirm": "Удалить «{name}»?", + "doc_too_large": "Файл слишком большой (макс. 25 МБ).", + "doc_upload_failed": "Не удалось загрузить.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Уже сохранён в другом месте — общий, без доп. места.", + "doc_dup_in_object": "Этот файл уже прикреплён к этому объекту.", + "doc_link_invalid": "Разрешены только ссылки http/https.", + "doc_cat_manual": "Инструкция", + "doc_cat_warranty": "Гарантия", + "doc_cat_invoice": "Счёт", + "doc_cat_spare_parts": "Запчасти", + "doc_cat_photo": "Фото", + "doc_cat_other": "Другое", + "doc_link_badge": "Ссылка", + "doc_storage_title": "Хранилище документов", + "doc_storage_saved": "Сэкономлено дедупликацией", + "doc_storage_refresh": "Обновить", + "doc_download": "Скачать", + "doc_close": "Закрыть", + "doc_camera": "Сделать фото", + "doc_drop_hint": "Перетащите файлы сюда", + "doc_task_none": "Нет документов, связанных с этой задачей.", + "doc_link_existing": "Связать документ…", + "doc_attach": "Связать", + "doc_unlink": "Отвязать", + "doc_page": "Страница", + "chart_range_7d": "7д", + "chart_range_30d": "30д", + "chart_range_90d": "90д", + "chart_range_1y": "1г", + "chart_since_service": "с последнего обслуживания", + "chart_no_stats": "Нет долгосрочной статистики для этой сущности — показаны только значения из записей обслуживания", + "auto_complete_on_recovery": "Автозавершение при восстановлении датчика", + "auto_complete_on_recovery_help": "Записывает выполнение (обновляет дату последнего обслуживания), когда триггер устраняется сам — например, соль досыпана, фильтр заменён.", + "doc_search": "Поиск документов…", + "doc_search_none": "Нет подходящих документов", + "link_device_optional": "Привязать к существующему устройству (необязательно)", + "parent_object_optional": "Родительский объект (необязательно)", + "parent_none": "(Без родителя)", + "paused": "Приостановлено", + "pause_object": "Приостановить", + "resume_object": "Возобновить", + "pause_until_prompt": "Заморозить расписания этого объекта — ничего не наступает и не уведомляет до возобновления. При желании укажите дату автоматического возобновления.", + "pause_until_label": "Возобновить (необязательно)", + "object_paused": "Объект приостановлен", + "object_resumed": "Объект возобновлён — расписания перезапущены", + "object_paused_badge": "Приостановлено", + "paused_until_label": "до", + "replace_object": "Заменить…", + "replace_object_prompt": "Списать этот объект и создать преемника. История и затраты остаются в архиве старого; задачи и документы переходят к новому, счётчики начинаются заново.", + "replace_name_label": "Имя преемника", + "object_replaced": "Объект заменён — преемник создан", + "reading_unit_label": "Единица показания (напр. кВт·ч, м³)", + "reading_unit_help": "Отображается рядом с записанным значением при выполнении этой задачи.", + "reading_value_label": "Показание", + "reading_label": "Показание", + "settings_templates_label": "Галерея шаблонов", + "settings_templates_hint": "Снимите отметку с шаблонов, которые вам не нужны — они исчезнут из выбора «Из шаблона» (панель и config flow). Больше ничего не меняется; можно включить обратно в любой момент.", + "worksheet": "Рабочий лист", + "worksheet_scan_view": "Сканируйте, чтобы открыть задачу", + "worksheet_scan_complete": "Сканируйте для выполнения", + "worksheet_manual_excerpt": "Выдержка из руководства", + "worksheet_pages": "страницы", + "worksheet_printed": "Напечатано", + "worksheet_never": "Никогда" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/sv.json b/custom_components/maintenance_supporter/frontend-src/locales/sv.json new file mode 100644 index 0000000..6e88345 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/sv.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Underhåll", + "objects": "Objekt", + "tasks": "Uppgifter", + "overdue": "Försenad", + "due_soon": "Snart", + "triggered": "Utlöst", + "ok": "OK", + "all": "Alla", + "new_object": "+ Nytt objekt", + "templates_from": "Från mall", + "templates_title": "Börja från en mall", + "templates_task_count": "{n} uppgifter", + "template_created": "Skapad från mall", + "onboard_hint": "Lägg till ditt första objekt för att spåra underhåll.", + "edit": "Redigera", + "duplicate": "Duplicera", + "task_duplicated": "Uppgift duplicerad", + "object_duplicated": "Objekt duplicerat", + "delete": "Ta bort", + "add_task": "+ Lägg till uppgift", + "complete": "Slutför", + "completed": "Slutförd", + "skip": "Hoppa över", + "skipped": "Hoppade över", + "missed": "Missed", + "reset": "Återställ", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Avbryt", + "bulk_select": "Välj", + "bulk_select_all": "Välj alla", + "bulk_n_selected": "{n} valda", + "bulk_completed": "{n} uppgifter slutförda", + "bulk_archived": "{n} uppgifter arkiverade", + "completing": "Slutför…", + "interval": "Intervall", + "warning": "Varning", + "last_performed": "Senast utförd", + "next_due": "Nästa förfallodatum", + "days_until_due": "Dagar till förfallodatum", + "avg_duration": "Snittlig varaktighet", + "trigger": "Utlösare", + "trigger_type": "Utlösartyp", + "threshold_above": "Övre gräns", + "threshold_below": "Undre gräns", + "threshold": "Tröskel", + "counter": "Räknare", + "state_change": "Tillståndsändring", + "runtime": "Körtid", + "runtime_hours": "Måltid (timmar)", + "target_value": "Målvärde", + "baseline": "Baslinje", + "target_changes": "Antal målförändringar", + "for_minutes": "Under (minuter)", + "time_based": "Tidsbaserad", + "sensor_based": "Sensorbaserad", + "manual": "Manuell", + "one_time": "Engångs", + "weekdays": "Veckodagar", + "nth_weekday": "N:te veckodag i månaden", + "day_of_month": "Dag i månaden", + "recurrence_on_days": "Upprepa på", + "recurrence_occurrence": "Förekomst", + "recurrence_weekday": "Veckodag", + "recurrence_day": "Dag i månaden (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1:a", + "ord_2": "2:a", + "ord_3": "3:e", + "ord_4": "4:e", + "ord_5": "5:e", + "ord_last": "Sista", + "day_word": "Dag", + "interval_value": "Intervall", + "interval_unit": "Enhet", + "unit_days": "Dagar", + "unit_weeks": "Veckor", + "unit_months": "Månader", + "unit_years": "År", + "due_date": "Förfallodatum", + "cleaning": "Rengöring", + "inspection": "Inspektion", + "replacement": "Byte", + "calibration": "Kalibrering", + "service": "Service", + "reading": "Avläsning", + "custom": "Anpassad", + "history": "Historik", + "cost": "Kostnad", + "report_button": "Rapport", + "report_title": "Underhållsrapport", + "report_generated": "Genererad", + "report_times_done": "Utfört", + "report_total_cost": "Total kostnad", + "report_every": "var {n}:e {unit}", + "report_notes": "Anteckningar", + "report_col_type": "Typ", + "report_col_status": "Status", + "report_col_schedule": "Schema", + "duration": "Varaktighet", + "both": "Båda", + "trigger_val": "Utlösarvärde", + "complete_title": "Slutför: ", + "checklist": "Checklista", + "checklist_steps_optional": "Checkliststeg (valfritt)", + "checklist_placeholder": "Rengör filter\nByt tätning\nTesta tryck", + "checklist_help": "Ett steg per rad. Max 100 objekt.", + "err_too_long": "{field}: för lång (max {n} tecken)", + "err_too_short": "{field}: för kort (min {n} tecken)", + "err_value_too_high": "{field}: för stor (max {n})", + "err_value_too_low": "{field}: för liten (min {n})", + "err_required": "{field}: krävs", + "err_wrong_type": "{field}: fel typ (förväntad: {type})", + "err_invalid_choice": "{field}: ej tillåtet värde", + "err_invalid_value": "{field}: ogiltigt värde", + "feat_schedule_time": "Schemaläggning per tid på dygnet", + "feat_schedule_time_desc": "Uppgifter blir försenade vid en specifik tid på dygnet istället för midnatt.", + "schedule_time_optional": "Förfaller kl. (valfritt, HH:MM)", + "schedule_time_help": "Tomt = midnatt (standard). HA-tidszon.", + "at_time": "kl.", + "notes_optional": "Anteckningar (valfritt)", + "cost_optional": "Kostnad (valfritt)", + "duration_minutes": "Varaktighet i minuter (valfritt)", + "days": "dagar", + "day": "dag", + "today": "Idag", + "d_overdue": "d försenad", + "no_tasks": "Inga underhållsuppgifter ännu. Skapa ett objekt för att komma igång.", + "no_tasks_short": "Inga uppgifter", + "no_history": "Inga historikposter ännu.", + "show_all": "Visa alla", + "cost_duration_chart": "Kostnad och varaktighet", + "installed": "Installerad", + "confirm_delete_object": "Ta bort detta objekt och alla dess uppgifter?", + "confirm_delete_task": "Ta bort denna uppgift?", + "min": "Min", + "max": "Max", + "save": "Spara", + "saving": "Sparar…", + "edit_task": "Redigera uppgift", + "new_task": "Ny underhållsuppgift", + "task_name": "Uppgiftsnamn", + "maintenance_type": "Underhållstyp", + "priority": "Prioritet", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Låg", + "priority_normal": "Normal", + "priority_high": "Hög", + "schedule_type": "Schematyp", + "interval_days": "Intervall (dagar)", + "warning_days": "Varningsdagar", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Senast utförd (valfritt)", + "interval_anchor": "Intervallankare", + "anchor_completion": "Från slutförandedatum", + "anchor_planned": "Från planerat datum (ingen drift)", + "edit_object": "Redigera objekt", + "name": "Namn", + "manufacturer_optional": "Tillverkare (valfritt)", + "model_optional": "Modell (valfritt)", + "serial_number_optional": "Serienummer (valfritt)", + "serial_number_label": "S/N", + "documentation_url_label": "Manual", + "object_notes_label": "Anteckningar", + "sort_due_date": "Förfallodatum", + "sort_object": "Objektnamn", + "sort_type": "Typ", + "sort_task_name": "Uppgiftsnamn", + "all_objects": "Alla objekt", + "tasks_lower": "uppgifter", + "no_tasks_yet": "Inga uppgifter ännu", + "add_first_task": "Lägg till första uppgift", + "trigger_configuration": "Utlösarkonfiguration", + "entity_id": "Entitets-ID", + "comma_separated": "kommaseparerad", + "entity_logic": "Entitetslogik", + "entity_logic_any": "Vilken entitet som helst utlöser", + "entity_logic_all": "Alla entiteter måste utlösa", + "entities": "entiteter", + "attribute_optional": "Attribut (valfritt, tomt = tillstånd)", + "use_entity_state": "Använd entitetstillstånd (inget attribut)", + "trigger_above": "Utlös över", + "trigger_below": "Utlös under", + "for_at_least_minutes": "Under minst (minuter)", + "safety_interval_days": "Säkerhetsintervall (dagar, valfritt)", + "safety_interval": "Säkerhetsintervall (valfritt)", + "delta_mode": "Delta-läge", + "from_state_optional": "Från tillstånd (valfritt)", + "to_state_optional": "Till tillstånd (valfritt)", + "documentation_url_optional": "Dokumentations-URL (valfritt)", + "object_notes_optional": "Anteckningar (valfritt)", + "nfc_tag_id_optional": "NFC-tagg-ID (valfritt)", + "nfc_tags_empty_help": "Inga NFC-taggar registrerade i Home Assistant än.", + "nfc_tags_open_settings": "Öppna tag-inställningar", + "nfc_tags_refresh": "Uppdatera", + "environmental_entity_optional": "Miljösensor (valfritt)", + "environmental_entity_helper": "t.ex. sensor.outdoor_temperature — justerar intervallet baserat på miljöförhållanden", + "environmental_attribute_optional": "Miljöattribut (valfritt)", + "nfc_tag_id": "NFC-tagg-ID", + "nfc_linked": "NFC-tagg länkad", + "nfc_link_hint": "Klicka för att länka NFC-tagg", + "responsible_user": "Ansvarig användare", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ingen användare tilldelad)", + "all_users": "Alla användare", + "my_tasks": "Mina uppgifter", + "tab_calendar": "Kalender", + "cal_no_events": "Inget underhåll", + "cal_window_7": "7 dagar", + "cal_window_14": "14 dagar", + "cal_window_30": "30 dagar", + "cal_window_365": "1 år", + "cal_every_n_days": "var {n} dag", + "cal_source_time": "Tidsbaserad", + "cal_source_time_adaptive": "Tidsbaserad (adaptiv)", + "cal_source_sensor": "Sensorbaserad", + "cal_predicted": "förutsagt", + "cal_confidence_high": "hög säkerhet", + "cal_confidence_medium": "medelhög säkerhet", + "cal_confidence_low": "låg säkerhet", + "budget_monthly": "Månatlig budget", + "budget_yearly": "Årlig budget", + "groups": "Grupper", + "new_group": "Ny grupp", + "edit_group": "Redigera grupp", + "no_groups": "Inga grupper ännu", + "delete_group": "Ta bort grupp", + "delete_group_confirm": "Ta bort grupp '{name}'?", + "group_select_tasks": "Välj uppgifter", + "group_name_required": "Namn krävs", + "description_optional": "Beskrivning (valfritt)", + "selected": "Valda", + "loading_chart": "Laddar diagramdata...", + "hide_outliers": "Dölj avvikare (sensorfel)", + "was_maintenance_needed": "Behövdes detta underhåll?", + "feedback_needed": "Behövdes", + "feedback_not_needed": "Behövdes inte", + "feedback_not_sure": "Osäker", + "suggested_interval": "Föreslaget intervall", + "apply_suggestion": "Tillämpa", + "reanalyze": "Analysera igen", + "reanalyze_result": "Ny analys", + "reanalyze_insufficient_data": "Otillräckligt med data för rekommendation", + "data_points": "datapunkter", + "dismiss_suggestion": "Avvisa", + "confidence_low": "Låg", + "confidence_medium": "Medel", + "confidence_high": "Hög", + "recommended": "rekommenderad", + "seasonal_awareness": "Säsongsmedvetenhet", + "edit_seasonal_overrides": "Redigera säsongsfaktorer", + "seasonal_overrides_title": "Säsongsfaktorer (åsidosätt)", + "seasonal_overrides_hint": "Faktor per månad (0.1–5.0). Tomt = lärt automatiskt.", + "seasonal_override_invalid": "Ogiltigt värde", + "seasonal_override_range": "Faktor måste vara mellan 0.1 och 5.0", + "clear_all": "Rensa alla", + "seasonal_chart_title": "Säsongsfaktorer", + "seasonal_learned": "Lärt", + "seasonal_manual": "Manuell", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "Maj", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Dec", + "sensor_prediction": "Sensorprediktion", + "degradation_trend": "Trend", + "trend_rising": "Stigande", + "trend_falling": "Fallande", + "trend_stable": "Stabil", + "trend_insufficient_data": "Otillräcklig data", + "days_until_threshold": "Dagar till tröskel", + "threshold_exceeded": "Tröskel överskriden", + "environmental_adjustment": "Miljöfaktor", + "sensor_prediction_urgency": "Sensor förutsäger tröskel om ~{days} dagar", + "day_short": "d", + "weibull_reliability_curve": "Tillförlitlighetskurva", + "weibull_failure_probability": "Felsannolikhet", + "weibull_r_squared": "Anpassning R²", + "beta_early_failures": "Tidiga fel", + "beta_random_failures": "Slumpmässiga fel", + "beta_wear_out": "Slitage", + "beta_highly_predictable": "Mycket förutsägbar", + "confidence_interval": "Konfidensintervall", + "confidence_conservative": "Konservativ", + "confidence_aggressive": "Optimistisk", + "current_interval_marker": "Aktuellt intervall", + "recommended_marker": "Rekommenderat", + "characteristic_life": "Karakteristisk livslängd", + "chart_mini_sparkline": "Trenddiagram", + "chart_history": "Kostnads- och varaktighetshistorik", + "chart_seasonal": "Säsongsfaktorer, 12 månader", + "chart_weibull": "Weibull tillförlitlighetskurva", + "chart_sparkline": "Sensorutlösarvärdesdiagram", + "days_progress": "Dagsförlopp", + "qr_code": "QR-kod", + "qr_generating": "Genererar QR-kod…", + "qr_error": "Kunde inte generera QR-kod.", + "qr_error_no_url": "Ingen HA-URL konfigurerad. Ange en extern eller intern URL i Inställningar → System → Nätverk.", + "save_error": "Kunde inte spara. Försök igen.", + "qr_print": "Skriv ut", + "qr_download": "Ladda ner SVG", + "qr_action": "Åtgärd vid skanning", + "qr_action_view": "Visa underhållsinformation", + "qr_action_complete": "Markera underhåll som slutfört", + "qr_url_mode": "Länktyp", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Översikt", + "analysis": "Analys", + "recent_activities": "Senaste aktiviteter", + "search_notes": "Sök i anteckningar", + "avg_cost": "Snittlig kostnad", + "no_advanced_features": "Inga avancerade funktioner aktiverade", + "no_advanced_features_hint": "Aktivera „Adaptiva intervall” eller „Säsongsmönster” i integrationsinställningar för att se analysdata här.", + "analysis_not_enough_data": "Inte tillräckligt med data för analys ännu.", + "analysis_not_enough_data_hint": "Weibull-analys kräver minst 5 slutförda underhåll; säsongsmönster blir synliga efter 6+ datapunkter per månad.", + "analysis_manual_task_hint": "Manuella uppgifter utan intervall genererar inte analysdata.", + "completions": "slutföranden", + "current": "Aktuell", + "shorter": "Kortare", + "longer": "Längre", + "normal": "Normal", + "disabled": "Inaktiverad", + "compound_logic": "Sammansatt logik", + "compound": "Sammansatt (flera villkor)", + "compound_logic_and": "OCH — alla villkor måste utlösas", + "compound_logic_or": "ELLER — ett villkor räcker", + "compound_help": "Kombinera flera sensorvillkor till en utlösare.", + "compound_no_conditions": "Inga villkor än — lägg till minst ett.", + "compound_add_condition": "Lägg till villkor", + "compound_condition": "Villkor", + "compound_remove_condition": "Ta bort villkor", + "card_title": "Titel", + "card_show_header": "Visa rubrik med statistik", + "card_show_actions": "Visa åtgärdsknappar", + "card_compact": "Kompakt läge", + "card_max_items": "Max objekt (0 = alla)", + "card_filter_status": "Filtrera efter status", + "card_filter_status_help": "Tomt = visa alla statusar.", + "card_filter_objects": "Filtrera efter objekt", + "card_filter_objects_help": "Tomt = visa alla objekt.", + "card_filter_entities": "Filtrera efter entiteter (entity_ids)", + "card_filter_entities_help": "Välj sensor- / binary_sensor-entiteter från denna integration. Tomt = alla.", + "card_loading_objects": "Laddar objekt…", + "card_load_error": "Kunde inte ladda objekt — kontrollera WebSocket-anslutningen.", + "card_no_tasks_title": "Inga underhållsuppgifter än", + "card_no_tasks_cta": "→ Skapa en i Maintenance-panelen", + "no_objects": "Inga objekt än.", + "action_error": "Åtgärden misslyckades. Försök igen.", + "area_id_optional": "Område (valfritt)", + "installation_date_optional": "Installationsdatum (valfritt)", + "warranty_expiry_optional": "Garantins utgång (valfritt)", + "warranty": "Garanti", + "warranty_valid_until": "giltig till {date}", + "warranty_expires_in": "går ut om {days} dagar", + "warranty_expired": "utgången", + "cal_past_windows": "Tidigare fönster", + "cal_forward_windows": "Kommande fönster", + "history_edit_title": "Redigera historikpost", + "history_edit_timestamp": "Tidsstämpel", + "manufacturer": "Tillverkare", + "model": "Modell", + "area": "Område", + "actions": "Åtgärder", + "view_mode_label": "Vy", + "view_cards": "Kortvy", + "view_table": "Tabellvy", + "objects_table_columns_label": "Kolumner i objekttabell", + "objects_table_columns_hint": "Välj vilka kolumner som visas i objekttabellvyn.", + "custom_icon_optional": "Ikon (valfritt, t.ex. mdi:wrench)", + "task_enabled": "Uppgift aktiverad", + "skip_reason_prompt": "Hoppa över denna uppgift?", + "reason_optional": "Anledning (valfritt)", + "reset_date_prompt": "Markera uppgift som utförd?", + "reset_date_optional": "Datum för senaste utförande (valfritt, standard idag)", + "notes_label": "Anteckningar", + "documentation_label": "Dokumentation", + "no_nfc_tag": "— Ingen tagg —", + "dashboard": "Översikt", + "tab_today": "Idag", + "palette_placeholder": "Sök objekt och uppgifter…", + "palette_no_results": "Inga träffar", + "palette_hint": "↑↓ navigera · Enter öppna · Esc stäng", + "today_all_caught_up": "Allt klart! Inget den här veckan.", + "today_overdue": "Försenade", + "today_due_today": "Förfaller idag", + "today_this_week": "Den här veckan", + "settings": "Inställningar", + "settings_features": "Avancerade funktioner", + "settings_features_desc": "Aktivera eller inaktivera avancerade funktioner. Inaktivering döljer dem från UI men tar inte bort data.", + "feat_adaptive": "Adaptiv schemaläggning", + "feat_adaptive_desc": "Lär dig optimala intervall från underhållshistorik", + "feat_predictions": "Sensorpredictions", + "feat_predictions_desc": "Förutsäg utlösningsdatum från sensordegradering", + "feat_seasonal": "Säsongsjusteringar", + "feat_seasonal_desc": "Justera intervall baserat på säsongsmönster", + "feat_environmental": "Miljökorrelation", + "feat_environmental_desc": "Korrelera intervall med temperatur/luftfuktighet", + "feat_budget": "Budgetuppföljning", + "feat_budget_desc": "Spåra månatliga och årliga underhållsutgifter", + "feat_groups": "Uppgiftsgrupper", + "feat_groups_desc": "Organisera uppgifter i logiska grupper", + "feat_checklists": "Checklistor", + "feat_checklists_desc": "Flerstegs procedurer för uppgiftens slutförande", + "settings_general": "Allmänt", + "settings_default_warning": "Standard varningsdagar", + "settings_panel_enabled": "Sidopanel", + "settings_panel_title": "Sidopanelens titel", + "settings_notifications": "Notifikationer", + "settings_notify_service": "Notifikationstjänst", + "test_notification": "Testnotifikation", + "send_test": "Skicka test", + "testing": "Skickar…", + "test_notification_success": "Testnotifikation skickad", + "test_notification_failed": "Testnotifikation misslyckades", + "settings_notify_due_soon": "Notifiera när snart förfallande", + "settings_notify_overdue": "Notifiera när försenad", + "settings_notify_triggered": "Notifiera när utlöst", + "settings_interval_hours": "Upprepningsintervall (timmar, 0 = en gång)", + "settings_quiet_hours": "Tysta timmar", + "settings_quiet_start": "Start", + "settings_quiet_end": "Slut", + "settings_max_per_day": "Max notifikationer per dag (0 = obegränsat)", + "settings_bundling": "Bunta notifikationer", + "settings_bundle_threshold": "Buntningströskel", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobila åtgärdsknappar", + "settings_action_complete": "Visa 'Slutför'-knapp", + "settings_action_skip": "Visa 'Hoppa över'-knapp", + "settings_action_snooze": "Visa 'Snooza'-knapp", + "settings_weekly_digest": "Veckosammanfattning", + "settings_weekly_digest_hint": "En sammanfattningsavisering måndag morgon när uppgifter förfaller.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Snooza-tid (timmar)", + "settings_budget": "Budget", + "settings_currency": "Valuta", + "settings_budget_monthly": "Månatlig budget", + "settings_budget_yearly": "Årlig budget", + "settings_budget_alerts": "Budgetvarningar", + "settings_budget_threshold": "Varningströskel (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Exportera JSON", + "settings_export_yaml": "Exportera YAML", + "settings_export_csv": "Exportera CSV", + "settings_import_csv": "Importera CSV", + "settings_import_placeholder": "Klistra in JSON- eller CSV-innehåll här…", + "settings_import_btn": "Importera", + "settings_import_success": "{count} objekt importerade.", + "settings_export_success": "Export nedladdad.", + "settings_saved": "Inställning sparad.", + "settings_include_history": "Inkludera historik", + "sort_alphabetical": "Alfabetisk", + "sort_due_soonest": "Närmaste förfallodatum", + "sort_task_count": "Antal uppgifter", + "sort_area": "Område", + "sort_assigned_user": "Tilldelad användare", + "sort_group": "Grupp", + "groupby_none": "Ingen gruppering", + "groupby_area": "Per område", + "groupby_group": "Per grupp", + "groupby_user": "Per användare", + "filter_label": "Filter", + "user_label": "Användare", + "sort_label": "Sortering", + "group_by_label": "Gruppera efter", + "state_value_help": "Använd HA-tillståndsvärdet (vanligtvis med små bokstäver, t.ex. \"on\"/\"off\"). Versaler normaliseras vid sparande.", + "target_changes_help": "Antal matchande övergångar innan utlösaren aktiveras (standard: 1).", + "qr_print_title": "Skriv ut QR-koder", + "qr_print_desc": "Skapa en utskriftssida med QR-koder att klippa ut och sätta på din utrustning.", + "qr_print_load": "Ladda objekt", + "qr_print_filter": "Filter", + "qr_print_objects": "Objekt", + "qr_print_actions": "Åtgärder", + "qr_print_url_mode": "Länktyp", + "qr_print_estimate": "Uppskattade QR-koder", + "qr_print_over_limit": "gräns 200, begränsa filtret", + "qr_print_generate": "Skapa QR-koder", + "qr_print_generating": "Skapar…", + "qr_print_ready": "QR-koder klara", + "qr_print_print_button": "Skriv ut", + "qr_print_empty": "Inget att skapa", + "qr_action_skip": "Hoppa över", + "vacation_title": "Semesterläge", + "vacation_active": "aktivt", + "vacation_ended": "avslutat", + "vacation_desc": "Planera din semester: aviseringar pausas under perioden plus några buffert-dagar. Du kan göra undantag per uppgift.", + "vacation_enable": "Aktivera semesterläge", + "vacation_start": "Start", + "vacation_end": "Slut", + "vacation_buffer": "Buffert (dagar)", + "vacation_exempt_title": "Avisera ändå under semester", + "vacation_exempt_desc": "Välj uppgifter som ska avisera även under semestern (t.ex. kritisk poolkemi).", + "vacation_load_tasks": "Ladda uppgifter", + "vacation_preview_btn": "Visa förhandsgranskning", + "vacation_preview_affected": "uppgifter berörda", + "vacation_event_due_soon": "blir snart förfallen", + "vacation_event_overdue": "blir försenad", + "vacation_event_triggered_est": "möjlig sensorutlösning", + "vacation_sensor_based": "(sensorbaserad)", + "vacation_action_notify": "Avisera ändå", + "vacation_action_unsilence": "Tysta igen", + "vacation_marked_complete": "Markerad som klar", + "vacation_marked_skip": "Hoppade över", + "vacation_end_now": "Avsluta semester nu", + "unassigned": "Otilldelad", + "no_area": "Inget område", + "has_overdue": "Har försenade uppgifter", + "object": "Objekt", + "settings_panel_access": "Paneltillgång", + "settings_panel_access_desc": "Administratörer har alltid full åtkomst. För att delegera skapa, redigera och ta bort till specifika icke-admin-användare, slå på detta och välj dem nedan — alla andra ser endast Slutför och Hoppa över.", + "settings_operator_write": "Tillåt valda användare att skapa, redigera och ta bort", + "settings_operator_write_desc": "Av: endast administratörer kan ändra innehåll. På: de valda användarna nedan får också full åtkomst.", + "no_non_admin_users": "Inga icke-admin-användare hittades. Lägg till några i Inställningar → Personer.", + "owner_label": "Ägare", + "feat_completion_actions": "Slutförande-åtgärder", + "feat_completion_actions_desc": "HA-åtgärd per uppgift vid slutförande + snabb-slutför-QR med förinställda värden.", + "on_complete_action_title": "Vid slutförande: utlös HA-åtgärd (valfritt)", + "on_complete_action_desc": "Anropar en HA-tjänst när uppgiften slutförs — t.ex. återställ en räknare på enheten.", + "on_complete_action_service": "Tjänst", + "on_complete_action_target": "Målentitet", + "on_complete_action_data": "Data (JSON, valfritt)", + "on_complete_action_test": "Testa åtgärd", + "on_complete_action_test_success": "Lyckades", + "on_complete_action_test_failed": "Misslyckades", + "quick_complete_defaults_title": "Snabb-slutför standardvärden (för QR-skanningar, valfritt)", + "quick_complete_defaults_desc": "Förinställda värden för snabb-slutför-QR. Utan dessa öppnar QR slutför-dialogen.", + "quick_complete_defaults_notes": "Anteckningar", + "quick_complete_defaults_cost": "Kostnad", + "quick_complete_defaults_duration": "Varaktighet (minuter)", + "quick_complete_defaults_feedback_none": "Ingen feedback", + "quick_complete_defaults_feedback_needed": "Var nödvändigt", + "quick_complete_defaults_feedback_not_needed": "Inte nödvändigt", + "quick_complete_success": "Snabbt markerad som klar", + "trigger_replaced": "Utlösare ersatt", + "add": "Lägg till", + "show_stats": "Visa statistik + grafer", + "hide_stats": "Dölj statistik", + "adaptive_no_data": "Ännu inte tillräckligt med slutförandehistorik för adaptiv analys. Slutför denna uppgift några gånger till för att låsa upp intervallrekommendationer och tillförlitlighetsdiagram.", + "suggestion_applied": "Föreslaget intervall tillämpat", + "vacation_mode": "Semesterläge", + "vacation_status_active": "Aktivt nu", + "vacation_status_scheduled": "Schemalagt", + "vacation_status_inactive": "Inaktivt", + "vacation_end_now_confirm": "Avsluta semestern omedelbart?", + "vacation_exempt_count": "undantagna", + "vacation_advanced": "Avancerat…", + "vacation_open_panel": "Öppna i panelen", + "enable": "Aktivera", + "saved": "Sparat", + "budget_monthly_set": "Ange månad", + "budget_yearly_set": "Ange år", + "budget_advanced": "Valuta, varningar…", + "budget_open_panel": "Öppna i panelen", + "groups_empty": "Inga grupper än.", + "group_new_placeholder": "Lägg till grupp…", + "group_delete_confirm": "Ta bort gruppen ”{name}”?", + "groups_manage_tasks": "Hantera uppgiftstilldelningar…", + "groups_open_panel": "Öppna i panelen", + "on_complete_action_target_hint": "Obs: entitetens domän måste matcha tjänsten — t.ex. 'button.press' fungerar bara på button.*, 'counter.increment' bara på counter.*, 'input_button.press' bara på input_button.* osv. Vid en avvikelse misslyckas åtgärden tyst (HA loggar 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Visa alla objekt", + "show_all_tasks": "Rensa filter — visa alla uppgifter", + "filter_to_overdue": "Filtrera listan till endast försenade", + "filter_to_due_soon": "Filtrera listan till endast snart förfallna", + "filter_to_triggered": "Filtrera listan till endast utlösta", + "open_task": "Öppna uppgift", + "show_details": "Visa historik + statistik", + "hide_details": "Dölj detaljer", + "history_empty": "Ingen historik än.", + "history_edit_button": "Redigera post", + "total_cost": "Total kostnad", + "times_performed": "Utförd", + "older_entries": "äldre", + "open_in_panel": "Öppna i Underhållspanelen", + "skip_reason": "Anledning till överhoppning (valfritt)", + "reset_to_date": "Återställ senast utförd till", + "delete_task_confirm": "Ta bort denna uppgift och dess historik?", + "delete_object_confirm": "Ta bort detta objekt och alla dess uppgifter?", + "loading": "Laddar…", + "archive": "Arkivera", + "undo": "Ångra", + "task_archived": "Uppgift arkiverad", + "object_archived": "Objekt arkiverat", + "unarchive": "Återställ", + "archived": "Arkiverad", + "show_archived": "Visa arkiverade", + "hide_archived": "Dölj arkiverade", + "confirm_archive_object": "Arkivera detta objekt och dess uppgifter? Historiken behålls och kan återställas senare.", + "settings_archive": "Arkiv och lagring", + "settings_archive_desc": "Lägg undan slutförda engångsuppgifter utan att radera dem. Arkiverade objekt döljs och är inaktiva men behåller historik och kostnad.", + "settings_archive_oneoff_days": "Arkivera slutförda engångsuppgifter automatiskt efter (dagar, 0 = av)", + "settings_delete_archived_oneoff_days": "Radera arkiverade engångsuppgifter automatiskt efter (dagar, 0 = aldrig)", + "archive_object": "Arkivera objekt", + "unarchive_object": "Återställ objekt", + "documents": "Dokument", + "documents_empty": "Inga dokument ännu.", + "doc_upload": "Ladda upp fil", + "doc_uploading": "Laddar upp…", + "doc_add_link": "Lägg till länk", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titel (valfritt)", + "doc_open": "Öppna", + "doc_delete_confirm": "Ta bort \"{name}\"?", + "doc_too_large": "Filen är för stor (max 25 MB).", + "doc_upload_failed": "Uppladdningen misslyckades.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Redan lagrad någon annanstans — delad, inget extra utrymme.", + "doc_dup_in_object": "Den här filen är redan kopplad till detta objekt.", + "doc_link_invalid": "Endast http/https-länkar tillåts.", + "doc_cat_manual": "Manual", + "doc_cat_warranty": "Garanti", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Reservdelar", + "doc_cat_photo": "Foto", + "doc_cat_other": "Övrigt", + "doc_link_badge": "Länk", + "doc_storage_title": "Dokumentlagring", + "doc_storage_saved": "Sparat genom deduplicering", + "doc_storage_refresh": "Uppdatera", + "doc_download": "Ladda ner", + "doc_close": "Stäng", + "doc_camera": "Ta foto", + "doc_drop_hint": "Släpp filer här", + "doc_task_none": "Inga dokument kopplade till denna uppgift.", + "doc_link_existing": "Koppla ett dokument…", + "doc_attach": "Koppla", + "doc_unlink": "Koppla bort", + "doc_page": "Sida", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1å", + "chart_since_service": "sedan senaste underhåll", + "chart_no_stats": "Ingen långtidsstatistik för denna entitet — visar endast värden från underhållsposter", + "auto_complete_on_recovery": "Slutför automatiskt när sensorn återhämtar sig", + "auto_complete_on_recovery_help": "Registrerar ett slutförande (sätter senast utförd) när utlösaren löser sig själv — t.ex. salt påfyllt, filter bytt.", + "doc_search": "Sök dokument…", + "doc_search_none": "Inga matchande dokument", + "link_device_optional": "Länka till befintlig enhet (valfritt)", + "parent_object_optional": "Överordnat objekt (valfritt)", + "parent_none": "(Inget överordnat)", + "paused": "Pausad", + "pause_object": "Pausa", + "resume_object": "Återuppta", + "pause_until_prompt": "Frys objektets scheman — inget förfaller och inget aviserar förrän det återupptas. Ange valfritt ett datum för automatisk återupptagning.", + "pause_until_label": "Återuppta den (valfritt)", + "object_paused": "Objekt pausat", + "object_resumed": "Objekt återupptaget — scheman omstartade", + "object_paused_badge": "Pausad", + "paused_until_label": "till", + "replace_object": "Ersätt…", + "replace_object_prompt": "Pensionera detta objekt och skapa en efterträdare. Historik och kostnader arkiveras på det gamla; uppgifter och dokument följer med till det nya, räknare börjar om.", + "replace_name_label": "Efterträdarens namn", + "object_replaced": "Objekt ersatt — efterträdare skapad", + "reading_unit_label": "Avläsningsenhet (t.ex. kWh, m³)", + "reading_unit_help": "Visas bredvid det registrerade värdet när uppgiften slutförs.", + "reading_value_label": "Avläst värde", + "reading_label": "Avläsning", + "settings_templates_label": "Mallgalleri", + "settings_templates_hint": "Avmarkera mallar du aldrig behöver — de försvinner från \"Från mall\"-väljarna (panel och config flow). Inget annat ändras; kan återaktiveras när som helst.", + "worksheet": "Arbetsblad", + "worksheet_scan_view": "Skanna för att öppna uppgiften", + "worksheet_scan_complete": "Skanna för att slutföra", + "worksheet_manual_excerpt": "Utdrag ur manualen", + "worksheet_pages": "sidor", + "worksheet_printed": "Utskriven", + "worksheet_never": "Aldrig" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/uk.json b/custom_components/maintenance_supporter/frontend-src/locales/uk.json new file mode 100644 index 0000000..ff18efc --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/uk.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Обслуговування", + "objects": "Об'єкти", + "tasks": "Завдання", + "overdue": "Прострочено", + "due_soon": "Незабаром", + "triggered": "Спрацювало", + "ok": "Норма", + "all": "Всі", + "new_object": "+ Новий об'єкт", + "templates_from": "Із шаблону", + "templates_title": "Почати із шаблону", + "templates_task_count": "{n} завдань", + "template_created": "Створено із шаблону", + "onboard_hint": "Додайте перший об'єкт, щоб відстежувати обслуговування.", + "edit": "Редагувати", + "duplicate": "Дублювати", + "task_duplicated": "Завдання продубльовано", + "object_duplicated": "Об'єкт продубльовано", + "delete": "Видалити", + "add_task": "+ Додати завдання", + "complete": "Виконати", + "completed": "Виконано", + "skip": "Пропустити", + "skipped": "Пропущено", + "missed": "Missed", + "reset": "Скинути", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Скасувати", + "bulk_select": "Вибрати", + "bulk_select_all": "Вибрати все", + "bulk_n_selected": "Вибрано: {n}", + "bulk_completed": "Виконано завдань: {n}", + "bulk_archived": "Архівовано завдань: {n}", + "completing": "Виконується…", + "interval": "Інтервал", + "warning": "Попередження", + "last_performed": "Останнє виконання", + "next_due": "Наступний термін", + "days_until_due": "Днів до терміну", + "avg_duration": "Сер. тривалість", + "trigger": "Тригер", + "trigger_type": "Тип тригера", + "threshold_above": "Верхня межа", + "threshold_below": "Нижня межа", + "threshold": "Поріг", + "counter": "Лічильник", + "state_change": "Зміна стану", + "runtime": "Напрацювання", + "runtime_hours": "Цільове напрацювання (години)", + "target_value": "Цільове значення", + "baseline": "Базове значення", + "target_changes": "Цільова кількість змін", + "for_minutes": "Протягом (хвилин)", + "time_based": "За часом", + "sensor_based": "За сенсором", + "manual": "Вручну", + "one_time": "Одноразово", + "weekdays": "Дні тижня", + "nth_weekday": "N-й день тижня в місяці", + "day_of_month": "День місяця", + "recurrence_on_days": "Повторювати у", + "recurrence_occurrence": "Входження", + "recurrence_weekday": "День тижня", + "recurrence_day": "День місяця (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1-й", + "ord_2": "2-й", + "ord_3": "3-й", + "ord_4": "4-й", + "ord_5": "5-й", + "ord_last": "Останній", + "day_word": "День", + "interval_value": "Інтервал", + "interval_unit": "Одиниця", + "unit_days": "Дні", + "unit_weeks": "Тижні", + "unit_months": "Місяці", + "unit_years": "Роки", + "due_date": "Дата виконання", + "cleaning": "Очищення", + "inspection": "Огляд", + "replacement": "Заміна", + "calibration": "Калібрування", + "service": "Сервіс", + "reading": "Показання", + "custom": "Власний", + "history": "Історія", + "cost": "Вартість", + "report_button": "Звіт", + "report_title": "Звіт про обслуговування", + "report_generated": "Створено", + "report_times_done": "Виконано", + "report_total_cost": "Загальна вартість", + "report_every": "кожні {n} {unit}", + "report_notes": "Нотатки", + "report_col_type": "Тип", + "report_col_status": "Статус", + "report_col_schedule": "Розклад", + "duration": "Тривалість", + "both": "Обидва", + "trigger_val": "Значення тригера", + "complete_title": "Виконати: ", + "checklist": "Чекліст", + "checklist_steps_optional": "Кроки чекліста (необов'язково)", + "checklist_placeholder": "Очистити фільтр\nЗамінити ущільнювач\nПеревірити тиск", + "checklist_help": "Один крок на рядок. Макс. 100 елементів.", + "err_too_long": "{field}: задовге (макс. {n} символів)", + "err_too_short": "{field}: закоротке (мін. {n} символів)", + "err_value_too_high": "{field}: завелике (макс. {n})", + "err_value_too_low": "{field}: замале (мін. {n})", + "err_required": "{field}: обов'язкове поле", + "err_wrong_type": "{field}: невірний тип (очікувалось: {type})", + "err_invalid_choice": "{field}: недопустиме значення", + "err_invalid_value": "{field}: невірне значення", + "feat_schedule_time": "Планування за часом доби", + "feat_schedule_time_desc": "Задачі стають простроченими у певний час доби, а не опівночі.", + "schedule_time_optional": "Час прострочення (необов'язково, HH:MM)", + "schedule_time_help": "Порожньо = опівночі (за замовчуванням). Часовий пояс HA.", + "at_time": "о", + "notes_optional": "Примітки (необов'язково)", + "cost_optional": "Вартість (необов'язково)", + "duration_minutes": "Тривалість у хвилинах (необов'язково)", + "days": "днів", + "day": "день", + "today": "Сьогодні", + "d_overdue": "д прострочено", + "no_tasks": "Завдань обслуговування ще немає. Створіть об'єкт, щоб почати.", + "no_tasks_short": "Немає завдань", + "no_history": "Записів в історії ще немає.", + "show_all": "Показати всі", + "cost_duration_chart": "Вартість і тривалість", + "installed": "Встановлено", + "confirm_delete_object": "Видалити цей об'єкт і всі його завдання?", + "confirm_delete_task": "Видалити це завдання?", + "min": "Мін", + "max": "Макс", + "save": "Зберегти", + "saving": "Збереження…", + "edit_task": "Редагувати завдання", + "new_task": "Нове завдання обслуговування", + "task_name": "Назва завдання", + "maintenance_type": "Тип обслуговування", + "priority": "Пріоритет", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Низький", + "priority_normal": "Звичайний", + "priority_high": "Високий", + "schedule_type": "Тип розкладу", + "interval_days": "Інтервал (дні)", + "warning_days": "Днів попередження", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "interval_anchor": "Прив'язка інтервалу", + "anchor_completion": "Від дати виконання", + "anchor_planned": "Від запланованої дати (без зміщення)", + "edit_object": "Редагувати об'єкт", + "name": "Назва", + "manufacturer_optional": "Виробник (необов'язково)", + "model_optional": "Модель (необов'язково)", + "serial_number_optional": "Серійний номер (необов'язково)", + "serial_number_label": "С/Н", + "documentation_url_label": "Посібник", + "object_notes_label": "Примітки", + "last_performed_optional": "Останнé виконання (необов'язково)", + "sort_due_date": "Дата терміну", + "sort_object": "Назва об'єкта", + "sort_type": "Тип", + "sort_task_name": "Назва завдання", + "all_objects": "Всі об'єкти", + "tasks_lower": "завдань", + "no_tasks_yet": "Завдань ще немає", + "add_first_task": "Додати перше завдання", + "trigger_configuration": "Налаштування тригера", + "entity_id": "ID об'єкта", + "comma_separated": "через кому", + "entity_logic": "Логіка об'єктів", + "entity_logic_any": "Будь-який об'єкт спрацьовує", + "entity_logic_all": "Всі об'єкти мають спрацювати", + "entities": "об'єктів", + "attribute_optional": "Атрибут (необов'язково, порожньо = стан)", + "use_entity_state": "Використовувати стан об'єкта (без атрибута)", + "trigger_above": "Спрацювати, коли вище", + "trigger_below": "Спрацювати, коли нижче", + "for_at_least_minutes": "Протягом не менше (хвилин)", + "safety_interval_days": "Страховий інтервал (дні, необов'язково)", + "safety_interval": "Страховий інтервал (необов'язково)", + "delta_mode": "Режим дельти", + "from_state_optional": "З стану (необов'язково)", + "to_state_optional": "До стану (необов'язково)", + "documentation_url_optional": "URL документації (необов'язково)", + "object_notes_optional": "Примітки (необов'язково)", + "nfc_tag_id_optional": "ID NFC-тега (необов'язково)", + "nfc_tags_empty_help": "У Home Assistant ще не зареєстровано NFC-теги.", + "nfc_tags_open_settings": "Відкрити налаштування тегів", + "nfc_tags_refresh": "Оновити", + "environmental_entity_optional": "Датчик навколишнього середовища (необов'язково)", + "environmental_entity_helper": "напр. sensor.outdoor_temperature — коригує інтервал відповідно до умов навколишнього середовища", + "environmental_attribute_optional": "Атрибут середовища (необов'язково)", + "nfc_tag_id": "ID NFC-тега", + "nfc_linked": "NFC-тег прив'язано", + "nfc_link_hint": "Натисніть, щоб прив'язати NFC-тег", + "responsible_user": "Відповідальний користувач", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Користувача не призначено)", + "all_users": "Всі користувачі", + "my_tasks": "Мої завдання", + "tab_calendar": "Календар", + "cal_no_events": "Без обслуговування", + "cal_window_7": "7 днів", + "cal_window_14": "14 днів", + "cal_window_30": "30 днів", + "cal_window_365": "1 рік", + "cal_every_n_days": "кожні {n} днів", + "cal_source_time": "За часом", + "cal_source_time_adaptive": "За часом (адаптивно)", + "cal_source_sensor": "За датчиком", + "cal_predicted": "прогноз", + "cal_confidence_high": "висока надійність", + "cal_confidence_medium": "середня надійність", + "cal_confidence_low": "низька надійність", + "budget_monthly": "Щомісячний бюджет", + "budget_yearly": "Щорічний бюджет", + "groups": "Групи", + "new_group": "Нова група", + "edit_group": "Редагувати групу", + "no_groups": "Груп ще немає", + "delete_group": "Видалити групу", + "delete_group_confirm": "Видалити групу '{name}'?", + "group_select_tasks": "Обрати завдання", + "group_name_required": "Потрібна назва", + "description_optional": "Опис (необов'язково)", + "selected": "Обрано", + "loading_chart": "Завантаження даних графіка...", + "hide_outliers": "Приховати викиди (збої датчика)", + "was_maintenance_needed": "Чи було потрібне це обслуговування?", + "feedback_needed": "Потрібне", + "feedback_not_needed": "Не потрібне", + "feedback_not_sure": "Не впевнений", + "suggested_interval": "Рекомендований інтервал", + "apply_suggestion": "Застосувати", + "reanalyze": "Повторно проаналізувати", + "reanalyze_result": "Новий аналіз", + "reanalyze_insufficient_data": "Недостатньо даних для рекомендації", + "data_points": "точок даних", + "dismiss_suggestion": "Відхилити", + "confidence_low": "Низька", + "confidence_medium": "Середня", + "confidence_high": "Висока", + "recommended": "рекомендовано", + "seasonal_awareness": "Сезонна корекція", + "edit_seasonal_overrides": "Редагувати сезонні коефіцієнти", + "seasonal_overrides_title": "Сезонні коефіцієнти (перевизначення)", + "seasonal_overrides_hint": "Коефіцієнт на місяць (0.1–5.0). Порожньо = автоматично.", + "seasonal_override_invalid": "Недійсне значення", + "seasonal_override_range": "Коефіцієнт має бути між 0.1 та 5.0", + "clear_all": "Очистити все", + "seasonal_chart_title": "Сезонні коефіцієнти", + "seasonal_learned": "Навчена", + "seasonal_manual": "Ручна", + "month_jan": "Січ", + "month_feb": "Лют", + "month_mar": "Бер", + "month_apr": "Кві", + "month_may": "Тра", + "month_jun": "Чер", + "month_jul": "Лип", + "month_aug": "Сер", + "month_sep": "Вер", + "month_oct": "Жов", + "month_nov": "Лис", + "month_dec": "Гру", + "sensor_prediction": "Прогноз сенсора", + "degradation_trend": "Тренд", + "trend_rising": "Зростає", + "trend_falling": "Спадає", + "trend_stable": "Стабільний", + "trend_insufficient_data": "Недостатньо даних", + "days_until_threshold": "Днів до порогу", + "threshold_exceeded": "Поріг перевищено", + "environmental_adjustment": "Екологічний коефіцієнт", + "sensor_prediction_urgency": "Сенсор прогнозує досягнення порогу через ~{days} днів", + "day_short": "день", + "weibull_reliability_curve": "Крива надійності", + "weibull_failure_probability": "Ймовірність відмови", + "weibull_r_squared": "Точність R²", + "beta_early_failures": "Ранні відмови", + "beta_random_failures": "Випадкові відмови", + "beta_wear_out": "Знос", + "beta_highly_predictable": "Дуже передбачуваний", + "confidence_interval": "Довірчий інтервал", + "confidence_conservative": "Консервативний", + "confidence_aggressive": "Оптимістичний", + "current_interval_marker": "Поточний інтервал", + "recommended_marker": "Рекомендовано", + "characteristic_life": "Характеристичний ресурс", + "chart_mini_sparkline": "Мінімальний графік тренду", + "chart_history": "Історія вартості та тривалості", + "chart_seasonal": "Сезонні коефіцієнти, 12 місяців", + "chart_weibull": "Крива надійності Вейбулла", + "chart_sparkline": "Графік значень тригера сенсора", + "days_progress": "Прогрес днів", + "qr_code": "QR-код", + "qr_generating": "Генерація QR-коду…", + "qr_error": "Не вдалося згенерувати QR-код.", + "qr_error_no_url": "URL Home Assistant не налаштовано. Задайте зовнішню або внутрішню URL-адресу в Налаштування → Система → Мережа.", + "save_error": "Не вдалося зберегти. Спробуйте ще раз.", + "qr_print": "Друкувати", + "qr_download": "Завантажити SVG", + "qr_action": "Дія при скануванні", + "qr_action_view": "Переглянути", + "qr_action_complete": "Позначити обслуговування виконаним", + "qr_url_mode": "Тип посилання", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Локальний (mDNS)", + "qr_mode_server": "URL сервера", + "overview": "Огляд", + "analysis": "Аналіз", + "recent_activities": "Остання активність", + "search_notes": "Пошук у примітках", + "avg_cost": "Сер. вартість", + "no_advanced_features": "Розширені функції не увімкнено", + "no_advanced_features_hint": "Увімкніть «Адаптивні інтервали» або «Сезонні закономірності» в налаштуваннях інтеграції, щоб побачити тут дані аналізу.", + "analysis_not_enough_data": "Недостатньо даних для аналізу.", + "analysis_not_enough_data_hint": "Аналіз Вейбулла потребує щонайменше 5 виконаних обслуговувань; сезонні закономірності стають видимими після 6+ записів на місяць.", + "analysis_manual_task_hint": "Ручні завдання без інтервалу не генерують дані аналізу.", + "completions": "виконань", + "current": "Поточний", + "shorter": "Коротший", + "longer": "Довший", + "normal": "Звичайний", + "disabled": "Вимкнено", + "compound_logic": "Складена логіка", + "compound": "Складений (кілька умов)", + "compound_logic_and": "І — усі умови мають спрацювати", + "compound_logic_or": "АБО — достатньо будь-якої умови", + "compound_help": "Об'єднайте кілька умов датчиків в один тригер.", + "compound_no_conditions": "Ще немає умов — додайте принаймні одну.", + "compound_add_condition": "Додати умову", + "compound_condition": "Умова", + "compound_remove_condition": "Видалити умову", + "card_title": "Заголовок", + "card_show_header": "Показувати заголовок зі статистикою", + "card_show_actions": "Показувати кнопки дій", + "card_compact": "Компактний режим", + "card_max_items": "Макс. елементів (0 = всі)", + "card_filter_status": "Фільтрувати за статусом", + "card_filter_status_help": "Порожньо = показати всі статуси.", + "card_filter_objects": "Фільтрувати за об'єктами", + "card_filter_objects_help": "Порожньо = показати всі об'єкти.", + "card_filter_entities": "Фільтрувати за сутностями (entity_ids)", + "card_filter_entities_help": "Виберіть сутності sensor / binary_sensor з цієї інтеграції. Порожньо = всі.", + "card_loading_objects": "Завантаження об'єктів…", + "card_load_error": "Не вдалося завантажити об'єкти — перевірте WebSocket-з'єднання.", + "card_no_tasks_title": "Поки немає завдань обслуговування", + "card_no_tasks_cta": "→ Створіть на панелі Maintenance", + "no_objects": "Поки немає об'єктів.", + "action_error": "Дія не вдалась. Спробуйте ще раз.", + "area_id_optional": "Зона (необов'язково)", + "installation_date_optional": "Дата встановлення (необов'язково)", + "warranty_expiry_optional": "Закінчення гарантії (необов'язково)", + "warranty": "Гарантія", + "warranty_valid_until": "дійсна до {date}", + "warranty_expires_in": "закінчується через {days} дн.", + "warranty_expired": "закінчилася", + "cal_past_windows": "Минулі вікна", + "cal_forward_windows": "Майбутні вікна", + "history_edit_title": "Редагувати запис історії", + "history_edit_timestamp": "Позначка часу", + "manufacturer": "Виробник", + "model": "Модель", + "area": "Зона", + "actions": "Дії", + "view_mode_label": "Вигляд", + "view_cards": "Картки", + "view_table": "Таблиця", + "objects_table_columns_label": "Стовпці таблиці об'єктів", + "objects_table_columns_hint": "Виберіть, які стовпці показувати в табличному вигляді об'єктів.", + "custom_icon_optional": "Іконка (необов'язково, наприклад mdi:wrench)", + "task_enabled": "Завдання увімкнено", + "skip_reason_prompt": "Пропустити це завдання?", + "reason_optional": "Причина (необов'язково)", + "reset_date_prompt": "Позначити як виконане?", + "reset_date_optional": "Дата останнього виконання (необов'язково, типово: сьогодні)", + "notes_label": "Примітки", + "documentation_label": "Документація", + "no_nfc_tag": "— Без тега —", + "dashboard": "Дашборд", + "tab_today": "Сьогодні", + "palette_placeholder": "Пошук об'єктів і завдань…", + "palette_no_results": "Немає збігів", + "palette_hint": "↑↓ навігація · Enter відкрити · Esc закрити", + "today_all_caught_up": "Усе зроблено! Цього тижня нічого немає.", + "today_overdue": "Прострочені", + "today_due_today": "Сьогодні", + "today_this_week": "Цього тижня", + "settings": "Налаштування", + "settings_features": "Розширені функції", + "settings_features_desc": "Увімкніть або вимкніть розширені функції. Вимкнення приховує їх з інтерфейсу, але не видаляє дані.", + "feat_adaptive": "Адаптивне планування", + "feat_adaptive_desc": "Навчатися оптимальним інтервалам з історії обслуговування", + "feat_predictions": "Прогнози за сенсорами", + "feat_predictions_desc": "Прогнозувати дати спрацювання за деградацією сенсора", + "feat_seasonal": "Сезонні корекції", + "feat_seasonal_desc": "Коригувати інтервали на основі сезонних закономірностей", + "feat_environmental": "Кореляція з довкіллям", + "feat_environmental_desc": "Корелювати інтервали з температурою/вологістю", + "feat_budget": "Відстеження бюджету", + "feat_budget_desc": "Відстежувати щомісячні та щорічні витрати на обслуговування", + "feat_groups": "Групи завдань", + "feat_groups_desc": "Організовувати завдання в логічні групи", + "feat_checklists": "Чеклісти", + "feat_checklists_desc": "Багатокрокові процедури для виконання завдань", + "settings_general": "Загальне", + "settings_default_warning": "Днів попередження за замовчуванням", + "settings_panel_enabled": "Панель у бічному меню", + "settings_panel_title": "Заголовок панелі", + "settings_notifications": "Сповіщення", + "settings_notify_service": "Служба сповіщень", + "test_notification": "Тестове сповіщення", + "send_test": "Надіслати тест", + "testing": "Надсилання…", + "test_notification_success": "Тестове сповіщення надіслано", + "test_notification_failed": "Не вдалося надіслати тестове сповіщення", + "settings_notify_due_soon": "Сповіщати, коли термін наближається", + "settings_notify_overdue": "Сповіщати про прострочення", + "settings_notify_triggered": "Сповіщати про спрацювання", + "settings_interval_hours": "Інтервал повторення (години, 0 = одноразово)", + "settings_quiet_hours": "Тихі години", + "settings_quiet_start": "Початок", + "settings_quiet_end": "Кінець", + "settings_max_per_day": "Макс. сповіщень на день (0 = без обмежень)", + "settings_bundling": "Групувати сповіщення", + "settings_bundle_threshold": "Поріг групування", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Кнопки дій у мобільних сповіщеннях", + "settings_action_complete": "Показувати кнопку «Виконати»", + "settings_action_skip": "Показувати кнопку «Пропустити»", + "settings_action_snooze": "Показувати кнопку «Відкласти»", + "settings_weekly_digest": "Щотижневий огляд", + "settings_weekly_digest_hint": "Одне зведене сповіщення в понеділок вранці, коли є завдання.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Тривалість відкладення (години)", + "settings_budget": "Бюджет", + "settings_currency": "Валюта", + "settings_budget_monthly": "Щомісячний бюджет", + "settings_budget_yearly": "Щорічний бюджет", + "settings_budget_alerts": "Сповіщення про бюджет", + "settings_budget_threshold": "Поріг сповіщення (%)", + "settings_import_export": "Імпорт / Експорт", + "settings_export_json": "Експортувати JSON", + "settings_export_yaml": "Експортувати YAML", + "settings_export_csv": "Експортувати CSV", + "settings_import_csv": "Імпортувати CSV", + "settings_import_placeholder": "Вставте вміст JSON або CSV сюди…", + "settings_import_btn": "Імпортувати", + "settings_import_success": "{count} об'єктів успішно імпортовано.", + "settings_export_success": "Експорт завантажено.", + "settings_saved": "Налаштування збережено.", + "settings_include_history": "Включити історію", + "sort_alphabetical": "За алфавітом", + "sort_due_soonest": "Найближчий термін", + "sort_task_count": "Кількість завдань", + "sort_area": "Зона", + "sort_assigned_user": "Призначений користувач", + "sort_group": "Група", + "groupby_none": "Без групування", + "groupby_area": "За зоною", + "groupby_group": "За групою", + "groupby_user": "За користувачем", + "filter_label": "Фільтр", + "user_label": "Користувач", + "sort_label": "Сортування", + "group_by_label": "Групувати за", + "state_value_help": "Використовуйте значення стану HA (зазвичай у нижньому регістрі, напр. \"on\"/\"off\"). Регістр нормалізується при збереженні.", + "target_changes_help": "Кількість відповідних переходів, після яких тригер спрацює (за замовчуванням: 1).", + "qr_print_title": "Друкувати QR-коди", + "qr_print_desc": "Створи сторінку для друку з QR-кодами, які можна вирізати та наклеїти на обладнання.", + "qr_print_load": "Завантажити об'єкти", + "qr_print_filter": "Фільтр", + "qr_print_objects": "Об'єкти", + "qr_print_actions": "Дії", + "qr_print_url_mode": "Тип посилання", + "qr_print_estimate": "Прогноз QR-кодів", + "qr_print_over_limit": "ліміт 200, звузь фільтр", + "qr_print_generate": "Створити QR-коди", + "qr_print_generating": "Створення…", + "qr_print_ready": "QR-коди готові", + "qr_print_print_button": "Друкувати", + "qr_print_empty": "Нічого створювати", + "qr_action_skip": "Пропустити", + "vacation_title": "Режим відпустки", + "vacation_active": "активний", + "vacation_ended": "завершено", + "vacation_desc": "Сплануй відпустку: сповіщення призупиняються на період плюс кілька буферних днів. Можна налаштувати винятки.", + "vacation_enable": "Увімкнути режим відпустки", + "vacation_start": "Початок", + "vacation_end": "Кінець", + "vacation_buffer": "Буфер (днів)", + "vacation_exempt_title": "Сповіщати попри відпустку", + "vacation_exempt_desc": "Обери завдання, які мають сповіщати навіть під час відпустки (наприклад, критична хімія басейну).", + "vacation_load_tasks": "Завантажити завдання", + "vacation_preview_btn": "Показати попередній перегляд", + "vacation_preview_affected": "завдань зачеплено", + "vacation_event_due_soon": "наближається термін", + "vacation_event_overdue": "стане простроченим", + "vacation_event_triggered_est": "можливе спрацювання сенсора", + "vacation_sensor_based": "(сенсорне)", + "vacation_action_notify": "Сповіщати все одно", + "vacation_action_unsilence": "Знову вимкнути сповіщення", + "vacation_marked_complete": "Позначено як виконане", + "vacation_marked_skip": "Пропущено", + "vacation_end_now": "Завершити відпустку зараз", + "unassigned": "Не призначено", + "no_area": "Без зони", + "has_overdue": "Прострочені завдання", + "object": "Об'єкт", + "settings_panel_access": "Доступ до панелі", + "settings_panel_access_desc": "Адміністратори завжди мають повний доступ. Щоб делегувати створення, редагування та видалення певним не-адмін користувачам, увімкніть це та виберіть їх нижче — інші бачать лише Виконати та Пропустити.", + "settings_operator_write": "Дозволити вибраним користувачам створювати, редагувати та видаляти", + "settings_operator_write_desc": "Вимкнено: лише адміністратори можуть змінювати вміст. Увімкнено: вибрані користувачі нижче також отримують повний доступ.", + "no_non_admin_users": "Не знайдено не-адмін користувачів. Додайте їх у Налаштуваннях → Особи.", + "owner_label": "Власник", + "feat_completion_actions": "Дії при завершенні", + "feat_completion_actions_desc": "Дія HA по завданню при завершенні + QR швидкого завершення з попередньо встановленими значеннями.", + "on_complete_action_title": "При завершенні: викликати HA-дію (опційно)", + "on_complete_action_desc": "Викликає HA-сервіс, коли завдання завершено — напр., скинути лічильник на пристрої.", + "on_complete_action_service": "Сервіс", + "on_complete_action_target": "Цільова сутність", + "on_complete_action_data": "Дані (JSON, опційно)", + "on_complete_action_test": "Тестувати дію", + "on_complete_action_test_success": "Успіх", + "on_complete_action_test_failed": "Помилка", + "quick_complete_defaults_title": "Стандартні значення швидкого завершення (для QR-сканів, опційно)", + "quick_complete_defaults_desc": "Попередньо встановлені значення для QR швидкого завершення. Без них QR відкриває діалог.", + "quick_complete_defaults_notes": "Нотатки", + "quick_complete_defaults_cost": "Вартість", + "quick_complete_defaults_duration": "Тривалість (хвилин)", + "quick_complete_defaults_feedback_none": "Без зворотного зв'язку", + "quick_complete_defaults_feedback_needed": "Було необхідно", + "quick_complete_defaults_feedback_not_needed": "Не було необхідно", + "quick_complete_success": "Швидко позначено виконаним", + "trigger_replaced": "Тригер замінено", + "add": "Додати", + "show_stats": "Показати статистику + графіки", + "hide_stats": "Сховати статистику", + "adaptive_no_data": "Поки що недостатньо історії виконань для адаптивного аналізу. Виконайте це завдання ще кілька разів, щоб розблокувати рекомендації щодо інтервалу та графіки надійності.", + "suggestion_applied": "Запропонований інтервал застосовано", + "vacation_mode": "Режим відпустки", + "vacation_status_active": "Активний зараз", + "vacation_status_scheduled": "Заплановано", + "vacation_status_inactive": "Неактивний", + "vacation_end_now_confirm": "Завершити відпустку негайно?", + "vacation_exempt_count": "виключено", + "vacation_advanced": "Додатково…", + "vacation_open_panel": "Відкрити на панелі", + "enable": "Увімкнути", + "saved": "Збережено", + "budget_monthly_set": "Задати місячний", + "budget_yearly_set": "Задати річний", + "budget_advanced": "Валюта, сповіщення…", + "budget_open_panel": "Відкрити на панелі", + "groups_empty": "Поки немає груп.", + "group_new_placeholder": "Додати групу…", + "group_delete_confirm": "Видалити групу «{name}»?", + "groups_manage_tasks": "Керування призначеннями завдань…", + "groups_open_panel": "Відкрити на панелі", + "on_complete_action_target_hint": "Примітка: домен сутності має відповідати сервісу — напр. 'button.press' працює лише з button.*, 'counter.increment' лише з counter.*, 'input_button.press' лише з input_button.* тощо. У разі невідповідності дія мовчки не виконається (HA запише в журнал 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Показати всі об'єкти", + "show_all_tasks": "Скинути фільтр — показати всі завдання", + "filter_to_overdue": "Фільтрувати список лише за простроченими", + "filter_to_due_soon": "Фільтрувати список лише за тими, що скоро настануть", + "filter_to_triggered": "Фільтрувати список лише за спрацьованими", + "open_task": "Відкрити завдання", + "show_details": "Показати історію + статистику", + "hide_details": "Сховати деталі", + "history_empty": "Поки немає історії.", + "history_edit_button": "Редагувати запис", + "total_cost": "Загальна вартість", + "times_performed": "Виконано", + "older_entries": "раніше", + "open_in_panel": "Відкрити на панелі обслуговування", + "skip_reason": "Причина пропуску (необов'язково)", + "reset_to_date": "Скинути останнє виконання на", + "delete_task_confirm": "Видалити це завдання та його історію?", + "delete_object_confirm": "Видалити цей об'єкт і всі його завдання?", + "loading": "Завантаження…", + "archive": "Архівувати", + "undo": "Скасувати", + "task_archived": "Завдання архівовано", + "object_archived": "Об'єкт архівовано", + "unarchive": "Розархівувати", + "archived": "В архіві", + "show_archived": "Показати архів", + "hide_archived": "Сховати архів", + "confirm_archive_object": "Архівувати цей об'єкт та його завдання? Історія збережеться, згодом можна відновити.", + "settings_archive": "Архів і зберігання", + "settings_archive_desc": "Відкладайте виконані одноразові завдання в архів, не видаляючи їх. Архівні елементи приховані й неактивні, але зберігають історію та витрати.", + "settings_archive_oneoff_days": "Автоархівування виконаних одноразових завдань через (днів, 0 = вимк)", + "settings_delete_archived_oneoff_days": "Автовидалення архівних одноразових завдань через (днів, 0 = ніколи)", + "archive_object": "Архівувати об'єкт", + "unarchive_object": "Розархівувати об'єкт", + "documents": "Документи", + "documents_empty": "Ще немає документів.", + "doc_upload": "Завантажити файл", + "doc_uploading": "Завантаження…", + "doc_add_link": "Додати посилання", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Назва (необов'язково)", + "doc_open": "Відкрити", + "doc_delete_confirm": "Видалити «{name}»?", + "doc_too_large": "Файл завеликий (макс. 25 МБ).", + "doc_upload_failed": "Не вдалося завантажити.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Уже збережено в іншому місці — спільний, без додаткового місця.", + "doc_dup_in_object": "Цей файл уже прикріплено до цього об'єкта.", + "doc_link_invalid": "Дозволені лише посилання http/https.", + "doc_cat_manual": "Інструкція", + "doc_cat_warranty": "Гарантія", + "doc_cat_invoice": "Рахунок", + "doc_cat_spare_parts": "Запчастини", + "doc_cat_photo": "Фото", + "doc_cat_other": "Інше", + "doc_link_badge": "Посилання", + "doc_storage_title": "Сховище документів", + "doc_storage_saved": "Заощаджено дедуплікацією", + "doc_storage_refresh": "Оновити", + "doc_download": "Завантажити", + "doc_close": "Закрити", + "doc_camera": "Зробити фото", + "doc_drop_hint": "Перетягніть файли сюди", + "doc_task_none": "Немає документів, пов'язаних із цим завданням.", + "doc_link_existing": "Пов'язати документ…", + "doc_attach": "Пов'язати", + "doc_unlink": "Відв'язати", + "doc_page": "Сторінка", + "chart_range_7d": "7д", + "chart_range_30d": "30д", + "chart_range_90d": "90д", + "chart_range_1y": "1р", + "chart_since_service": "з останнього обслуговування", + "chart_no_stats": "Немає довгострокової статистики для цієї сутності — показано лише значення із записів обслуговування", + "auto_complete_on_recovery": "Автозавершення при відновленні датчика", + "auto_complete_on_recovery_help": "Записує виконання (оновлює дату останнього обслуговування), коли тригер зникає сам — наприклад, сіль досипано, фільтр замінено.", + "doc_search": "Пошук документів…", + "doc_search_none": "Немає відповідних документів", + "link_device_optional": "Прив'язати до наявного пристрою (необов'язково)", + "parent_object_optional": "Батьківський об'єкт (необов'язково)", + "parent_none": "(Без батьківського)", + "paused": "Призупинено", + "pause_object": "Призупинити", + "resume_object": "Відновити", + "pause_until_prompt": "Заморозити розклади цього об'єкта — нічого не настає і не сповіщає до відновлення. За бажанням укажіть дату автоматичного відновлення.", + "pause_until_label": "Відновити (необов'язково)", + "object_paused": "Об'єкт призупинено", + "object_resumed": "Об'єкт відновлено — розклади перезапущено", + "object_paused_badge": "Призупинено", + "paused_until_label": "до", + "replace_object": "Замінити…", + "replace_object_prompt": "Списати цей об'єкт і створити наступника. Історія та витрати лишаються в архіві старого; завдання й документи переходять до нового, лічильники починаються заново.", + "replace_name_label": "Ім'я наступника", + "object_replaced": "Об'єкт замінено — наступника створено", + "reading_unit_label": "Одиниця показання (напр. кВт·год, м³)", + "reading_unit_help": "Показується поруч із записаним значенням під час виконання цієї задачі.", + "reading_value_label": "Показання", + "reading_label": "Показання", + "settings_templates_label": "Галерея шаблонів", + "settings_templates_hint": "Зніміть позначку з шаблонів, які вам не потрібні — вони зникнуть із вибору «З шаблону» (панель і config flow). Більше нічого не змінюється; можна ввімкнути знову будь-коли.", + "worksheet": "Робочий аркуш", + "worksheet_scan_view": "Скануйте, щоб відкрити задачу", + "worksheet_scan_complete": "Скануйте для виконання", + "worksheet_manual_excerpt": "Витяг з посібника", + "worksheet_pages": "сторінки", + "worksheet_printed": "Надруковано", + "worksheet_never": "Ніколи" +} diff --git a/custom_components/maintenance_supporter/frontend-src/locales/zh.json b/custom_components/maintenance_supporter/frontend-src/locales/zh.json new file mode 100644 index 0000000..c1cee8a --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/locales/zh.json @@ -0,0 +1,696 @@ +{ + "maintenance": "维护", + "objects": "维护项", + "tasks": "任务", + "overdue": "逾期", + "due_soon": "即将到期", + "triggered": "已触发", + "trigger_replaced": "触发器已更换", + "ok": "正常", + "all": "全部", + "new_object": "+ 新建维护项", + "templates_from": "从模板", + "templates_title": "从模板开始", + "templates_task_count": "{n} 个任务", + "template_created": "已从模板创建", + "onboard_hint": "添加第一个对象以开始跟踪维护。", + "edit": "编辑", + "duplicate": "复制", + "task_duplicated": "任务已复制", + "object_duplicated": "对象已复制", + "delete": "删除", + "add_task": "+ 添加任务", + "complete": "完成", + "completed": "已完成", + "skip": "跳过", + "skipped": "已跳过", + "missed": "Missed", + "reset": "重置", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "取消", + "bulk_select": "选择", + "bulk_select_all": "全选", + "bulk_n_selected": "已选 {n} 项", + "bulk_completed": "已完成 {n} 个任务", + "bulk_archived": "已归档 {n} 个任务", + "completing": "正在完成…", + "interval": "间隔", + "warning": "预警", + "last_performed": "上次执行", + "next_due": "下次到期", + "days_until_due": "距离到期天数", + "avg_duration": "平均耗时", + "trigger": "触发器", + "trigger_type": "触发类型", + "threshold_above": "超过阈值", + "threshold_below": "低于阈值", + "threshold": "阈值", + "counter": "计数器", + "state_change": "状态变化", + "runtime": "运行时间", + "runtime_hours": "目标运行时长 (小时)", + "target_value": "目标值", + "baseline": "基准线", + "target_changes": "目标变化次数", + "for_minutes": "持续 (分钟)", + "time_based": "基于时间", + "sensor_based": "基于传感器", + "manual": "手动", + "one_time": "一次性", + "weekdays": "工作日", + "nth_weekday": "每月第 N 个工作日", + "day_of_month": "每月某天", + "recurrence_on_days": "重复日期", + "recurrence_occurrence": "发生次数", + "recurrence_weekday": "星期", + "recurrence_day": "每月日期 (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "第 1", + "ord_2": "第 2", + "ord_3": "第 3", + "ord_4": "第 4", + "ord_5": "第 5", + "ord_last": "最后", + "day_word": "日", + "interval_value": "间隔数值", + "interval_unit": "单位", + "unit_days": "天", + "unit_weeks": "周", + "unit_months": "月", + "unit_years": "年", + "due_date": "到期日期", + "cleaning": "清洁", + "inspection": "检查", + "replacement": "更换", + "calibration": "校准", + "service": "保养", + "reading": "读数", + "custom": "自定义", + "history": "历史记录", + "cost": "成本", + "report_button": "报告", + "report_title": "维护报告", + "report_generated": "生成于", + "report_times_done": "完成次数", + "report_total_cost": "总费用", + "report_every": "每 {n} {unit}", + "report_notes": "备注", + "report_col_type": "类型", + "report_col_status": "状态", + "report_col_schedule": "计划", + "duration": "耗时", + "both": "两者", + "trigger_val": "触发值", + "complete_title": "完成: ", + "checklist": "检查清单", + "checklist_steps_optional": "检查步骤 (可选)", + "checklist_placeholder": "清理过滤器\n更换密封圈\n测试压力", + "checklist_help": "每行一个步骤。最多 100 项。", + "err_too_long": "{field}: 太长 (最多 {n} 个字符)", + "err_too_short": "{field}: 太短 (最少 {n} 个字符)", + "err_value_too_high": "{field}: 太大 (最大 {n})", + "err_value_too_low": "{field}: 太小 (最小 {n})", + "err_required": "{field}: 必填项", + "err_wrong_type": "{field}: 类型错误 (预期: {type})", + "err_invalid_choice": "{field}: 无效选项", + "err_invalid_value": "{field}: 无效数值", + "feat_schedule_time": "具体时间调度", + "feat_schedule_time_desc": "任务将在特定时间点变为逾期,而非默认的午夜。", + "schedule_time_optional": "到期时间 (可选, HH:MM)", + "schedule_time_help": "留空 = 午夜 (默认)。基于 HA 时区。", + "at_time": "于", + "notes_optional": "备注 (可选)", + "cost_optional": "成本 (可选)", + "duration_minutes": "耗时 (分钟, 可选)", + "days": "天", + "day": "天", + "today": "今天", + "d_overdue": "天逾期", + "no_tasks": "尚无维护任务。请创建一个维护项开始使用。", + "no_tasks_short": "无任务", + "no_history": "尚无历史记录。", + "show_all": "全部显示", + "cost_duration_chart": "成本与耗时", + "installed": "已安装", + "confirm_delete_object": "确定删除此维护项及其所有任务吗?", + "confirm_delete_task": "确定删除此任务吗?", + "min": "最小", + "max": "最大", + "save": "保存", + "saving": "正在保存…", + "edit_task": "编辑任务", + "new_task": "新建维护任务", + "task_name": "任务名称", + "maintenance_type": "维护类型", + "priority": "优先级", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "低", + "priority_normal": "普通", + "priority_high": "高", + "schedule_type": "计划类型", + "interval_days": "间隔 (天)", + "warning_days": "预警天数", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "上次执行时间 (可选)", + "interval_anchor": "计划锚点", + "anchor_completion": "从完成日期起算", + "anchor_planned": "从计划日期起算 (无偏差)", + "edit_object": "编辑维护项", + "name": "名称", + "manufacturer_optional": "制造商 (可选)", + "model_optional": "型号 (可选)", + "serial_number_optional": "序列号 (可选)", + "serial_number_label": "序列号", + "documentation_url_label": "手册", + "object_notes_label": "备注", + "sort_due_date": "到期时间", + "sort_object": "维护项名称", + "sort_type": "类型", + "sort_task_name": "任务名称", + "all_objects": "所有维护项", + "tasks_lower": "任务", + "no_tasks_yet": "尚无任务", + "add_first_task": "添加首个任务", + "trigger_configuration": "触发器配置", + "entity_id": "实体 ID", + "comma_separated": "逗号分隔", + "entity_logic": "实体逻辑", + "entity_logic_any": "任一实体触发", + "entity_logic_all": "所有实体均触发", + "entities": "实体", + "attribute_optional": "属性 (可选, 留空 = 状态)", + "use_entity_state": "使用实体状态 (不使用属性)", + "trigger_above": "高于此值触发", + "trigger_below": "低于此值触发", + "for_at_least_minutes": "持续至少 (分钟)", + "safety_interval_days": "安全间隔 (天, 可选)", + "safety_interval": "安全间隔 (可选)", + "delta_mode": "增量模式", + "from_state_optional": "起始状态 (可选)", + "to_state_optional": "目标状态 (可选)", + "documentation_url_optional": "文档链接 (可选)", + "object_notes_optional": "备注 (可选)", + "nfc_tag_id_optional": "NFC 标签 ID (可选)", + "nfc_tags_empty_help": "Home Assistant 中尚未注册任何 NFC 标签。", + "nfc_tags_open_settings": "打开标签设置", + "nfc_tags_refresh": "刷新", + "environmental_entity_optional": "环境传感器 (可选)", + "environmental_entity_helper": "例如:sensor.outdoor_temperature — 根据环境条件自动调整间隔", + "environmental_attribute_optional": "环境属性 (可选)", + "nfc_tag_id": "NFC 标签 ID", + "nfc_linked": "NFC 标签已链接", + "nfc_link_hint": "点击链接 NFC 标签", + "responsible_user": "负责人", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(未分配)", + "all_users": "所有用户", + "my_tasks": "我的任务", + "tab_calendar": "日历", + "cal_no_events": "无维护事项", + "cal_window_7": "7 天", + "cal_window_14": "14 天", + "cal_window_30": "30 天", + "cal_window_365": "1 年", + "cal_every_n_days": "每 {n} 天", + "cal_source_time": "基于时间", + "cal_source_time_adaptive": "基于时间 (自适应)", + "cal_source_sensor": "基于传感器", + "cal_predicted": "预测", + "cal_confidence_high": "高置信度", + "cal_confidence_medium": "中置信度", + "cal_confidence_low": "低置信度", + "budget_monthly": "月度预算", + "budget_yearly": "年度预算", + "groups": "分组", + "new_group": "新建分组", + "edit_group": "编辑分组", + "no_groups": "尚无分组", + "delete_group": "删除分组", + "delete_group_confirm": "确定删除分组 '{name}' 吗?", + "group_select_tasks": "选择任务", + "group_name_required": "名称必填", + "description_optional": "描述 (可选)", + "selected": "已选择", + "loading_chart": "正在加载图表数据...", + "hide_outliers": "隐藏离群值(传感器故障)", + "was_maintenance_needed": "此次维护是否确实需要?", + "feedback_needed": "需要", + "feedback_not_needed": "不需要", + "feedback_not_sure": "不确定", + "suggested_interval": "建议间隔", + "apply_suggestion": "应用建议", + "reanalyze": "重新分析", + "reanalyze_result": "新分析结果", + "reanalyze_insufficient_data": "数据不足,无法生成建议", + "data_points": "个数据点", + "dismiss_suggestion": "忽略", + "confidence_low": "低", + "confidence_medium": "中", + "confidence_high": "高", + "recommended": "推荐", + "seasonal_awareness": "季节性感知", + "edit_seasonal_overrides": "编辑季节性修正系数", + "seasonal_overrides_title": "季节性修正系数 (覆盖)", + "seasonal_overrides_hint": "每月系数 (0.1–5.0)。留空则自动学习。", + "seasonal_override_invalid": "无效数值", + "seasonal_override_range": "系数必须介于 0.1 到 5.0 之间", + "clear_all": "全部清除", + "seasonal_chart_title": "季节性因素", + "seasonal_learned": "自动学习", + "seasonal_manual": "手动设置", + "month_jan": "一月", + "month_feb": "二月", + "month_mar": "三月", + "month_apr": "四月", + "month_may": "五月", + "month_jun": "六月", + "month_jul": "七月", + "month_aug": "八月", + "month_sep": "九月", + "month_oct": "十月", + "month_nov": "十一月", + "month_dec": "十二月", + "sensor_prediction": "传感器预测", + "degradation_trend": "趋势", + "trend_rising": "上升", + "trend_falling": "下降", + "trend_stable": "稳定", + "trend_insufficient_data": "数据不足", + "days_until_threshold": "距离阈值天数", + "threshold_exceeded": "已超过阈值", + "environmental_adjustment": "环境因子", + "sensor_prediction_urgency": "传感器预测约 {days} 天后达到阈值", + "day_short": "天", + "weibull_reliability_curve": "韦伯可靠性曲线", + "weibull_failure_probability": "故障概率", + "weibull_r_squared": "拟合度 R²", + "beta_early_failures": "早期失效", + "beta_random_failures": "随机失效", + "beta_wear_out": "耗损失效", + "beta_highly_predictable": "高度可预测", + "confidence_interval": "置信区间", + "confidence_conservative": "保守", + "confidence_aggressive": "进取", + "current_interval_marker": "当前间隔", + "recommended_marker": "推荐", + "characteristic_life": "特征寿命", + "chart_mini_sparkline": "趋势走势图", + "chart_history": "成本与时长历史", + "chart_seasonal": "12个月季节性因素图表", + "chart_weibull": "韦伯可靠性曲线图表", + "chart_sparkline": "传感器触发值图表", + "days_progress": "天数进度", + "qr_code": "二维码", + "qr_generating": "正在生成二维码…", + "qr_error": "无法生成二维码。", + "qr_error_no_url": "未配置 HA URL。请前往“设置 → 系统 → 网络”设置外部或内部 URL。", + "save_error": "保存失败。请重试。", + "qr_print": "打印", + "qr_download": "下载 SVG", + "qr_action": "扫码后动作", + "qr_action_view": "查看维护信息", + "qr_action_complete": "标记维护为已完成", + "qr_url_mode": "链接类型", + "qr_mode_companion": "Companion App", + "qr_mode_local": "本地 (mDNS)", + "qr_mode_server": "服务器 URL", + "overview": "概览", + "analysis": "分析", + "recent_activities": "近期活动", + "search_notes": "搜索备注", + "avg_cost": "平均成本", + "no_advanced_features": "未启用高级功能", + "no_advanced_features_hint": "请在集成设置中启用“自适应间隔”或“季节性模式”以在此查看分析数据。", + "analysis_not_enough_data": "暂无足够的分析数据。", + "analysis_not_enough_data_hint": "韦伯分析需要至少 5 次完成记录;季节性模式在每月有 6 个以上数据点后可见。", + "analysis_manual_task_hint": "无间隔的手动任务不会生成分析数据。", + "completions": "次完成记录", + "current": "当前", + "shorter": "较短", + "longer": "较长", + "normal": "正常", + "disabled": "已禁用", + "compound_logic": "组合逻辑", + "compound": "组合(多个条件)", + "compound_logic_and": "AND — 所有条件都需满足", + "compound_logic_or": "OR — 任一条件即可", + "compound_help": "将多个传感器条件组合成一个触发器。", + "compound_no_conditions": "还没有条件 — 至少添加一个。", + "compound_add_condition": "添加条件", + "compound_condition": "条件", + "compound_remove_condition": "删除条件", + "card_title": "标题", + "card_show_header": "显示统计信息页眉", + "card_show_actions": "显示操作按钮", + "card_compact": "紧凑模式", + "card_max_items": "最大显示项 (0 = 全部)", + "card_filter_status": "按状态过滤", + "card_filter_status_help": "留空则显示所有状态。", + "card_filter_objects": "按维护项过滤", + "card_filter_objects_help": "留空则显示所有维护项。", + "card_filter_entities": "按实体过滤 (entity_ids)", + "card_filter_entities_help": "选择该集成的传感器或二进制传感器实体。留空则显示全部。", + "card_loading_objects": "正在加载维护项…", + "card_load_error": "无法加载维护项 — 请检查 WebSocket 连接。", + "card_no_tasks_title": "暂无维护任务", + "card_no_tasks_cta": "→ 请前往维护面板创建", + "no_objects": "暂无维护项。", + "action_error": "操作失败。请重试。", + "area_id_optional": "区域 (可选)", + "installation_date_optional": "安装日期 (可选)", + "warranty_expiry_optional": "保修到期 (可选)", + "warranty": "保修", + "warranty_valid_until": "有效期至 {date}", + "warranty_expires_in": "{days} 天后到期", + "warranty_expired": "已过期", + "cal_past_windows": "过去的窗口", + "cal_forward_windows": "未来的窗口", + "history_edit_title": "编辑历史记录", + "history_edit_timestamp": "时间戳", + "manufacturer": "制造商", + "model": "型号", + "area": "区域", + "actions": "操作", + "view_mode_label": "视图", + "view_cards": "卡片视图", + "view_table": "表格视图", + "objects_table_columns_label": "对象表格列", + "objects_table_columns_hint": "选择对象表格视图中显示的列。", + "custom_icon_optional": "图标 (可选,例如 mdi:wrench)", + "task_enabled": "任务已启用", + "skip_reason_prompt": "跳过此任务?", + "reason_optional": "原因 (可选)", + "reset_date_prompt": "标记任务为已执行?", + "reset_date_optional": "最后执行日期 (可选,默认为今天)", + "notes_label": "备注", + "documentation_label": "文档", + "no_nfc_tag": "— 无标签 —", + "dashboard": "仪表盘", + "tab_today": "今天", + "palette_placeholder": "搜索对象和任务…", + "palette_no_results": "无匹配", + "palette_hint": "↑↓ 导航 · Enter 打开 · Esc 关闭", + "today_all_caught_up": "全部完成!本周没有待办。", + "today_overdue": "逾期", + "today_due_today": "今天到期", + "today_this_week": "本周", + "settings": "设置", + "settings_features": "高级功能", + "settings_features_desc": "启用或禁用高级功能。禁用将从界面隐藏相关功能,但不会删除数据。", + "feat_adaptive": "自适应计划", + "feat_adaptive_desc": "根据维护历史学习最佳间隔", + "feat_predictions": "传感器预测", + "feat_predictions_desc": "根据传感器损耗趋势预测触发日期", + "feat_seasonal": "季节性调整", + "feat_seasonal_desc": "根据季节性模式自动调整间隔", + "feat_environmental": "环境关联", + "feat_environmental_desc": "将维护间隔与温度或湿度关联", + "feat_budget": "预算追踪", + "feat_budget_desc": "追踪月度及年度维护支出", + "feat_groups": "任务组", + "feat_groups_desc": "将任务组织进逻辑组", + "feat_checklists": "检查清单", + "feat_checklists_desc": "为任务完成提供多步骤操作流程", + "settings_general": "常规", + "settings_default_warning": "默认预警天数", + "settings_panel_enabled": "侧边栏面板", + "settings_panel_title": "侧边栏面板标题", + "settings_notifications": "通知", + "settings_notify_service": "通知服务", + "test_notification": "测试通知", + "send_test": "发送测试", + "testing": "正在发送…", + "test_notification_success": "测试通知已发送", + "test_notification_failed": "测试通知发送失败", + "settings_notify_due_soon": "到期前通知提醒", + "settings_notify_overdue": "超期后通知提醒", + "settings_notify_triggered": "触发时通知提醒", + "settings_interval_hours": "重复提醒间隔 (小时,0 = 仅一次)", + "settings_quiet_hours": "静默时段", + "settings_quiet_start": "开始时间", + "settings_quiet_end": "结束时间", + "settings_max_per_day": "每日最高通知次数 (0 = 不限)", + "settings_bundling": "合并通知", + "settings_bundle_threshold": "合并阈值", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "移动端操作按钮", + "settings_action_complete": "显示“完成”按钮", + "settings_action_skip": "显示“跳过”按钮", + "settings_action_snooze": "显示“稍后”按钮", + "settings_weekly_digest": "每周摘要", + "settings_weekly_digest_hint": "当有待办任务时,周一早上发送一条汇总通知。", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "稍后提醒间隔 (小时)", + "settings_budget": "预算", + "settings_currency": "货币单位", + "settings_budget_monthly": "月度预算", + "settings_budget_yearly": "年度预算", + "settings_budget_alerts": "预算警报", + "settings_budget_threshold": "警报阈值 (%)", + "settings_import_export": "导入 / 导出", + "settings_export_json": "导出 JSON", + "settings_export_yaml": "导出 YAML", + "settings_export_csv": "导出 CSV", + "settings_import_csv": "导入 CSV", + "settings_import_placeholder": "在此粘贴 JSON 或 CSV 内容…", + "settings_import_btn": "导入", + "settings_import_success": "成功导入 {count} 个维护项。", + "settings_export_success": "导出已下载。", + "settings_saved": "设置已保存。", + "settings_include_history": "包含历史记录", + "sort_alphabetical": "字母排序", + "sort_due_soonest": "最近到期", + "sort_task_count": "任务数量", + "sort_area": "区域", + "sort_assigned_user": "负责人", + "sort_group": "任务组", + "groupby_none": "不分组", + "groupby_area": "按区域分组", + "groupby_group": "按分组", + "groupby_user": "按负责人", + "filter_label": "过滤器", + "user_label": "用户", + "sort_label": "排序", + "group_by_label": "分组依据", + "state_value_help": "使用 HA 状态值(通常为小写,例如 \"on\"/\"off\")。保存时大小写会自动规范化。", + "target_changes_help": "触发前需要满足的状态转换次数(默认为 1)。", + "qr_print_title": "打印二维码", + "qr_print_desc": "生成一张包含多个二维码的可打印页面,方便裁剪并贴在设备上。", + "qr_print_load": "加载设备", + "qr_print_filter": "过滤器", + "qr_print_objects": "设备", + "qr_print_actions": "动作", + "qr_print_url_mode": "链接类型", + "qr_print_estimate": "预计二维码数量", + "qr_print_over_limit": "上限为 200,请缩小过滤范围", + "qr_print_generate": "生成二维码", + "qr_print_generating": "正在生成…", + "qr_print_ready": "二维码已就绪", + "qr_print_print_button": "打印", + "qr_print_empty": "无内容可生成", + "qr_action_skip": "跳过", + "vacation_title": "度假模式", + "vacation_active": "已激活", + "vacation_ended": "已结束", + "vacation_desc": "计划假期:在此时段及设定的缓冲天数内将暂停通知。您可以为特定任务设置例外。", + "vacation_enable": "开启度假模式", + "vacation_start": "开始日期", + "vacation_end": "结束日期", + "vacation_buffer": "缓冲(天)", + "vacation_exempt_title": "度假期间仍保持通知的任务", + "vacation_exempt_desc": "选择在假期中仍需接收通知的任务(例如关键的水池维护)。", + "vacation_load_tasks": "加载任务", + "vacation_preview_btn": "显示预览", + "vacation_preview_affected": "受影响的任务", + "vacation_event_due_soon": "即将到期", + "vacation_event_overdue": "将超期", + "vacation_event_triggered_est": "可能触发传感器", + "vacation_sensor_based": "(基于传感器)", + "vacation_action_notify": "依然通知", + "vacation_action_unsilence": "恢复静默", + "vacation_marked_complete": "已标记为完成", + "vacation_marked_skip": "已跳过", + "vacation_end_now": "立即结束度假", + "add": "添加", + "show_stats": "显示统计与图表", + "hide_stats": "隐藏统计", + "adaptive_no_data": "尚无足够的完成记录用于自适应分析。请完成此任务几次,以解锁间隔建议和可靠性图表。", + "suggestion_applied": "已应用建议间隔", + "vacation_mode": "度假模式", + "vacation_status_active": "当前激活", + "vacation_status_scheduled": "已计划", + "vacation_status_inactive": "未激活", + "vacation_end_now_confirm": "确定立即结束度假吗?", + "vacation_exempt_count": "例外项", + "vacation_advanced": "高级选项…", + "vacation_open_panel": "在面板中打开", + "enable": "启用", + "saved": "已保存", + "budget_monthly_set": "设置月度预算", + "budget_yearly_set": "设置年度预算", + "budget_advanced": "货币、预警…", + "budget_open_panel": "在面板中打开", + "groups_empty": "尚无分组。", + "group_new_placeholder": "添加分组…", + "group_delete_confirm": "确定删除分组 \"{name}\" 吗?", + "groups_manage_tasks": "管理任务分配…", + "groups_open_panel": "在面板中打开", + "unassigned": "未分配", + "no_area": "无区域", + "has_overdue": "有超期任务", + "object": "设备", + "settings_panel_access": "面板访问权限", + "settings_panel_access_desc": "管理员始终拥有完整访问权限。要将创建、编辑和删除权限委派给特定的普通用户,请开启此项并在下方选择他们 — 其他用户将仅能看到“完成”和“跳过”。", + "settings_operator_write": "允许所选用户创建、编辑和删除", + "settings_operator_write_desc": "关闭时:仅管理员可修改内容。开启时:下方所选用户也获得完整访问权限。", + "no_non_admin_users": "未找到普通用户。请在“设置 -> 人员”中添加。", + "owner_label": "所有者", + "feat_completion_actions": "完成动作", + "feat_completion_actions_desc": "为每个任务配置完成时的 HA 动作 + 预设值的快速完成二维码。", + "on_complete_action_title": "完成时:触发 HA 动作(可选)", + "on_complete_action_desc": "任务完成时调用 HA 服务 — 例如重置设备上的物理计数器。", + "on_complete_action_service": "服务", + "on_complete_action_target": "目标实体", + "on_complete_action_target_hint": "注意:实体的域必须与服务匹配 — 例如 'button.press' 仅适用于 button.* 实体,'counter.increment' 仅适用于 counter.* 实体等。如果不匹配,动作将静默失败。", + "on_complete_action_data": "数据(JSON, 可选)", + "on_complete_action_test": "验证配置", + "on_complete_action_test_success": "✓ 配置有效(动作将在任务完成时触发)", + "on_complete_action_test_failed": "失败", + "quick_complete_defaults_title": "快速完成默认值(用于二维码扫描, 可选)", + "quick_complete_defaults_desc": "扫描快速完成二维码时使用的预设值。如果不设置,二维码将打开完成对话框。", + "quick_complete_defaults_notes": "备注", + "quick_complete_defaults_cost": "费用", + "quick_complete_defaults_duration": "耗时(分钟)", + "quick_complete_defaults_feedback_none": "无反馈", + "quick_complete_defaults_feedback_needed": "是必要的", + "quick_complete_defaults_feedback_not_needed": "不必要的", + "quick_complete_success": "已快速标记为完成", + "show_all_objects": "显示所有设备", + "show_all_tasks": "清除过滤 — 显示所有任务", + "filter_to_overdue": "仅显示超期任务", + "filter_to_due_soon": "仅显示即将到期任务", + "filter_to_triggered": "仅显示已触发任务", + "open_task": "打开任务", + "show_details": "显示历史与统计", + "hide_details": "隐藏详情", + "history_empty": "尚无历史记录。", + "history_edit_button": "编辑条目", + "total_cost": "总计费用", + "times_performed": "执行次数", + "older_entries": "更早的条目", + "open_in_panel": "在维护面板中打开", + "skip_reason": "跳过原因(可选)", + "reset_to_date": "重置最后执行日期为", + "delete_task_confirm": "确定删除此任务及其历史记录吗?", + "delete_object_confirm": "确定删除此设备及其所有任务吗?", + "loading": "加载中…", + "archive": "归档", + "undo": "撤销", + "task_archived": "任务已归档", + "object_archived": "对象已归档", + "unarchive": "取消归档", + "archived": "已归档", + "show_archived": "显示已归档", + "hide_archived": "隐藏已归档", + "confirm_archive_object": "归档此对象及其任务?历史记录将保留,以后可恢复。", + "settings_archive": "归档与保留", + "settings_archive_desc": "将已完成的一次性任务归档而不删除。已归档项目会被隐藏且不再活动,但保留其历史记录和费用。", + "settings_archive_oneoff_days": "已完成的一次性任务在多少天后自动归档(0 = 关闭)", + "settings_delete_archived_oneoff_days": "已归档的一次性任务在多少天后自动删除(0 = 永不)", + "archive_object": "归档对象", + "unarchive_object": "取消归档对象", + "documents": "文档", + "documents_empty": "暂无文档。", + "doc_upload": "上传文件", + "doc_uploading": "上传中…", + "doc_add_link": "添加链接", + "doc_link_url": "网址 (https://…)", + "doc_link_title": "标题(可选)", + "doc_open": "打开", + "doc_delete_confirm": "删除“{name}”?", + "doc_too_large": "文件过大(最大 25 MB)。", + "doc_upload_failed": "上传失败。", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "已在别处存储——共享,不占用额外空间。", + "doc_dup_in_object": "此文件已附加到该对象。", + "doc_link_invalid": "仅允许 http/https 链接。", + "doc_cat_manual": "手册", + "doc_cat_warranty": "保修", + "doc_cat_invoice": "发票", + "doc_cat_spare_parts": "备件", + "doc_cat_photo": "照片", + "doc_cat_other": "其他", + "doc_link_badge": "链接", + "doc_storage_title": "文档存储", + "doc_storage_saved": "通过去重节省", + "doc_storage_refresh": "刷新", + "doc_download": "下载", + "doc_close": "关闭", + "doc_camera": "拍照", + "doc_drop_hint": "将文件拖放到此处", + "doc_task_none": "没有与此任务关联的文档。", + "doc_link_existing": "关联文档…", + "doc_attach": "关联", + "doc_unlink": "取消关联", + "doc_page": "页码", + "chart_range_7d": "7天", + "chart_range_30d": "30天", + "chart_range_90d": "90天", + "chart_range_1y": "1年", + "chart_since_service": "自上次维护以来", + "chart_no_stats": "此实体没有长期统计数据 — 仅显示维护记录中的数值", + "auto_complete_on_recovery": "传感器恢复时自动完成", + "auto_complete_on_recovery_help": "当触发条件自行解除时(如已加盐、已更换滤芯),记录一次完成并更新上次执行日期。", + "doc_search": "搜索文档…", + "doc_search_none": "没有匹配的文档", + "link_device_optional": "关联到现有设备(可选)", + "parent_object_optional": "父对象(可选)", + "parent_none": "(无父对象)", + "paused": "已暂停", + "pause_object": "暂停", + "resume_object": "恢复", + "pause_until_prompt": "冻结此对象的日程——在恢复之前不会有任何到期或通知。可选设置自动恢复日期。", + "pause_until_label": "恢复日期(可选)", + "object_paused": "对象已暂停", + "object_resumed": "对象已恢复——日程已重新开始", + "object_paused_badge": "已暂停", + "paused_until_label": "至", + "replace_object": "更换…", + "replace_object_prompt": "让此对象退役并创建继任者。历史和费用留在旧对象的归档中;任务和文档转移到新对象,计数器重新开始。", + "replace_name_label": "继任者名称", + "object_replaced": "对象已更换——已创建继任者", + "reading_unit_label": "读数单位(如 kWh、m³)", + "reading_unit_help": "完成此任务时显示在记录值旁边。", + "reading_value_label": "读数值", + "reading_label": "读数", + "settings_templates_label": "模板库", + "settings_templates_hint": "取消勾选你永远不需要的模板——它们会从\"从模板\"选择器(面板和配置流程)中消失。其他一切不变;可随时重新启用。", + "worksheet": "工作单", + "worksheet_scan_view": "扫码打开任务", + "worksheet_scan_complete": "扫码完成", + "worksheet_manual_excerpt": "手册摘录", + "worksheet_pages": "页", + "worksheet_printed": "打印于", + "worksheet_never": "从未" +} diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-calendar-card.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-calendar-card.ts new file mode 100644 index 0000000..e4ef695 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-calendar-card.ts @@ -0,0 +1,586 @@ +/** Maintenance Supporter — Calendar Card. + * + * Standalone Lovelace card extracted from the panel's Calendar tab (v1.5.0+). + * All the visuals: 7/14/30/365 day window chips, per-event source icons + * (clock vs trending-up), prediction-confidence pill, projected recurrences + * at 55% opacity, today-pill highlight, empty-day collapsing in the year view. + * + * Click on an event fires an ``ll-custom`` event with payload + * ``{type: "maintenance-supporter:open-task", entry_id, task_id}``. The + * dashboard-strategy bundle's document-level handler picks that up and + * either opens the task dialog in-place (preferred) or deep-links into the + * panel as a fallback. + * + * Card config: + * + * type: custom:maintenance-supporter-calendar-card + * title: My maintenance calendar # optional + * window_days: 30 # 7 | 14 | 30 | 365 — default 30 + * show_window_chips: true # default true; hide for embedded use + * show_user_filter: true # default true + * user_filter: "" # "" | "current_user" | "" + */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import { + buildCalendarBuckets, + buildPastBuckets, + isoDateLocal, + type CalendarEvent, +} from "./helpers/calendar-bucket"; +import { calendarStyles } from "./calendar-styles"; +import { sharedStyles, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded } from "./styles"; +import type { + HomeAssistant, + MaintenanceObjectResponse, + StatisticsResponse, +} from "./types"; + +type WindowDays = 7 | 14 | 30 | 365; +type PastDays = 30 | 90; + +interface CalendarCardConfig { + type: string; + title?: string; + window_days?: WindowDays; + show_window_chips?: boolean; + show_user_filter?: boolean; + user_filter?: string; + /** v2.2.0 — when set, the card renders past N days from history instead + * of forward N days from next_due. Mutually exclusive with window_days. */ + past_days?: PastDays; +} + +export class MaintenanceCalendarCard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @state() private _config: CalendarCardConfig = { + type: "custom:maintenance-supporter-calendar-card", + }; + @state() private _objects: MaintenanceObjectResponse[] = []; + @state() private _stats: StatisticsResponse | null = null; + @state() private _windowDays: WindowDays = 30; + @state() private _pastDays: PastDays | 0 = 0; // 0 = forward mode + @state() private _userFilter = ""; + @state() private _unsub: (() => void) | null = null; + + private _dataLoaded = false; + private _lastConnection: unknown = null; + + static getConfigElement() { + return document.createElement("maintenance-supporter-calendar-card-editor"); + } + + static getStubConfig() { + // Opinionated default: 30-day rolling window, both controls visible. + return { + type: "custom:maintenance-supporter-calendar-card", + window_days: 30, + show_window_chips: true, + show_user_filter: true, + }; + } + + setConfig(config: CalendarCardConfig): void { + this._config = { ...config }; + if (config.past_days && [30, 90].includes(config.past_days)) { + this._pastDays = config.past_days as PastDays; + } else if (config.window_days && [7, 14, 30, 365].includes(config.window_days)) { + this._windowDays = config.window_days as WindowDays; + this._pastDays = 0; + } + if (typeof config.user_filter === "string") { + this._userFilter = config.user_filter; + } + } + + getCardSize(): number { + return 6; + } + + private get _lang(): string { + return this.hass?.language || "en"; + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + if (this._unsub) { + try { this._unsub(); } catch { /* ignore */ } + this._unsub = null; + } + this._dataLoaded = false; + this._lastConnection = null; + } + + updated(changedProps: Map): void { + super.updated(changedProps); + const lang = this.hass?.language; + if (lang && !isLocaleLoaded(lang)) { + ensureLocale(lang).then(() => this.requestUpdate()); + } + if (changedProps.has("hass") && this.hass) { + if (!this._dataLoaded) { + this._dataLoaded = true; + this._lastConnection = this.hass.connection; + this._loadData(); + this._subscribe(); + } else if (this.hass.connection !== this._lastConnection) { + this._lastConnection = this.hass.connection; + if (this._unsub) { + try { this._unsub(); } catch { /* ignore */ } + this._unsub = null; + } + this._subscribe(); + this._loadData(); + } + } + } + + private async _loadData(): Promise { + try { + const [objResult, statsResult] = await Promise.all([ + this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/objects", + }), + this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/statistics", + }), + ]); + this._objects = (objResult as { objects: MaintenanceObjectResponse[] }).objects; + this._stats = statsResult as StatisticsResponse; + } catch { + // WS not available yet + } + } + + private async _subscribe(): Promise { + try { + const unsub = await this.hass.connection.subscribeMessage( + (msg: unknown) => { + const data = msg as { objects: MaintenanceObjectResponse[] }; + this._objects = data.objects; + }, + { type: "maintenance_supporter/subscribe" }, + ); + // Detached mid-subscribe → drop the orphaned subscription. + if (!this.isConnected) { + unsub(); + return; + } + this._unsub = unsub; + } catch { + // Subscription failed + } + } + + private _onEventClick(ev: CalendarEvent): void { + // Past events carry a history_timestamp — those open the history-edit + // dialog instead of the task editor. Future / next_due events open + // the task editor as usual. + if (ev.history_timestamp) { + this.dispatchEvent( + new CustomEvent("ll-custom", { + detail: { + type: "maintenance-supporter:edit-history", + entry_id: ev.entry_id, + task_id: ev.task_id, + original_timestamp: ev.history_timestamp, + }, + bubbles: true, + composed: true, + }), + ); + return; + } + this.dispatchEvent( + new CustomEvent("ll-custom", { + detail: { + type: "maintenance-supporter:open-task", + entry_id: ev.entry_id, + task_id: ev.task_id, + }, + bubbles: true, + composed: true, + }), + ); + } + + render() { + if (!this.hass) return nothing; + + const L = this._lang; + const showChips = this._config.show_window_chips !== false; + const showUserFilter = this._config.show_user_filter !== false; + const title = this._config.title; + + let userFilter: string | null = null; + if (this._userFilter) { + // No UserService here (lives in panel only) — current_user resolves + // via hass.user when the user picks "my tasks". + userFilter = this._userFilter === "current_user" + ? (this.hass?.user?.id ?? null) + : this._userFilter; + } + + const today = new Date(); + today.setHours(0, 0, 0, 0); + const isPast = this._pastDays > 0; + const buckets = isPast + ? buildPastBuckets(this._objects, today, this._pastDays, userFilter) + : buildCalendarBuckets(this._objects, today, this._windowDays, userFilter); + + const todayIso = isoDateLocal(today); + // Year view AND past views collapse empty days — past windows can be + // sparse (most days have no maintenance recorded), so listing "no events" + // for 30 rows is noise. + const hideEmptyDays = this._windowDays === 365 || isPast; + const visibleBuckets = hideEmptyDays + ? buckets.filter((b) => b.events.length > 0) + : buckets; + + const renderEvent = (ev: CalendarEvent) => { + const statusClass = `cal-status-${ev.status}`; + const projClass = ev.projected ? "cal-event-projected" : ""; + const overdueLabel = ev.status === "overdue" && ev.days_until_due != null + ? ` (${Math.abs(ev.days_until_due)}d ${t("overdue", L).toLowerCase()})` + : ""; + const recurEvery = ev.projected && ev.interval_days + ? html`${ + ev.interval_unit && ev.interval_unit !== "days" + ? `${ev.interval_days} ${t("unit_" + ev.interval_unit, L)}` + : t("cal_every_n_days", L).replace("{n}", String(ev.interval_days)) + }` + : nothing; + const isSensor = ev.schedule_type === "sensor_based"; + const sourceIcon = isSensor + ? html`` + : html``; + const predictionSubtitle = isSensor && ev.prediction_confidence + && ev.status !== "triggered" && !ev.projected + ? html` + ${t("cal_predicted", L)} · ${t(`cal_confidence_${ev.prediction_confidence}`, L)} + ` + : nothing; + const currencySymbol = this._stats?.budget?.currency_symbol || DEFAULT_CURRENCY_SYMBOL; + // Past mode: ev.history_type is the actual event ('completed', 'skipped', + // 'reset', 'triggered', 'trigger_replaced'). Showing the derived status + // ('OK' for a completion) was confusing — discussion #49 user-feedback: + // *"im Panel sind die alten Auslösungen drin nicht die completion oder + // skips"* — completion events looked like they hadn't happened because + // the badge said "OK" (current task status) not "Completed" (the + // actual past event). In past mode we now label with the event type. + // Forward mode still uses the projected status (overdue/due_soon/etc). + const badgeLabel = ev.history_type + ? t(ev.history_type, L) + : t(ev.status, L); + return html` +
    this._onEventClick(ev)}> + ${sourceIcon} + ${badgeLabel} +
    +
    ${ev.object_name} · ${ev.task_name}${overdueLabel}
    + ${predictionSubtitle} + ${recurEvery} +
    + ${ev.avg_cost != null && ev.avg_cost > 0 + ? html`${ev.avg_cost.toFixed(0)} ${currencySymbol}` + : nothing} +
    + `; + }; + + const renderDayRow = (bucket: { date: string; events: CalendarEvent[] }) => { + const [y, m, d] = bucket.date.split("-").map(Number); + const date = new Date(y, m - 1, d); + const isToday = bucket.date === todayIso; + const weekday = date.toLocaleDateString(L, { weekday: "short" }); + const monthLabel = date.toLocaleDateString(L, { month: "long" }); + return html` +
    +
    + ${weekday} + ${date.getDate()} +
    +
    +
    + ${monthLabel} + ${isToday ? html`${t("today", L)}` : nothing} +
    + ${bucket.events.length === 0 + ? html`
    ${t("cal_no_events", L)}
    ` + : bucket.events.map(renderEvent)} +
    +
    + `; + }; + + return html` + + ${showChips || showUserFilter + ? html` +
    + ${showChips + ? html` +
    + ${[30, 90].map((p) => html` + + `)} +
    + +
    + ${[7, 14, 30, 365].map((w) => html` + + `)} +
    + ` + : nothing} + ${showUserFilter + ? html` + + ` + : nothing} +
    + ` + : nothing} +
    + ${visibleBuckets.length === 0 && hideEmptyDays + ? html`
    ${t("cal_no_events", L)}
    ` + : visibleBuckets.map(renderDayRow)} +
    +
    + `; + } + + static styles = [ + sharedStyles, + calendarStyles, + css` + :host { display: block; } + ha-card { padding: 0; overflow: hidden; } + `, + ]; +} + +// ── Calendar Card Editor ──────────────────────────────────────────────────── +// +// Visual config: pick window_days from a dropdown, toggle the in-card chips +// and user-filter on/off. Same pattern as MaintenanceSupporterCardEditor — +// LitElement with setConfig + dispatched config-changed. + +const WINDOW_DAY_OPTIONS: Array<{ value: WindowDays; label: string }> = [ + { value: 7, label: "Week (7 days)" }, + { value: 14, label: "Fortnight (14 days)" }, + { value: 30, label: "Month (30 days, default)" }, + { value: 365, label: "Year (365 days, empty days collapsed)" }, +]; + +class MaintenanceCalendarCardEditor extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @state() private _config: CalendarCardConfig = { + type: "custom:maintenance-supporter-calendar-card", + }; + + setConfig(config: CalendarCardConfig): void { + this._config = { ...config }; + } + + private _valueChanged(key: keyof CalendarCardConfig, value: unknown): void { + const newConfig = { ...this._config, [key]: value } as CalendarCardConfig; + // Drop default-equivalent values so saved YAML stays minimal + if (key === "show_window_chips" && value === true) { + delete (newConfig as unknown as Record).show_window_chips; + } + if (key === "show_user_filter" && value === true) { + delete (newConfig as unknown as Record).show_user_filter; + } + if (key === "title" && (!value || (typeof value === "string" && value.trim() === ""))) { + delete (newConfig as unknown as Record).title; + } + if (key === "user_filter" && value === "") { + delete (newConfig as unknown as Record).user_filter; + } + this._config = newConfig; + this.dispatchEvent( + new CustomEvent("config-changed", { + detail: { config: newConfig }, + bubbles: true, + composed: true, + }), + ); + } + + render() { + const currentWindow = this._config.window_days ?? 30; + const showChips = this._config.show_window_chips !== false; + const showUserFilter = this._config.show_user_filter !== false; + const userFilter = this._config.user_filter ?? ""; + const title = this._config.title ?? ""; + return html` +
    +
    + + + this._valueChanged("title", (e.target as HTMLInputElement).value)} + /> +
    +
    + + +
    +
    + + + this._valueChanged( + "show_window_chips", + (e.target as HTMLInputElement).checked, + )} + /> +
    +
    + Hide the chips when the card is embedded in a strategy view that + already serves as the window selector. +
    +
    + + + this._valueChanged( + "show_user_filter", + (e.target as HTMLInputElement).checked, + )} + /> +
    +
    + + +
    +
    + `; + } + + static styles = css` + :host { display: block; padding: 8px 0; } + .editor { display: flex; flex-direction: column; gap: 12px; } + .row { display: flex; flex-direction: column; gap: 4px; } + .row.toggle { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 12px; + } + label { font-weight: 500; color: var(--primary-text-color); font-size: 14px; } + input[type="text"], select { + padding: 8px; + font-size: 14px; + background: var(--card-background-color, white); + color: var(--primary-text-color, black); + border: 1px solid var(--divider-color, #e0e0e0); + border-radius: 4px; + } + .hint { + margin-top: -4px; + font-size: 12px; + color: var(--secondary-text-color, #666); + } + `; +} + +if (!customElements.get("maintenance-supporter-calendar-card")) { + customElements.define( + "maintenance-supporter-calendar-card", + MaintenanceCalendarCard, + ); +} +if (!customElements.get("maintenance-supporter-calendar-card-editor")) { + customElements.define( + "maintenance-supporter-calendar-card-editor", + MaintenanceCalendarCardEditor, + ); +} + +// Register with HACS / customCards so the picker shows it +const w = window as unknown as { + customCards?: Array<{ + type: string; + name: string; + description: string; + preview?: boolean; + }>; +}; +w.customCards = w.customCards || []; +// HA's custom-card resolver maps ``custom:X`` → element tag ``X``. Our element +// is registered as ``maintenance-supporter-calendar-card``, so the customCards +// type MUST match that exact suffix or the card-picker entry resolves to a +// non-existent element and the strategy's calendar mode throws a config error. +const CALENDAR_CARD_TYPE = "maintenance-supporter-calendar-card"; +const alreadyRegistered = w.customCards.some( + (c) => c.type === CALENDAR_CARD_TYPE, +); +if (!alreadyRegistered) { + w.customCards.push({ + type: CALENDAR_CARD_TYPE, + name: "Maintenance Supporter — Calendar", + description: + "Rolling calendar of maintenance tasks with 7/14/30/365 day windows, source icons, and prediction-confidence pills.", + preview: true, + }); +} + +export {}; diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-card-editor.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-card-editor.ts new file mode 100644 index 0000000..6c9c07d --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-card-editor.ts @@ -0,0 +1,278 @@ +/** Card editor for the Maintenance Supporter Lovelace card. */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import { t } from "./styles"; +import type { HomeAssistant, CardConfig, MaintenanceObjectResponse } from "./types"; + +const STATUS_KEYS = ["overdue", "triggered", "due_soon", "ok"] as const; + +export class MaintenanceSupporterCardEditor extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @state() private _config: CardConfig = { type: "custom:maintenance-supporter-card" }; + @state() private _objects: MaintenanceObjectResponse[] = []; + @state() private _loadingObjects = true; + @state() private _loadError = false; + + private _objectsLoaded = false; + + private get _lang(): string { + return this.hass?.language || "en"; + } + + setConfig(config: CardConfig): void { + this._config = { ...config }; + } + + updated(changedProps: Map): void { + super.updated(changedProps); + if (changedProps.has("hass") && this.hass && !this._objectsLoaded) { + this._objectsLoaded = true; + this._loadObjects(); + } + } + + private async _loadObjects(): Promise { + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/objects", + }) as { objects: MaintenanceObjectResponse[] }; + this._objects = result.objects || []; + this._loadError = false; + } catch { + this._objects = []; + this._loadError = true; + } + this._loadingObjects = false; + } + + private _valueChanged(key: string, value: unknown): void { + const newConfig = { ...this._config, [key]: value }; + // Drop empty arrays so the saved YAML stays clean + if (Array.isArray(value) && value.length === 0) { + delete (newConfig as Record)[key]; + } + this._config = newConfig; + this.dispatchEvent( + new CustomEvent("config-changed", { detail: { config: newConfig } }) + ); + } + + private _toggleStatus(status: string, on: boolean): void { + const current = new Set(this._config.filter_status || []); + if (on) current.add(status); else current.delete(status); + this._valueChanged("filter_status", [...current]); + } + + private _toggleObject(name: string, on: boolean): void { + const current = new Set(this._config.filter_objects || []); + if (on) current.add(name); else current.delete(name); + this._valueChanged("filter_objects", [...current]); + } + + private _onEntitiesChanged = (e: CustomEvent<{ value: string[] }>): void => { + this._valueChanged("entity_ids", e.detail.value || []); + }; + + render() { + const L = this._lang; + const selectedStatuses = new Set(this._config.filter_status || []); + const selectedObjects = new Set(this._config.filter_objects || []); + const objectNames = [...this._objects] + .map((o) => o.object.name) + .sort((a, b) => a.localeCompare(b)); + // Build the list of OUR sensor + binary_sensor entity_ids so the + // ha-entities-picker only shows maintenance_supporter entities, not + // every sensor in HA. + const ourEntities: string[] = []; + for (const o of this._objects) { + for (const t of o.tasks) { + if (t.sensor_entity_id) ourEntities.push(t.sensor_entity_id); + if (t.binary_sensor_entity_id) ourEntities.push(t.binary_sensor_entity_id); + } + } + + return html` +
    + + this._valueChanged("title", (e.target as HTMLInputElement).value)} + > + + +
    +
    ${t("card_filter_status", L)}
    +
    + ${STATUS_KEYS.map((s) => html` + + `)} +
    +
    ${t("card_filter_status_help", L)}
    +
    + + +
    +
    ${t("card_filter_objects", L)}
    + ${this._loadingObjects + ? html`
    ${t("card_loading_objects", L)}
    ` + : this._loadError + ? html`
    ${t("card_load_error", L)}
    ` + : objectNames.length === 0 + ? html`
    ${t("no_objects", L)}
    ` + : html` +
    + ${objectNames.map((name) => html` + + `)} +
    +
    ${t("card_filter_objects_help", L)}
    + ` + } +
    + + +
    +
    ${t("card_filter_entities", L)}
    + +
    ${t("card_filter_entities_help", L)}
    +
    + + + + this._valueChanged("show_header", (e.target as HTMLInputElement).checked)} + > + + + + + this._valueChanged("show_actions", (e.target as HTMLInputElement).checked)} + > + + + + + this._valueChanged("compact", (e.target as HTMLInputElement).checked)} + > + + + + this._valueChanged("max_items", parseInt((e.target as HTMLInputElement).value, 10) || 0)} + > + ${nothing} +
    + `; + } + + static styles = css` + .editor { + display: flex; + flex-direction: column; + gap: 16px; + padding: 16px; + } + ha-textfield { display: block; } + .field { + display: flex; + flex-direction: column; + gap: 6px; + } + .field-label { + font-size: 13px; + color: var(--secondary-text-color); + font-weight: 500; + } + .field-help { + font-size: 12px; + color: var(--secondary-text-color); + font-style: italic; + } + .field-help.error-text { + color: var(--error-color, #f44336); + font-style: normal; + } + .chip-row { + display: flex; + gap: 6px; + flex-wrap: wrap; + } + .chip { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 4px 10px; + border: 1px solid var(--divider-color); + border-radius: 14px; + cursor: pointer; + font-size: 13px; + user-select: none; + transition: background 0.15s, border-color 0.15s; + } + .chip:hover { + background: var(--secondary-background-color); + } + .chip.active { + background: var(--primary-color); + color: var(--text-primary-color); + border-color: var(--primary-color); + } + .chip input { + display: none; + } + .object-list { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); + gap: 4px 12px; + padding: 6px 0; + } + .object-row { + display: flex; + align-items: center; + gap: 6px; + padding: 3px 0; + font-size: 13px; + cursor: pointer; + } + .object-row input { cursor: pointer; } + `; +} + +// Module-bottom registration so esbuild's tree-shaker doesn't drop the +// element class when the only reference looks "type-only" (the +// @customElement decorator pattern triggered exactly that bug for the +// dialog components — same fix applied here for the card editor). +if (!customElements.get("maintenance-supporter-card-editor")) { + customElements.define( + "maintenance-supporter-card-editor", + MaintenanceSupporterCardEditor, + ); +} diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-card.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-card.ts new file mode 100644 index 0000000..4e49e4e --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-card.ts @@ -0,0 +1,419 @@ +/** Maintenance Supporter Lovelace Card. */ + +import { LitElement, html, css, nothing } from "lit"; +import { property, state } from "lit/decorators.js"; +import { sharedStyles, STATUS_COLORS, t, ensureLocale, isLocaleLoaded } from "./styles"; +import type { + HomeAssistant, + MaintenanceObjectResponse, + MaintenanceTask, + StatisticsResponse, + CardConfig, +} from "./types"; +import "./maintenance-card-editor"; +import "./components/complete-dialog"; +import { + openCreateObjectDialog, + openCreateTaskDialog, + openTaskQuickActions, +} from "./dialog-mount"; + +interface FlatTask { + entry_id: string; + object_name: string; + task: MaintenanceTask; +} + +export class MaintenanceSupporterCard extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @state() private _config: CardConfig = { type: "custom:maintenance-supporter-card" }; + @state() private _objects: MaintenanceObjectResponse[] = []; + @state() private _stats: StatisticsResponse | null = null; + @state() private _unsub: (() => void) | null = null; + + private get _lang(): string { + return this.hass?.language || "en"; + } + + static getConfigElement() { + return document.createElement("maintenance-supporter-card-editor"); + } + + static getStubConfig() { + // Opinionated default: when a user picks "Maintenance Supporter" from the + // card picker, show only the actionable tasks (overdue + triggered + due + // soon, max 10) — not the full task list which is overwhelming on first + // add. The user can broaden the filter via the editor afterwards. + return { + type: "custom:maintenance-supporter-card", + show_header: true, + show_actions: true, + filter_status: ["overdue", "triggered", "due_soon"], + max_items: 10, + }; + } + + setConfig(config: CardConfig): void { + this._config = config; + } + + getCardSize(): number { + return 3; + } + + private _dataLoaded = false; + private _lastConnection: unknown = null; + + connectedCallback(): void { + super.connectedCallback(); + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + if (this._unsub) { + this._unsub(); + this._unsub = null; + } + this._dataLoaded = false; + this._lastConnection = null; + } + + updated(changedProps: Map): void { + super.updated(changedProps); + const lang = this.hass?.language; + if (lang && !isLocaleLoaded(lang)) { + ensureLocale(lang).then(() => this.requestUpdate()); + } + if (changedProps.has("hass") && this.hass) { + if (!this._dataLoaded) { + this._dataLoaded = true; + this._lastConnection = this.hass.connection; + this._loadData(); + this._subscribe(); + } else if (this.hass.connection !== this._lastConnection) { + this._lastConnection = this.hass.connection; + if (this._unsub) { + try { this._unsub(); } catch { /* ignore */ } + this._unsub = null; + } + this._subscribe(); + this._loadData(); + } + } + } + + private async _loadData(): Promise { + try { + const [objResult, statsResult] = await Promise.all([ + this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/objects" }), + this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/statistics" }), + ]); + this._objects = (objResult as { objects: MaintenanceObjectResponse[] }).objects; + this._stats = statsResult as StatisticsResponse; + } catch { + // WS not available yet + } + } + + private async _subscribe(): Promise { + try { + const unsub = await this.hass.connection.subscribeMessage( + (msg: unknown) => { + const data = msg as { objects: MaintenanceObjectResponse[] }; + this._objects = data.objects; + }, + { type: "maintenance_supporter/subscribe" } + ); + // Detached mid-subscribe → drop the orphaned subscription. + if (!this.isConnected) { + unsub(); + return; + } + this._unsub = unsub; + } catch { + // Subscription failed + } + } + + private get _flatTasks(): FlatTask[] { + const tasks: FlatTask[] = []; + const { + filter_status, + filter_objects, + entity_ids, + filter_due_min_days, + filter_due_max_days, + max_items, + } = this._config; + const entityFilter = entity_ids?.length ? new Set(entity_ids) : null; + const hasDueRange = + filter_due_min_days !== undefined || filter_due_max_days !== undefined; + + for (const obj of this._objects) { + if (filter_objects?.length && !filter_objects.includes(obj.object.name)) continue; + for (const task of obj.tasks) { + // Completed one-time tasks ("done") are hidden from the active list. + if (task.is_done) continue; + // v2.10.0: archived tasks (and tasks of an archived object) are inert — + // never shown on the Lovelace card. + if (task.archived || obj.object.archived) continue; + if (filter_status?.length && !filter_status.includes(task.status)) continue; + // entity_ids: HA-native filter — match the task's sensor or + // binary_sensor entity_id. Both fields come pre-resolved from the + // backend WS response (see _build_task_summary in websocket/__init__.py). + if (entityFilter) { + const matches = (task.sensor_entity_id && entityFilter.has(task.sensor_entity_id)) + || (task.binary_sensor_entity_id && entityFilter.has(task.binary_sensor_entity_id)); + if (!matches) continue; + } + // due-days range — used by group_by=due_date strategy buckets. + // Tasks without a numeric days_until_due are excluded from any + // ranged view; they show up in unfiltered or status-only views. + if (hasDueRange) { + const days = task.days_until_due; + if (days === null || days === undefined) continue; + if (filter_due_min_days !== undefined && days < filter_due_min_days) continue; + if (filter_due_max_days !== undefined && days > filter_due_max_days) continue; + } + tasks.push({ entry_id: obj.entry_id, object_name: obj.object.name, task }); + } + } + + const order: Record = { overdue: 0, triggered: 1, due_soon: 2, ok: 3 }; + tasks.sort((a, b) => { + const byStatus = (order[a.task.status] ?? 9) - (order[b.task.status] ?? 9); + if (byStatus !== 0) return byStatus; + // Within a status, soonest-due first; tasks without a due date go last. + return (a.task.days_until_due ?? Infinity) - (b.task.days_until_due ?? Infinity); + }); + + if (max_items && max_items > 0) { + return tasks.slice(0, max_items); + } + return tasks; + } + + private _onCompleted = async (): Promise => { + await this._loadData(); + }; + + /** Open the per-task quick-actions dialog (Complete / Skip / Reset / Edit / + * QR / Delete) — full per-task panel parity. Mounted on document.body via + * the shared dialog-mount helper, so the card works on any dashboard + * without depending on the strategy bundle's ll-custom handler. */ + private _openTaskDetail(entryId: string, taskId: string): void { + openTaskQuickActions(entryId, taskId); + } + + render() { + const L = this._lang; + const title = this._config.title || t("maintenance", L); + const showHeader = this._config.show_header !== false; + const showActions = this._config.show_actions !== false; + const compact = this._config.compact || false; + const tasks = this._flatTasks; + const s = this._stats; + + return html` + +
    +

    ${title}

    +
    + ${showHeader && s + ? html` +
    + ${s.overdue > 0 ? html`${s.overdue}` : nothing} + ${s.due_soon > 0 ? html`${s.due_soon}` : nothing} + ${s.triggered > 0 ? html`${s.triggered}` : nothing} +
    + ` + : nothing} + ${showActions + ? html` + openCreateObjectDialog()} + > + + + openCreateTaskDialog()} + > + + + ` + : nothing} +
    +
    + ${tasks.length === 0 + ? html`
    +
    ${t("card_no_tasks_title", L)}
    + ${t("card_no_tasks_cta", L)} +
    ` + : html` +
    + ${tasks.map( + ({ entry_id, object_name, task }) => html` +
    this._openTaskDetail(entry_id, task.id)} + title="${t("open_task", L) || "Open task"}"> +
    +
    +
    ${task.name}
    + ${!compact ? html`
    ${object_name} · ${t(task.type, L)}
    ` : nothing} +
    +
    + ${task.days_until_due !== null && task.days_until_due !== undefined + ? task.days_until_due < 0 + ? html`${Math.abs(task.days_until_due)}${L.startsWith("de") ? "T" : "d"}` + : task.days_until_due === 0 + ? t("today", L) + : `${task.days_until_due}${L.startsWith("de") ? "T" : "d"}` + : task.trigger_active + ? "⚡" + : "—"} +
    + ${showActions + ? html` + { + // Stop the row's open-task handler from also firing + e.stopPropagation(); + const dlg = this.shadowRoot!.querySelector("maintenance-complete-dialog") as any; + dlg.entryId = entry_id; + dlg.taskId = task.id; + dlg.taskName = task.name; + dlg.checklist = task.checklist || []; + dlg.adaptiveEnabled = !!task.adaptive_config?.enabled; + dlg.taskType = task.type || ""; + dlg.readingUnit = (task as any).reading_unit || ""; + dlg.lang = L; + dlg.open(); + }} + > + + + ` + : nothing} +
    + ` + )} +
    + `} +
    + + `; + } + + static styles = [ + sharedStyles, + css` + ha-card { overflow: hidden; } + + .card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px 16px 8px; + } + + .card-header h1 { margin: 0; font-size: 18px; font-weight: 500; } + .header-right { display: flex; align-items: center; gap: 6px; } + .header-stats { display: flex; gap: 6px; } + .hdr-add { + --mdc-icon-button-size: 32px; + --mdc-icon-size: 20px; + color: var(--primary-color); + } + + .badge { + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 22px; + height: 22px; + border-radius: 11px; + font-size: 12px; + font-weight: 600; + color: white; + padding: 0 6px; + } + + .badge.overdue { background: var(--error-color, #f44336); } + .badge.due_soon { background: var(--warning-color, #ff9800); } + .badge.triggered { background: #ff5722; } + + .empty-card { + padding: 24px 16px; + text-align: center; + color: var(--secondary-text-color); + display: flex; + flex-direction: column; + gap: 10px; + align-items: center; + } + .empty-link { + color: var(--primary-color); + text-decoration: none; + font-size: 13px; + } + .empty-link:hover { text-decoration: underline; } + .task-list { padding: 0 16px 16px; } + + .task-item { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 0; + border-bottom: 1px solid var(--divider-color); + } + .task-item:last-child { border-bottom: none; } + .task-list.compact .task-item { padding: 4px 0; } + /* Row click opens the task editor (in-place via the strategy bundle's + ll-custom handler). Hover state hints that the row is interactive. */ + .task-item.clickable { cursor: pointer; transition: background 0.12s; } + .task-item.clickable:hover { + background: var(--state-icon-color, rgba(255,255,255,0.04)); + } + + .status-dot { width: 10px; height: 10px; border-radius: 50%; flex-shrink: 0; } + .task-info { flex: 1; min-width: 0; } + .task-name { font-size: 14px; font-weight: 500; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } + .task-meta { font-size: 12px; color: var(--secondary-text-color); } + + .task-due { font-size: 13px; color: var(--secondary-text-color); min-width: 40px; text-align: right; } + .overdue-text { color: var(--error-color); font-weight: 500; } + + .complete-btn { + --mdc-icon-button-size: 32px; + --mdc-icon-size: 18px; + color: var(--primary-color); + } + `, + ]; +} + +// Module-bottom registration so esbuild's tree-shaker doesn't drop the +// element class when the only reference is the @customElement decorator +// (which triggered this exact bug for the dialog components — issue #32: +// "card not showing in the card selector, busy spinner only"). +if (!customElements.get("maintenance-supporter-card")) { + customElements.define("maintenance-supporter-card", MaintenanceSupporterCard); +} + +// Register as custom card so the Lovelace card picker lists it. +(window as any).customCards = (window as any).customCards || []; +(window as any).customCards.push({ + type: "maintenance-supporter-card", + name: "Maintenance Supporter", + description: "Overview of your maintenance tasks with quick actions.", + preview: true, +}); diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-dashboard-strategy.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-dashboard-strategy.ts new file mode 100644 index 0000000..44273fb --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-dashboard-strategy.ts @@ -0,0 +1,1174 @@ +/** Maintenance Supporter — custom dashboard strategy. + * + * Generates a complete Lovelace dashboard from the integration's WS feed. + * Honors a ``group_by`` config so YAML users can pick the layout: + * + * strategy: + * type: custom:maintenance-supporter + * group_by: area # default — one view per area + Unassigned + * + * strategy: + * type: custom:maintenance-supporter + * group_by: status # one view per status (Overdue / Triggered / Due Soon / OK) + * + * strategy: + * type: custom:maintenance-supporter + * group_by: floor # one view per floor with area sub-headings (#155 follow-up) + * + * strategy: + * type: custom:maintenance-supporter + * group_by: due_date # 5 buckets — Overdue / Today / This Week / This Month / Later + * + * Layout pattern follows HA's own ``maintenance-view-strategy``, + * ``areas-dashboard-strategy`` and ``home-overview-view-strategy``: a leading + * "Overview" view of actionable tasks, empty groups skipped, and + * STATE_NOT_RUNNING / recovery_mode handled with the same starting / + * recovery-mode card placeholders HA core uses. + * + * Requires Home Assistant 2026.5+ to appear in the "Add Dashboard" picker + * (that's when ``window.customStrategies`` got picked up by the frontend). + * On older HA versions the registration is a silent no-op. + */ + +interface MaintenanceObjectResp { + entry_id: string; + object: { + id: string; + name: string; + area_id: string | null; + }; + tasks: Array<{ status?: string; days_until_due?: number | null }>; +} + +interface AreaEntry { + area_id: string; + name: string; + icon?: string | null; + floor_id?: string | null; +} + +interface FloorEntry { + floor_id: string; + name: string; + icon?: string | null; + level?: number | null; +} + +interface HassLike { + language?: string; + config?: { + state?: string; + recovery_mode?: boolean; + }; + connection: { + sendMessagePromise(msg: Record): Promise; + }; + areas?: Record; + floors?: Record; +} + +type GroupBy = "area" | "status" | "floor" | "due_date" | "calendar"; + +interface MaintenanceDashboardStrategyConfig { + type: "custom:maintenance-supporter" | "maintenance-supporter"; + group_by?: GroupBy; +} + +interface DashboardConfig { + title?: string; + views: ViewConfig[]; +} + +interface ViewConfig { + title?: string; + path?: string; + icon?: string; + type?: string; + subview?: boolean; + cards?: CardConfig[]; + sections?: SectionConfig[]; + // HA 2026.5+ — banner card above the view (mobile + desktop), and a + // narrow column on the right (large screens only). See + // home-overview-view-strategy.ts for the canonical usage. + header?: { layout?: string; card: CardConfig }; + sidebar?: { + sections: SectionConfig[]; + visibility?: ViewColumnsCondition[]; + content_label?: string; + sidebar_label?: string; + }; + max_columns?: number; +} + +interface ViewColumnsCondition { + condition: "view_columns"; + min?: number; + max?: number; +} + +interface SectionConfig { + type?: string; + cards: CardConfig[]; +} + +interface CardConfig { + type: string; + [key: string]: unknown; +} + +const STRATEGY_TYPE = "maintenance-supporter"; +const STRATEGY_TAG = `ll-strategy-dashboard-${STRATEGY_TYPE}`; +const EDITOR_TAG = "hui-maintenance-supporter-strategy-editor"; + +const STATE_NOT_RUNNING = "NOT_RUNNING"; + +const STATUS_VIEWS: Array<{ + status: string; + title: string; + icon: string; + path: string; +}> = [ + { status: "overdue", title: "Overdue", icon: "mdi:alert-circle", path: "overdue" }, + { status: "triggered", title: "Triggered", icon: "mdi:flash", path: "triggered" }, + { status: "due_soon", title: "Due Soon", icon: "mdi:clock-alert-outline", path: "due-soon" }, + { status: "ok", title: "OK", icon: "mdi:check-circle-outline", path: "ok" }, +]; + +const DUE_DATE_VIEWS: Array<{ + title: string; + icon: string; + path: string; + filter: { filter_due_min_days?: number; filter_due_max_days?: number }; + // Pre-check predicate so we can skip empty buckets without a card render. + matches: (days: number) => boolean; +}> = [ + { + title: "Overdue", + icon: "mdi:alert-circle", + path: "overdue", + filter: { filter_due_max_days: -1 }, + matches: (d) => d <= -1, + }, + { + title: "Today", + icon: "mdi:calendar-today", + path: "today", + filter: { filter_due_min_days: 0, filter_due_max_days: 0 }, + matches: (d) => d === 0, + }, + { + title: "This Week", + icon: "mdi:calendar-week", + path: "this-week", + filter: { filter_due_min_days: 1, filter_due_max_days: 7 }, + matches: (d) => d >= 1 && d <= 7, + }, + { + title: "This Month", + icon: "mdi:calendar-month", + path: "this-month", + filter: { filter_due_min_days: 8, filter_due_max_days: 30 }, + matches: (d) => d >= 8 && d <= 30, + }, + { + title: "Later", + icon: "mdi:calendar-clock", + path: "later", + filter: { filter_due_min_days: 31 }, + matches: (d) => d >= 31, + }, +]; + +function makeCardSection(card: CardConfig): SectionConfig { + return { type: "grid", cards: [card] }; +} + +// Replicate HA core's responsive conditions (see panels/lovelace/strategies/ +// helpers/view-columns-conditions.ts). They use the "view_columns" condition +// rather than raw media queries so they follow whatever breakpoint the user's +// view actually has. +const LARGE_SCREEN_CONDITION: ViewColumnsCondition = { + condition: "view_columns", + min: 2, +}; +const SMALL_SCREEN_CONDITION: ViewColumnsCondition = { + condition: "view_columns", + max: 1, +}; + +// Headline counts come from the global summary sensors — the single source of +// truth shared with the panel KPI chips and the statistics WS endpoint — so the +// numbers can't drift. Rendered live via a markdown template instead of being +// counted client-side at generation time (which froze them until reload). +function kpiMarkdownCard(): CardConfig { + return { + type: "markdown", + text_only: true, + content: [ + "🔴 **{{ states('sensor.maintenance_supporter_overdue') }}** overdue", + "⚡ **{{ states('sensor.maintenance_supporter_triggered') }}** triggered", + "🟡 **{{ states('sensor.maintenance_supporter_due_soon') }}** due soon", + "🟢 **{{ states('sensor.maintenance_supporter_ok') }}** ok", + ].join(" · "), + }; +} + +// Onboarding card shown when there are zero maintenance objects. Pattern +// follows HA core's home-overview-view-strategy empty-state branch. +// +// Two buttons: +// • "Open Maintenance panel" — plain navigate, the dependable path. +// • "Add object" — fire-dom-event with a custom ll-custom payload that +// our document-level handler (see registerLlCustomHandler below) catches +// and turns into a deep-link navigation. This demonstrates the fire-dom +// pattern HA core uses for in-place actions, without requiring us to +// rip our object-add dialog out of the panel into a custom element. +function emptyStateView(): ViewConfig { + return { + title: "Maintenance", + type: "panel", + cards: [ + { + type: "empty-state", + icon: "mdi:wrench-clock", + icon_color: "primary", + content_only: true, + title: "No maintenance objects yet", + content: + "Open the Maintenance panel to add your first object — pool pump, HVAC filter, vehicle, anything that needs scheduled care.", + buttons: [ + { + icon: "mdi:wrench", + text: "Open Maintenance panel", + appearance: "filled", + variant: "brand", + tap_action: { + action: "navigate", + navigation_path: "/maintenance-supporter", + }, + }, + { + icon: "mdi:plus", + text: "Add object", + appearance: "outlined", + variant: "brand", + tap_action: { + action: "fire-dom-event", + ll_custom: { type: "maintenance-supporter:add-object" }, + }, + }, + ], + }, + ], + }; +} + +function overviewView(): ViewConfig { + const kpiCard = kpiMarkdownCard(); + return { + title: "Overview", + icon: "mdi:wrench-clock", + path: "overview", + type: "sections", + // Header runs above the main content — present on every screen size + // so the user always sees the headline counts. Layout "responsive" + // matches HA core's home strategy. + header: { layout: "responsive", card: kpiCard }, + // Sidebar appears only on screens wide enough for ≥2 columns. Mirrors + // the same KPI card so power users with a wide dashboard never lose + // sight of it as they scroll long lists. + sidebar: { + sections: [ + { + type: "grid", + cards: [ + { + type: "heading", + heading: "Status", + heading_style: "title", + }, + kpiCard, + ], + }, + ], + visibility: [LARGE_SCREEN_CONDITION], + content_label: "Tasks", + sidebar_label: "Status", + }, + sections: [ + makeCardSection({ + type: "custom:maintenance-supporter-card", + show_header: false, + filter_status: ["overdue", "triggered", "due_soon"], + }), + ], + }; +} + +function viewsByArea( + objects: MaintenanceObjectResp[], + areas: Record, +): ViewConfig[] { + const byArea = new Map(); + for (const obj of objects) { + const aid = obj.object.area_id || null; + if (!byArea.has(aid)) byArea.set(aid, []); + byArea.get(aid)!.push(obj); + } + + const areaIds = Array.from(byArea.keys()).filter( + (a): a is string => a !== null, + ); + areaIds.sort((a, b) => { + const na = areas[a]?.name || a; + const nb = areas[b]?.name || b; + return na.localeCompare(nb); + }); + + const views: ViewConfig[] = []; + for (const areaId of areaIds) { + const objs = byArea.get(areaId)!; + if (objs.length === 0) continue; + const areaInfo = areas[areaId]; + views.push({ + title: areaInfo?.name || areaId, + icon: areaInfo?.icon || "mdi:floor-plan", + path: `area-${areaId}`, + type: "sections", + sections: [ + makeCardSection({ + type: "custom:maintenance-supporter-card", + show_header: false, + filter_objects: objs.map((o) => o.object.name), + }), + ], + }); + } + + const unassigned = byArea.get(null); + if (unassigned && unassigned.length > 0) { + views.push({ + title: "Unassigned", + icon: "mdi:help-circle-outline", + path: "unassigned", + type: "sections", + sections: [ + makeCardSection({ + type: "custom:maintenance-supporter-card", + show_header: false, + filter_objects: unassigned.map((o) => o.object.name), + }), + ], + }); + } + + return views; +} + +function viewsByStatus(objects: MaintenanceObjectResp[]): ViewConfig[] { + const present = new Set(); + for (const obj of objects) { + for (const task of obj.tasks || []) { + if (task.status) present.add(task.status); + } + } + + const views: ViewConfig[] = []; + for (const v of STATUS_VIEWS) { + if (!present.has(v.status)) continue; + views.push({ + title: v.title, + icon: v.icon, + path: v.path, + type: "sections", + sections: [ + makeCardSection({ + type: "custom:maintenance-supporter-card", + show_header: false, + filter_status: [v.status], + }), + ], + }); + } + return views; +} + +function viewsByFloor( + objects: MaintenanceObjectResp[], + areas: Record, + floors: Record, +): ViewConfig[] { + // Build {floor_id → [object…]} via area.floor_id lookup; null bucket for + // objects whose area has no floor OR whose object has no area at all. + const byFloor = new Map(); + for (const obj of objects) { + const aid = obj.object.area_id; + const fid = aid ? areas[aid]?.floor_id || null : null; + if (!byFloor.has(fid)) byFloor.set(fid, []); + byFloor.get(fid)!.push(obj); + } + + // Floor sort: by `level` then name (matches HA's own floor sorting). + const floorIds = Array.from(byFloor.keys()).filter( + (f): f is string => f !== null, + ); + floorIds.sort((a, b) => { + const fa = floors[a]; + const fb = floors[b]; + const la = fa?.level ?? 0; + const lb = fb?.level ?? 0; + if (la !== lb) return la - lb; + return (fa?.name || a).localeCompare(fb?.name || b); + }); + + const views: ViewConfig[] = []; + for (const floorId of floorIds) { + const objs = byFloor.get(floorId)!; + if (objs.length === 0) continue; + const floor = floors[floorId]; + views.push({ + title: floor?.name || floorId, + icon: floor?.icon || "mdi:home-floor-1", + path: `floor-${floorId}`, + type: "sections", + sections: [ + makeCardSection({ + type: "custom:maintenance-supporter-card", + show_header: false, + filter_objects: objs.map((o) => o.object.name), + }), + ], + }); + } + + const unassigned = byFloor.get(null); + if (unassigned && unassigned.length > 0) { + views.push({ + title: "Other", + icon: "mdi:help-circle-outline", + path: "other", + type: "sections", + sections: [ + makeCardSection({ + type: "custom:maintenance-supporter-card", + show_header: false, + filter_objects: unassigned.map((o) => o.object.name), + }), + ], + }); + } + + return views; +} + +function viewsByCalendar(_objects: MaintenanceObjectResp[]): ViewConfig[] { + // One view per window. Each is a panel-typed view with a single + // ``maintenance-supporter-calendar`` card pinned to the matching + // window_days. Window-chips inside the card are hidden because the + // tab-bar already serves as the window selector. + const WINDOWS: Array<{ + title: string; + icon: string; + path: string; + window_days: 7 | 14 | 30 | 365; + }> = [ + { title: "Week", icon: "mdi:calendar-week", path: "cal-7", window_days: 7 }, + { title: "Fortnight", icon: "mdi:calendar-week-begin", path: "cal-14", window_days: 14 }, + { title: "Month", icon: "mdi:calendar-month", path: "cal-30", window_days: 30 }, + { title: "Year", icon: "mdi:calendar-clock", path: "cal-365", window_days: 365 }, + ]; + + return WINDOWS.map((w) => ({ + title: w.title, + icon: w.icon, + path: w.path, + type: "panel", + cards: [ + { + type: "custom:maintenance-supporter-calendar-card", + window_days: w.window_days, + show_window_chips: false, + show_user_filter: true, + }, + ], + })); +} + +function viewsByDueDate(objects: MaintenanceObjectResp[]): ViewConfig[] { + // Bucket-presence pre-check so we don't render an empty "Today" tab. + const presentBuckets = new Set(); + for (const obj of objects) { + for (const task of obj.tasks || []) { + const d = task.days_until_due; + if (d === null || d === undefined) continue; + DUE_DATE_VIEWS.forEach((v, i) => { + if (v.matches(d)) presentBuckets.add(i); + }); + } + } + + const views: ViewConfig[] = []; + DUE_DATE_VIEWS.forEach((v, i) => { + if (!presentBuckets.has(i)) return; + views.push({ + title: v.title, + icon: v.icon, + path: v.path, + type: "sections", + sections: [ + makeCardSection({ + type: "custom:maintenance-supporter-card", + show_header: false, + ...v.filter, + }), + ], + }); + }); + return views; +} + +export class MaintenanceDashboardStrategy extends HTMLElement { + static getCreateSuggestions(_hass: HassLike) { + return { + title: "Maintenance Supporter", + icon: "mdi:wrench-clock", + }; + } + + // Lazy-load editor so picker users who never hit "Edit Dashboard" don't + // pay for the LitElement bundle. HA's areas-dashboard-strategy uses the + // same pattern. + static async getConfigElement() { + // The editor element is registered in the same bundle below — just + // construct it. The dynamic import would work too, but with esbuild + // bundling everything into one file there's nothing extra to load. + return document.createElement(EDITOR_TAG); + } + + static async generate( + config: MaintenanceDashboardStrategyConfig | undefined, + hass: HassLike, + ): Promise { + if (hass.config?.state === STATE_NOT_RUNNING) { + return { + views: [ + { type: "sections", sections: [{ cards: [{ type: "starting" }] }] }, + ], + }; + } + if (hass.config?.recovery_mode) { + return { + views: [ + { + type: "sections", + sections: [{ cards: [{ type: "recovery-mode" }] }], + }, + ], + }; + } + + let response: { objects: MaintenanceObjectResp[] }; + try { + response = await hass.connection.sendMessagePromise<{ + objects: MaintenanceObjectResp[]; + }>({ type: "maintenance_supporter/objects" }); + } catch { + return { + title: "Maintenance", + views: [ + { + title: "Maintenance", + cards: [ + { + type: "markdown", + content: + "**Maintenance Supporter** is not loaded. Install/enable the integration first.", + }, + ], + }, + ], + }; + } + + const objects = response.objects || []; + + // Onboarding short-circuit. With zero objects the area / status / floor / + // due_date branches all produce just the Overview view (which shows + // "no tasks") — that's a useless first impression. Show an actionable + // empty-state instead so the user knows where to click next. + if (objects.length === 0) { + return { title: "Maintenance", views: [emptyStateView()] }; + } + + const groupBy: GroupBy = config?.group_by ?? "area"; + + // Record-of-builders pattern from HA core's home-overview-view-strategy + // — each entry is a no-arg lambda producing the per-mode views. Extending + // with a new group_by mode means adding one entry instead of growing an + // if/else chain. + const viewBuilders: Record ViewConfig[]> = { + area: () => viewsByArea(objects, hass.areas || {}), + status: () => viewsByStatus(objects), + floor: () => viewsByFloor(objects, hass.areas || {}, hass.floors || {}), + due_date: () => viewsByDueDate(objects), + calendar: () => viewsByCalendar(objects), + }; + + return { + title: "Maintenance", + views: [ + overviewView(), + ...(viewBuilders[groupBy] ?? viewBuilders.area)(), + ], + }; + } +} + +// ── Editor ────────────────────────────────────────────────────────────────── +// +// Minimal LovelaceStrategyEditor: a single dropdown for ``group_by``. Pattern +// follows HA core's ``hui-areas-dashboard-strategy-editor`` — LitElement that +// exposes setConfig(), holds the current config in @state, and dispatches +// "config-changed" with the new config on user input. +// +// We keep the editor as a plain HTMLElement (no Lit dependency) because the +// strategy file otherwise stays Lit-free. It's enough HTML for one ${options} +
    + The "Overview" view is always first. Empty groups are skipped. +
    + + `; + const select = this.querySelector("#group-by") as HTMLSelectElement | null; + select?.addEventListener("change", () => { + const newConfig: MaintenanceDashboardStrategyConfig = { + ...this._config, + group_by: select.value as GroupBy, + }; + this._config = newConfig; + this.dispatchEvent( + new CustomEvent("config-changed", { + detail: { config: newConfig }, + bubbles: true, + composed: true, + }), + ); + }); + } +} + +// ── Section Strategy ──────────────────────────────────────────────────────── +// +// A standalone section users can drop into ANY view (HA's home dashboard, the +// areas dashboard's per-area view, a custom dashboard) to surface a slice of +// maintenance tasks in context. Mirrors HA core's ``common-controls`` section +// strategy pattern. YAML usage: +// +// sections: +// - strategy: +// type: custom:maintenance-supporter-section +// area_id: kitchen # optional — restrict to one area +// filter_status: # optional — restrict to certain statuses +// - overdue +// - triggered +// title: Kitchen upkeep # optional heading card +// max_items: 5 # optional row limit +// +// At least one filter is recommended; with no filters it shows everything, +// which is what the dashboard strategy already does. + +const SECTION_STRATEGY_TYPE = "maintenance-supporter-section"; +const SECTION_STRATEGY_TAG = `ll-strategy-section-${SECTION_STRATEGY_TYPE}`; + +interface MaintenanceSectionStrategyConfig { + type: "custom:maintenance-supporter-section" | "maintenance-supporter-section"; + area_id?: string; + filter_status?: string[]; + filter_objects?: string[]; + filter_due_min_days?: number; + filter_due_max_days?: number; + title?: string; + max_items?: number; +} + +class MaintenanceSectionStrategy extends HTMLElement { + static async generate( + config: MaintenanceSectionStrategyConfig | undefined, + hass: HassLike, + ): Promise { + const card: CardConfig = { + type: "custom:maintenance-supporter-card", + show_header: false, + }; + + // Pass through optional filters + if (config?.filter_status?.length) card.filter_status = config.filter_status; + if (config?.max_items) card.max_items = config.max_items; + if (config?.filter_due_min_days !== undefined) { + card.filter_due_min_days = config.filter_due_min_days; + } + if (config?.filter_due_max_days !== undefined) { + card.filter_due_max_days = config.filter_due_max_days; + } + + // Resolve area_id → object names via WS (the card filters by name). + // Falls back gracefully if the WS call fails — empty filter means + // "show everything", which still renders something useful. + let names: string[] | undefined = config?.filter_objects; + if (config?.area_id && !names) { + try { + const r = await hass.connection.sendMessagePromise<{ + objects: MaintenanceObjectResp[]; + }>({ type: "maintenance_supporter/objects" }); + names = (r.objects || []) + .filter((o) => o.object.area_id === config.area_id) + .map((o) => o.object.name); + } catch { + // ignore — card will show all + } + } + if (names && names.length > 0) card.filter_objects = names; + + const cards: CardConfig[] = []; + if (config?.title) { + cards.push({ + type: "heading", + heading: config.title, + heading_style: "title", + }); + } + cards.push(card); + + return { type: "grid", cards }; + } +} + +// v2.3.5 (issue #52 follow-up): wrap registration in try/catch + emit a +// banner-style console.log so that when ANY user hits a strategy timeout +// we can see in the browser console exactly which step failed instead of +// guessing. The non-error console.log also serves as a positive load +// signal — if the user doesn't see "[maintenance-supporter] strategy +// bundle loaded" at all, the script never ran (cached index.html / CSP / +// MIME-type problem). If they see the log but still get the timeout, +// something else (Lovelace resource conflict, HACS plugin clash) is +// holding the registry. +const STRATEGY_VERSION = "2.3.6"; +const LOG_PREFIX = "[maintenance-supporter]"; + +function safeDefine(tag: string, ctor: CustomElementConstructor): "ok" | "skipped" | string { + try { + if (customElements.get(tag)) { + return "skipped"; + } + customElements.define(tag, ctor); + return "ok"; + } catch (err) { + return `ERROR: ${String(err)}`; + } +} + +// NOTE: the dashboard tag (STRATEGY_TAG) is intentionally NOT defined here. +// It is owned by the tiny zero-import shim (maintenance-strategy-shim.ts), +// which is what HA loads via frontend_extra_module_url so registration wins +// HA's 5 s whenDefined race even under heavy plugin load. This heavy bundle is +// lazy-loaded by the shim on first generate()/getConfigElement(); the shim +// calls our exported MaintenanceDashboardStrategy.generate(). We still define +// the editor + section strategies here (not on HA's timeout-critical path). +const _defineResults = { + editor: safeDefine(EDITOR_TAG, MaintenanceStrategyEditor), + section: safeDefine(SECTION_STRATEGY_TAG, MaintenanceSectionStrategy), +}; + +console.log( + `${LOG_PREFIX} strategy bundle v${STRATEGY_VERSION} loaded — registrations:`, + _defineResults, + "— run maintenanceSupporterDiagnose() in console for full state", +); + +// Global devtools helper — typing maintenanceSupporterDiagnose() in the +// browser console returns a snapshot of registration state + module URLs +// + Lovelace resources. Easier than asking users to paste a multi-line +// snippet they then can't run for whatever reason. +(window as unknown as { maintenanceSupporterDiagnose?: () => Promise }).maintenanceSupporterDiagnose = + async () => { + const tag = STRATEGY_TAG; + let resources: Array<{ url?: string }> = []; + try { + const ha = document.querySelector("home-assistant") as + | (HTMLElement & { hass?: { connection: { sendMessagePromise(m: Record): Promise } } }) + | null; + if (ha?.hass) { + const r = await ha.hass.connection.sendMessagePromise<{ resources?: Array<{ url?: string }> }>( + { type: "lovelace/resources" }, + ); + resources = r.resources || []; + } + } catch (e) { resources = [{ url: `` }]; } + return { + version: STRATEGY_VERSION, + el_registered: !!customElements.get(tag), + el_class_excerpt: customElements.get(tag)?.toString().slice(0, 100) || null, + customStrategies_dashboard: (window as unknown as { customStrategies?: Array<{ type: string; strategyType: string }> }) + .customStrategies?.filter((s) => s.strategyType === "dashboard").map((s) => s.type) || [], + scriptTagsInPage: Array.from(document.querySelectorAll("script[type=module]")) + .map((s) => (s as HTMLScriptElement).src) + .filter((s) => /maint|strateg/i.test(s)), + lovelaceResources_matching: resources.filter((r) => /maint|strateg/i.test(r.url || "")).map((r) => r.url), + registrations: _defineResults, + }; + }; + +// ── Phase 5: Status Section-Strategies ────────────────────────────────────── +// +// Three small status sections so users with a Lovelace-only setup +// can monitor vacation / budget / groups state without leaving the dashboard. +// v2.4.0 update: each strategy now delegates to a live interactive custom +// card (vacation-section-card / budget-section-card / groups-section-card) +// that lets admins edit values inline. +// +// v2.3.4 (issue #52): the section-card modules are LAZY-loaded inside +// generate() — bundling them at module top blew the strategy file up to +// 509 KB, and HA's 5 s strategy-load timeout was firing on slow connections +// before customElements.define for ll-strategy-dashboard-* could run. Now +// the strategy bundle is tiny (~10 KB) and registers instantly; the heavy +// section-card code only loads when a section actually renders. + +class MaintenanceVacationSectionStrategy extends HTMLElement { + static async generate( + _config: { type: string; title?: string }, + _hass: HassLike, + ): Promise { + await import("./components/vacation-section-card"); + return { + type: "grid", + cards: [ + { + type: "custom:maintenance-vacation-section-card", + title: _config?.title, + }, + ], + }; + } +} + +class MaintenanceBudgetSectionStrategy extends HTMLElement { + static async generate( + _config: { type: string; title?: string }, + _hass: HassLike, + ): Promise { + await import("./components/budget-section-card"); + return { + type: "grid", + cards: [ + { + type: "custom:maintenance-budget-section-card", + title: _config?.title, + }, + ], + }; + } +} + +class MaintenanceGroupsSectionStrategy extends HTMLElement { + static async generate( + _config: { type: string; title?: string }, + _hass: HassLike, + ): Promise { + await import("./components/groups-section-card"); + return { + type: "grid", + cards: [ + { + type: "custom:maintenance-groups-section-card", + title: _config?.title, + }, + ], + }; + } +} + +// Register the three section strategies +const VACATION_TAG = "ll-strategy-section-maintenance-supporter-vacation"; +const BUDGET_TAG = "ll-strategy-section-maintenance-supporter-budget"; +const GROUPS_TAG = "ll-strategy-section-maintenance-supporter-groups"; +if (!customElements.get(VACATION_TAG)) customElements.define(VACATION_TAG, MaintenanceVacationSectionStrategy); +if (!customElements.get(BUDGET_TAG)) customElements.define(BUDGET_TAG, MaintenanceBudgetSectionStrategy); +if (!customElements.get(GROUPS_TAG)) customElements.define(GROUPS_TAG, MaintenanceGroupsSectionStrategy); + +// ── fire-dom-event handler ────────────────────────────────────────────────── +// +// HA's standard ``tap_action: { action: "fire-dom-event", ll_custom: {...} }`` +// dispatches an ``ll-custom`` CustomEvent on the tapped element. We register +// one bubble-phase listener at document level that handles every payload +// whose ``type`` is namespaced with ``maintenance-supporter:`` — currently: +// +// maintenance-supporter:add-object → in-place open of MaintenanceObjectDialog +// maintenance-supporter:open-task → in-place open of MaintenanceTaskDialog +// (with entry_id + task_id in detail) +// +// The dialog components are imported via dialog-mount.ts and mounted lazily +// onto document.body the first time they are needed. If the in-place mount +// fails (e.g. not yet ready), we fall back to deep-linking +// the panel via ?ms_action=… so the user still ends up where they want. + +// v2.3.4 (issue #52): dialog-mount is heavy (pulls in 7 dialog components + +// their Lit runtime). We only need it when the user actually triggers a +// fire-dom-event from a strategy-generated card, so the import is dynamic +// and runs the first time a maintenance-supporter:* event fires. +type DialogMountModule = typeof import("./dialog-mount"); +let _dialogMountPromise: Promise | null = null; +function getDialogMount(): Promise { + if (!_dialogMountPromise) { + // Don't cache a REJECTED import: after a version bump the browser may hold a + // stale strategy bundle whose hashed dialog-mount chunk 404s once; caching + // that rejection would dead-end every dialog action until a full reload. + // Clear the cache on failure so a later attempt (or fixed chunk) can retry. + _dialogMountPromise = import("./dialog-mount").catch((err) => { + _dialogMountPromise = null; + throw err; + }); + } + return _dialogMountPromise; +} + +interface LlCustomEventDetail { + type?: string; + entry_id?: unknown; + task_id?: unknown; + [key: string]: unknown; +} + +const LL_CUSTOM_HANDLER_FLAG = "_msSupporterLlCustomBound"; + +function deepLink(path: string): void { + history.pushState(null, "", path); + window.dispatchEvent(new CustomEvent("location-changed")); +} + +function registerLlCustomHandler(): void { + const w = window as unknown as Record; + if (w[LL_CUSTOM_HANDLER_FLAG]) return; // idempotent — strategy file may load twice + w[LL_CUSTOM_HANDLER_FLAG] = true; + + document.addEventListener("ll-custom", (event: Event) => { + const raw = (event as CustomEvent).detail; + if (!raw || typeof raw !== "object") return; + // HA's `tap_action: { action: "fire-dom-event", ll_custom: {…} }` dispatches + // `ll-custom` with the ENTIRE action config as `detail`, nesting our payload + // under `.ll_custom` (verified live on HA 2026.6 — issue #69: "Add object" + // did nothing because we read `detail.type`, which was undefined). Direct + // dispatchers (the calendar card, the panel) put the payload at the top + // level. Accept both: prefer the nested `ll_custom` object when present. + const nested = (raw as { ll_custom?: unknown }).ll_custom; + const detail = ( + nested && typeof nested === "object" ? nested : raw + ) as LlCustomEventDetail; + if (!detail || typeof detail.type !== "string") return; + if (!detail.type.startsWith("maintenance-supporter:")) return; + const action = detail.type.slice("maintenance-supporter:".length); + + if (action === "deep-link" && typeof detail.path === "string") { + deepLink(detail.path); + return; + } + + // Everything else needs the heavy dialog-mount module — load it lazily. + void (async () => { + let dm: DialogMountModule; + try { + dm = await getDialogMount(); + } catch { + // dialog-mount chunk unavailable (e.g. stale bundle after an update) — + // degrade to opening the panel instead of a silent, unhandled failure. + deepLink("/maintenance-supporter"); + return; + } + + if (action === "add-object") { + if (dm.openCreateObjectDialog()) return; + deepLink("/maintenance-supporter?ms_action=add_object"); + return; + } + + if ( + action === "open-task" && + typeof detail.entry_id === "string" && + typeof detail.task_id === "string" + ) { + if (dm.openTaskQuickActions(detail.entry_id, detail.task_id)) return; + if (dm.openEditTaskDialog(detail.entry_id, detail.task_id)) return; + deepLink( + `/maintenance-supporter?entry_id=${encodeURIComponent(detail.entry_id)}` + + `&task_id=${encodeURIComponent(detail.task_id)}`, + ); + return; + } + + if (action === "open-object" && typeof detail.entry_id === "string") { + if (dm.openObjectQuickActions(detail.entry_id)) return; + deepLink(`/maintenance-supporter?entry_id=${encodeURIComponent(detail.entry_id)}`); + return; + } + + if ( + action === "edit-history" && + typeof detail.entry_id === "string" && + typeof detail.task_id === "string" && + typeof detail.original_timestamp === "string" + ) { + const haRoot = document.querySelector( + "home-assistant", + ) as (HTMLElement & { hass?: { connection: { sendMessagePromise(m: Record): Promise } } }) | null; + const hass = haRoot?.hass; + if (!hass) return; + try { + const r = await hass.connection.sendMessagePromise<{ + tasks?: Array<{ id?: string; history?: Array> }>; + }>({ + type: "maintenance_supporter/object", + entry_id: detail.entry_id, + }); + const task = r.tasks?.find((t) => t.id === detail.task_id); + const histEntry = task?.history?.find( + (h) => h.timestamp === detail.original_timestamp, + ); + if (!histEntry) return; + dm.openHistoryEditDialog({ + entry_id: detail.entry_id as string, + task_id: detail.task_id as string, + original_timestamp: detail.original_timestamp as string, + type: (histEntry.type as string) || "completed", + timestamp: (histEntry.timestamp as string) || (detail.original_timestamp as string), + notes: (histEntry.notes as string) ?? null, + cost: (histEntry.cost as number | undefined) ?? null, + duration: (histEntry.duration as number | undefined) ?? null, + completed_by: (histEntry.completed_by as string) ?? null, + }); + } catch { + deepLink("/maintenance-supporter"); + } + } + })(); + }); +} + +registerLlCustomHandler(); + +// Discovery — picked up by HA 2026.5+. Older HA ignores it silently. +const w = window as unknown as { + customStrategies?: Array<{ + type: string; + strategyType: string; + name: string; + description?: string; + documentationURL?: string; + }>; +}; +w.customStrategies = w.customStrategies || []; + +function registerStrategy(entry: { + type: string; + strategyType: string; + name: string; + description?: string; + documentationURL?: string; +}): void { + const exists = w.customStrategies!.some( + (s) => s.type === entry.type && s.strategyType === entry.strategyType, + ); + if (!exists) w.customStrategies!.push(entry); +} + +registerStrategy({ + type: STRATEGY_TYPE, + strategyType: "dashboard", + name: "Maintenance Supporter", + description: + "Auto-generated dashboard. Group views by area, status, floor, or due date — picked from the strategy editor or YAML.", + documentationURL: + "https://github.com/iluebbe/maintenance_supporter#dashboard-strategy", +}); + +registerStrategy({ + type: SECTION_STRATEGY_TYPE, + strategyType: "section", + name: "Maintenance Supporter — Section", + description: + "Embed maintenance tasks (filterable by area, status, due date) as a section in any dashboard view.", + documentationURL: + "https://github.com/iluebbe/maintenance_supporter#section-strategy", +}); + +// Phase 5 status sections +registerStrategy({ + type: "maintenance-supporter-vacation", + strategyType: "section", + name: "Maintenance Supporter — Vacation Status", + description: "Compact vacation-mode status banner. Tap to open settings in the panel.", +}); +registerStrategy({ + type: "maintenance-supporter-budget", + strategyType: "section", + name: "Maintenance Supporter — Budget Status", + description: "Monthly + yearly maintenance budget overview. Tap for details.", +}); +registerStrategy({ + type: "maintenance-supporter-groups", + strategyType: "section", + name: "Maintenance Supporter — Groups", + description: "List of configured task groups with member counts.", +}); + +export {}; diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts new file mode 100644 index 0000000..688d340 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-panel.ts @@ -0,0 +1,2903 @@ +/** Maintenance Supporter Sidebar Panel. */ + +import { LitElement, html, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators.js"; +import { sharedStyles, STATUS_COLORS, STATUS_ICONS, DEFAULT_CURRENCY_SYMBOL, t, ensureLocale, isLocaleLoaded, formatDate, formatDueDays, formatInterval, formatRecurrence } from "./styles"; +import { daysProgress } from "./helpers/interval"; +import { buildObjectReportHtml, type ReportLabels } from "./helpers/report"; +import { warrantyStatus } from "./helpers/warranty"; +import { OBJECT_COLUMNS, DEFAULT_OBJECTS_TABLE_COLUMNS, sanitizeColumns } from "./helpers/object-columns"; +import { downloadTextFile } from "./helpers/download"; +import { buildTaskWorksheetHtml, type WorksheetExcerpt, type WorksheetLabels } from "./helpers/worksheet"; +import { describeWsError } from "./ws-errors"; +import { panelStyles } from "./panel-styles"; +import type { + HomeAssistant, + MaintenanceObjectResponse, + MaintenanceTask, + MaintenanceGroup, + StatisticsResponse, + BudgetStatus, + AdvancedFeatures, + TaskRow, + HistoryEntry, + TriggerConfig, + StatisticsPoint, +} from "./types"; +import { StatisticsService } from "./statistics-service"; +import { UserService } from "./user-service"; +import "./components/object-dialog"; +import type { MaintenanceObjectDialog } from "./components/object-dialog"; +import "./components/documents-section"; +import "./components/task-documents"; +import "./components/task-dialog"; +import type { MaintenanceTaskDialog } from "./components/task-dialog"; +import "./components/complete-dialog"; +import type { MaintenanceCompleteDialog } from "./components/complete-dialog"; +import "./components/qr-dialog"; +import type { MaintenanceQrDialog } from "./components/qr-dialog"; +// v2.0.0: panel uses the extracted Calendar Card instead of its own +// _renderCalendar() method — single source of truth for the calendar view. +import "./maintenance-calendar-card"; +// v2.2.0: in-place edit dialog for past history entries +import "./components/history-edit-dialog"; +import type { + MaintenanceHistoryEditDialog, + HistoryEntryDraft, +} from "./components/history-edit-dialog"; +import "./components/confirm-dialog"; +import type { MaintenanceConfirmDialog } from "./components/confirm-dialog"; +import "./components/settings-view"; +import "./components/storage-section-card"; +import "./components/seasonal-overrides-dialog"; +import type { SeasonalOverridesDialog } from "./components/seasonal-overrides-dialog"; +import "./components/group-dialog"; +import type { MaintenanceGroupDialog } from "./components/group-dialog"; +import { type SparklineContext } from "./renderers/sparkline"; +import { buildCalendarBuckets, isoDateLocal, type CalendarEvent } from "./helpers/calendar-bucket"; +import { renderTriggerProgress, renderMiniSparkline } from "./renderers/progress"; +import { type HistoryContext } from "./renderers/history"; +import { renderTaskDetail, renderUserBadge, type TaskDetailContext } from "./renderers/task-detail"; +import { computeWindow, VIRTUAL_MIN_ROWS } from "./helpers/virtual-window"; + +type View = "overview" | "object" | "task" | "all_objects"; +type SortMode = "due_date" | "object" | "type" | "task_name" | "area" | "assigned_user" | "group"; +type ObjectSortMode = "alphabetical" | "due_soonest" | "task_count"; +type GroupByMode = "none" | "area" | "group" | "user"; + +// Chart dimension constants for mini sparklines (overview) + +@customElement("maintenance-supporter-panel") +export class MaintenanceSupporterPanel extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + @property({ type: Boolean, reflect: true }) public narrow = false; + @property({ attribute: false }) public panel: Record = {}; + + @state() private _objects: MaintenanceObjectResponse[] = []; + @state() private _stats: StatisticsResponse | null = null; + @state() private _view: View = "overview"; + @state() private _selectedEntryId: string | null = null; + @state() private _selectedTaskId: string | null = null; + @state() private _filterStatus = ""; + @state() private _filterUser: string | null = null; + @state() private _unsub: (() => void) | null = null; + @state() private _chartRangeDays = (() => { + try { + const v = parseInt(localStorage.getItem("msp-chart-range") || "", 10); + return [7, 30, 90, 365].includes(v) ? v : 30; + } catch { + return 30; + } + })(); + @state() private _hideOutliers = (() => { + try { return localStorage.getItem("msp-chart-hide-outliers") === "1"; } catch { return false; } + })(); + @state() private _historyFilter: string | null = null; + @state() private _budget: BudgetStatus | null = null; + @state() private _groups: Record = {}; + @state() private _detailStatsData: Map = new Map(); + @state() private _miniStatsData: Map = new Map(); + @state() private _features: AdvancedFeatures = { adaptive: false, predictions: false, seasonal: false, environmental: false, budget: false, groups: false, checklists: false, schedule_time: false, completion_actions: false }; + // HA user IDs (UUIDs) granted full panel access despite not being HA admins. + @state() private _adminPanelUserIds: string[] = []; + // v2.8.4: master switch — the allowlist only grants the full panel when this + // is on (default off → every non-admin is read-only, admins unaffected). + @state() private _operatorWriteEnabled = false; + // Default warning_days from the global config entry — used as the initial + // value in the task-create dialog so the Settings → General → "Default + // warning days" choice actually flows through to new tasks. + @state() private _defaultWarningDays = 7; + @state() private _actionLoading = false; + @state() private _moreMenuOpen = false; + @state() private _toastMessage = ""; + @state() private _toastUndo: (() => void) | null = null; + private _toastTimer: ReturnType | null = null; + private _dismissedSuggestions = new Set(); + + // Dashboard redesign state + @state() private _overviewTab: "today" | "dashboard" | "calendar" | "settings" = (() => { + try { + const v = localStorage.getItem("msp-overview-tab"); + return v === "today" || v === "calendar" ? v : "dashboard"; + } catch { return "dashboard"; } + })(); + @state() private _activeTab: "overview" | "history" = "overview"; + @state() private _costDurationToggle: "cost" | "duration" | "both" = "both"; + @state() private _historySearch = ""; + @state() private _sortMode: SortMode = "due_date"; + @state() private _objectSortMode: ObjectSortMode = "alphabetical"; + @state() private _groupByMode: GroupByMode = "none"; + // (#67): All-Objects view mode (cards|table) + the configurable table + // columns sourced from the global setting (sanitised; defaults until loaded). + @state() private _objectViewMode: "cards" | "table" = "cards"; + @state() private _objectsTableColumns: string[] = DEFAULT_OBJECTS_TABLE_COLUMNS; + // v2.10.0: archived tasks/objects are hidden until this toggle is on. + @state() private _showArchived = false; + // v2.15.0: bulk selection on the dashboard task list (multi-select + + // complete/archive). Keys are `${entry_id}:${task_id}`. + @state() private _bulkMode = false; + @state() private _bulkSelected = new Set(); + // Virtualized dashboard task table (large installs): only rows in the + // scroll window are in the DOM; spacers keep the scrollbar honest. The + // window is recomputed from `.content` scroll/resize (rAF-throttled). + @state() private _virtStart = 0; + @state() private _virtEnd = 0; // 0 = uninitialized → initial slice + private _virtRowHeight = 53; + private _virtTotalRows = 0; + private _virtScrollAttached = false; + private _virtRaf = 0; + // v2.15.0: collapsed analysis sections on the task-detail overview tab, + // remembered per section across visits. + @state() private _collapsedSections: Set = (() => { + try { return new Set(JSON.parse(localStorage.getItem("msp-collapsed-sections") || "[]")); } + catch { return new Set(); } + })(); + // v2.15.0: command palette ("/" since 2.18.1 — Ctrl+K clashed with HA's own + // global search) — global fuzzy search over objects + tasks. + @state() private _paletteOpen = false; + @state() private _paletteQuery = ""; + @state() private _paletteActive = 0; + // v2.15.0: template gallery (surfaces the config-flow object templates). + @state() private _templateGalleryOpen = false; + @state() private _templates: Array<{ id: string; name: string; category: string; tasks: unknown[]; disabled?: boolean }> = []; + @state() private _templateCategories: Record = {}; + @state() private _templateBusy = false; + // v1.5.0: Calendar tab state + // v2.0.0: window-days + user-filter state moved into the + // custom element — the panel just + // renders the card and intercepts its ll-custom open-task events. + + private _statsService: StatisticsService | null = null; + private _userService: UserService | null = null; + private _dataLoaded = false; + private _lastConnection: unknown = null; + + private get _lang(): string { + return this.hass?.language || "en"; + } + + /** + * Operator mode = read-only end-user mode (hides every create/edit/delete action). + * + * Gating (full panel only when an exception applies): + * - admins (incl. the owner) always see the full panel + * - a non-admin sees the full panel only when operator-write delegation is + * enabled AND their ID is in the `admin_panel_user_ids` allowlist + * - everyone else sees operator mode (Complete / Skip only) + * + * Delegation defaults OFF, so out of the box every non-admin is read-only. + * The switch + allowlist are managed by an admin under Settings → Panel + * Access (either the panel's Settings tab or the HA config flow), and the + * server mirrors this exact rule in helpers/permissions.user_may_write. + */ + private get _isOperator(): boolean { + const u = this.hass?.user; + if (!u) return true; // pre-hass state: render safe default + if (u.is_admin) return false; + return !(this._operatorWriteEnabled && this._adminPanelUserIds.includes(u.id)); + } + + private _popstateHandler = (e: PopStateEvent) => this._onPopState(e); + + connectedCallback(): void { + super.connectedCallback(); + window.addEventListener("popstate", this._popstateHandler); + window.addEventListener("keydown", this._paletteKeydown); + window.addEventListener("resize", this._onVirtualScroll, { passive: true }); + const saved = localStorage.getItem("maintenance_supporter_sort"); + if (saved && ["due_date", "object", "type", "task_name", "area", "assigned_user", "group"].includes(saved)) { + this._sortMode = saved as SortMode; + } + const savedObj = localStorage.getItem("maintenance_supporter_object_sort"); + if (savedObj && ["alphabetical", "due_soonest", "task_count"].includes(savedObj)) { + this._objectSortMode = savedObj as ObjectSortMode; + } + const savedGroup = localStorage.getItem("maintenance_supporter_groupby"); + if (savedGroup && ["none", "area", "group", "user"].includes(savedGroup)) { + this._groupByMode = savedGroup as GroupByMode; + } + const savedView = localStorage.getItem("maintenance_supporter_object_view"); + if (savedView === "cards" || savedView === "table") { + this._objectViewMode = savedView; + } + } + + disconnectedCallback(): void { + super.disconnectedCallback(); + window.removeEventListener("popstate", this._popstateHandler); + window.removeEventListener("keydown", this._paletteKeydown); + window.removeEventListener("resize", this._onVirtualScroll); + this.shadowRoot?.querySelector(".content")?.removeEventListener("scroll", this._onVirtualScroll); + this._virtScrollAttached = false; + if (this._virtRaf) cancelAnimationFrame(this._virtRaf); + if (this._unsub) { + this._unsub(); + this._unsub = null; + } + this._dataLoaded = false; + this._lastConnection = null; + this._deepLinkHandled = false; + this._statsService?.clearCache(); + this._statsService = null; + } + + updated(changedProps: Map): void { + super.updated(changedProps); + // Lazy-load the user's UI language (non-EN tables aren't bundled) and + // re-render once it arrives. EN is bundled, so strings read in English + // until then rather than as raw keys. + const lang = this.hass?.language; + if (lang && !isLocaleLoaded(lang)) { + ensureLocale(lang).then(() => this.requestUpdate()); + } + if (changedProps.has("hass") && this.hass) { + if (!this._dataLoaded) { + this._dataLoaded = true; + this._lastConnection = this.hass.connection; + // Seed initial history state so back button returns to overview + history.replaceState({ msp_view: "overview", msp_entry: null, msp_task: null }, ""); + this._loadData(); + this._subscribe(); + } else if (this.hass.connection !== this._lastConnection) { + this._lastConnection = this.hass.connection; + if (this._unsub) { + try { this._unsub(); } catch { /* ignore */ } + this._unsub = null; + } + this._subscribe(); + this._loadData(); + } + + if (!this._statsService) { + this._statsService = new StatisticsService(this.hass); + this._fetchMiniStatsForOverview(); + } else { + this._statsService.updateHass(this.hass); + } + + if (!this._userService) { + this._userService = new UserService(this.hass); + this._userService.getUsers(); + } else { + this._userService.updateHass(this.hass); + } + } + + // Virtualized task table: attach the scroll listener once `.content` + // exists (lit keeps the node stable across renders), then reconcile the + // window with the just-rendered DOM (row-height probe + clamps). Only + // sets state when the window actually moved, so this can't loop. + const content = this.shadowRoot?.querySelector(".content"); + if (content && !this._virtScrollAttached) { + content.addEventListener("scroll", this._onVirtualScroll, { passive: true }); + this._virtScrollAttached = true; + } + this._updateVirtualWindow(); + } + + /** rAF-throttled scroll/resize handler for the virtualized task table. */ + private _onVirtualScroll = (): void => { + if (this._virtRaf) return; + this._virtRaf = requestAnimationFrame(() => { + this._virtRaf = 0; + this._updateVirtualWindow(); + }); + }; + + /** Recompute the rendered window of the virtualized task table from the + * live scroll position. No-op unless the virtual table is in the DOM. */ + private _updateVirtualWindow(): void { + const content = this.shadowRoot?.querySelector(".content"); + const table = this.shadowRoot?.querySelector(".task-table.virtual"); + if (!content || !table) return; + // Probe the real row height from a rendered row (uniform rows; the + // hidden sizer row is height 0 and excluded). + const probe = table.querySelector(".task-row:not(.virt-sizer)"); + if (probe && probe.offsetHeight > 20) this._virtRowHeight = probe.offsetHeight; + const listTop = + table.getBoundingClientRect().top - content.getBoundingClientRect().top + content.scrollTop; + const w = computeWindow({ + scrollTop: content.scrollTop, + viewportHeight: content.clientHeight, + listTop, + rowHeight: this._virtRowHeight, + total: this._virtTotalRows, + }); + if (w.start !== this._virtStart || w.end !== this._virtEnd) { + this._virtStart = w.start; + this._virtEnd = w.end; + } + } + + private async _loadData(): Promise { + const [objResult, statsResult, budgetResult, groupsResult, settingsResult] = await Promise.all([ + this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/objects" }).catch(() => null), + this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/statistics" }).catch(() => null), + this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/budget_status" }).catch(() => null), + this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/groups" }).catch(() => null), + this.hass.connection.sendMessagePromise({ type: "maintenance_supporter/settings" }).catch(() => null), + ]); + if (objResult) this._objects = (objResult as { objects: MaintenanceObjectResponse[] }).objects; + if (statsResult) this._stats = statsResult as StatisticsResponse; + if (budgetResult) this._budget = budgetResult as BudgetStatus; + if (groupsResult) this._groups = (groupsResult as { groups: Record }).groups || {}; + if (settingsResult) { + const sr = settingsResult as { + features: AdvancedFeatures; + admin_panel_user_ids?: string[]; + operator_write_enabled?: boolean; + general?: { default_warning_days?: number }; + objects_table_columns?: string[]; + }; + this._features = sr.features; + this._adminPanelUserIds = sr.admin_panel_user_ids || []; + this._operatorWriteEnabled = sr.operator_write_enabled ?? false; + const dwd = sr.general?.default_warning_days; + if (typeof dwd === "number" && dwd >= 0 && dwd <= 365) { + this._defaultWarningDays = dwd; + } + this._objectsTableColumns = sanitizeColumns(sr.objects_table_columns); + } + + // Fetch mini-sparkline data for overview (non-blocking) + this._fetchMiniStatsForOverview(); + + // Handle deep-link URL parameters (from QR code scan) + this._handleDeepLink(); + } + + private _deepLinkHandled = false; + + private _handleDeepLink(): void { + if (this._deepLinkHandled) return; + const params = new URLSearchParams(window.location.search); + + // v1.8.1: ms_action deep links from the dashboard strategy's fire-dom-event + // Empty-state buttons. Cleared from the URL after one fire so refreshing + // the page doesn't re-open it. + // + // v2.3.0 Phase 5/6: also handles open_vacation / open_budget / open_groups + // / open_settings from the section strategies' status banners. Each + // routes to the right tab + scroll target inside the Settings view. + const msAction = params.get("ms_action"); + const cleanMsActionUrl = () => { + const cleanUrl = window.location.pathname + window.location.hash; + history.replaceState(history.state, "", cleanUrl); + }; + if (msAction === "add_object") { + this._deepLinkHandled = true; + cleanMsActionUrl(); + requestAnimationFrame(() => { + this.shadowRoot + ?.querySelector("maintenance-object-dialog") + ?.openCreate(); + }); + return; + } + if (msAction === "open_vacation" + || msAction === "open_budget" + || msAction === "open_groups" + || msAction === "open_settings") { + this._deepLinkHandled = true; + cleanMsActionUrl(); + // Switch to the Settings tab — that's where Vacation/Budget/Groups live. + this._overviewTab = "settings"; + // Hint the settings-view which sub-section to scroll to (its own + // settings-view component reads this on attribute change). + requestAnimationFrame(() => { + const settingsView = this.shadowRoot?.querySelector( + "maintenance-settings-view", + ) as (HTMLElement & { scrollToSection?: (s: string) => void }) | null; + const target = msAction.replace("open_", ""); // "vacation" / "budget" / "groups" / "settings" + settingsView?.scrollToSection?.(target); + }); + return; + } + + const entryId = params.get("entry_id"); + if (!entryId) return; + this._deepLinkHandled = true; + + const taskId = params.get("task_id"); + const action = params.get("action"); + + // Always clean URL params — they are consumed once + const cleanUrl = window.location.pathname + window.location.hash; + history.replaceState(history.state, "", cleanUrl); + + // Validate that the referenced object exists + const obj = this._getObject(entryId); + if (!obj) { + this._showOverview(); + return; + } + + // Navigate to the right view + if (taskId) { + const task = obj.tasks.find((t) => t.id === taskId); + if (!task) { + this._showObject(entryId); + return; + } + this._showTask(entryId, taskId); + if (action === "complete") { + requestAnimationFrame(() => { + this._openCompleteDialog(entryId, taskId, task.name, this._features.checklists ? task.checklist : undefined, this._features.adaptive && !!task.adaptive_config?.enabled); + }); + } else if (action === "quick_complete") { + // v1.3.0: silent complete using pre-configured defaults; falls back + // to the normal dialog if the task has none. + requestAnimationFrame(() => { + this._handleQuickComplete(entryId, taskId, task); + }); + } + } else { + this._showObject(entryId); + } + } + + private _isCounterEntity(tc: TriggerConfig | null | undefined): boolean { + if (!tc) return false; + const type = tc.type || "threshold"; + return type === "counter" || type === "state_change"; + } + + private async _fetchDetailStats(entityId: string, isCounter: boolean): Promise { + if (!this._statsService) return; + const points = await this._statsService.getDetailStats(entityId, isCounter, this._chartRangeDays); + const updated = new Map(this._detailStatsData); + updated.set(entityId, points); + this._detailStatsData = updated; + } + + /** Chart range chips (7/30/90/365 d): persist the choice and refetch the + * open task's stats at the new window. */ + private _setChartRange(days: number): void { + if (days === this._chartRangeDays) return; + this._chartRangeDays = days; + try { localStorage.setItem("msp-chart-range", String(days)); } catch { /* private mode */ } + const task = this._selectedEntryId && this._selectedTaskId + ? this._getTask(this._selectedEntryId, this._selectedTaskId) + : null; + const entityId = task?.trigger_config?.entity_id; + if (entityId) { + // Drop the stale-range series so the chart shows its loading state. + const updated = new Map(this._detailStatsData); + updated.delete(entityId); + this._detailStatsData = updated; + void this._fetchDetailStats(entityId, this._isCounterEntity(task!.trigger_config)); + } + } + + private _setHideOutliers(hide: boolean): void { + if (hide === this._hideOutliers) return; + // Outlier filtering is client-side on the already-fetched series, so just + // flip the flag and let renderChart re-filter — no re-fetch needed. + this._hideOutliers = hide; + try { localStorage.setItem("msp-chart-hide-outliers", hide ? "1" : "0"); } catch { /* private mode */ } + } + + private async _fetchMiniStatsForOverview(): Promise { + if (!this._statsService) return; + + const entities: Array<{ entityId: string; isCounter: boolean }> = []; + for (const obj of this._objects) { + for (const task of obj.tasks) { + const entityId = task.trigger_config?.entity_id; + if (!entityId) continue; + entities.push({ entityId, isCounter: this._isCounterEntity(task.trigger_config) }); + } + } + + if (entities.length === 0) return; + const batchResult = await this._statsService.getBatchMiniStats(entities); + this._miniStatsData = new Map([...this._miniStatsData, ...batchResult]); + } + + private async _subscribe(): Promise { + try { + const unsub = await this.hass.connection.subscribeMessage( + (msg: unknown) => { + const data = msg as { objects: MaintenanceObjectResponse[] }; + this._objects = data.objects; + }, + { type: "maintenance_supporter/subscribe" } + ); + // If the element was detached while the subscribe was in flight, drop the + // now-orphaned subscription instead of storing it on a dead component. + if (!this.isConnected) { + unsub(); + return; + } + this._unsub = unsub; + } catch { + // Subscription failed; fall back to polling + } + } + + // --- Data accessors --- + + private get _taskRows(): TaskRow[] { + const rows: TaskRow[] = []; + for (const obj of this._objects) { + for (const task of obj.tasks) { + // v2.10.0: archived tasks are hidden unless the toggle reveals them. + if (!this._showArchived && task.archived) continue; + if (this._filterStatus && task.status !== this._filterStatus) continue; + + // User filter + if (this._filterUser) { + const userId = this._filterUser === "current_user" + ? this._userService?.getCurrentUserId() + : this._filterUser; + if (task.responsible_user_id !== userId) continue; + } + + // Collect groups that contain this task + const groupNames: string[] = []; + for (const group of Object.values(this._groups)) { + if (group.task_refs?.some((r) => r.entry_id === obj.entry_id && r.task_id === task.id)) { + groupNames.push(group.name); + } + } + + rows.push({ + entry_id: obj.entry_id, + task_id: task.id, + object_name: obj.object.name, + task_name: task.name, + type: task.type, + schedule_type: task.schedule_type, + status: task.status, + days_until_due: task.days_until_due ?? null, + next_due: task.next_due ?? null, + trigger_active: task.trigger_active, + trigger_current_value: task.trigger_current_value ?? null, + trigger_current_delta: task.trigger_current_delta ?? null, + trigger_config: task.trigger_config ?? null, + trigger_entity_info: task.trigger_entity_info ?? null, + times_performed: task.times_performed, + total_cost: task.total_cost, + interval_days: task.interval_days ?? null, + interval_unit: task.interval_unit ?? null, + interval_anchor: task.interval_anchor ?? null, + is_done: task.is_done ?? false, + archived: task.archived ?? false, + history: task.history || [], + enabled: task.enabled, + nfc_tag_id: task.nfc_tag_id ?? null, + priority: task.priority ?? "normal", + labels: task.labels ?? [], + area_id: obj.object.area_id ?? null, + responsible_user_id: task.responsible_user_id ?? null, + group_names: groupNames, + }); + } + } + const statusOrder: Record = { overdue: 0, triggered: 1, due_soon: 2, ok: 3 }; + const byStatus = (a: TaskRow, b: TaskRow) => (statusOrder[a.status] ?? 9) - (statusOrder[b.status] ?? 9); + const byDays = (a: TaskRow, b: TaskRow) => (a.days_until_due ?? 99999) - (b.days_until_due ?? 99999); + const byDue = (a: TaskRow, b: TaskRow) => byStatus(a, b) || byDays(a, b); + const areaName = (r: TaskRow) => + r.area_id ? this.hass?.areas?.[r.area_id]?.name || "" : ""; + const userName = (r: TaskRow) => + r.responsible_user_id ? this._userService?.getUserName(r.responsible_user_id) || "" : ""; + const groupName = (r: TaskRow) => r.group_names[0] || ""; + const sorts: Record number> = { + due_date: byDue, + object: (a, b) => a.object_name.localeCompare(b.object_name) || byDue(a, b), + type: (a, b) => a.type.localeCompare(b.type) || byDue(a, b), + task_name: (a, b) => a.task_name.localeCompare(b.task_name), + area: (a, b) => { + const an = areaName(a), bn = areaName(b); + // Empty areas at end + if (!an && bn) return 1; + if (an && !bn) return -1; + return an.localeCompare(bn) || byDue(a, b); + }, + assigned_user: (a, b) => { + const an = userName(a), bn = userName(b); + if (!an && bn) return 1; + if (an && !bn) return -1; + return an.localeCompare(bn) || byDue(a, b); + }, + group: (a, b) => { + const an = groupName(a), bn = groupName(b); + if (!an && bn) return 1; + if (an && !bn) return -1; + return an.localeCompare(bn) || byDue(a, b); + }, + }; + rows.sort(sorts[this._sortMode]); + return rows; + } + + private _getObject(entryId: string): MaintenanceObjectResponse | undefined { + return this._objects.find((o) => o.entry_id === entryId); + } + + private _getTask(entryId: string, taskId: string): MaintenanceTask | undefined { + const obj = this._getObject(entryId); + return obj?.tasks.find((t) => t.id === taskId); + } + + // --- Navigation --- + + /** Push a browser history entry so the back button navigates within the panel. */ + private _pushPanelState(view: View, entryId?: string | null, taskId?: string | null): void { + const state = { msp_view: view, msp_entry: entryId || null, msp_task: taskId || null }; + history.pushState(state, ""); + } + + /** Handle browser back/forward button. */ + private _onPopState(e: PopStateEvent): void { + const s = e.state as { msp_view?: View; msp_entry?: string; msp_task?: string } | null; + if (!s?.msp_view) { + // No panel state → we're leaving the panel, let HA handle it + return; + } + // Restore view without pushing another history entry + this._view = s.msp_view; + this._selectedEntryId = s.msp_entry || null; + this._selectedTaskId = s.msp_task || null; + this._moreMenuOpen = false; + if (s.msp_view === "task" && s.msp_entry && s.msp_task) { + this._historyFilter = null; + const task = this._getTask(s.msp_entry, s.msp_task); + if (task?.trigger_config?.entity_id) { + this._fetchDetailStats(task.trigger_config.entity_id, this._isCounterEntity(task.trigger_config)); + } + } + } + + private _showOverview(): void { + this._pushPanelState("overview"); + this._view = "overview"; + this._selectedEntryId = null; + this._selectedTaskId = null; + this._moreMenuOpen = false; + this._scrollContentToTop(); + } + + private _showAllObjects(): void { + this._pushPanelState("all_objects"); + this._view = "all_objects"; + this._selectedEntryId = null; + this._selectedTaskId = null; + this._scrollContentToTop(); + } + + /** v2.1.0 (Discussion #49 — @byoung79): tap a KPI value to auto-filter + * the task list. Empty string clears the filter (used by "Tasks" KPI). */ + private _filterByStatus(status: string): void { + this._filterStatus = status; + // Make sure we're on the dashboard tab so the task list is actually + // visible — pointless to filter on the calendar/settings tabs. + if (this._overviewTab !== "dashboard") { + this._overviewTab = "dashboard"; + } + this._scrollContentToTop(); + } + + /** Reset scroll on the panel's .content container. Used by every + * navigation action so the user always lands at the top of the new + * view, never at a stale scroll position from the previous view. + * + * History: an earlier version targeted .tab-bar via scrollIntoView, + * which worked for the 4 filter KPIs (stay in overview, .tab-bar + * exists) but silently no-op'd for the Objects KPI (switches to the + * all_objects view where .tab-bar isn't rendered). Targeting .content + * directly works regardless of which view is now showing. Plus same + * helper is now called from every navigation entry-point so e.g. + * clicking on a deep object in a long list lands you at the top of + * the object-detail view, not partway through it. */ + private _scrollContentToTop(): void { + requestAnimationFrame(() => { + const content = this.shadowRoot?.querySelector(".content"); + if (content) content.scrollTo({ top: 0, behavior: "smooth" }); + }); + } + + private _showObject(entryId: string): void { + this._pushPanelState("object", entryId); + this._view = "object"; + this._selectedEntryId = entryId; + this._selectedTaskId = null; + this._scrollContentToTop(); + } + + private _showTask(entryId: string, taskId: string): void { + this._pushPanelState("task", entryId, taskId); + this._view = "task"; + this._selectedEntryId = entryId; + this._selectedTaskId = taskId; + this._activeTab = "overview"; + this._historyFilter = null; + this._scrollContentToTop(); + + // Lazy-load statistics for the task's trigger entity + const task = this._getTask(entryId, taskId); + if (task?.trigger_config?.entity_id) { + const entityId = task.trigger_config.entity_id; + const isCounter = this._isCounterEntity(task.trigger_config); + this._fetchDetailStats(entityId, isCounter); + } + } + + // --- Toast --- + + private _showToast(msg: string): void { + if (this._toastTimer) clearTimeout(this._toastTimer); + this._toastUndo = null; + this._toastMessage = msg; + this._toastTimer = setTimeout(() => { this._toastMessage = ""; this._toastTimer = null; }, 4000); + } + + /** A toast with an Undo action — used for reversible actions (archive) that + * run immediately instead of behind a confirm dialog. Longer-lived so the + * user has time to react; the undo callback dismisses it. */ + private _showUndoToast(msg: string, undo: () => void): void { + if (this._toastTimer) clearTimeout(this._toastTimer); + this._toastMessage = msg; + this._toastUndo = undo; + this._toastTimer = setTimeout(() => { + this._toastMessage = ""; this._toastUndo = null; this._toastTimer = null; + }, 7000); + } + + private _runToastUndo(): void { + const undo = this._toastUndo; + if (this._toastTimer) clearTimeout(this._toastTimer); + this._toastMessage = ""; this._toastUndo = null; this._toastTimer = null; + undo?.(); + } + + // --- Command palette ("/") --- + + private _paletteKeydown = (e: KeyboardEvent): void => { + // "/" opens the palette (GitHub/Discourse convention; Shift+7 on a German + // layout still yields key === "/"). Deliberately NOT Ctrl/Cmd+K: that is + // HA's own global-search hotkey, and this window-level preventDefault + // would shadow it whenever the panel is open. + if ( + e.key === "/" + && !e.ctrlKey && !e.metaKey && !e.altKey + && !this._paletteOpen + ) { + const target = e.composedPath()[0]; + const typing = target instanceof HTMLElement + && (target.tagName === "INPUT" || target.tagName === "TEXTAREA" + || target.tagName === "SELECT" || target.isContentEditable); + if (typing) return; + e.preventDefault(); + this._openPalette(); + return; + } + if (!this._paletteOpen) return; + const results = this._paletteResults; + if (e.key === "Escape") { e.preventDefault(); this._closePalette(); } + else if (e.key === "ArrowDown") { e.preventDefault(); this._paletteActive = Math.min(this._paletteActive + 1, results.length - 1); } + else if (e.key === "ArrowUp") { e.preventDefault(); this._paletteActive = Math.max(this._paletteActive - 1, 0); } + else if (e.key === "Enter") { e.preventDefault(); const r = results[this._paletteActive]; if (r) this._selectPaletteResult(r); } + }; + + private _openPalette(): void { + this._paletteQuery = ""; + this._paletteActive = 0; + this._paletteOpen = true; + this.updateComplete.then(() => { + this.shadowRoot?.querySelector(".palette-input")?.focus(); + }); + } + + private _closePalette(): void { + this._paletteOpen = false; + this._paletteQuery = ""; + } + + private get _paletteResults(): Array<{ kind: "object" | "task"; entryId: string; taskId?: string; label: string; sub: string }> { + const q = this._paletteQuery.trim().toLowerCase(); + const out: Array<{ kind: "object" | "task"; entryId: string; taskId?: string; label: string; sub: string }> = []; + for (const obj of this._objects) { + const oname = obj.object.name || ""; + if (!q || oname.toLowerCase().includes(q)) { + out.push({ kind: "object", entryId: obj.entry_id, label: oname, sub: t("object", this._lang) }); + } + for (const task of obj.tasks) { + if (task.archived) continue; + const tname = task.name || ""; + const labelHit = (task.labels || []).some((lb) => lb.toLowerCase().includes(q)); + if (!q || tname.toLowerCase().includes(q) || oname.toLowerCase().includes(q) || labelHit) { + const labelSub = (task.labels || []).length ? ` #${(task.labels || []).join(" #")}` : ""; + out.push({ kind: "task", entryId: obj.entry_id, taskId: task.id, label: tname, sub: oname + labelSub }); + } + } + if (out.length > 60) break; // cap the working set; sliced below + } + return out.slice(0, 40); + } + + private _selectPaletteResult(r: { kind: "object" | "task"; entryId: string; taskId?: string }): void { + this._closePalette(); + if (r.kind === "task" && r.taskId) this._showTask(r.entryId, r.taskId); + else this._showObject(r.entryId); + } + + private _renderPalette() { + if (!this._paletteOpen) return nothing; + const L = this._lang; + const results = this._paletteResults; + return html` +
    this._closePalette()}> +
    e.stopPropagation()}> + { this._paletteQuery = (e.target as HTMLInputElement).value; this._paletteActive = 0; }} + /> +
    + ${results.length === 0 + ? html`
    ${t("palette_no_results", L)}
    ` + : results.map((r, i) => html` +
    { this._paletteActive = i; }} + @click=${() => this._selectPaletteResult(r)}> + + ${r.label} + ${r.sub} +
    + `)} +
    +
    ${t("palette_hint", L)}
    +
    +
    + `; + } + + // --- Template gallery --- + + private async _openTemplateGallery(): Promise { + this._templateGalleryOpen = true; + if (this._templates.length > 0) return; + try { + const res = await this.hass.connection.sendMessagePromise<{ + categories: Record; + templates: Array<{ id: string; name: string; category: string; tasks: unknown[]; disabled?: boolean }>; + }>({ type: "maintenance_supporter/templates", language: this._lang }); + this._templateCategories = res.categories || {}; + // v2.21: admin-hidden templates stay out of the gallery. + this._templates = (res.templates || []).filter((tpl) => !tpl.disabled); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + private async _createFromTemplate(templateId: string): Promise { + this._templateBusy = true; + try { + const res = await this.hass.connection.sendMessagePromise<{ entry_id?: string }>({ + type: "maintenance_supporter/object/from_template", + language: this._lang, + template_id: templateId, + }); + this._templateGalleryOpen = false; + await this._loadData(); + this._showToast(t("template_created", this._lang)); + if (res?.entry_id) this._showObject(res.entry_id); + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._templateBusy = false; + } + } + + private _categoryName(catId: string): string { + const cat = this._templateCategories[catId] as Record | undefined; + if (!cat) return catId; + return cat[`name_${this._lang}`] || cat["name_en"] || catId; + } + + private _renderTemplateGallery() { + if (!this._templateGalleryOpen) return nothing; + const L = this._lang; + // Group templates by category, preserving category declaration order. + const byCat = new Map(); + for (const tpl of this._templates) { + if (!byCat.has(tpl.category)) byCat.set(tpl.category, []); + byCat.get(tpl.category)!.push(tpl); + } + return html` +
    { this._templateGalleryOpen = false; }}> + +
    + `; + } + + // --- Bulk selection (dashboard task list) --- + + private _bulkKey(row: TaskRow): string { + return `${row.entry_id}:${row.task_id}`; + } + + private _toggleBulkMode(): void { + this._bulkMode = !this._bulkMode; + if (!this._bulkMode) this._bulkSelected = new Set(); + } + + private _toggleBulkRow(row: TaskRow): void { + const key = this._bulkKey(row); + const next = new Set(this._bulkSelected); + if (next.has(key)) next.delete(key); else next.add(key); + this._bulkSelected = next; + } + + private _bulkSelectAll(rows: TaskRow[]): void { + const keys = rows.map((r) => this._bulkKey(r)); + const allSelected = keys.every((k) => this._bulkSelected.has(k)); + this._bulkSelected = allSelected ? new Set() : new Set(keys); + } + + /** Run one WS message per selected row (the endpoints are per-task); reports + * how many succeeded and refreshes once at the end. */ + private async _runBulk( + rows: TaskRow[], + build: (row: TaskRow) => Record, + doneMsg: (n: number) => string, + undo?: () => void, + ): Promise { + const selected = rows.filter((r) => this._bulkSelected.has(this._bulkKey(r))); + if (selected.length === 0) return; + this._actionLoading = true; + let ok = 0; + for (const row of selected) { + try { + await this.hass.connection.sendMessagePromise(build(row)); + ok++; + } catch { /* keep going; report the successful count */ } + } + this._actionLoading = false; + this._bulkSelected = new Set(); + this._bulkMode = false; + await this._loadData(); + if (undo && ok > 0) this._showUndoToast(doneMsg(ok), undo); + else this._showToast(doneMsg(ok)); + } + + private _bulkComplete(rows: TaskRow[]): void { + void this._runBulk( + rows, + (row) => ({ type: "maintenance_supporter/task/complete", entry_id: row.entry_id, task_id: row.task_id }), + (n) => t("bulk_completed", this._lang).replace("{n}", String(n)), + ); + } + + private _bulkArchive(rows: TaskRow[]): void { + // Capture the selection so the undo can unarchive exactly those. + const keys = rows.filter((r) => this._bulkSelected.has(this._bulkKey(r))) + .map((r) => ({ entry_id: r.entry_id, task_id: r.task_id })); + void this._runBulk( + rows, + (row) => ({ type: "maintenance_supporter/task/archive", entry_id: row.entry_id, task_id: row.task_id }), + (n) => t("bulk_archived", this._lang).replace("{n}", String(n)), + async () => { + for (const k of keys) { + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/unarchive", entry_id: k.entry_id, task_id: k.task_id, + }); + } catch { /* best effort */ } + } + await this._loadData(); + }, + ); + } + + // --- Actions --- + + private async _deleteObject(entryId: string): Promise { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + const ok = await dlg?.confirm({ + title: t("delete", this._lang), + message: t("confirm_delete_object", this._lang), + confirmText: t("delete", this._lang), + danger: true, + }); + if (!ok) return; + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/object/delete", + entry_id: entryId, + }); + this._showOverview(); + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + /** Open a printable maintenance report for the object in a new tab (the user + * prints / saves as PDF from there). Self-contained HTML, no dependency. */ + private _printObjectReport(entryId: string): void { + const resp = this._getObject(entryId); + if (!resp) return; + const L = this._lang; + const labels: ReportLabels = { + title: t("report_title", L), + generated: t("report_generated", L), + manufacturer: t("manufacturer", L), + model: t("model", L), + serial: t("serial_number_label", L), + installed: t("installed", L), + warranty: t("warranty", L), + area: t("area", L), + notes: t("report_notes", L), + tasksHeading: t("tasks", L), + colTask: t("task_name", L), + colType: t("report_col_type", L), + colStatus: t("report_col_status", L), + colSchedule: t("report_col_schedule", L), + colLastDone: t("last_performed", L), + colNextDue: t("next_due", L), + colCost: t("cost", L), + colTimes: t("report_times_done", L), + totalCost: t("report_total_cost", L), + scheduleLabel: (task) => formatRecurrence(task, L), + none: "—", + statusLabel: (s: string) => t(s, L), + typeLabel: (ty: string) => t(ty, L), + }; + const html = buildObjectReportHtml( + resp.object, resp.tasks, labels, + (iso) => (iso ? formatDate(iso, L) : ""), + this._budget?.currency_symbol || DEFAULT_CURRENCY_SYMBOL, + new Date().toISOString(), + ); + const url = URL.createObjectURL(new Blob([html], { type: "text/html" })); + window.open(url, "_blank"); + setTimeout(() => URL.revokeObjectURL(url), 60000); + } + + private async _duplicateObject(entryId: string): Promise { + this._actionLoading = true; + try { + const res = await this.hass.connection.sendMessagePromise<{ entry_id?: string }>({ + type: "maintenance_supporter/object/duplicate", + entry_id: entryId, + }); + await this._loadData(); + this._showToast(t("object_duplicated", this._lang)); + if (res?.entry_id) this._showObject(res.entry_id); + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._actionLoading = false; + } + } + + private async _deleteTask(entryId: string, taskId: string): Promise { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + const ok = await dlg?.confirm({ + title: t("delete", this._lang), + message: t("confirm_delete_task", this._lang), + confirmText: t("delete", this._lang), + danger: true, + }); + if (!ok) return; + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/delete", + entry_id: entryId, + task_id: taskId, + }); + this._showObject(entryId); + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + // v2.10.0: archive / unarchive a single task (reversible — no confirm). + private async _duplicateTask(entryId: string, taskId: string): Promise { + this._moreMenuOpen = false; + this._actionLoading = true; + try { + const res = await this.hass.connection.sendMessagePromise<{ task_id?: string }>({ + type: "maintenance_supporter/task/duplicate", + entry_id: entryId, + task_id: taskId, + }); + await this._loadData(); + this._showToast(t("task_duplicated", this._lang)); + // Jump straight to the copy so the user can rename/adjust it. + if (res?.task_id) this._showTask(entryId, res.task_id); + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._actionLoading = false; + } + } + + private async _toggleArchiveTask(entryId: string, taskId: string, archived: boolean): Promise { + this._actionLoading = true; + try { + await this.hass.connection.sendMessagePromise({ + type: archived + ? "maintenance_supporter/task/unarchive" + : "maintenance_supporter/task/archive", + entry_id: entryId, + task_id: taskId, + }); + await this._loadData(); + // Just archived → offer a one-tap undo (unarchive) instead of a confirm. + if (!archived) { + this._showUndoToast(t("task_archived", this._lang), + () => this._toggleArchiveTask(entryId, taskId, true)); + } + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._actionLoading = false; + } + } + + // v2.10.0: archive / unarchive an object. Archiving cascades to its tasks but + // is fully reversible, so instead of a blocking confirm we run it immediately + // and offer an Undo toast (v2.14.0). + private async _toggleArchiveObject(entryId: string, archived: boolean): Promise { + try { + await this.hass.connection.sendMessagePromise({ + type: archived + ? "maintenance_supporter/object/unarchive" + : "maintenance_supporter/object/archive", + entry_id: entryId, + }); + await this._loadData(); + if (!archived) { + this._showUndoToast(t("object_archived", this._lang), + () => this._toggleArchiveObject(entryId, true)); + } + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + // v2.20 (N3): seasonal pause / resume. Pausing asks for an optional + // auto-resume date; resuming re-anchors recurring tasks to a fresh cycle. + private async _togglePauseObject(entryId: string, paused: boolean): Promise { + if (!paused) { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + const result = await dlg?.prompt({ + title: t("pause_object", this._lang), + message: t("pause_until_prompt", this._lang), + confirmText: t("pause_object", this._lang), + inputLabel: t("pause_until_label", this._lang), + inputType: "date", + }); + if (!result?.confirmed) return; + try { + const msg: Record = { + type: "maintenance_supporter/object/pause", + entry_id: entryId, + }; + if (result.value) msg.until = result.value; + await this.hass.connection.sendMessagePromise(msg); + await this._loadData(); + this._showUndoToast(t("object_paused", this._lang), + () => this._togglePauseObject(entryId, true)); + } catch (e) { + this._showToast(describeWsError(e, this._lang)); + } + return; + } + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/object/resume", + entry_id: entryId, + }); + await this._loadData(); + this._showToast(t("object_resumed", this._lang)); + } catch (e) { + this._showToast(describeWsError(e, this._lang)); + } + } + + // v2.20 (N1): replace a worn-out object with a successor — the old one is + // archived in place (history/costs stay browsable), the new one starts as a + // pre-filled fresh unit with tasks and documents carried over. + private async _replaceObject(entryId: string, currentName: string): Promise { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + const result = await dlg?.prompt({ + title: t("replace_object", this._lang), + message: t("replace_object_prompt", this._lang), + confirmText: t("replace_object", this._lang), + inputLabel: t("replace_name_label", this._lang), + inputType: "text", + inputValue: currentName, + }); + if (!result?.confirmed) return; + this._actionLoading = true; + try { + const res = await this.hass.connection.sendMessagePromise<{ entry_id?: string }>({ + type: "maintenance_supporter/object/replace", + entry_id: entryId, + name: result.value || currentName, + }); + await this._loadData(); + this._showToast(t("object_replaced", this._lang)); + if (res?.entry_id) this._showObject(res.entry_id); + } catch (e) { + this._showToast(describeWsError(e, this._lang)); + } finally { + this._actionLoading = false; + } + } + + private async _skipTask(entryId: string, taskId: string, reason?: string): Promise { + this._actionLoading = true; + try { + const msg: Record = { + type: "maintenance_supporter/task/skip", + entry_id: entryId, + task_id: taskId, + }; + if (reason) msg.reason = reason; + await this.hass.connection.sendMessagePromise(msg); + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._actionLoading = false; + } + } + + private async _resetTask(entryId: string, taskId: string, resetDate?: string): Promise { + this._actionLoading = true; + try { + const msg: Record = { + type: "maintenance_supporter/task/reset", + entry_id: entryId, + task_id: taskId, + }; + if (resetDate) msg.date = resetDate; + await this.hass.connection.sendMessagePromise(msg); + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._actionLoading = false; + } + } + + private async _applySuggestion(entryId: string, taskId: string, interval: number): Promise { + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/apply_suggestion", + entry_id: entryId, + task_id: taskId, + interval: interval, + }); + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + private _openSeasonalOverrides(task: MaintenanceTask): void { + const dlg = this.shadowRoot!.querySelector("maintenance-seasonal-overrides-dialog"); + if (!dlg || !this._selectedEntryId) return; + const overrides = task.adaptive_config?.seasonal_overrides as Record | null | undefined; + dlg.open(this._selectedEntryId, task.id, overrides); + } + + private async _reanalyzeInterval(entryId: string, taskId: string): Promise { + try { + const res = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/analyze_interval", + entry_id: entryId, + task_id: taskId, + }) as { + recommended_interval: number | null; + confidence: string; + data_points: number; + recommendation_reason: string | null; + }; + if (res.recommended_interval) { + this._showToast( + `${t("reanalyze_result", this._lang)}: ${res.recommended_interval} ${t("days", this._lang)} ` + + `(${t(`confidence_${res.confidence}`, this._lang)}, ${res.data_points} ${t("data_points", this._lang)})`, + ); + } else { + this._showToast(t("reanalyze_insufficient_data", this._lang)); + } + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + private async _promptSkipTask(entryId: string, taskId: string): Promise { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + if (!dlg) return; + const result = await dlg.prompt({ + title: t("skip", this._lang), + message: t("skip_reason_prompt", this._lang), + confirmText: t("skip", this._lang), + inputLabel: t("reason_optional", this._lang), + inputType: "text", + }); + if (!result.confirmed) return; + this._skipTask(entryId, taskId, result.value || undefined); + } + + private async _promptResetTask(entryId: string, taskId: string): Promise { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + if (!dlg) return; + const result = await dlg.prompt({ + title: t("reset", this._lang), + message: t("reset_date_prompt", this._lang), + confirmText: t("reset", this._lang), + inputLabel: t("reset_date_optional", this._lang), + inputType: "date", + }); + if (!result.confirmed) return; + this._resetTask(entryId, taskId, result.value || undefined); + } + + private async _snoozeTask(entryId: string, taskId: string): Promise { + this._actionLoading = true; + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/snooze", + entry_id: entryId, + task_id: taskId, + }); + this._showToast(t("snoozed", this._lang)); + } catch { + this._showToast(t("action_error", this._lang)); + } finally { + this._actionLoading = false; + } + } + + private _dismissSuggestion(entryId?: string, taskId?: string): void { + if (entryId && taskId) { + this._dismissedSuggestions.add(`${entryId}_${taskId}`); + } + this.requestUpdate(); + } + + // v1.3.0: silent complete-via-QR. Tries the quick endpoint first; if the + // task has no quick_complete_defaults configured, falls back to the + // normal complete dialog so the user is never stuck after a scan. + private async _handleQuickComplete( + entryId: string, taskId: string, task: MaintenanceTask, + ): Promise { + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/quick_complete", + entry_id: entryId, task_id: taskId, + }); + this._showToast(t("quick_complete_success", this._lang)); + } catch (e: unknown) { + const code = (e as { code?: string })?.code || ""; + if (code === "no_defaults") { + // No defaults set — open the dialog so the user can fill them in. + this._openCompleteDialog( + entryId, taskId, task.name, + this._features.checklists ? task.checklist : undefined, + this._features.adaptive && !!task.adaptive_config?.enabled, + ); + } else { + this._showToast(t("action_error", this._lang)); + } + } + } + + // v2.21: printable one-pager for a task — details, checklist tick boxes, + // QR pair, and (when a linked PDF manual has a page hint for this task) a + // link to the server-cut manual excerpt. Opens in a new tab for printing. + private async _printTaskWorksheet(entryId: string, taskId: string): Promise { + const obj = this._getObject(entryId); + const task = obj?.tasks.find((tk) => tk.id === taskId); + if (!obj || !task) return; + this._actionLoading = true; + try { + const qrBase: Record = { + type: "maintenance_supporter/qr/generate", + entry_id: entryId, task_id: taskId, url_mode: "server", + }; + const [qrView, qrComplete] = await Promise.all([ + this.hass.connection.sendMessagePromise<{ svg_data_uri?: string }>({ ...qrBase, action: "view" }).catch(() => null), + this.hass.connection.sendMessagePromise<{ svg_data_uri?: string }>({ ...qrBase, action: "complete" }).catch(() => null), + ]); + + // Manual excerpt: first linked PDF with a page hint for this task. + let excerpt: WorksheetExcerpt | null = null; + try { + const docs = await this.hass.connection.sendMessagePromise<{ + documents: Array<{ id: string; kind: string; mime?: string; title?: string; + filename?: string; task_ids?: string[]; task_pages?: Record }>; + }>({ type: "maintenance_supporter/documents/list", entry_id: entryId }); + const manual = (docs.documents || []).find( + (d) => d.kind === "file" && d.mime === "application/pdf" + && (d.task_ids || []).includes(taskId) && d.task_pages?.[taskId], + ); + if (manual) { + const start = manual.task_pages![taskId]; + const count = 4; + const signed = await this.hass.connection.sendMessagePromise<{ path: string }>({ + type: "auth/sign_path", + path: `/api/maintenance_supporter/document/${manual.id}/excerpt?start=${start}&count=${count}`, + expires: 3600, + }); + excerpt = { + title: manual.title || manual.filename || "Manual", + startPage: start, endPage: start + count - 1, + // Absolute URL: the sheet lives on a blob: page, where a + // relative /api/... href cannot resolve (caught live). + url: new URL(signed.path, window.location.origin).toString(), + vendorBase: new URL("/maintenance_supporter_vendor", window.location.origin).toString(), + }; + } + } catch { /* worksheet works without the excerpt */ } + + const L = this._lang; + const labels: WorksheetLabels = { + title: t("worksheet", L), + object: t("object", L), + type: t("maintenance_type", L), + interval: t("interval", L), + nextDue: t("next_due", L), + lastDone: t("last_performed", L), + priority: t("priority", L), + checklist: t("checklist", L), + notes: t("notes_label", L), + scanView: t("worksheet_scan_view", L), + scanComplete: t("worksheet_scan_complete", L), + manualExcerpt: t("worksheet_manual_excerpt", L), + pages: t("worksheet_pages", L), + printedOn: t("worksheet_printed", L), + never: t("worksheet_never", L), + typeLabel: (ty: string) => t(ty, L), + statusLabel: (st: string) => t(st, L), + }; + const html = buildTaskWorksheetHtml( + task, obj.object.name, labels, + (iso) => formatDate(iso, L), + (tk) => formatRecurrence(tk, L), + qrView?.svg_data_uri || null, + qrComplete?.svg_data_uri || null, + excerpt, + new Date().toISOString(), + ); + const url = URL.createObjectURL(new Blob([html], { type: "text/html" })); + window.open(url, "_blank"); + setTimeout(() => URL.revokeObjectURL(url), 60000); + } finally { + this._actionLoading = false; + } + } + + private _openCompleteDialog(entryId: string, taskId: string, taskName: string, checklist?: string[], adaptiveEnabled?: boolean): void { + const dlg = this.shadowRoot!.querySelector("maintenance-complete-dialog"); + if (!dlg) return; + dlg.entryId = entryId; + dlg.taskId = taskId; + dlg.taskName = taskName; + dlg.lang = this._lang; + dlg.checklist = checklist || []; + dlg.adaptiveEnabled = !!adaptiveEnabled; + // v2.20 (#83): reading tasks get a value field — resolve type + unit here + // so none of the many call sites need to thread them through. + const tk = this._objects + .find((o) => o.entry_id === entryId) + ?.tasks.find((tsk) => tsk.id === taskId); + dlg.taskType = tk?.type || ""; + dlg.readingUnit = tk?.reading_unit || ""; + dlg.open(); + } + + private _openQrForObject(entryId: string, objectName: string): void { + const dlg = this.shadowRoot!.querySelector("maintenance-qr-dialog"); + dlg?.openForObject(entryId, objectName); + } + + private _openQrForTask(entryId: string, taskId: string, objectName: string, taskName: string): void { + const dlg = this.shadowRoot!.querySelector("maintenance-qr-dialog"); + dlg?.openForTask(entryId, taskId, objectName, taskName); + } + + private _onDialogEvent = async (): Promise => { + try { await this._loadData(); } catch { /* subscription will sync */ } + }; + + // --- Render --- + + render() { + return html` +
    + ${this.narrow || this._view !== "overview" ? this._renderHeader() : nothing} +
    + ${this._view === "overview" + ? this._renderOverview() + : this._view === "all_objects" + ? this._renderAllObjects() + : this._view === "object" + ? this._renderObjectDetail() + : this._renderTaskDetail()} +
    +
    + + + + + + + + + ${this._toastMessage ? html`
    + ${this._toastMessage} + ${this._toastUndo ? html`` : nothing} +
    ` : nothing} + ${this._renderPalette()} + ${this._renderTemplateGallery()} + `; + } + + private _renderHeader() { + const crumbs: { label: string; action?: () => void }[] = [ + { label: t("maintenance", this._lang), action: () => this._showOverview() }, + ]; + if (this._view === "object" && this._selectedEntryId) { + const obj = this._getObject(this._selectedEntryId); + crumbs.push({ label: obj?.object.name || "Object" }); + } + if (this._view === "task" && this._selectedEntryId && this._selectedTaskId) { + const obj = this._getObject(this._selectedEntryId); + crumbs.push({ + label: obj?.object.name || "Object", + action: () => this._showObject(this._selectedEntryId!), + }); + const task = this._getTask(this._selectedEntryId, this._selectedTaskId); + crumbs.push({ label: task?.name || "Task" }); + } + + return html` +
    + ${this.narrow ? html`` : nothing} + ${this._view !== "overview" + ? html` { + if (this._view === "task") this._showObject(this._selectedEntryId!); + else this._showOverview(); + }} + >` + : nothing} + +
    + `; + } + + private _renderOverview() { + const L = this._lang; + const isAdmin = !!this.hass?.user?.is_admin; + const s = this._stats; + // Only true HA admins get the Settings tab: global config, feature toggles, + // vacation and import live there and stay admin-only server-side. Operators + // (incl. allowlisted ones with full content CRUD) and plain users can't open + // it. The Calendar tab IS visible to everyone (read-only view). + if (!isAdmin && this._overviewTab === "settings") { + this._overviewTab = "dashboard"; + } + return html` + ${s + ? html` +
    +
    this._showAllObjects()} + title=${t("show_all_objects", L)}> + ${s.total_objects} + ${t("objects", L)} +
    +
    this._filterByStatus("")} + title=${t("show_all_tasks", L)}> + ${s.total_tasks} + ${t("tasks", L)} +
    +
    this._filterByStatus("overdue")} + title=${t("filter_to_overdue", L)}> + ${s.overdue} + ${t("overdue", L)} +
    +
    this._filterByStatus("due_soon")} + title=${t("filter_to_due_soon", L)}> + ${s.due_soon} + ${t("due_soon", L)} +
    +
    this._filterByStatus("triggered")} + title=${t("filter_to_triggered", L)}> + ${s.triggered} + ${t("triggered", L)} +
    +
    + ` + : nothing} +
    +
    this._setOverviewTab("today")}> + ${t("tab_today", L)} +
    +
    this._setOverviewTab("dashboard")}> + ${t("dashboard", L)} +
    +
    this._setOverviewTab("calendar")}> + ${t("tab_calendar", L)} +
    + ${isAdmin ? html` +
    this._setOverviewTab("settings")}> + ${t("settings", L)} +
    + ` : nothing} +
    + ${this._overviewTab === "today" + ? this._renderToday() + : this._overviewTab === "dashboard" + ? this._renderDashboard() + : this._overviewTab === "calendar" + ? html` +
    + +
    + ` + : html``} + `; + } + + /** v2.0.0: intercept ll-custom open-task events from the embedded calendar + * card so we navigate within the panel (using _showTask) instead of + * letting the document-level dialog-mount handler open a duplicate + * dialog on top. The handler stops propagation so the document listener + * (registered by the dashboard-strategy bundle) doesn't also fire. */ + private _onCalendarLlCustom = (e: Event): void => { + const detail = (e as CustomEvent<{ type?: string; entry_id?: string; task_id?: string }>).detail; + if ( + detail?.type === "maintenance-supporter:open-task" && + detail.entry_id && + detail.task_id + ) { + e.stopPropagation(); + this._showTask(detail.entry_id, detail.task_id); + } + }; + + // v2.0.0: _renderCalendar() removed — replaced by an embedded + // . The card holds all the same + // logic (window chips, source icons, prediction pills, projected events) + // and is the single source of truth for calendar rendering across the + // panel and stand-alone Lovelace use. Kept for reference: the old + // implementation rendered ~120 lines of Lit template literals here, + // duplicating what the card now does. + + /** Status badge with a shape icon so status isn't conveyed by colour alone + * (colour-blind accessibility) — icon + text + colour together. */ + private _statusBadge(archived: boolean, isDone: boolean, status: string) { + const L = this._lang; + const cls = archived ? "archived" : (isDone ? "done" : status); + const iconKey = archived ? "archived" : (isDone ? "completed" : status); + const label = archived ? t("archived", L) : (isDone ? t("completed", L) : t(status, L)); + return html`${label}`; + } + + private _setOverviewTab(tab: "today" | "dashboard" | "calendar" | "settings"): void { + this._overviewTab = tab; + try { localStorage.setItem("msp-overview-tab", tab); } catch { /* private mode */ } + this._scrollContentToTop(); + } + + /** Mobile-first focus list: what needs attention now, grouped by urgency. */ + private _renderToday() { + const L = this._lang; + const rows = this._taskRows; // already excludes archived + disabled + const key = (r: TaskRow) => `${r.entry_id}:${r.task_id}`; + const overdue = rows.filter((r) => r.status === "overdue" || r.trigger_active); + const claimed = new Set(overdue.map(key)); + const dueToday = rows.filter((r) => !claimed.has(key(r)) && r.days_until_due === 0); + dueToday.forEach((r) => claimed.add(key(r))); + const thisWeek = rows.filter((r) => !claimed.has(key(r)) + && r.days_until_due != null && r.days_until_due > 0 && r.days_until_due <= 7); + + if (overdue.length + dueToday.length + thisWeek.length === 0) { + return html` +
    + +

    ${t("today_all_caught_up", L)}

    +
    + `; + } + return html` +
    + ${this._renderTodaySection("today_overdue", overdue, "overdue")} + ${this._renderTodaySection("today_due_today", dueToday, "due_soon")} + ${this._renderTodaySection("today_this_week", thisWeek, "")} +
    + `; + } + + private _renderTodaySection(titleKey: string, rows: TaskRow[], cls: string) { + if (rows.length === 0) return nothing; + const L = this._lang; + return html` +
    +
    + ${t(titleKey, L)}${rows.length} +
    + ${rows.map((row) => html` +
    this._showTask(row.entry_id, row.task_id)}> + +
    +
    ${row.task_name}
    +
    ${row.object_name} · ${formatDueDays(row.days_until_due, L)}
    +
    + { e.stopPropagation(); this._openCompleteDialogForRow(row); }}> + + +
    + `)} +
    + `; + } + + private _renderDashboard() { + const s = this._stats; + const rows = this._taskRows; + const L = this._lang; + const isOperator = this._isOperator; + // v2.10.0: count archived tasks so the reveal toggle can show how many are + // hidden (and hide itself entirely when there is nothing archived). + const archivedCount = this._objects.reduce( + (n, o) => n + o.tasks.filter((tk) => tk.archived).length, 0, + ); + + return html` + ${this._features.budget ? this._renderBudgetBar() : nothing} + +
    + + + + + ${archivedCount > 0 ? html` + { this._showArchived = !this._showArchived; }} + > + + ${this._showArchived ? t("hide_archived", L) : `${t("show_archived", L)} (${archivedCount})`} + + ` : nothing} + ${!isOperator && rows.length > 0 ? html` + this._toggleBulkMode()} + > + + ${this._bulkMode ? t("cancel", L) : t("bulk_select", L)} + + ` : nothing} + ${!isOperator ? html` + this.shadowRoot!.querySelector("maintenance-object-dialog")?.openCreate()} + > + ${t("new_object", L)} + + this._openTemplateGallery()}> + ${t("templates_from", L)} + + this.shadowRoot!.querySelector("maintenance-task-dialog")?.openCreate("", this._objects)} + > + ${t("new_task", L)} + + ` : nothing} +
    + + ${rows.length === 0 + ? html` +
    + +

    ${t("no_tasks", L)}

    + ${!isOperator && this._objects.length === 0 ? html` +

    ${t("onboard_hint", L)}

    +
    + this._openTemplateGallery()}> + ${t("templates_from", L)} + + this.shadowRoot!.querySelector("maintenance-object-dialog")?.openCreate()}> + ${t("new_object", L)} + +
    + ` : nothing} +
    + ` + : html` + ${this._bulkMode ? this._renderBulkBar(rows, L) : nothing} + ${this._groupByMode === "none" + ? this._renderTaskTable(rows) + : this._renderGroupedTasks(rows, L)} + `} + + ${this._features.groups && !isOperator ? this._renderGroupsSection() : nothing} + ${!isOperator + ? html`) => { + const eid = e.detail?.entry_id; + if (eid) this._showObject(eid); + }} + >` + : nothing} + `; + } + + /** The flat dashboard task table. Below VIRTUAL_MIN_ROWS (or on narrow + * layouts, where rows aren't uniform-height) every row renders directly. + * Above it, only the scroll window is in the DOM: two grid-spanning + * spacers keep the scrollbar honest, and a hidden "sizer" row pins the + * content-sized badge column to the widest badge set across ALL rows so + * the shared subgrid tracks can't jitter as rows enter/leave the window. */ + private _renderTaskTable(rows: TaskRow[]) { + const bulkCls = this._bulkMode ? " bulk" : ""; + this._virtTotalRows = rows.length; + if (this.narrow || rows.length < VIRTUAL_MIN_ROWS) { + return html` +
    + ${rows.map((row) => this._renderOverviewRow(row))} +
    + `; + } + const total = rows.length; + const rh = this._virtRowHeight; + let start = Math.max(0, Math.min(this._virtStart, total)); + let end = this._virtEnd > 0 ? Math.min(this._virtEnd, total) : Math.min(total, 40); + if (end < start) { start = 0; end = Math.min(total, 40); } + const padTop = start * rh; + const padBottom = (total - end) * rh; + return html` +
    + ${this._renderVirtSizerRow(rows)} + ${padTop > 0 ? html`
    ` : nothing} + ${rows.slice(start, end).map((row) => this._renderOverviewRow(row))} + ${padBottom > 0 ? html`
    ` : nothing} +
    + `; + } + + /** Invisible zero-height row whose badge cell carries the union of the + * widest badges across ALL rows. Because the subgrid's `auto` badge track + * sizes to the max over rendered rows, including this row makes the track + * width independent of which data rows happen to be in the window. */ + private _renderVirtSizerRow(rows: TaskRow[]) { + const L = this._lang; + let widest = ""; + let anyDisabled = false; + let anyNfc = false; + let anyPriority = false; + for (const r of rows) { + const label = r.archived ? t("archived", L) : (r.is_done ? t("completed", L) : t(r.status, L)); + if (label.length > widest.length) widest = label; + if (!r.enabled) anyDisabled = true; + if (r.nfc_tag_id) anyNfc = true; + if (r.priority === "high" || r.priority === "low") anyPriority = true; + } + return html` + + `; + } + + private _renderBulkBar(rows: TaskRow[], L: string) { + const n = this._bulkSelected.size; + const allSelected = rows.length > 0 && rows.every((r) => this._bulkSelected.has(this._bulkKey(r))); + return html` +
    + + ${t("bulk_n_selected", L).replace("{n}", String(n))} + + this._bulkComplete(rows)}> + ${t("complete", L)} + + this._bulkArchive(rows)}> + ${t("archive", L)} + + +
    + `; + } + + private _renderGroupedTasks(rows: TaskRow[], L: string) { + const groups = new Map(); + const noneLabel = t("unassigned", L); + for (const row of rows) { + let keys: string[] = []; + if (this._groupByMode === "area") { + const a = row.area_id ? this.hass?.areas?.[row.area_id]?.name : null; + keys = [a || noneLabel]; + } else if (this._groupByMode === "user") { + const u = row.responsible_user_id ? this._userService?.getUserName(row.responsible_user_id) : null; + keys = [u || noneLabel]; + } else if (this._groupByMode === "group") { + keys = row.group_names.length > 0 ? row.group_names : [noneLabel]; + } + for (const k of keys) { + if (!groups.has(k)) groups.set(k, []); + groups.get(k)!.push(row); + } + } + const sorted = [...groups.entries()].sort(([a], [b]) => { + // Push "unassigned" / "no area" to bottom + if (a === noneLabel && b !== noneLabel) return 1; + if (b === noneLabel && a !== noneLabel) return -1; + return a.localeCompare(b); + }); + const icon = this._groupByMode === "area" ? "mdi:map-marker-outline" + : this._groupByMode === "group" ? "mdi:folder-outline" + : "mdi:account-outline"; + return html` + ${sorted.map(([key, taskRows]) => html` +
    + + + ${key} + (${taskRows.length}) + +
    + ${taskRows.map((row) => this._renderOverviewRow(row))} +
    +
    + `)} + `; + } + + // (#67) Localised warranty label, shared by the detail meta + table cell. + private _warrantyLabel(ws: ReturnType, iso: string, L: string): string { + if (ws.kind === "expired") return t("warranty_expired", L); + if (ws.kind === "expiring") { + return t("warranty_expires_in", L).replace("{days}", String(ws.days ?? 0)); + } + return t("warranty_valid_until", L).replace("{date}", formatDate(iso, L)); + } + + // (#67) Warranty status as a coloured chip in the object detail meta. + private _renderWarrantyMeta(iso: string, L: string) { + const ws = warrantyStatus(iso); + return html`

    ${t("warranty", L)}: + ${this._warrantyLabel(ws, iso, L)}

    `; + } + + private _renderAllObjects() { + const L = this._lang; + const isOperator = this._isOperator; + // (#67) Table mode is desktop-only; narrow viewports always fall back to + // cards (the table's many columns don't fit a phone). + const tableMode = this._objectViewMode === "table" && !this.narrow; + // v2.10.0: how many objects are archived (drives the reveal toggle below). + const archivedObjCount = this._objects.filter((o) => o.object.archived).length; + + // Sort + group helpers + const minDays = (obj: MaintenanceObjectResponse): number => { + let m = Infinity; + for (const t of obj.tasks) { + const d = t.days_until_due; + if (d !== null && d !== undefined && d < m) m = d; + } + return m; + }; + const sorted = this._objects.filter((o) => this._showArchived || !o.object.archived); + if (this._objectSortMode === "alphabetical") { + sorted.sort((a, b) => a.object.name.localeCompare(b.object.name)); + } else if (this._objectSortMode === "task_count") { + sorted.sort((a, b) => b.tasks.length - a.tasks.length || a.object.name.localeCompare(b.object.name)); + } else { + // due_soonest + sorted.sort((a, b) => minDays(a) - minDays(b) || a.object.name.localeCompare(b.object.name)); + } + + // Group by area for objects view + const groupedByArea = (): Map => { + const map = new Map(); + for (const obj of sorted) { + const areaId = obj.object.area_id; + const key = areaId ? this.hass?.areas?.[areaId]?.name || t("unassigned", L) : t("no_area", L); + if (!map.has(key)) map.set(key, []); + map.get(key)!.push(obj); + } + return new Map([...map.entries()].sort(([a], [b]) => a.localeCompare(b))); + }; + + const renderObject = (obj: MaintenanceObjectResponse) => { + const overdue = obj.tasks.some(t => t.status === "overdue" || t.status === "triggered"); + return html` +
    this._showObject(obj.entry_id)}> + ${overdue ? html`` : nothing} +
    + ${obj.object.name} + ${obj.object.paused + ? html` + + ` + : nothing} + ${obj.object.document_count + ? html` + ${obj.object.document_count} + ` + : nothing} + ${obj.tasks.length} ${t("tasks_lower", L)} +
    + ${obj.object.manufacturer || obj.object.model + ? html`
    ${[obj.object.manufacturer, obj.object.model].filter(Boolean).join(" ")}
    ` + : nothing} + ${obj.tasks.length === 0 + ? html`
    ${t("no_tasks_yet", L)}
    ` + : nothing} +
    + `; + }; + + return html` + +
    + + ${!this.narrow ? html` +
    + + +
    + ` : nothing} + ${!tableMode ? html` + + ` : nothing} + ${!isOperator ? html` + this.shadowRoot!.querySelector("maintenance-object-dialog")?.openCreate()} + > + ${t("new_object", L)} + + ` : nothing} + this._exportObjectsCsv()}> + ${t("settings_export_csv", L)} + + ${archivedObjCount > 0 ? html` + { this._showArchived = !this._showArchived; }} + > + + ${this._showArchived ? t("hide_archived", L) : `${t("show_archived", L)} (${archivedObjCount})`} + + ` : nothing} +
    + ${tableMode + ? this._renderObjectsTable(sorted) + : this._groupByMode === "area" + ? html` + ${[...groupedByArea().entries()].map(([area, objs]) => html` +
    + + + ${area} + (${objs.length}) + +
    ${objs.map(renderObject)}
    +
    + `)} + ` + : html`
    ${sorted.map(renderObject)}
    `} + `; + } + + private _setObjectViewMode(mode: "cards" | "table"): void { + this._objectViewMode = mode; + localStorage.setItem("maintenance_supporter_object_view", mode); + } + + // (#67 / Phase 3) Download all objects as a one-row-per-object CSV. + private async _exportObjectsCsv(): Promise { + try { + const result = await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/objects/csv", + }) as { csv: string }; + const ts = new Date().toISOString().slice(0, 10); + downloadTextFile(result.csv, `maintenance_objects_${ts}.csv`, "text/csv;charset=utf-8"); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + // (#67) Tabular All-Objects view honouring the configured columns. Desktop + // only — the caller falls back to cards on narrow viewports. + private _renderObjectsTable(objs: MaintenanceObjectResponse[]) { + const L = this._lang; + const cols = this._objectsTableColumns; + return html` +
    + + + + ${cols.map((key) => { + const def = OBJECT_COLUMNS.find((c) => c.key === key); + // The actions column header stays blank (icon-only cells). + const label = def && def.key !== "actions" ? t(def.labelKey, L) : ""; + return html``; + })} + + + + ${objs.map((obj) => html` + this._showObject(obj.entry_id)}> + ${cols.map((key) => this._renderObjectCell(key, obj, L))} + + `)} + +
    ${label}
    +
    + `; + } + + private _renderObjectCell(key: string, obj: MaintenanceObjectResponse, L: string) { + const o = obj.object; + switch (key) { + case "name": + return html` + ${o.name} + ${o.document_count + ? html` + ${o.document_count} + ` + : nothing} + `; + case "manufacturer": + return html`${o.manufacturer || "—"}`; + case "model": + return html`${o.model || "—"}`; + case "serial_number": + return html`${o.serial_number || "—"}`; + case "installation_date": + return html`${o.installation_date ? formatDate(o.installation_date, L) : "—"}`; + case "warranty_expiry": + return html`${this._renderWarrantyCell(o.warranty_expiry, L)}`; + case "area_id": { + const area = o.area_id ? (this.hass?.areas?.[o.area_id]?.name || o.area_id) : "—"; + return html`${area}`; + } + case "documentation_url": + return html`${ + o.documentation_url && /^https?:\/\//i.test(o.documentation_url) + ? html` e.stopPropagation()}>` + : "—" + }`; + case "notes": + return html`${o.notes || "—"}`; + case "task_count": + return html`${obj.tasks.length}`; + case "actions": + return html` + { e.stopPropagation(); this._openQrForObject(obj.entry_id, o.name); }}> + + + `; + default: + return html``; + } + } + + // Warranty chip for a table cell — like _renderWarrantyMeta but renders an + // em-dash placeholder when the object has no warranty date. + private _renderWarrantyCell(iso: string | null | undefined, L: string) { + const ws = warrantyStatus(iso); + if (ws.kind === "none") return html``; + return html`${this._warrantyLabel(ws, iso as string, L)}`; + } + + private async _onSettingsChanged(): Promise { + await this._loadData(); + } + + private _renderGroupsSection() { + if (!this._features.groups) return nothing; + const entries = Object.entries(this._groups); + const L = this._lang; + + return html` +
    +
    +

    ${t("groups", L)}

    + this._openGroupCreate()}> + ${t("new_group", L)} + +
    + ${entries.length === 0 + ? html`
    ${t("no_groups", L)}
    ` + : html` +
    + ${entries.map(([gid, group]) => { + const taskNames = group.task_refs + .map((ref) => this._getTask(ref.entry_id, ref.task_id)?.name) + .filter(Boolean); + return html` +
    +
    +
    ${group.name}
    +
    + this._openGroupEdit(gid)}> + + + this._deleteGroup(gid, group.name)}> + + +
    +
    + ${group.description ? html`
    ${group.description}
    ` : nothing} +
    + ${taskNames.length > 0 + ? taskNames.map((n) => html`${n}`) + : html`${t("no_tasks_short", L)}`} +
    +
    + `; + })} +
    + `} +
    + `; + } + + private _openGroupCreate(): void { + this.shadowRoot!.querySelector("maintenance-group-dialog")?.openCreate(); + } + + private _openGroupEdit(groupId: string): void { + const group = this._groups[groupId]; + if (!group) return; + this.shadowRoot!.querySelector("maintenance-group-dialog")?.openEdit(groupId, group); + } + + private async _deleteGroup(groupId: string, name: string): Promise { + const dlg = this.shadowRoot!.querySelector("maintenance-confirm-dialog"); + const ok = dlg + ? await dlg.confirm({ + title: t("delete_group", this._lang), + message: t("delete_group_confirm", this._lang).replace("{name}", name), + confirmText: t("delete", this._lang), + }) + : confirm(`${t("delete_group_confirm", this._lang).replace("{name}", name)}`); + if (!ok) return; + try { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/group/delete", + group_id: groupId, + }); + await this._loadData(); + } catch { + this._showToast(t("action_error", this._lang)); + } + } + + private _renderBudgetBar() { + const b = this._budget; + if (!b) return nothing; + const L = this._lang; + const cs = b.currency_symbol || DEFAULT_CURRENCY_SYMBOL; + const bars: { label: string; spent: number; budget: number }[] = []; + if (b.monthly_budget > 0) bars.push({ label: t("budget_monthly", L), spent: b.monthly_spent, budget: b.monthly_budget }); + if (b.yearly_budget > 0) bars.push({ label: t("budget_yearly", L), spent: b.yearly_spent, budget: b.yearly_budget }); + if (bars.length === 0) return nothing; + + return html` +
    + ${bars.map((bar) => { + const pct = Math.min(100, Math.max(0, (bar.spent / bar.budget) * 100)); + const color = pct >= 100 ? "var(--error-color, #f44336)" : pct >= b.alert_threshold_pct ? "var(--warning-color, #ff9800)" : "var(--success-color, #4caf50)"; + return html` +
    +
    + ${bar.label} + ${bar.spent.toFixed(2)} / ${bar.budget.toFixed(2)} ${cs} +
    +
    +
    +
    +
    + `; + })} +
    + `; + } + + private _renderOverviewRow(row: TaskRow) { + const L = this._lang; + // Compute days progress bar + const hasDaysBar = row.schedule_type === "time_based" && row.interval_days && row.interval_days > 0; + let pct = 0; + let barColor = STATUS_COLORS.ok; + let daysOverflow = false; + if (hasDaysBar && row.days_until_due !== null) { + const p = daysProgress(row.interval_days, row.days_until_due, row.interval_unit); + pct = p.pct; + daysOverflow = p.overflow; + if (row.status === "overdue") barColor = STATUS_COLORS.overdue; + else if (row.status === "due_soon") barColor = STATUS_COLORS.due_soon; + } + + const areaName = row.area_id ? this.hass?.areas?.[row.area_id]?.name : null; + const userName = row.responsible_user_id ? this._userService?.getUserName(row.responsible_user_id) : null; + const hasSub = row.group_names.length > 0 || areaName || userName; + + const bulkSelected = this._bulkMode && this._bulkSelected.has(this._bulkKey(row)); + return html` +
    + ${this._bulkMode ? html` + + ` : nothing} + + ${this._statusBadge(!!row.archived, row.is_done, row.status)} + ${!row.enabled ? html`${t("disabled", L)}` : nothing} + ${row.nfc_tag_id ? html`` : nothing} + ${row.priority === "high" ? html`` : nothing} + ${row.priority === "low" ? html`` : nothing} + + { e.stopPropagation(); this._showObject(row.entry_id); }}>${row.object_name} + this._showTask(row.entry_id, row.task_id)}>${row.task_name} + + ${row.group_names.length > 0 ? html` + + ${row.group_names.join(", ")} + ` : nothing} + ${areaName ? html` + + ${areaName} + ` : nothing} + ${userName ? html` + + ${userName} + ` : nothing} + ${(row.labels || []).map((label) => html` + + ${label} + `)} + + ${t(row.type, L)} + this._showTask(row.entry_id, row.task_id)}> + ${formatDueDays(row.days_until_due, L)} + ${hasDaysBar + ? html`
    ` + : nothing} + ${row.trigger_config + ? renderTriggerProgress(row) + : !hasDaysBar && row.trigger_active + ? html`` + : nothing} + ${renderMiniSparkline(row, this._miniStatsData, this._lang)} +
    + + { e.stopPropagation(); this._openCompleteDialogForRow(row); }}> + + + { e.stopPropagation(); this._promptSkipTask(row.entry_id, row.task_id); }}> + + + +
    + `; + } + + private _openCompleteDialogForRow(row: TaskRow): void { + const obj = this._objects.find(o => o.entry_id === row.entry_id); + const task = obj?.tasks.find(t => t.id === row.task_id); + this._openCompleteDialog( + row.entry_id, row.task_id, row.task_name, + this._features.checklists ? task?.checklist : undefined, + this._features.adaptive && !!task?.adaptive_config?.enabled + ); + } + + /** + * Render a compact trigger progress bar for overview rows. + * Works for threshold (value vs limit), counter (value/delta vs target), state_change (count vs target). + */ + private _renderObjectDetail() { + if (!this._selectedEntryId) return nothing; + const obj = this._getObject(this._selectedEntryId); + if (!obj) return html`

    Object not found.

    `; + const o = obj.object; + const L = this._lang; + + const isOperator = this._isOperator; + + // v2.10.0: archived tasks are hidden in the object detail too, unless the + // shared show-archived toggle is on; a per-detail toggle reveals them when + // this object has any (e.g. a task archived individually on an active object). + const archivedInObj = obj.tasks.filter((tk) => tk.archived).length; + const visibleTasks = obj.tasks.filter((tk) => this._showArchived || !tk.archived); + + return html` +
    +
    +

    ${o.name}

    +
    + ${!isOperator ? html` + { + const dlg = this.shadowRoot!.querySelector("maintenance-object-dialog"); + dlg?.openEdit(obj.entry_id, o); + }}>${t("edit", L)} + { + const dlg = this.shadowRoot!.querySelector("maintenance-task-dialog"); + dlg?.openCreate(obj.entry_id); + }}>${t("add_task", L)} + this._duplicateObject(obj.entry_id)}> + ${t("duplicate", L)} + + this._toggleArchiveObject(obj.entry_id, !!o.archived)}> + + ${o.archived ? t("unarchive_object", L) : t("archive_object", L)} + + ${!o.archived ? html` + this._togglePauseObject(obj.entry_id, !!o.paused)}> + + ${o.paused ? t("resume_object", L) : t("pause_object", L)} + + this._replaceObject(obj.entry_id, o.name)}> + ${t("replace_object", L)} + + ` : nothing} + this._deleteObject(obj.entry_id)}>${t("delete", L)} + ` : nothing} + this._openQrForObject(obj.entry_id, o.name)}> ${t("qr_code", L)} + this._printObjectReport(obj.entry_id)}> ${t("report_button", L)} +
    +
    + ${o.paused + ? html`

    + + ${t("object_paused_badge", L)}${o.paused_until + ? html` — ${t("paused_until_label", L)} ${formatDate(o.paused_until, L)}` + : nothing} +

    ` + : nothing} + ${o.manufacturer || o.model + ? html`

    ${[o.manufacturer, o.model].filter(Boolean).join(" ")}

    ` + : nothing} + ${o.serial_number ? html`

    ${t("serial_number_label", L)}: ${o.serial_number}

    ` : nothing} + ${o.documentation_url && /^https?:\/\//i.test(o.documentation_url) + ? html`

    ${t("documentation_url_label", L)}: + ${o.documentation_url} +

    ` + : nothing} + ${o.installation_date ? html`

    ${t("installed", L)}: ${formatDate(o.installation_date, L)}

    ` : nothing} + ${o.warranty_expiry ? this._renderWarrantyMeta(o.warranty_expiry, L) : nothing} + ${o.notes + ? html`
    +
    ${t("object_notes_label", L)}
    +
    ${o.notes}
    +
    ` + : nothing} + + + +

    ${t("tasks", L)} (${visibleTasks.length})${archivedInObj > 0 ? html` + { this._showArchived = !this._showArchived; }} + > + + ${this._showArchived ? t("hide_archived", L) : `${t("show_archived", L)} (${archivedInObj})`} + ` : nothing}

    + ${obj.tasks.length === 0 + ? html`
    +

    ${t("no_tasks_yet", L)}

    + { + const dlg = this.shadowRoot!.querySelector("maintenance-task-dialog"); + dlg?.openCreate(obj.entry_id); + }}>${t("add_first_task", L)} +
    ` + : html`
    ${[...visibleTasks].sort((a, b) => { + const so: Record = { overdue: 0, triggered: 1, due_soon: 2, ok: 3 }; + return (so[a.status] ?? 9) - (so[b.status] ?? 9) || (a.days_until_due ?? 99999) - (b.days_until_due ?? 99999); + }).map((task) => html` +
    + + ${this._statusBadge(!!task.archived, !!task.is_done, task.status)} + ${!task.enabled ? html`${t("disabled", L)}` : nothing} + ${task.nfc_tag_id ? html`` : nothing} + + this._showTask(obj.entry_id, task.id)}>${task.name} + ${renderUserBadge(task, (id) => this._userService?.getUserName(id) ?? null)} + ${t(task.type, L)} + this._showTask(obj.entry_id, task.id)}> + ${formatDueDays(task.days_until_due, L)} + ${task.trigger_config + ? renderTriggerProgress(task) + : nothing} + ${renderMiniSparkline(task, this._miniStatsData, this._lang)} + + + { e.stopPropagation(); this._openCompleteDialog(obj.entry_id, task.id, task.name, this._features.checklists ? task.checklist : undefined, this._features.adaptive && !!task.adaptive_config?.enabled); }}> + + + { e.stopPropagation(); this._promptSkipTask(obj.entry_id, task.id); }}> + + + +
    + `)}
    `} +
    + `; + } + + /** + * Render compact task header with status chip and action buttons. + */ + private _toggleMoreMenu(): void { + this._moreMenuOpen = !this._moreMenuOpen; + if (this._moreMenuOpen) { + // Close menu on next outside click + const handler = () => { this._moreMenuOpen = false; document.removeEventListener("click", handler); }; + setTimeout(() => document.addEventListener("click", handler, { once: true }), 0); + } + } + + private _closeMoreMenu(): void { + this._moreMenuOpen = false; + } + + private get _sparklineCtx(): SparklineContext { + return { + lang: this._lang, + detailStatsData: this._detailStatsData, + hasStatsService: !!this._statsService, + isCounterEntity: (tc) => this._isCounterEntity(tc), + rangeDays: this._chartRangeDays, + setRangeDays: (days) => this._setChartRange(days), + hideOutliers: this._hideOutliers, + setHideOutliers: (hide) => this._setHideOutliers(hide), + }; + } + + private _toggleSection(key: string): void { + const next = new Set(this._collapsedSections); + if (next.has(key)) next.delete(key); else next.add(key); + this._collapsedSections = next; + try { localStorage.setItem("msp-collapsed-sections", JSON.stringify([...next])); } catch { /* private mode */ } + } + + /** Build the context the history renderers need from panel state. */ + private _historyCtx(): HistoryContext { + // v2.20 (#83): reading tasks show value + delta per entry. The delta is + // against the chronologically PREVIOUS entry that carries a reading. + const task = this._selectedEntryId && this._selectedTaskId + ? this._getObject(this._selectedEntryId)?.tasks.find((tk) => tk.id === this._selectedTaskId) + : undefined; + const readings = (task?.history || []) + .filter((h) => h.reading_value != null) + .sort((a, b) => a.timestamp.localeCompare(b.timestamp)); + return { + lang: this._lang, + hass: this.hass, + filter: this._historyFilter, + search: this._historySearch, + currencySymbol: this._budget?.currency_symbol || DEFAULT_CURRENCY_SYMBOL, + setFilter: (f) => { this._historyFilter = f; }, + setSearch: (s) => { this._historySearch = s; }, + openEdit: (entry) => this._openHistoryEdit(entry), + readingUnit: task?.reading_unit ?? null, + readingDelta: (entry) => { + const idx = readings.findIndex((h) => h.timestamp === entry.timestamp); + if (idx <= 0) return null; + return (entry.reading_value as number) - (readings[idx - 1].reading_value as number); + }, + }; + } + + /** Build the context the task-detail renderers need from panel state. + * Dialog-opening callbacks stay panel-side (the dialogs live in THIS + * shadow root), so the extracted module never touches a dialog itself. */ + private _taskDetailCtx(): TaskDetailContext { + const entryId = this._selectedEntryId!; + const taskId = this._selectedTaskId!; + const obj = this._getObject(entryId); + return { + lang: this._lang, + hass: this.hass, + entryId, + taskId, + objectName: obj?.object.name || "", + objectDocUrl: obj?.object?.documentation_url ?? null, + isOperator: this._isOperator, + actionLoading: this._actionLoading, + moreMenuOpen: this._moreMenuOpen, + activeTab: this._activeTab, + features: this._features, + currencySymbol: this._budget?.currency_symbol || DEFAULT_CURRENCY_SYMBOL, + collapsedSections: this._collapsedSections, + costDurationToggle: this._costDurationToggle, + suggestionDismissed: this._dismissedSuggestions.has(`${entryId}_${taskId}`), + sparkline: this._sparklineCtx, + history: this._historyCtx(), + getUserName: (id) => this._userService?.getUserName(id) ?? null, + setActiveTab: (tab) => { this._activeTab = tab; }, + toggleSection: (key) => this._toggleSection(key), + setCostDurationToggle: (v) => { this._costDurationToggle = v; }, + showTaskView: () => { this._view = "task"; }, + showObject: () => this._showObject(entryId), + toggleMoreMenu: () => this._toggleMoreMenu(), + closeMoreMenu: () => this._closeMoreMenu(), + openEdit: (tk) => { this.shadowRoot!.querySelector("maintenance-task-dialog")?.openEdit(entryId, tk); }, + openComplete: (tk) => this._openCompleteDialog(entryId, taskId, tk.name, this._features.checklists ? tk.checklist : undefined, this._features.adaptive && !!tk.adaptive_config?.enabled), + promptSkip: () => this._promptSkipTask(entryId, taskId), + toggleArchive: (archived) => this._toggleArchiveTask(entryId, taskId, archived), + openQr: (taskName) => this._openQrForTask(entryId, taskId, obj?.object.name || "", taskName), + duplicateTask: () => this._duplicateTask(entryId, taskId), + promptReset: () => this._promptResetTask(entryId, taskId), + snoozeTask: () => this._snoozeTask(entryId, taskId), + printWorksheet: () => this._printTaskWorksheet(entryId, taskId), + deleteTask: () => this._deleteTask(entryId, taskId), + applySuggestion: (interval) => this._applySuggestion(entryId, taskId, interval), + reanalyze: () => this._reanalyzeInterval(entryId, taskId), + dismissSuggestion: () => this._dismissSuggestion(entryId, taskId), + openSeasonalOverrides: (tk) => this._openSeasonalOverrides(tk), + }; + } + + private _renderTaskDetail() { + if (!this._selectedEntryId || !this._selectedTaskId) return nothing; + const task = this._getTask(this._selectedEntryId, this._selectedTaskId); + if (!task) return html`

    Task not found.

    `; + return renderTaskDetail(task, this._taskDetailCtx()); + } + + /** v2.2.0: open the in-place history-edit dialog for the given entry. + * After save, the dialog fires `history-entry-saved` which we listen + * for in the template to trigger a data refresh. */ + private _openHistoryEdit(entry: HistoryEntry): void { + if (!this._selectedEntryId || !this._selectedTaskId) return; + const draft: HistoryEntryDraft = { + entry_id: this._selectedEntryId, + task_id: this._selectedTaskId, + original_timestamp: entry.timestamp, + type: entry.type, + timestamp: entry.timestamp, + notes: entry.notes ?? null, + cost: entry.cost ?? null, + duration: entry.duration ?? null, + completed_by: entry.completed_by ?? null, + }; + this.shadowRoot + ?.querySelector("maintenance-history-edit-dialog") + ?.openEdit(draft); + } + + /** Triggered by the dialog after a successful WS save. Refresh the loaded + * data so the user sees the updated entry. */ + private _onHistoryEntrySaved = async (): Promise => { + await this._loadData(); + }; + + static styles = [sharedStyles, panelStyles]; +} diff --git a/custom_components/maintenance_supporter/frontend-src/maintenance-strategy-shim.ts b/custom_components/maintenance_supporter/frontend-src/maintenance-strategy-shim.ts new file mode 100644 index 0000000..0225090 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/maintenance-strategy-shim.ts @@ -0,0 +1,348 @@ +/** Maintenance Supporter — dashboard-strategy registration shim. + * + * WHY THIS FILE EXISTS + * -------------------- + * HA serves custom frontend modules via `frontend_extra_module_url`, which the + * frontend loads as fire-and-forget `import("")` calls in index.html — no + * ordering, no await, no completion guarantee. For a `custom:` dashboard + * strategy, HA's renderer then only waits ~5 s via + * `customElements.whenDefined("ll-strategy-dashboard-…")` for the element to + * appear (unlike built-in strategies, which HA imports and awaits itself). + * + * On systems with many heavy HACS frontend plugins (card-mod, layout-card, …) + * our full strategy bundle (~14 KB + a transitive graph) can lose that race: + * the module is downloaded but its top-level `customElements.define` hasn't + * finished executing within the 5 s window → "Timeout waiting for strategy + * element ll-strategy-dashboard-maintenance-supporter to be registered". + * Reproduced ~30–40 % of cold loads on a plugin-heavy live system. + * + * THE FIX + * ------- + * This shim is the `extra_module_url` instead of the full bundle. It has ZERO + * static imports and does ONE thing synchronously: define a tiny wrapper + * element for the dashboard strategy (and push its picker entry). Parsing a + * few hundred bytes with no import graph reaches `define()` in well under a + * microtask, so it wins HA's 5 s race essentially every time. The heavy + * bundle is `import()`-ed lazily on first `generate()` / `getConfigElement()` + * — exactly how HA's own built-in strategies defer their code. + * + * The heavy bundle keeps registering the editor, the section strategies, the + * `ll-custom` tap handler, and the section picker entries at its own top level + * (all of which only matter once a dashboard is actually being generated). + */ + +const STRATEGY_TYPE = "maintenance-supporter"; +const STRATEGY_TAG = `ll-strategy-dashboard-${STRATEGY_TYPE}`; +const EDITOR_TAG = "hui-maintenance-supporter-strategy-editor"; + +// Absolute URL of the full strategy bundle (served at STRATEGY_URL by the +// integration). The shim's only dependency, loaded on demand. +const BUNDLE_URL = + "/maintenance_supporter_strategy/maintenance-dashboard-strategy.js"; + +let _bundle: Promise | null = null; +function loadBundle(): Promise { + if (!_bundle) _bundle = import(/* @vite-ignore */ BUNDLE_URL); + return _bundle; +} + +// The real dashboard class, exported by the heavy bundle once it loads. We +// resolve it from the registered editor's sibling export at call time. +type DashboardStrategyClass = { + generate(config: unknown, hass: unknown): Promise; +}; + +async function realDashboard(): Promise { + const mod = (await loadBundle()) as { + MaintenanceDashboardStrategy?: DashboardStrategyClass; + }; + if (!mod.MaintenanceDashboardStrategy) { + throw new Error( + "[maintenance-supporter] strategy bundle loaded but did not export " + + "MaintenanceDashboardStrategy", + ); + } + return mod.MaintenanceDashboardStrategy; +} + +class MaintenanceDashboardStrategyShim extends HTMLElement { + static getCreateSuggestions(_hass: unknown) { + return { title: "Maintenance Supporter", icon: "mdi:wrench-clock" }; + } + + static async getConfigElement(): Promise { + // Loading the bundle defines the editor element as a side effect. + await loadBundle(); + return document.createElement(EDITOR_TAG); + } + + static async generate(config: unknown, hass: unknown): Promise { + const real = await realDashboard(); + return real.generate(config, hass); + } +} + +function defineShim(): void { + // Attempt unconditionally: customElements.get() is unreliable across HA's + // scoped registries (it can read true while the active dashboard registry has + // no registration), so we always try and swallow the "already defined" throw. + // On a fresh (cache-busted) re-import this re-registers on the now-active + // registry, which is what the self-heal relies on. + try { + customElements.define(STRATEGY_TAG, MaintenanceDashboardStrategyShim); + } catch { + /* already defined in this registry — fine */ + } +} +defineShim(); + +// Picker discovery (HA 2026.5+ reads window.customStrategies when the +// "Add Dashboard" dialog opens). Idempotent — the heavy bundle pushes the +// same entry too once it loads; whichever runs first wins, the other skips. +const w = window as unknown as { + customStrategies?: Array<{ + type: string; + strategyType: string; + name: string; + description?: string; + documentationURL?: string; + }>; +}; +w.customStrategies = w.customStrategies || []; +if ( + !w.customStrategies.some( + (s) => s.type === STRATEGY_TYPE && s.strategyType === "dashboard", + ) +) { + w.customStrategies.push({ + type: STRATEGY_TYPE, + strategyType: "dashboard", + name: "Maintenance Supporter", + description: + "Auto-generated dashboard. Group views by area, status, floor, or due date — picked from the strategy editor or YAML.", + documentationURL: + "https://github.com/iluebbe/maintenance_supporter#dashboard-strategy", + }); +} + +// ── Self-heal ─────────────────────────────────────────────────────────────── +// Root cause (diagnosed live): HA's frontend uses a SCOPED custom-element +// registry that is swapped in during boot. Our element, defined at module load, +// can end up registered on a registry that the dashboard renderer no longer +// uses — so `customElements.get(STRATEGY_TAG)` reads false in the active +// registry and HA's 5 s whenDefined times out (console-only error, empty view). +// +// Verified recovery (9/9 on a live plugin-heavy system): RE-DEFINE on the +// now-current registry, then bounce the view (navigate away + back) so HA's +// reconnects and re-runs _fetchConfig — generate() then +// finds the element and the real dashboard renders. +// +// Fully defensive: only acts on a Lovelace path when our element is missing +// from the active registry; on a healthy load it never fires. +(() => { + const heal = window as unknown as { __msStrategyHealActive?: boolean }; + if (heal.__msStrategyHealActive) return; // one heal loop per page session + heal.__msStrategyHealActive = true; + + const NON_LOVELACE = + /^\/(auth|config|developer-tools|profile|hassio|history|logbook|map|media-browser|energy|todo|calendar)\b/; + + // NOTE: customElements.get(STRATEGY_TAG) is NOT a reliable health signal — + // HA's scoped registry makes it read `true` here even when the dashboard's + // renderer (a different registry) failed. So we gate on the RENDERED OUTCOME: + // a strategy dashboard that failed shows an essentially empty view. + const ERROR_RE = + /Timeout waiting for strategy element ll-strategy-(dashboard-)?maintenance-supporter/i; + + // The dashboard/view strategy type our generated dashboards carry. + const OUR_STRATEGY = `custom:${STRATEGY_TYPE}`; + + // Deep querySelector through open shadow roots (HA nests the panel deeply). + function findDeep(tag: string): Element | null { + const stack: Array = [document.documentElement]; + let scanned = 0; + while (stack.length && scanned < 9000) { + const node = stack.pop(); + scanned++; + if (!node) continue; + const el = node as Element & { shadowRoot?: ShadowRoot | null }; + if (el.nodeType === 1 && el.tagName && el.tagName.toLowerCase() === tag) { + return el; + } + if (el.shadowRoot) stack.push(el.shadowRoot); + const kids = (node as ParentNode).children; + if (kids) for (const k of Array.from(kids)) stack.push(k); + } + return null; + } + + // The view config for the path we are on (by view `path`, else numeric index, + // else the first view). + function currentViewCfg( + cfg: { views?: Array> }, + ): Record | null { + const views = cfg?.views; + if (!Array.isArray(views) || !views.length) return null; + const seg = window.location.pathname.split("/").filter(Boolean).pop() || ""; + const byPath = views.find((v) => (v as { path?: string })?.path === seg); + if (byPath) return byPath; + const idx = Number(seg); + if (Number.isInteger(idx) && views[idx]) return views[idx]; + return views[0]; + } + + // True ONLY when the active dashboard/view is generated by our strategy. + // Reads the Lovelace config off (both the processed + // `config` and the original `rawConfig`, at dashboard level and current-view + // level). This scopes the self-heal so it never touches a dashboard that is + // not ours — the fix for issue #68, where the blank-view heuristic bounced + // unrelated dashboards that happened to have < 3 cards. Defensive: any read + // failure → false (never heal a view we cannot confirm is ours). + function isOurStrategyView(): boolean { + try { + const panel = findDeep("ha-panel-lovelace") as unknown as { + lovelace?: { + config?: Record; + rawConfig?: Record; + }; + } | null; + const lov = panel?.lovelace; + if (!lov) return false; + const stratType = (s: unknown): string | undefined => + (s as { type?: string } | undefined)?.type; + for (const cfg of [lov.config, lov.rawConfig]) { + if (!cfg) continue; + if (stratType((cfg as { strategy?: unknown }).strategy) === OUR_STRATEGY) { + return true; + } + const v = currentViewCfg( + cfg as { views?: Array> }, + ); + if (v && stratType((v as { strategy?: unknown }).strategy) === OUR_STRATEGY) { + return true; + } + } + return false; + } catch { + return false; + } + } + + // Heuristic for "we are on a broken strategy view": the lovelace view exists + // but is essentially empty (HA rendered the failed strategy as a blank view), + // OR HA surfaced our strategy error card. Walks light + shadow DOM. + function viewIsBroken(): boolean { + let hasView = false; + let cards = 0; + let ourError = false; + let hasEmptyState = false; + const stack: Array = [document.documentElement]; + let scanned = 0; + while (stack.length && scanned < 9000) { + const node = stack.pop(); + scanned++; + if (!node) continue; + const el = node as Element & { shadowRoot?: ShadowRoot | null }; + if (el.nodeType === 1 && el.tagName) { + const tag = el.tagName.toLowerCase(); + if (tag === "hui-view" || tag === "hui-sections-view") hasView = true; + if (tag === "ha-card" || tag === "hui-card") cards++; + // The zero-objects onboarding view renders exactly ONE hui-empty-state- + // card. That is a SUCCESSFUL strategy render, but it trips the "< 3 + // cards" blank-view heuristic below — issue #69, where the empty state + // was mistaken for a broken render and the page reloaded every ~5 s. + if (tag === "hui-empty-state-card") hasEmptyState = true; + if (tag === "hui-error-card" && ERROR_RE.test(el.textContent || "")) { + ourError = true; + } + } + if (el.shadowRoot) stack.push(el.shadowRoot); + const kids = (node as ParentNode).children; + if (kids) for (const k of Array.from(kids)) stack.push(k); + } + // ourError (HA's error card naming OUR strategy element) is unambiguous — + // act on it directly. The "blank view" signal (< 3 cards) is ambiguous on + // its own (a small or still-loading dashboard looks identical), so gate it + // on the active dashboard actually being our strategy. Without that gate the + // self-heal bounced unrelated dashboards with few cards (issue #68). + if (ourError) return true; + // A rendered empty-state card means generate() ran and produced our + // onboarding view — that is healthy by definition, so never bounce it. A + // genuine strategy timeout shows hui-error-card or zero cards, never this + // card (issue #69). + if (hasEmptyState) return false; + return hasView && cards < 3 && isOurStrategyView(); + } + + // URL of THIS module, so recovery can force a fresh re-import (a cache-busted + // import re-evaluates the module and re-runs customElements.define against the + // registry that is active NOW — the boot-time define may have landed on a + // scoped registry HA has since discarded, and a plain defineShim() call is a + // no-op because customElements.get() reads stale-true across scoped registries). + const SHIM_URL = "/maintenance_supporter_strategy_shim.js"; + + let bounces = 0; + let lastBounce = 0; + function recover(): void { + const now = Date.now(); + if (now - lastBounce < 5000 || bounces >= 3) return; // never thrash / loop + lastBounce = now; + bounces += 1; + // 1) Force a fresh re-import so define() re-runs on the now-active registry. + // (Verified live: this is what makes the element resolvable to HA's + // dashboard renderer; a guarded re-define is not enough.) + void import(/* @vite-ignore */ `${SHIM_URL}?heal=${now}`) + .catch(() => undefined) + .finally(() => { + // 2) Bounce so HA re-fetches the strategy config; generate() now + // resolves the element and the real dashboard renders. + const back = window.location.pathname + window.location.search; + history.pushState(null, "", "/lovelace"); + window.dispatchEvent(new CustomEvent("location-changed")); + window.setTimeout(() => { + history.pushState(null, "", back); + window.dispatchEvent(new CustomEvent("location-changed")); + }, 200); + }); + } + + function watch(): void { + if (NON_LOVELACE.test(window.location.pathname)) return; + let ticks = 0; + const startedAt = Date.now(); + const id = window.setInterval(() => { + ticks++; + try { + // Give HA past its ~5 s strategy timeout before judging the outcome. + if (Date.now() - startedAt < 6000) return; + if (NON_LOVELACE.test(window.location.pathname)) { + window.clearInterval(id); + return; + } + if (viewIsBroken()) { + recover(); + } else { + window.clearInterval(id); // healthy / recovered + } + if (ticks >= 30) window.clearInterval(id); // ~15 s safety cap + } catch { + window.clearInterval(id); + } + }, 500); + } + + try { + if (document.readyState === "loading") { + window.addEventListener("DOMContentLoaded", watch); + } else { + watch(); + } + window.addEventListener("location-changed", () => { + if (!NON_LOVELACE.test(window.location.pathname)) watch(); + }); + } catch { + /* best-effort; never block registration */ + } +})(); + +export {}; diff --git a/custom_components/maintenance_supporter/frontend-src/package-lock.json b/custom_components/maintenance_supporter/frontend-src/package-lock.json new file mode 100644 index 0000000..cd407e3 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/package-lock.json @@ -0,0 +1,5611 @@ +{ + "name": "maintenance-supporter-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "maintenance-supporter-frontend", + "version": "0.1.0", + "dependencies": { + "pdfjs-dist": "^4.10.38", + "playwright": "1.61" + }, + "devDependencies": { + "@open-wc/testing": "^4.0.0", + "@web/dev-server-esbuild": "^1.0.0", + "@web/test-runner": "^0.20.0", + "@web/test-runner-playwright": "^0.11.0", + "esbuild": "^0.28.1", + "lit": "^3.3.3", + "typescript": "^6.0.3" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esm-bundle/chai": { + "version": "4.3.4-fix.0", + "resolved": "https://registry.npmjs.org/@esm-bundle/chai/-/chai-4.3.4-fix.0.tgz", + "integrity": "sha512-26SKdM4uvDWlY8/OOOxSB1AqQWeBosCX3wRYUZO7enTAj03CtVxIiCimYVG2WpULcyV51qapK4qTovwkUr5Mlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^4.2.12" + } + }, + "node_modules/@hapi/bourne": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-3.0.0.tgz", + "integrity": "sha512-Waj1cwPXJDucOib4a3bAISsKJVb15MKi9IvmTI/7ssVEm6sywXGjVJDhl6/umt1pK1ZS7PacXU3A1PmFKHEZ2w==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@lit-labs/ssr-dom-shim": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.5.1.tgz", + "integrity": "sha512-Aou5UdlSpr5whQe8AA/bZG0jMj96CoJIWbGfZ91qieWu5AWUMKw8VR/pAkQkJYvBNhmCcWnZlyyk5oze8JIqYA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@lit/reactive-element": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@lit/reactive-element/-/reactive-element-2.1.2.tgz", + "integrity": "sha512-pbCDiVMnne1lYUIaYNN5wrwQXDtHaYtg7YEFPeW+hws6U47WeFvISGUWekPGKWOP1ygrs0ef0o1VJMk1exos5A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0" + } + }, + "node_modules/@mdn/browser-compat-data": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@mdn/browser-compat-data/-/browser-compat-data-4.2.1.tgz", + "integrity": "sha512-EWUguj2kd7ldmrF9F+vI5hUOralPd+sdsUnYbRy33vZTuZkduC1shE9TtEMEjAQwyfyMb4ole5KtjF8MsnQOlA==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/@napi-rs/canvas": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.100.tgz", + "integrity": "sha512-xglYA6q3XO5P3BNJYxVZ1IV7DLVjp1Py6nwag88YntrS+3vKHyYcMqXVS4ZztJmwz2uGvz1FWhI/4LgbR5uQDA==", + "license": "MIT", + "optional": true, + "workspaces": [ + "e2e/*" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "optionalDependencies": { + "@napi-rs/canvas-android-arm64": "0.1.100", + "@napi-rs/canvas-darwin-arm64": "0.1.100", + "@napi-rs/canvas-darwin-x64": "0.1.100", + "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.100", + "@napi-rs/canvas-linux-arm64-gnu": "0.1.100", + "@napi-rs/canvas-linux-arm64-musl": "0.1.100", + "@napi-rs/canvas-linux-riscv64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-gnu": "0.1.100", + "@napi-rs/canvas-linux-x64-musl": "0.1.100", + "@napi-rs/canvas-win32-arm64-msvc": "0.1.100", + "@napi-rs/canvas-win32-x64-msvc": "0.1.100" + } + }, + "node_modules/@napi-rs/canvas-android-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.100.tgz", + "integrity": "sha512-hjhCKhntPv9+t4ckHymdx0phYNcVW+GKQR6Lzw2zE+pOVjOplSmtx9nNNknTjbEDLcuLZqA1y8ufKg1XfgftzQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-arm64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.100.tgz", + "integrity": "sha512-2PcswRaC7Ly645DGt88///zuFDhJxJYdKAs1uU3mfk1atYkXufgcgLfBpk6Tm12nCQBaNt1wpybuPZ4qOhTo8A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-darwin-x64": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.100.tgz", + "integrity": "sha512-ePNZtj7pNIva/siZMg+HmbeozkIjqUIYdoymH8HaA3qK7LfzFN4WMBM8G6HQ9ZC+H3+Dnn5pqtiXpgLykaPOhw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.100.tgz", + "integrity": "sha512-d5cDB48oWFGU8/XPhUOFAlySgb/VAu7D+s8fi55K1Pcfg8aPplHWqMgibhVLU8ky7Pyg/fuiVLz4Nf3JrSTuUA==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.100.tgz", + "integrity": "sha512-rDxgxRu69RvDlX/bh9o22DxLsGr8EqsNgotL9+RwQE1S0b0cqeatqsw6aW45mukm0B42DIAaAacKaYQ8cqS1nw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-arm64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.100.tgz", + "integrity": "sha512-K3mDW66N+xT2/V439u1alFANiBUjdEx2gLiNYnCmUsva5jZMxWTjafBYwTzYK+EMFMHrUoabuU+T1BIP5CgbYQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-riscv64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.100.tgz", + "integrity": "sha512-mooqUBTIsccZpnoQC4NgrC1v6C1vof39etLNMnBwCY+p0gajWJvAHLGQ6g/gGyS5YrpDW+GefSN4+Cvcr08UWw==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-gnu": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.100.tgz", + "integrity": "sha512-1eCvkDCazm7FFhsT7DfGOdSaHgZVK3bt/dSBl5EWHOWmnz+I7j8tPseJqqD81NF+MH21jKUK4wQSDjN0mdhnTg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-linux-x64-musl": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.100.tgz", + "integrity": "sha512-20arT6lnI19S68qNlii73TSEDbECNgzMz2EpldC1V3mZFuRkeujXkcebRk0LRJe9SEUAooYiLokfMViY8IX7yA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-arm64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.100.tgz", + "integrity": "sha512-DZFFT1wIAg37LJw37yhMRFfjATd3vTQzjZ1Yki8u2vhO6Hi5VE6BVaGQ1aaDu7xb4iMErz+9EOwjpS7xcxFeBw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@napi-rs/canvas-win32-x64-msvc": { + "version": "0.1.100", + "resolved": "https://registry.npmjs.org/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.100.tgz", + "integrity": "sha512-MyT1j3mHC2+Lu4pBi9mKyMJhtP6U7k7EldY7sj/uS5gJA65gTXt8MefJQXLJo5d/vZbuWmfxzkEUNc/urV3pHA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@open-wc/dedupe-mixin": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@open-wc/dedupe-mixin/-/dedupe-mixin-2.0.1.tgz", + "integrity": "sha512-+R4VxvceUxHAUJXJQipkkoV9fy10vNo+OnUnGKZnVmcwxMl460KLzytnUM4S35SI073R0yZQp9ra0MbPUwVcEA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@open-wc/scoped-elements": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/@open-wc/scoped-elements/-/scoped-elements-3.0.6.tgz", + "integrity": "sha512-w1ayJaUUmBw8tALtqQ6cBueld+op+bufujzbrOdH0uCTXnSQkONYZzOH+9jyQ8auVgKLqcxZ8oU6SzfqQhQkPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-wc/dedupe-mixin": "^2.0.0", + "lit": "^3.0.0" + } + }, + "node_modules/@open-wc/semantic-dom-diff": { + "version": "0.20.1", + "resolved": "https://registry.npmjs.org/@open-wc/semantic-dom-diff/-/semantic-dom-diff-0.20.1.tgz", + "integrity": "sha512-mPF/RPT2TU7Dw41LEDdaeP6eyTOWBD4z0+AHP4/d0SbgcfJZVRymlIB6DQmtz0fd2CImIS9kszaMmwMt92HBPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^4.3.1", + "@web/test-runner-commands": "^0.9.0" + } + }, + "node_modules/@open-wc/testing": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@open-wc/testing/-/testing-4.0.0.tgz", + "integrity": "sha512-KI70O0CJEpBWs3jrTju4BFCy7V/d4tFfYWkg8pMzncsDhD7TYNHLw5cy+s1FHXIgVFetnMDhPpwlKIPvtTQW7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@esm-bundle/chai": "^4.3.4-fix.0", + "@open-wc/semantic-dom-diff": "^0.20.0", + "@open-wc/testing-helpers": "^3.0.0", + "@types/chai-dom": "^1.11.0", + "@types/sinon-chai": "^3.2.3", + "chai-a11y-axe": "^1.5.0" + } + }, + "node_modules/@open-wc/testing-helpers": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@open-wc/testing-helpers/-/testing-helpers-3.0.1.tgz", + "integrity": "sha512-hyNysSatbgT2FNxHJsS3rGKcLEo6+HwDFu1UQL6jcSQUabp/tj3PyX7UnXL3H5YGv0lJArdYLSnvjLnjn3O2fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@open-wc/scoped-elements": "^3.0.2", + "lit": "^2.0.0 || ^3.0.0", + "lit-html": "^2.0.0 || ^3.0.0" + } + }, + "node_modules/@puppeteer/browsers": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.13.0.tgz", + "integrity": "sha512-46BZJYJjc/WwmKjsvDFykHtXrtomsCIrwYQPOP7VfMJoZY2bsDF9oROBABR3paDjDcmkUye1Pb1BqdcdiipaWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^4.4.3", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.7.4", + "tar-fs": "^3.1.1", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/plugin-node-resolve": { + "version": "15.3.1", + "resolved": "https://registry.npmjs.org/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz", + "integrity": "sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/pluginutils": "^5.0.1", + "@types/resolve": "1.20.2", + "deepmerge": "^4.2.2", + "is-module": "^1.0.0", + "resolve": "^1.22.1" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^2.78.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", + "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "estree-walker": "^2.0.2", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=14.0.0" + }, + "peerDependencies": { + "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + }, + "peerDependenciesMeta": { + "rollup": { + "optional": true + } + } + }, + "node_modules/@rollup/pluginutils/node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.2.tgz", + "integrity": "sha512-dnlp69efPPg6Uaw2dVqzWRfAWRnYVb1XJ8CyyhIbZeaq4CA5/mLeZ1IEt9QqQxmbdvagjLIm2ZL8BxXv5lH4Yw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.2.tgz", + "integrity": "sha512-OqZTwDRDchGRHHm/hwLOL7uVPB9aUvI0am/eQuWMNyFHf5PSEQmyEeYYheA0EPPKUO/l0uigCp+iaTjoLjVoHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.2.tgz", + "integrity": "sha512-UwRE7CGpvSVEQS8gUMBe1uADWjNnVgP3Iusyda1nSRwNDCsRjnGc7w6El6WLQsXmZTbLZx9cecegumcitNfpmA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.2.tgz", + "integrity": "sha512-gjEtURKLCC5VXm1I+2i1u9OhxFsKAQJKTVB8WvDAHF+oZlq0GTVFOlTlO1q3AlCTE/DF32c16ESvfgqR7343/g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.2.tgz", + "integrity": "sha512-Bcl6CYDeAgE70cqZaMojOi/eK63h5Me97ZqAQoh77VPjMysA/4ORQBRGo3rRy45x4MzVlU9uZxs8Uwy7ZaKnBw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.2.tgz", + "integrity": "sha512-LU+TPda3mAE2QB0/Hp5VyeKJivpC6+tlOXd1VMoXV/YFMvk/MNk5iXeBfB4MQGRWyOYVJ01625vjkr0Az98OJQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.2.tgz", + "integrity": "sha512-2QxQrM+KQ7DAW4o22j+XZ6RKdxjLD7BOWTP0Bv0tmjdyhXSsr2Ul1oJDQqh9Zf5qOwTuTc7Ek83mOFaKnodPjg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.2.tgz", + "integrity": "sha512-TbziEu2DVsTEOPif2mKWkMeDMLoYjx95oESa9fkQQK7r/Orta0gnkcDpzwufEcAO2BLBsD7mZkXGFqEdMRRwfw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.2.tgz", + "integrity": "sha512-bO/rVDiDUuM2YfuCUwZ1t1cP+/yqjqz+Xf2VtkdppefuOFS2OSeAfgafaHNkFn0t02hEyXngZkxtGqXcXwO8Rg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.2.tgz", + "integrity": "sha512-hr26p7e93Rl0Za+JwW7EAnwAvKkehh12BU1Llm9Ykiibg4uIr2rbpxG9WCf56GuvidlTG9KiiQT/TXT1yAWxTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.2.tgz", + "integrity": "sha512-pOjB/uSIyDt+ow3k/RcLvUAOGpysT2phDn7TTUB3n75SlIgZzM6NKAqlErPhoFU+npgY3/n+2HYIQVbF70P9/A==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.2.tgz", + "integrity": "sha512-2/w+q8jszv9Ww1c+6uJT3OwqhdmGP2/4T17cu8WuwyUuuaCDDJ2ojdyYwZzCxx0GcsZBhzi3HmH+J5pZNXnd+Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.2.tgz", + "integrity": "sha512-11+aL5vKheYgczxtPVVRhdptAM2H7fcDR5Gw4/bTcteuZBlH4oP9f5s9zYO9aGZvoGeBpqXI/9TZZihZ609wKw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.2.tgz", + "integrity": "sha512-i16fokAGK46IVZuV8LIIwMdtqhin9hfYkCh8pf8iC3QU3LpwL+1FSFGej+O7l3E/AoknL6Dclh2oTdnRMpTzFQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.2.tgz", + "integrity": "sha512-49FkKS6RGQoriDSK/6E2GkAsAuU5kETFCh7pG4yD/ylj9rKhTmO3elsnmBvRD4PgJPds5W2PkhC82aVwmUcJ7A==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.2.tgz", + "integrity": "sha512-mjYNkHPfGpUR00DuM1ZZIgs64Hpf4bWcz9Z41+4Q+pgDx73UwWdAYyf6EG/lRFldmdHHzgrYyge5akFUW0D3mQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.2.tgz", + "integrity": "sha512-ALyvJz965BQk8E9Al/JDKKDLH2kfKFLTGMlgkAbbYtZuJt9LU8DW3ZoDMCtQpXAltZxwBHevXz5u+gf0yA0YoA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.2.tgz", + "integrity": "sha512-UQjrkIdWrKI626Du8lCQ6MJp/6V1LAo2bOK9OTu4mSn8GGXIkPXk/Vsp4bLHCd9Z9Iz2OTEaokUE90VweJgIYQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.2.tgz", + "integrity": "sha512-bTsRGj6VlSdn/XD4CGyzMnzaBs9bsRxy79eTqTCBsA8TMIEky7qg48aPkvJvFe1HyzQ5oMZdg7AnVlWQSKLTnw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.2.tgz", + "integrity": "sha512-6d4Z3534xitaA1FcMWP7mQPq5zGwBmGbhphh2DwaA1aNIXUu3KTOfwrWpbwI4/Gr0uANo7NTtaykFyO2hPuFLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.2.tgz", + "integrity": "sha512-NetAg5iO2uN7eB8zE5qrZ3CSil+7IJt4WDFLcC75Ymywq1VZVD6qJ6EvNLjZ3rEm6gB7XW5JdT60c6MN35Z85Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.2.tgz", + "integrity": "sha512-NCYhOotpgWZ5kdxCZsv6Iudx0wX8980Q/oW4pNFNihpBKsDbEA1zpkfxJGC0yugsUuyDZ7gL37dbzwhR0VI7pQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.2.tgz", + "integrity": "sha512-RXsaOqXxfoUBQoOgvmmijVxJnW2IGB0eoMO7F8FAjaj0UTywUO/luSqimWBJn04WNgUkeNhh7fs7pESXajWmkg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.2.tgz", + "integrity": "sha512-qdAzEULD+/hzObedtmV6iBpdL5TIbKVztGiK7O3/KYSf+HIzU257+MX1EXJcyIiDbMAqmbwaufcYPvyRryeZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.2.tgz", + "integrity": "sha512-Nd/SgG27WoA9e+/TdK74KnHz852TLa94ovOYySo/yMPuTmpckK/jIF2jSwS3g7ELSKXK13/cVdmg1Z/DaCWKxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/accepts": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/@types/accepts/-/accepts-1.3.7.tgz", + "integrity": "sha512-Pay9fq2lM2wXPWbteBsRAGiWH2hig4ZE2asK+mm7kUzlxRTfL961rj89I6zV/E3PcIkDqyuBEcMxFT7rccugeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/babel__code-frame": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__code-frame/-/babel__code-frame-7.27.0.tgz", + "integrity": "sha512-Dwlo+LrxDx/0SpfmJ/BKveHf7QXWvLBLc+x03l5sbzykj3oB9nHygCpSECF1a+s+QIxbghe+KHqC90vGtxLRAA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/body-parser": { + "version": "1.19.6", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", + "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } + }, + "node_modules/@types/chai": { + "version": "4.3.20", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-4.3.20.tgz", + "integrity": "sha512-/pC9HAB5I/xMlc5FP77qjCnI16ChlJfW0tGa0IUcFn38VJrTV6DeZ60NU5KZBtaOZqjdpwTWohz5HU1RrhiYxQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai-dom": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@types/chai-dom/-/chai-dom-1.11.3.tgz", + "integrity": "sha512-EUEZI7uID4ewzxnU7DJXtyvykhQuwe+etJ1wwOiJyQRTH/ifMWKX+ghiXkxCUvNJ6IQDodf0JXhuP6zZcy2qXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "*" + } + }, + "node_modules/@types/co-body": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@types/co-body/-/co-body-6.1.3.tgz", + "integrity": "sha512-UhuhrQ5hclX6UJctv5m4Rfp52AfG9o9+d9/HwjxhVB5NjXxr5t9oKgJxN8xRHgr35oo8meUEHUPFWiKg6y71aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*" + } + }, + "node_modules/@types/command-line-args": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/command-line-args/-/command-line-args-5.2.3.tgz", + "integrity": "sha512-uv0aG6R0Y8WHZLTamZwtfsDLVRnOa+n+n5rEvFWL5Na5gZ8V2Teab/duDPFzIIIhs9qizDpcavCusCLJZu62Kw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/content-disposition": { + "version": "0.5.9", + "resolved": "https://registry.npmjs.org/@types/content-disposition/-/content-disposition-0.5.9.tgz", + "integrity": "sha512-8uYXI3Gw35MhiVYhG3s295oihrxRyytcRHjSjqnqZVDDy/xcGBRny7+Xj1Wgfhv5QzRtN2hB2dVRBUX9XW3UcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/convert-source-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/convert-source-map/-/convert-source-map-2.0.3.tgz", + "integrity": "sha512-ag0BfJLZf6CQz8VIuRIEYQ5Ggwk/82uvTQf27RcpyDNbY0Vw49LIPqAxk5tqYfrCs9xDaIMvl4aj7ZopnYL8bA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/cookies": { + "version": "0.9.2", + "resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.2.tgz", + "integrity": "sha512-1AvkDdZM2dbyFybL4fxpuNCaWyv//0AwsuUk2DWeXyM1/5ZKm6W3z6mQi24RZ4l2ucY+bkSHzbDVpySqPGuV8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/connect": "*", + "@types/express": "*", + "@types/keygrip": "*", + "@types/node": "*" + } + }, + "node_modules/@types/debounce": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/@types/debounce/-/debounce-1.2.4.tgz", + "integrity": "sha512-jBqiORIzKDOToaF63Fm//haOCHuwQuLa2202RK4MozpA6lh93eCBc+/8+wZn5OzjJt3ySdc+74SXWXB55Ewtyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/express": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/@types/express/-/express-5.0.6.tgz", + "integrity": "sha512-sKYVuV7Sv9fbPIt/442koC7+IIwK5olP1KWeD88e/idgoJqDm3JV/YUiPwkoKK92ylff2MGxSz1CSjsXelx0YA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^5.0.0", + "@types/serve-static": "^2" + } + }, + "node_modules/@types/express-serve-static-core": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.1.1.tgz", + "integrity": "sha512-v4zIMr/cX7/d2BpAEX3KNKL/JrT1s43s96lLvvdTmza1oEvDudCqK9aF/djc/SWgy8Yh0h30TZx5VpzqFCxk5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, + "node_modules/@types/http-assert": { + "version": "1.5.6", + "resolved": "https://registry.npmjs.org/@types/http-assert/-/http-assert-1.5.6.tgz", + "integrity": "sha512-TTEwmtjgVbYAzZYWyeHPrrtWnfVkm8tQkP8P21uQifPgMRgjrow3XDEYqucuC8SKZJT7pUnhU/JymvjggxO9vw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", + "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/keygrip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/@types/keygrip/-/keygrip-1.0.6.tgz", + "integrity": "sha512-lZuNAY9xeJt7Bx4t4dx0rYCDqGPW8RXhQZK1td7d4H6E9zYbLoOtjBvfwdTKpsyxQI/2jv+armjX/RW+ZNpXOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/koa": { + "version": "2.15.0", + "resolved": "https://registry.npmjs.org/@types/koa/-/koa-2.15.0.tgz", + "integrity": "sha512-7QFsywoE5URbuVnG3loe03QXuGajrnotr3gQkXcEBShORai23MePfFYdhz90FEtBBpkyIYQbVD+evKtloCgX3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/accepts": "*", + "@types/content-disposition": "*", + "@types/cookies": "*", + "@types/http-assert": "*", + "@types/http-errors": "*", + "@types/keygrip": "*", + "@types/koa-compose": "*", + "@types/node": "*" + } + }, + "node_modules/@types/koa-compose": { + "version": "3.2.9", + "resolved": "https://registry.npmjs.org/@types/koa-compose/-/koa-compose-3.2.9.tgz", + "integrity": "sha512-BroAZ9FTvPiCy0Pi8tjD1OfJ7bgU1gQf0eR6e1Vm+JJATy9eKOG3hQMFtMciMawiSOVnLMdmUOC46s7HBhSTsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/koa": "*" + } + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/parse5": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@types/parse5/-/parse5-6.0.3.tgz", + "integrity": "sha512-SuT16Q1K51EAVPz1K29DJ/sXjhSQ0zjvsypYJ6tlwVsRV9jwW5Adq2ch8Dq8kDBCkYnELS7N7VNCSB5nC56t/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/qs": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.15.0.tgz", + "integrity": "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/resolve": { + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/@types/resolve/-/resolve-1.20.2.tgz", + "integrity": "sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@types/send/-/send-1.2.1.tgz", + "integrity": "sha512-arsCikDvlU99zl1g69TcAB3mzZPpxgw0UQnaHeC1Nwb015xp8bknZv5rIfri9xTOcMuaVgvabfIRA7PSZVuZIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/serve-static": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-2.2.0.tgz", + "integrity": "sha512-8mam4H1NHLtu7nmtalF7eyBH14QyOASmcxHhSfEoRyr0nP/YdoesEtU+uSRvMe96TW/HPTtkoKqQLl53N7UXMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-errors": "*", + "@types/node": "*" + } + }, + "node_modules/@types/sinon": { + "version": "21.0.1", + "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-21.0.1.tgz", + "integrity": "sha512-5yoJSqLbjH8T9V2bksgRayuhpZy+723/z6wBOR+Soe4ZlXC0eW8Na71TeaZPUWDQvM7LYKa9UGFc6LRqxiR5fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sinonjs__fake-timers": "*" + } + }, + "node_modules/@types/sinon-chai": { + "version": "3.2.12", + "resolved": "https://registry.npmjs.org/@types/sinon-chai/-/sinon-chai-3.2.12.tgz", + "integrity": "sha512-9y0Gflk3b0+NhQZ/oxGtaAJDvRywCa5sIyaVnounqLvmf93yBF4EgIRspePtkMs3Tr844nCclYMlcCNmLCvjuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "*", + "@types/sinon": "*" + } + }, + "node_modules/@types/sinonjs__fake-timers": { + "version": "15.0.1", + "resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-15.0.1.tgz", + "integrity": "sha512-Ko2tjWJq8oozHzHV+reuvS5KYIRAokHnGbDwGh/J64LntgpbuylF74ipEL24HCyRjf9FOlBiBHWBR1RlVKsI1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/trusted-types": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/@types/trusted-types/-/trusted-types-2.0.7.tgz", + "integrity": "sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/ws": { + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@web/browser-logs": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@web/browser-logs/-/browser-logs-0.4.1.tgz", + "integrity": "sha512-ypmMG+72ERm+LvP+loj9A64MTXvWMXHUOu773cPO4L1SV/VWg6xA9Pv7vkvkXQX+ItJtCJt+KQ+U6ui2HhSFUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "errorstacks": "^2.4.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/config-loader": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@web/config-loader/-/config-loader-0.3.3.tgz", + "integrity": "sha512-ilzeQzrPpPLWZhzFCV+4doxKDGm7oKVfdKpW9wiUNVgive34NSzCw+WzXTvjE4Jgr5CkyTDIObEmMrqQEjhT0g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/dev-server": { + "version": "0.4.6", + "resolved": "https://registry.npmjs.org/@web/dev-server/-/dev-server-0.4.6.tgz", + "integrity": "sha512-jj/1bcElAy5EZet8m2CcUdzxT+CRvUjIXGh8Lt7vxtthkN9PzY9wlhWx/9WOs5iwlnG1oj0VGo6f/zvbPO0s9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.11", + "@types/command-line-args": "^5.0.0", + "@web/config-loader": "^0.3.0", + "@web/dev-server-core": "^0.7.2", + "@web/dev-server-rollup": "^0.6.1", + "camelcase": "^6.2.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^7.0.1", + "debounce": "^1.2.0", + "deepmerge": "^4.2.2", + "internal-ip": "^6.2.0", + "nanocolors": "^0.2.1", + "open": "^8.0.2", + "portfinder": "^1.0.32" + }, + "bin": { + "wds": "dist/bin.js", + "web-dev-server": "dist/bin.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/dev-server-core": { + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/@web/dev-server-core/-/dev-server-core-0.7.5.tgz", + "integrity": "sha512-Da65zsiN6iZPMRuj4Oa6YPwvsmZmo5gtPWhW2lx3GTUf5CAEapjVpZVlUXnKPL7M7zRuk72jSsIl8lo+XpTCtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/koa": "^2.11.6", + "@types/ws": "^7.4.0", + "@web/parse5-utils": "^2.1.0", + "chokidar": "^4.0.1", + "clone": "^2.1.2", + "es-module-lexer": "^1.0.0", + "get-stream": "^6.0.0", + "is-stream": "^2.0.0", + "isbinaryfile": "^5.0.0", + "koa": "^2.13.0", + "koa-etag": "^4.0.0", + "koa-send": "^5.0.1", + "koa-static": "^5.0.0", + "lru-cache": "^8.0.4", + "mime-types": "^2.1.27", + "parse5": "^6.0.1", + "picomatch": "^2.2.2", + "ws": "^7.5.10" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/dev-server-esbuild": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@web/dev-server-esbuild/-/dev-server-esbuild-1.0.5.tgz", + "integrity": "sha512-QkKfnBuo+ogcMXLE9kKhw8+UwfbuIgiKKATY26ssgSwtMmAhgq+/SSG+QM55VMkVVzDDLi97aMDEcdr3V2sYvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mdn/browser-compat-data": "^4.0.0", + "@web/dev-server-core": "^0.7.4", + "esbuild": "^0.27.0", + "parse5": "^6.0.1", + "ua-parser-js": "^1.0.33" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/dev-server-rollup": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/@web/dev-server-rollup/-/dev-server-rollup-0.6.4.tgz", + "integrity": "sha512-sJZfTGCCrdku5xYnQQG51odGI092hKY9YFM0X3Z0tRY3iXKXcYRaLZrErw5KfCxr6g0JRuhe4BBhqXTA5Q2I3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rollup/plugin-node-resolve": "^15.0.1", + "@web/dev-server-core": "^0.7.2", + "nanocolors": "^0.2.1", + "parse5": "^6.0.1", + "rollup": "^4.4.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/parse5-utils": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@web/parse5-utils/-/parse5-utils-2.1.1.tgz", + "integrity": "sha512-7rBVZEMGfrq2iPcAEwJ0KSNSvmA2a6jT2CK8/gyIOHgn4reg7bSSRbzyWIEYWyIkeRoYEukX/aW+nAeCgSSqhQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/parse5": "^6.0.1", + "parse5": "^6.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/test-runner": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/@web/test-runner/-/test-runner-0.20.2.tgz", + "integrity": "sha512-zfEGYEDnS0EI8qgoWFjmtkIXhqP15W40NW3dCaKtbxj5eU0a7E53f3GV/tZGD0GlZKF8d4Fyw+AFrwOJU9Z4GA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/browser-logs": "^0.4.0", + "@web/config-loader": "^0.3.0", + "@web/dev-server": "^0.4.0", + "@web/test-runner-chrome": "^0.18.1", + "@web/test-runner-commands": "^0.9.0", + "@web/test-runner-core": "^0.13.0", + "@web/test-runner-mocha": "^0.9.0", + "camelcase": "^6.2.0", + "command-line-args": "^5.1.1", + "command-line-usage": "^7.0.1", + "convert-source-map": "^2.0.0", + "diff": "^5.0.0", + "globby": "^11.0.1", + "nanocolors": "^0.2.1", + "portfinder": "^1.0.32", + "source-map": "^0.7.3" + }, + "bin": { + "web-test-runner": "dist/bin.js", + "wtr": "dist/bin.js" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/test-runner-chrome": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@web/test-runner-chrome/-/test-runner-chrome-0.18.1.tgz", + "integrity": "sha512-eO6ctCaqSguGM6G3cFobGHnrEs9wlv9Juj/Akyr4XLjeEMTheNULdvOXw9Bygi+QC/ir/0snMmt+/YKnfy8rYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.13.0", + "@web/test-runner-coverage-v8": "^0.8.0", + "chrome-launcher": "^0.15.0", + "puppeteer-core": "^24.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/test-runner-commands": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-commands/-/test-runner-commands-0.9.0.tgz", + "integrity": "sha512-zeLI6QdH0jzzJMDV5O42Pd8WLJtYqovgdt0JdytgHc0d1EpzXDsc7NTCJSImboc2NcayIsWAvvGGeRF69SMMYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.13.0", + "mkdirp": "^1.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/test-runner-core": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/@web/test-runner-core/-/test-runner-core-0.13.4.tgz", + "integrity": "sha512-84E1025aUSjvZU1j17eCTwV7m5Zg3cZHErV3+CaJM9JPCesZwLraIa0ONIQ9w4KLgcDgJFw9UnJ0LbFf42h6tg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.11", + "@types/babel__code-frame": "^7.0.2", + "@types/co-body": "^6.1.0", + "@types/convert-source-map": "^2.0.0", + "@types/debounce": "^1.2.0", + "@types/istanbul-lib-coverage": "^2.0.3", + "@types/istanbul-reports": "^3.0.0", + "@web/browser-logs": "^0.4.0", + "@web/dev-server-core": "^0.7.3", + "chokidar": "^4.0.1", + "cli-cursor": "^3.1.0", + "co-body": "^6.1.0", + "convert-source-map": "^2.0.0", + "debounce": "^1.2.0", + "dependency-graph": "^0.11.0", + "globby": "^11.0.1", + "internal-ip": "^6.2.0", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-report": "^3.0.1", + "istanbul-reports": "^3.0.2", + "log-update": "^4.0.0", + "nanocolors": "^0.2.1", + "nanoid": "^3.1.25", + "open": "^8.0.2", + "picomatch": "^2.2.2", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/test-runner-coverage-v8": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-coverage-v8/-/test-runner-coverage-v8-0.8.0.tgz", + "integrity": "sha512-PskiucYpjUtgNfR2zF2AWqWwjXL7H3WW/SnCAYmzUrtob7X9o/+BjdyZ4wKbOxWWSbJO4lEdGIDLu+8X2Xw+lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.13.0", + "istanbul-lib-coverage": "^3.0.0", + "lru-cache": "^8.0.4", + "picomatch": "^2.2.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/test-runner-mocha": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@web/test-runner-mocha/-/test-runner-mocha-0.9.0.tgz", + "integrity": "sha512-ZL9F6FXd0DBQvo/h/+mSfzFTSRVxzV9st/AHhpgABtUtV/AIpVE9to6+xdkpu6827kwjezdpuadPfg+PlrBWqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.13.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@web/test-runner-playwright": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/@web/test-runner-playwright/-/test-runner-playwright-0.11.1.tgz", + "integrity": "sha512-l9tmX0LtBqMaKAApS4WshpB87A/M8sOHZyfCobSGuYqnREgz5rqQpX314yx+4fwHXLLTa5N64mTrawsYkLjliw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@web/test-runner-core": "^0.13.0", + "@web/test-runner-coverage-v8": "^0.8.0", + "playwright": "^1.53.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/array-back": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-3.1.0.tgz", + "integrity": "sha512-TkuxA4UCOvxuDK6NZYXCalszEzj+TLszyASooky+i742l9TqsOdYCMJJupxRic61hwquNtppB3hgcuq9SVSH1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/axe-core": { + "version": "4.11.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.11.3.tgz", + "integrity": "sha512-zBQouZixDTbo3jMGqHKyePxYxr1e5W8UdTmBQ7sNtaA9M2bE32daxxPLS/jojhKOHxQ7LWwPjfiwf/fhaJWzlg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/b4a": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.8.0.tgz", + "integrity": "sha512-qRuSmNSkGQaHwNbM7J78Wwy+ghLEYF1zNrSeMxj4Kgw6y33O3mXcQ6Ie9fRvfU/YnxWkOchPXbaLb73TkIsfdg==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "react-native-b4a": "*" + }, + "peerDependenciesMeta": { + "react-native-b4a": { + "optional": true + } + } + }, + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } + } + }, + "node_modules/bare-fs": { + "version": "4.7.1", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.1.tgz", + "integrity": "sha512-WDRsyVN52eAx/lBamKD6uyw8H4228h/x0sGGGegOamM2cd7Pag88GfMQalobXI+HaEUxpCkbKQUDOQqt9wawRw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } + } + }, + "node_modules/bare-os": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.9.0.tgz", + "integrity": "sha512-JTjuZyNIDpw+GytMO4a6TK1VXdVKKJr6DRxEHasyuYyShV2deuiHJK/ahGZlebc+SG0/wJCB9XK8gprBGDFi/Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" + } + }, + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-os": "^3.0.1" + } + }, + "node_modules/bare-stream": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.13.0.tgz", + "integrity": "sha512-3zAJRZMDFGjdn+RVnNpF9kuELw+0Fl3lpndM4NcEOhb9zwtSo/deETfuIwMSE5BXanA0FrN1qVjffGwAg2Y7EA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "streamx": "^2.25.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-abort-controller": "*", + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + }, + "bare-buffer": { + "optional": true + }, + "bare-events": { + "optional": true + } + } + }, + "node_modules/bare-url": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.4.2.tgz", + "integrity": "sha512-/9a2j4ac6ckpmAHvod/ob7x439OAHst/drc2Clnq+reRYd/ovddwcF4LfoxHyNk5AuGBnPg+HqFjmE/Zpq6v0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-path": "^3.0.0" + } + }, + "node_modules/basic-ftp": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.3.1.tgz", + "integrity": "sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cache-content-type": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-content-type/-/cache-content-type-1.0.1.tgz", + "integrity": "sha512-IKufZ1o4Ut42YUrZSo8+qnMTrFuKkvyoLXUywKz9GJ5BrhOFGhLdkx9sG4KAnVvbY6kEcSFjLQul+DVmBm2bgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-types": "^2.1.18", + "ylru": "^1.2.0" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai-a11y-axe": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/chai-a11y-axe/-/chai-a11y-axe-1.5.0.tgz", + "integrity": "sha512-V/Vg/zJDr9aIkaHJ2KQu7lGTQQm5ZOH4u1k5iTMvIXuSVlSuUo0jcSpSqf9wUn9zl6oQXa4e4E0cqH18KOgKlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "axe-core": "^4.3.3" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk-template": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk-template/-/chalk-template-0.4.0.tgz", + "integrity": "sha512-/ghrgmhfY8RaSdeo43hNXxpoHAtxdbskUHjPpfqUWGttFgycUhYPGx3YZBCnUCvOa7Doivn1IZec3DEGFoMgLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/chalk-template?sponsor=1" + } + }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/chrome-launcher": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-0.15.2.tgz", + "integrity": "sha512-zdLEwNo3aUVzIhKhTtXfxhdvZhUghrnmkvcAq2NoDd+LeOHKf03H5jwZ8T/STsAlzyALkBVK552iaG1fGf1xVQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^1.0.0" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } + }, + "node_modules/chromium-bidi": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-14.0.0.tgz", + "integrity": "sha512-9gYlLtS6tStdRWzrtXaTMnqcM4dudNegMXJxkR0I/CXObHalYeYcAMPrL19eroNZHtJ8DQmu1E+ZNOYu/IXMXw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "^3.0.1", + "zod": "^3.24.1" + }, + "peerDependencies": { + "devtools-protocol": "*" + } + }, + "node_modules/cli-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", + "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "restore-cursor": "^3.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/co-body": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/co-body/-/co-body-6.2.0.tgz", + "integrity": "sha512-Kbpv2Yd1NdL1V/V4cwLVxraHDV6K8ayohr2rmH0J87Er8+zJjcTa6dAn9QMPC9CRgU8+aNajKbSf1TzDB1yKPA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@hapi/bourne": "^3.0.0", + "inflation": "^2.0.0", + "qs": "^6.5.2", + "raw-body": "^2.3.3", + "type-is": "^1.6.16" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/command-line-args": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/command-line-args/-/command-line-args-5.2.1.tgz", + "integrity": "sha512-H4UfQhZyakIjC74I9d34fGYDwk3XpSr17QhEd0Q3I9Xq1CETHo4Hcuo87WyWHpAF1aSLjLRf5lD9ZGX2qStUvg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^3.1.0", + "find-replace": "^3.0.0", + "lodash.camelcase": "^4.3.0", + "typical": "^4.0.0" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/command-line-usage": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-7.0.4.tgz", + "integrity": "sha512-85UdvzTNx/+s5CkSgBm/0hzP80RFHAa7PsfeADE5ezZF3uHz3/Tqj9gIKGT9PTtpycc3Ua64T0oVulGfKxzfqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "chalk-template": "^0.4.0", + "table-layout": "^4.1.1", + "typical": "^7.3.0" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/command-line-usage/node_modules/array-back": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.3.tgz", + "integrity": "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/command-line-usage/node_modules/typical": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-7.3.0.tgz", + "integrity": "sha512-ya4mg/30vm+DOWfBg4YK3j2WD6TWtRkCbasOJr40CseYENzCUby/7rIvXA99JGsQHeNxLbnXdyLLxKSv3tauFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cookies": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/cookies/-/cookies-0.9.1.tgz", + "integrity": "sha512-TG2hpqe4ELx54QER/S3HQ9SRVnQnGBtKUz5bLQWtYAQ+o6GpgMs6sYUvaiJjVxb+UXwhRhAEP3m7LbsIZ77Hmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "keygrip": "~1.1.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-equal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz", + "integrity": "sha512-bHtC0iYvWhyaTzvV3CZgPeZQqCOBGyGsVV7v4eevpdkLHfiSrXUdBG+qAuSz4RI70sszvjQ1QSZ98An1yNwpSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/dependency-graph": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-0.11.0.tgz", + "integrity": "sha512-JeMq7fEshyepOWDfcfHK06N3MhyPhz++vtqWhMT5O9A3K42rdsEDpfdVqjaqaAhsw6a+ZqeDvQVtD0hFHQWrzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/devtools-protocol": { + "version": "0.0.1595872", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1595872.tgz", + "integrity": "sha512-kRfgp8vWVjBu/fbYCiVFiOqsCk3CrMKEo3WbgGT2NXK2dG7vawWPBljixajVgGK9II8rDO9G0oD0zLt3I1daRg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", + "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/errorstacks": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/errorstacks/-/errorstacks-2.4.1.tgz", + "integrity": "sha512-jE4i0SMYevwu/xxAuzhly/KTwtj0xDhbzB6m1xPImxTkw8wcCbgarOQPfCVMi5JKVyW7in29pNJCCJrry3Ynnw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", + "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fastq": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz", + "integrity": "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-replace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-replace/-/find-replace-3.0.0.tgz", + "integrity": "sha512-6Tb2myMioCAgv5kfvP5/PkZZ/ntTpVK39fHY7WkWBgvbeE+VHd/tZuZ4mrC+bxh4cfOZeYKVPaJIZtZXV7GNCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^3.0.1" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/generator-function": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/generator-function/-/generator-function-2.0.1.tgz", + "integrity": "sha512-SFdFmIJi+ybC0vjlHN0ZGVGHc3lgE0DxPAT0djjVg+kjOnSqclqmj0KQ7ykTOLP6YxoqOvuAODGdcHJn+43q3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.3.tgz", + "integrity": "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-assert": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/http-assert/-/http-assert-1.5.0.tgz", + "integrity": "sha512-uPpH7OKX4H25hBmU6G1jWNaqJGpTXxey+YOUizJUAgu0AjLUeC8D73hTrhvDS5D+GJN1DN1+hhc/eF/wpxtp0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-equal": "~1.0.1", + "http-errors": "~1.8.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.8.1.tgz", + "integrity": "sha512-Kpk9Sm7NmI+RHhnj6OIWDI1d6fIoFAtFt9RLaTMRlg/8w49juAStsrBgp0Dp4OdxdVbRIeKhtCUvoi/RuAhO4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": ">= 1.5.0 < 2", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-errors/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/inflation": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.1.0.tgz", + "integrity": "sha512-t54PPJHG1Pp7VQvxyVCJ9mBbjG3Hqryges9bXoOO6GExCPa+//i/d5GSuFtpx3ALLd7lgIAur6zrIlBQyJuMlQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/internal-ip": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-6.2.0.tgz", + "integrity": "sha512-D8WGsR6yDt8uq7vDMu7mjcR+yRMm3dW8yufyChmszWRjcSHuxLBkR3GdS2HZAjodsaGuCvXeEJpueisXJULghg==", + "dev": true, + "license": "MIT", + "dependencies": { + "default-gateway": "^6.0.0", + "ipaddr.js": "^1.9.1", + "is-ip": "^3.1.0", + "p-event": "^4.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/internal-ip?sponsor=1" + } + }, + "node_modules/ip-address": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.2.0.tgz", + "integrity": "sha512-/+S6j4E9AHvW9SWMSEY9Xfy66O5PWvVEJ08O0y5JGyEKQpojb0K0GKpz/v5HJ/G0vi3D2sjGK78119oXZeE0qA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-regex": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-4.3.0.tgz", + "integrity": "sha512-B9ZWJxHHOHUhUjCPrMpLD4xEq35bUTClHM1S6CBU5ixQnkZmwipwgc96vAd7AAGM9TGHvJR+Uss+/Ak6UphK+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "license": "MIT", + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.2.tgz", + "integrity": "sha512-upqt1SkGkODW9tsGNG5mtXTXtECizwtS2kA161M+gJPc1xdb/Ax629af6YrTwcOeQHbewrPNlE5Dx7kzvXTizA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.4", + "generator-function": "^2.0.0", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-ip": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-ip/-/is-ip-3.1.0.tgz", + "integrity": "sha512-35vd5necO7IitFPjd/YBeqwWnyDWbuLH9ZXQdMfDA8TEo7pv5X8yfrvVO3xbJbLUlERCMvf6X0hTUamQxCYJ9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-regex": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-module": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-module/-/is-module-1.0.0.tgz", + "integrity": "sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isbinaryfile": { + "version": "5.0.7", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-5.0.7.tgz", + "integrity": "sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/keygrip": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/keygrip/-/keygrip-1.1.0.tgz", + "integrity": "sha512-iYSchDJ+liQ8iwbSI2QqsQOvqv58eJCEanyJPJi+Khyu8smkcKSFUCbPwzFcL7YVtZ6eONjqRX/38caJ7QjRAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tsscmp": "1.0.6" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/koa": { + "version": "2.16.4", + "resolved": "https://registry.npmjs.org/koa/-/koa-2.16.4.tgz", + "integrity": "sha512-3An0GCLDSR34tsCO4H8Tef8Pp2ngtaZDAZnsWJYelqXUK5wyiHvGItgK/xcSkmHLSTn1Jcho1mRQs2ehRzvKKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "^1.3.5", + "cache-content-type": "^1.0.0", + "content-disposition": "~0.5.2", + "content-type": "^1.0.4", + "cookies": "~0.9.0", + "debug": "^4.3.2", + "delegates": "^1.0.0", + "depd": "^2.0.0", + "destroy": "^1.0.4", + "encodeurl": "^1.0.2", + "escape-html": "^1.0.3", + "fresh": "~0.5.2", + "http-assert": "^1.3.0", + "http-errors": "^1.6.3", + "is-generator-function": "^1.0.7", + "koa-compose": "^4.1.0", + "koa-convert": "^2.0.0", + "on-finished": "^2.3.0", + "only": "~0.0.2", + "parseurl": "^1.3.2", + "statuses": "^1.5.0", + "type-is": "^1.6.16", + "vary": "^1.1.2" + }, + "engines": { + "node": "^4.8.4 || ^6.10.1 || ^7.10.1 || >= 8.1.4" + } + }, + "node_modules/koa-compose": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/koa-compose/-/koa-compose-4.1.0.tgz", + "integrity": "sha512-8ODW8TrDuMYvXRwra/Kh7/rJo9BtOfPc6qO8eAfC80CnCvSjSl0bkRM24X6/XBBEyj0v1nRUQ1LyOy3dbqOWXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/koa-convert": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/koa-convert/-/koa-convert-2.0.0.tgz", + "integrity": "sha512-asOvN6bFlSnxewce2e/DK3p4tltyfC4VM7ZwuTuepI7dEQVcvpyFuBcEARu1+Hxg8DIwytce2n7jrZtRlPrARA==", + "dev": true, + "license": "MIT", + "dependencies": { + "co": "^4.6.0", + "koa-compose": "^4.1.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/koa-etag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/koa-etag/-/koa-etag-4.0.0.tgz", + "integrity": "sha512-1cSdezCkBWlyuB9l6c/IFoe1ANCDdPBxkDkRiaIup40xpUub6U/wwRXoKBZw/O5BifX9OlqAjYnDyzM6+l+TAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "etag": "^1.8.1" + } + }, + "node_modules/koa-send": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/koa-send/-/koa-send-5.0.1.tgz", + "integrity": "sha512-tmcyQ/wXXuxpDxyNXv5yNNkdAMdFRqwtegBXUaowiQzUKqJehttS0x2j0eOZDQAyloAth5w6wwBImnFzkUz3pQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.1.1", + "http-errors": "^1.7.3", + "resolve-path": "^1.4.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/koa-static": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/koa-static/-/koa-static-5.0.0.tgz", + "integrity": "sha512-UqyYyH5YEXaJrf9S8E23GoJFQZXkBVJ9zYYMPGz919MSX1KuvAcycIuS0ci150HCoPf4XQVhQ84Qf8xRPWxFaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.1.0", + "koa-send": "^5.0.0" + }, + "engines": { + "node": ">= 7.6.0" + } + }, + "node_modules/koa-static/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/lighthouse-logger": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-1.4.2.tgz", + "integrity": "sha512-gPWxznF6TKmUHrOQjlVo2UbaL2EJ71mb2CCeRs/2qBpi4L/g4LUVc9+3lKQ6DTUZwJswfM7ainGrLO1+fOqa2g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/lit": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/lit/-/lit-3.3.3.tgz", + "integrity": "sha512-fycuvZg/hkpozL00lm1pEJH5nN/lr9ZXd6mJI2HSN4+Bzc+LDNdEApJ6HFbPkdFNHLvOplIIuJvxkS4XUxqirw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit/reactive-element": "^2.1.0", + "lit-element": "^4.2.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-element": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/lit-element/-/lit-element-4.2.2.tgz", + "integrity": "sha512-aFKhNToWxoyhkNDmWZwEva2SlQia+jfG0fjIWV//YeTaWrVnOxD89dPKfigCUspXFmjzOEUQpOkejH5Ly6sG0w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@lit-labs/ssr-dom-shim": "^1.5.0", + "@lit/reactive-element": "^2.1.0", + "lit-html": "^3.3.0" + } + }, + "node_modules/lit-html": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/lit-html/-/lit-html-3.3.2.tgz", + "integrity": "sha512-Qy9hU88zcmaxBXcc10ZpdK7cOLXvXpRoBxERdtqV9QOrfpMZZ6pSYP91LhpPtap3sFMUiL7Tw2RImbe0Al2/kw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@types/trusted-types": "^2.0.2" + } + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/log-update": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/log-update/-/log-update-4.0.0.tgz", + "integrity": "sha512-9fkkDevMefjg0mmzWFBW8YkFP91OrizzkW3diF7CpG+S2EYdy4+TVfGwz1zeF8x7hCx1ovSPTOE9Ngib74qqUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^4.3.0", + "cli-cursor": "^3.1.0", + "slice-ansi": "^4.0.0", + "wrap-ansi": "^6.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lru-cache": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz", + "integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16.14" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marky": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz", + "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true, + "license": "MIT" + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanocolors": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/nanocolors/-/nanocolors-0.2.13.tgz", + "integrity": "sha512-0n3mSAQLPpGLV9ORXT5+C/D4mwew7Ebws69Hx4E2sgz2ZA5+32Q80B9tL8PbL7XHnRDiAxH/pnrUJ9a4fkTNTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/only": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/only/-/only-0.0.2.tgz", + "integrity": "sha512-Fvw+Jemq5fjjyWz6CpKx6w9s7xxqo3+JCyM0WXWeCSOboZ8ABkyvP8ID4CZuChA/wxSx+XSJmdOm8rGVyJ1hdQ==", + "dev": true + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-event": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-4.2.0.tgz", + "integrity": "sha512-KXatOjCRXXkSePPb1Nbi0p0m+gQAwdlbhi4wQKJPI1HsMQS9g+Sqp2o+QHziPr7eYJyOZet836KoHEVM1mwOrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^3.1.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pdfjs-dist": { + "version": "4.10.38", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.10.38.tgz", + "integrity": "sha512-/Y3fcFrXEAsMjJXeL9J8+ZG9U01LbuWaYypvDW2ycW1jL269L3js3DVBjDJ0Up9Np1uqDXsDrRihHANhZOlwdQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "optionalDependencies": { + "@napi-rs/canvas": "^0.1.65" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.2.tgz", + "integrity": "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/playwright": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.61.1.tgz", + "integrity": "sha512-DWnY5o3YbLWK4GovuAVwpqL+1VwGNdUGrRr++8j8PtQQzvAVZUIMjKQ90fY689sEJZJBbZVw1rXaOKSTitkzPQ==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.61.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.61.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.61.1.tgz", + "integrity": "sha512-h7Qlt6m4REp25qvIdvbDtVmD4LqVXfpRxhORv9L0jzETM05p4fuPJ3dKyuSXQxDSbXnmS79HAgi9589lGSpLkg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/portfinder": { + "version": "1.0.38", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.38.tgz", + "integrity": "sha512-rEwq/ZHlJIKw++XtLAO8PPuOQA/zaPJOZJ37BVuN97nLpMJeuDVLVGRwbFoBgLudgdTMP2hdRJP++H+8QOA3vg==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/pump": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.4.tgz", + "integrity": "sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/puppeteer-core": { + "version": "24.42.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-24.42.0.tgz", + "integrity": "sha512-T4zXokk/izH01fYPhyyev1A4piWiOKrYq7CUFpdoYQxmOnXoV6YjUabmfIjCYkNspSoAXIxRid3Tw+Vg0fthYg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.13.0", + "chromium-bidi": "14.0.0", + "debug": "^4.4.3", + "devtools-protocol": "0.0.1595872", + "typed-query-selector": "^2.12.1", + "webdriver-bidi-protocol": "0.4.1", + "ws": "^8.19.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/qs": { + "version": "6.15.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", + "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-path": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/resolve-path/-/resolve-path-1.4.0.tgz", + "integrity": "sha512-i1xevIst/Qa+nA9olDxLWnLk8YZbi8R/7JPbCMcgyWaFR6bKWaexgJgEB5oc2PKMjYdrHynyz0NY+if+H98t1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "http-errors": "~1.6.2", + "path-is-absolute": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/resolve-path/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/resolve-path/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/resolve-path/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true, + "license": "ISC" + }, + "node_modules/resolve-path/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/restore-cursor": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", + "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "onetime": "^5.1.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.60.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.2.tgz", + "integrity": "sha512-J9qZyW++QK/09NyN/zeO0dG/1GdGfyp9lV8ajHnRVLfo/uFsbji5mHnDgn/qYdUHyCkM2N+8VyspgZclfAh0eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.2", + "@rollup/rollup-android-arm64": "4.60.2", + "@rollup/rollup-darwin-arm64": "4.60.2", + "@rollup/rollup-darwin-x64": "4.60.2", + "@rollup/rollup-freebsd-arm64": "4.60.2", + "@rollup/rollup-freebsd-x64": "4.60.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.2", + "@rollup/rollup-linux-arm-musleabihf": "4.60.2", + "@rollup/rollup-linux-arm64-gnu": "4.60.2", + "@rollup/rollup-linux-arm64-musl": "4.60.2", + "@rollup/rollup-linux-loong64-gnu": "4.60.2", + "@rollup/rollup-linux-loong64-musl": "4.60.2", + "@rollup/rollup-linux-ppc64-gnu": "4.60.2", + "@rollup/rollup-linux-ppc64-musl": "4.60.2", + "@rollup/rollup-linux-riscv64-gnu": "4.60.2", + "@rollup/rollup-linux-riscv64-musl": "4.60.2", + "@rollup/rollup-linux-s390x-gnu": "4.60.2", + "@rollup/rollup-linux-x64-gnu": "4.60.2", + "@rollup/rollup-linux-x64-musl": "4.60.2", + "@rollup/rollup-openbsd-x64": "4.60.2", + "@rollup/rollup-openharmony-arm64": "4.60.2", + "@rollup/rollup-win32-arm64-msvc": "4.60.2", + "@rollup/rollup-win32-ia32-msvc": "4.60.2", + "@rollup/rollup-win32-x64-gnu": "4.60.2", + "@rollup/rollup-win32-x64-msvc": "4.60.2", + "fsevents": "~2.3.2" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.1.tgz", + "integrity": "sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/streamx": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.25.0.tgz", + "integrity": "sha512-0nQuG6jf1w+wddNEEXCF4nTg3LtufWINB5eFEN+5TNZW7KWJp6x87+JFL43vaAUPyCfH1wID+mNVyW6OHtFamg==", + "dev": true, + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/table-layout": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/table-layout/-/table-layout-4.1.1.tgz", + "integrity": "sha512-iK5/YhZxq5GO5z8wb0bY1317uDF3Zjpha0QFFLA8/trAoiLbQD0HUbMesEaxyzUgDxi2QlcbM8IvqOlEjgoXBA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-back": "^6.2.2", + "wordwrapjs": "^5.1.0" + }, + "engines": { + "node": ">=12.17" + } + }, + "node_modules/table-layout/node_modules/array-back": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/array-back/-/array-back-6.2.3.tgz", + "integrity": "sha512-SGDvmg6QTYiTxCBkYVmThcoa67uLl35pyzRHdpCGBOcqFy6BtwnphoFPk7LhJshD+Yk1Kt35WGWeZPTgwR4Fhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/tar-fs": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.2.tgz", + "integrity": "sha512-QGxxTxxyleAdyM3kpFs14ymbYmNFrfY+pHj7Z8FgtbZ7w2//VAgLMac7sT6nRpIHjppXO2AwwEOg0bPFVRcmXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "pump": "^3.0.0", + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" + } + }, + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" + } + }, + "node_modules/text-decoder": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.2.7.tgz", + "integrity": "sha512-vlLytXkeP4xvEq2otHeJfSQIRyWxo/oZGEbXrtEEF9Hnmrdly59sUbzZ/QgyWuLYHctCHxFF4tRQZNQ9k60ExQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "b4a": "^1.6.4" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsscmp": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", + "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6.x" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.2", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz", + "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", + "integrity": "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typical": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/typical/-/typical-4.0.0.tgz", + "integrity": "sha512-VAH4IvQ7BDFYglMd7BPRDfLgxZZX4O4TFcRDA6EN5X7erNJJq+McIEp8np9aVtxrCJ6qx4GTYVfOWNjcqwZgRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ua-parser-js": { + "version": "1.0.41", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-1.0.41.tgz", + "integrity": "sha512-LbBDqdIC5s8iROCUjMbW1f5dJQTEFB1+KO9ogbvlb3nm9n4YHa5p4KTvFPWvh2Hs8gZMBuiB1/8+pdfe/tDPug==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "license": "MIT", + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "dev": true, + "license": "MIT" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webdriver-bidi-protocol": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.1.tgz", + "integrity": "sha512-ARrjNjtWRRs2w4Tk7nqrf2gBI0QXWuOmMCx2hU+1jUt6d00MjMxURrhxhGbrsoiZKJrhTSTzbIrc554iKI10qw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrapjs": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/wordwrapjs/-/wordwrapjs-5.1.1.tgz", + "integrity": "sha512-0yweIbkINJodk27gX9LBGMzyQdBDan3s/dEAiwBOj+Mf0PPyWL6/rikalkv8EeD0E8jm4o5RXEOrFTP3NXbhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.17" + } + }, + "node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + }, + "node_modules/ylru": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/ylru/-/ylru-1.4.0.tgz", + "integrity": "sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/custom_components/maintenance_supporter/frontend-src/package.json b/custom_components/maintenance_supporter/frontend-src/package.json new file mode 100644 index 0000000..6564b46 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/package.json @@ -0,0 +1,27 @@ +{ + "name": "maintenance-supporter-frontend", + "version": "0.1.0", + "private": true, + "type": "module", + "scripts": { + "build": "node esbuild.mjs", + "test": "web-test-runner --config web-test-runner.config.mjs", + "test:watch": "web-test-runner --config web-test-runner.config.mjs --watch" + }, + "devDependencies": { + "@open-wc/testing": "^4.0.0", + "@web/dev-server-esbuild": "^1.0.0", + "@web/test-runner": "^0.20.0", + "@web/test-runner-playwright": "^0.11.0", + "esbuild": "^0.28.1", + "lit": "^3.3.3", + "typescript": "^6.0.3" + }, + "dependencies": { + "pdfjs-dist": "^4.10.38", + "playwright": "1.61" + }, + "overrides": { + "esbuild": "$esbuild" + } +} diff --git a/custom_components/maintenance_supporter/frontend-src/panel-styles.ts b/custom_components/maintenance_supporter/frontend-src/panel-styles.ts new file mode 100644 index 0000000..8f9eadc --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/panel-styles.ts @@ -0,0 +1,1393 @@ +/** Component-specific styles for the maintenance-supporter-panel. */ + +import { css } from "lit"; + +export const panelStyles = css` + :host { + display: block; + height: 100%; + background: var(--primary-background-color); + } + + .panel { + height: 100%; + display: flex; + flex-direction: column; + } + + .header { + display: flex; + align-items: center; + gap: 4px; + background: var(--app-header-background-color, var(--primary-color)); + color: var(--app-header-text-color, white); + padding: 12px 16px; + font-size: 16px; + } + + .header ha-menu-button { + margin-right: 4px; + color: var(--app-header-text-color, white); + } + .header ha-icon-button { + --mdc-icon-button-size: 36px; + --mdc-icon-size: 20px; + color: var(--app-header-text-color, white); + } + + .breadcrumbs { display: flex; align-items: center; gap: 4px; } + .breadcrumbs a { color: inherit; opacity: 0.8; cursor: pointer; text-decoration: none; } + .breadcrumbs a:hover { opacity: 1; text-decoration: underline; } + .breadcrumbs .sep { opacity: 0.5; margin: 0 4px; } + .breadcrumbs .current { font-weight: 500; } + + .content { flex: 1; overflow-y: auto; padding: 0 16px 16px; } + + .filter-bar { + display: flex; + justify-content: space-between; + align-items: flex-end; + padding: 8px 0; + gap: 8px; + } + + .filter-field { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 0; + } + .filter-label { + font-size: 11px; + font-weight: 500; + text-transform: uppercase; + letter-spacing: 0.4px; + color: var(--secondary-text-color); + padding-left: 2px; + } + + .filter-bar select { + padding: 8px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + } + + /* Desktop: the LIST owns the 7-column grid and every row is a subgrid + spanning all columns. Sharing the tracks across rows is what actually + keeps the title (and every other column) aligned regardless of which + optional badges/chips a given row carries. A per-row grid can't: each + row would size its auto badges column independently, so a row with an + NFC badge pushed its title right of the others (issue #66). */ + .task-table { + display: grid; + grid-template-columns: + auto /* badges */ + minmax(100px, 180px) /* object-name */ + minmax(120px, 1fr) /* task-name */ + minmax(0, 220px) /* task-sub (chips) */ + 100px /* type */ + 150px /* due-cell */ + auto; /* row-actions */ + column-gap: 12px; + } + + .task-row { + display: grid; + grid-template-columns: subgrid; + grid-column: 1 / -1; + align-items: center; + column-gap: 12px; + padding: 10px 12px; + border-bottom: 1px solid var(--divider-color); + cursor: pointer; + transition: background 0.15s; + } + + /* Virtualized task table (large installs): only the scroll window of rows + is in the DOM. The spacers span all columns and carry the off-window + height so the scrollbar stays honest. The sizer row is invisible and + zero-height but its badge cell still participates in subgrid track + sizing — pinning the content-sized badge column to the widest badge set + across ALL rows, so columns can't jitter while scrolling. */ + .virt-spacer { grid-column: 1 / -1; } + .task-row.virt-sizer { + height: 0; + min-height: 0; + padding-top: 0; + padding-bottom: 0; + border: none; + overflow: hidden; + visibility: hidden; + pointer-events: none; + } + + /* Bulk selection: a leading checkbox column while selecting. */ + .task-table.bulk { grid-template-columns: auto auto minmax(100px, 180px) minmax(120px, 1fr) minmax(0, 220px) 100px 150px auto; } + .bulk-check { display: flex; align-items: center; justify-content: center; cursor: pointer; } + .bulk-check input, .bulk-selectall input { width: 17px; height: 17px; cursor: pointer; accent-color: var(--primary-color); } + .task-row.bulk-selected { background: color-mix(in srgb, var(--primary-color) 12%, transparent); } + .bulk-bar { + display: flex; align-items: center; gap: 16px; flex-wrap: wrap; + padding: 8px 12px; margin-bottom: 8px; border-radius: 8px; + background: var(--secondary-background-color); border: 1px solid var(--divider-color); + position: sticky; top: 0; z-index: 5; + } + .bulk-selectall { display: inline-flex; align-items: center; gap: 6px; cursor: pointer; font-size: 13px; } + .bulk-count { color: var(--secondary-text-color); font-size: 13px; } + .bulk-actions { margin-left: auto; display: inline-flex; gap: 8px; } + .bulk-toggle.active { --mdc-theme-primary: var(--primary-color); } + + /* Collapsible analysis sections on the task-detail overview tab. The header + owns the title, so the wrapped card's own title row is hidden to avoid + showing it twice. */ + .collapsible { margin: 8px 0; border: 1px solid var(--divider-color); border-radius: 10px; overflow: hidden; } + .collapsible-head { + display: flex; align-items: center; gap: 8px; width: 100%; + font: inherit; font-weight: 600; font-size: 14px; text-align: left; + padding: 10px 12px; cursor: pointer; background: var(--secondary-background-color); + border: none; color: var(--primary-text-color); + } + .collapsible-head:hover { background: var(--table-row-alternative-background-color, rgba(0,0,0,.04)); } + .collapsible-head ha-icon { --mdc-icon-size: 20px; color: var(--secondary-text-color); } + .collapsible-body { padding: 4px 12px 12px; } + .collapsible-body > .weibull-section > .weibull-title, + .collapsible-body > .seasonal-chart > .seasonal-chart-title { display: none; } + + /* "Today" focus view — mobile-first list grouped by urgency. */ + .today-view { display: flex; flex-direction: column; gap: 16px; padding: 4px 0 12px; } + .today-section { border: 1px solid var(--divider-color); border-radius: 12px; overflow: hidden; } + .today-section-header { + display: flex; align-items: center; justify-content: space-between; + padding: 10px 14px; font-weight: 600; font-size: 14px; + background: var(--secondary-background-color); + border-left: 4px solid var(--divider-color); + } + .today-section-header.overdue { border-left-color: var(--error-color, #f44336); } + .today-section-header.due_soon { border-left-color: var(--warning-color, #ff9800); } + .today-badge { + min-width: 22px; text-align: center; padding: 1px 8px; border-radius: 11px; + background: var(--primary-color); color: var(--text-primary-color, #fff); font-size: 12.5px; + } + .today-row { + display: flex; align-items: center; gap: 12px; padding: 11px 14px; + border-top: 1px solid var(--divider-color); cursor: pointer; + content-visibility: auto; contain-intrinsic-size: auto 46px; + } + .today-row:hover { background: var(--table-row-alternative-background-color, rgba(0,0,0,.04)); } + .today-dot { width: 10px; height: 10px; border-radius: 50%; flex: none; background: var(--success-color, #4caf50); } + .today-dot.overdue { background: var(--error-color, #f44336); } + .today-dot.due_soon { background: var(--warning-color, #ff9800); } + .today-dot.triggered { background: #ff5722; } + .today-main { flex: 1; min-width: 0; } + .today-task { font-weight: 600; font-size: 14px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .today-object { color: var(--secondary-text-color); font-size: 12.5px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .today-row .btn-complete { color: var(--success-color, #4caf50); flex: none; } + .today-empty { + display: flex; flex-direction: column; align-items: center; gap: 10px; + padding: 48px 16px; color: var(--secondary-text-color); text-align: center; + } + .today-empty ha-icon { --mdc-icon-size: 56px; color: var(--success-color, #4caf50); } + + /* Command palette (Ctrl/Cmd+K). */ + .palette-backdrop { + position: fixed; inset: 0; z-index: 1100; background: rgba(0,0,0,.4); + display: flex; align-items: flex-start; justify-content: center; padding-top: 12vh; + animation: toast-in .15s ease; + } + .palette { + width: min(620px, 92vw); max-height: 66vh; display: flex; flex-direction: column; + background: var(--card-background-color, #fff); border-radius: 12px; overflow: hidden; + box-shadow: 0 12px 48px rgba(0,0,0,.4); + } + .palette-input { + font: inherit; font-size: 16px; padding: 16px 18px; border: none; outline: none; + background: transparent; color: var(--primary-text-color); + border-bottom: 1px solid var(--divider-color); + } + .palette-results { overflow-y: auto; } + .palette-item { + display: flex; align-items: center; gap: 10px; padding: 10px 16px; cursor: pointer; + } + .palette-item.active { background: color-mix(in srgb, var(--primary-color) 14%, transparent); } + .palette-item ha-icon { --mdc-icon-size: 20px; color: var(--secondary-text-color); flex: none; } + .palette-label { font-weight: 500; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } + .palette-sub { margin-left: auto; color: var(--secondary-text-color); font-size: 12.5px; flex: none; padding-left: 10px; } + .palette-empty { padding: 20px 16px; color: var(--secondary-text-color); text-align: center; } + .palette-hint { padding: 8px 16px; font-size: 12px; color: var(--secondary-text-color); border-top: 1px solid var(--divider-color); } + + /* Template gallery. */ + .template-gallery { + width: min(720px, 94vw); max-height: 80vh; display: flex; flex-direction: column; + background: var(--card-background-color, #fff); border-radius: 12px; overflow: hidden; + box-shadow: 0 12px 48px rgba(0,0,0,.4); + } + .template-gallery-head { + display: flex; align-items: center; justify-content: space-between; + padding: 12px 8px 12px 18px; font-weight: 600; font-size: 16px; + border-bottom: 1px solid var(--divider-color); + } + .template-gallery-body { overflow-y: auto; padding: 8px 16px 16px; } + .template-cat { margin-top: 12px; } + .template-cat-head { + display: flex; align-items: center; gap: 8px; font-weight: 600; font-size: 13px; + color: var(--secondary-text-color); margin-bottom: 8px; + } + .template-cat-head ha-icon { --mdc-icon-size: 20px; } + .template-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 10px; } + .template-card { + display: flex; flex-direction: column; gap: 4px; text-align: left; cursor: pointer; + padding: 12px 14px; border: 1px solid var(--divider-color); border-radius: 10px; + background: var(--card-background-color); font: inherit; color: var(--primary-text-color); + } + .template-card:hover { border-color: var(--primary-color); background: color-mix(in srgb, var(--primary-color) 6%, transparent); } + .template-card[disabled] { opacity: .5; pointer-events: none; } + .template-card-name { font-weight: 600; font-size: 14px; } + .template-card-count { font-size: 12px; color: var(--secondary-text-color); } + .empty-onboard-hint { color: var(--secondary-text-color); font-size: 13px; margin: 4px 0 12px; } + .empty-onboard-actions { display: flex; gap: 8px; flex-wrap: wrap; justify-content: center; } + + .task-row:hover { + background: var(--table-row-alternative-background-color, rgba(0, 0, 0, 0.04)); + } + + /* Wrapper for status + optional disabled/NFC badges so they share one grid column */ + .cell-badges { + display: flex; + align-items: center; + gap: 6px; + } + + .cell { font-size: 14px; } + .cell.object-name { color: var(--primary-color); cursor: pointer; } + .cell.task-name { font-weight: 500; } + .cell.type { color: var(--secondary-text-color); } + + /* Task subline chips (group / area / assigned user) — desktop shows inline, mobile wraps below */ + .task-sub { + display: flex; + gap: 6px; + align-items: center; + font-size: 12px; + color: var(--secondary-text-color); + flex-wrap: wrap; + justify-content: flex-end; + } + /* Empty subline still occupies its grid slot so neighbouring columns line up */ + .task-sub-empty { min-height: 1px; } + .sub-chip { + display: inline-flex; + align-items: center; + gap: 4px; + white-space: nowrap; + padding: 2px 8px; + border-radius: 10px; + background: var(--secondary-background-color, rgba(127, 127, 127, 0.1)); + line-height: 1.4; + } + .sub-chip ha-icon { + --mdc-icon-size: 14px; + opacity: 0.75; + } + + /* Row action buttons (Complete / Skip): right-aligned in their column and a + bit larger — the default mwc glyph reads small inside its padded button. */ + .row-actions { + display: flex; + align-items: center; + justify-content: flex-end; + gap: 2px; + } + .row-actions mwc-icon-button { + --mdc-icon-button-size: 44px; + --mdc-icon-size: 26px; + } + + .detail-section { padding: 16px 0; } + + .detail-header { + display: flex; + justify-content: space-between; + align-items: center; + flex-wrap: wrap; + gap: 8px; + } + .detail-header h2 { margin: 0; font-size: 22px; } + h3 { margin: 16px 0 8px; font-size: 16px; font-weight: 500; } + .meta { color: var(--secondary-text-color); margin: 4px 0; } + /* v1.4.10 (#46): per-object free-form notes block */ + .object-notes { + margin: 12px 0 4px; + padding: 12px 14px; + background: var(--card-background-color, var(--ha-card-background, #1c1c1c)); + border-left: 3px solid var(--primary-color, #03a9f4); + border-radius: 4px; + } + .object-notes-label { + font-size: 12px; + font-weight: 500; + color: var(--secondary-text-color); + text-transform: uppercase; + letter-spacing: 0.4px; + margin-bottom: 6px; + } + .object-notes-body { + color: var(--primary-text-color); + white-space: pre-wrap; + word-break: break-word; + line-height: 1.45; + } + .empty { color: var(--secondary-text-color); font-style: italic; } + .analysis-empty-state { text-align: center; padding: 24px 16px; } + .analysis-empty-state .empty { font-size: 15px; margin-bottom: 8px; } + .analysis-empty-state .empty-icon { + --mdc-icon-size: 48px; + color: var(--secondary-text-color); + opacity: 0.4; + display: block; + margin: 0 auto 12px; + } + .empty-hint { color: var(--secondary-text-color); font-size: 13px; margin: 4px 0; } + .analysis-progress { + width: 120px; margin: 12px auto 4px; height: 6px; + background: var(--divider-color, #e0e0e0); border-radius: 3px; overflow: hidden; + } + .analysis-progress-bar { + height: 100%; background: var(--primary-color); border-radius: 3px; + } + + .info-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + gap: 8px; + } + + .info-item { + display: flex; + flex-direction: column; + padding: 8px; + background: var(--card-background-color, #fff); + border-radius: 8px; + } + + .info-item .label { + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 2px; + } + + /* Dashboard redesign styles */ + + .task-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 8px; + margin-bottom: 16px; + gap: 12px; + flex-wrap: wrap; + } + + .task-header-title { + display: flex; + align-items: center; + gap: 8px; + flex: 1; + } + + .task-name-breadcrumb, + .object-name-breadcrumb { + cursor: pointer; + color: var(--primary-text-color); + text-decoration: none; + } + + .task-name-breadcrumb:hover, + .object-name-breadcrumb:hover { + text-decoration: underline; + } + + .breadcrumb-separator { + color: var(--secondary-text-color); + margin: 0 4px; + } + + .status-chip { + display: inline-block; + padding: 4px 12px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + } + + .status-chip.ok { + background: #4caf50; + color: white; + } + + .status-chip.warning { + background: #ff9800; + color: white; + } + + .status-chip.overdue { + background: #f44336; + color: white; + } + + .status-chip.done { + background: #78909c; + color: white; + } + + /* (#67) Warranty status chip — object detail meta + objects table */ + .warranty-chip { + display: inline-block; + padding: 2px 8px; + border-radius: 10px; + font-size: 12px; + font-weight: 500; + white-space: nowrap; + } + .warranty-valid { + background: rgba(76, 175, 80, 0.15); + color: var(--success-color, #2e7d32); + } + .warranty-expiring { + background: rgba(255, 152, 0, 0.18); + color: var(--warning-color, #e65100); + } + .warranty-expired { + background: rgba(244, 67, 54, 0.16); + color: var(--error-color, #c62828); + } + .warranty-none { + color: var(--secondary-text-color); + } + + /* (#67) All-Objects view-mode toggle (cards / table) */ + .view-toggle { + display: inline-flex; + border: 1px solid var(--divider-color); + border-radius: 8px; + overflow: hidden; + align-self: end; + } + .view-toggle-btn { + display: inline-flex; + align-items: center; + justify-content: center; + padding: 6px 10px; + background: var(--card-background-color, #fff); + color: var(--secondary-text-color); + border: none; + cursor: pointer; + } + .view-toggle-btn + .view-toggle-btn { border-left: 1px solid var(--divider-color); } + .view-toggle-btn.active { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + } + .view-toggle-btn ha-icon { --mdc-icon-size: 18px; } + + /* (#67) Objects table (desktop All-Objects view) */ + .objects-table-wrap { + overflow-x: auto; + border: 1px solid var(--divider-color); + border-radius: 8px; + } + .objects-table { + width: 100%; + border-collapse: collapse; + font-size: 14px; + } + .objects-table th, + .objects-table td { + text-align: left; + padding: 8px 12px; + border-bottom: 1px solid var(--divider-color); + white-space: nowrap; + } + .objects-table thead th { + font-weight: 600; + color: var(--secondary-text-color); + background: var(--secondary-background-color, rgba(0, 0, 0, 0.03)); + position: sticky; + top: 0; + } + .objects-table tbody tr { cursor: pointer; } + .objects-table tbody tr:hover { + background: var(--secondary-background-color, rgba(0, 0, 0, 0.04)); + } + .objects-table tbody tr:last-child td { border-bottom: none; } + .objects-table-name { font-weight: 500; color: var(--primary-text-color); } + .doc-badge { + display: inline-flex; align-items: center; gap: 2px; vertical-align: middle; + margin-left: 8px; padding: 1px 7px 1px 5px; border-radius: 10px; + font-size: 12px; font-weight: 600; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.06)); + color: var(--secondary-text-color, #888); + } + .doc-badge ha-icon { --mdc-icon-size: 14px; } + /* v2.20 (N3): paused marker on object cards + the detail meta line. */ + .paused-badge { + display: inline-flex; align-items: center; vertical-align: middle; + margin-left: 8px; color: var(--info-color, #2196f3); + } + .paused-badge ha-icon { --mdc-icon-size: 16px; } + .paused-meta { + display: flex; align-items: center; gap: 6px; + color: var(--info-color, #2196f3); font-weight: 500; + } + .paused-meta ha-icon { --mdc-icon-size: 16px; } + .objects-table .oc-notes { + max-width: 220px; + overflow: hidden; + text-overflow: ellipsis; + } + .objects-table .oc-task_count, + .objects-table .oc-actions { text-align: center; } + + .user-badge { + display: inline-flex; + align-items: center; + gap: 3px; + padding: 2px 8px; + margin-left: 8px; + background: var(--primary-color); + color: var(--text-primary-color); + border-radius: 10px; + font-size: 11px; + font-weight: 500; + line-height: 1.4; + } + + .user-badge ha-icon { + --mdc-icon-size: 12px; + } + + .nfc-badge { + display: inline-flex; + align-items: center; + gap: 3px; + padding: 3px 8px; + margin-left: 6px; + background: var(--secondary-background-color, #e8e8e8); + color: var(--primary-text-color); + border-radius: 12px; + font-size: 11px; + font-weight: 500; + } + .priority-badge { + display: inline-flex; + align-items: center; + margin-left: 6px; + border-radius: 12px; + padding: 2px; + } + .priority-badge ha-icon { + --mdc-icon-size: 16px; + } + .priority-high { + color: var(--error-color, #db4437); + } + .priority-low { + color: var(--secondary-text-color, #888); + } + .label-chip { + background: var(--primary-color, #03a9f4); + color: var(--text-primary-color, #fff); + opacity: 0.85; + } + .label-chip ha-icon { + --mdc-icon-size: 13px; + } + .nfc-badge ha-icon { + --mdc-icon-size: 14px; + } + .nfc-badge.unlinked { + opacity: 0.4; + cursor: pointer; + border: 1px dashed var(--divider-color); + background: transparent; + } + .nfc-badge.unlinked:hover { + opacity: 0.7; + } + + .task-header-actions { + display: flex; + gap: 8px; + } + + .more-menu-wrapper { + position: relative; + } + + .popup-menu { + position: absolute; + top: 100%; + right: 0; + background: var(--card-background-color, #fff); + border: 1px solid var(--divider-color); + border-radius: 8px; + box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15); + z-index: 100; + min-width: 180px; + overflow: hidden; + } + + .popup-menu-item { + display: flex; + align-items: center; + gap: 8px; + padding: 10px 16px; + cursor: pointer; + font-size: 14px; + color: var(--primary-text-color); + } + + .popup-menu-item:hover { + background: var(--table-row-alternative-background-color, rgba(0, 0, 0, 0.04)); + } + + .popup-menu-item.danger { + color: var(--error-color, #f44336); + } + + .popup-menu-item ha-icon { + --mdc-icon-size: 18px; + } + + .popup-menu-divider { + height: 1px; + background: var(--divider-color); + margin: 4px 0; + } + + .tab-bar { + display: flex; + gap: 4px; + border-bottom: 2px solid var(--divider-color); + margin-bottom: 16px; + } + + .tab { + padding: 12px 24px; + cursor: pointer; + font-weight: 500; + color: var(--secondary-text-color); + border-bottom: 2px solid transparent; + margin-bottom: -2px; + transition: all 0.2s; + } + + .tab:hover { + color: var(--primary-text-color); + } + + .tab.active { + color: var(--primary-color); + border-bottom-color: var(--primary-color); + } + + .tab-content { + padding: 16px 0; + } + + .kpi-bar { + display: grid; + grid-template-columns: repeat(7, 1fr); + gap: 12px; + margin-bottom: 24px; + } + + .kpi-card { + background: var(--card-background-color, #fff); + border-radius: 8px; + padding: 16px 12px; + text-align: center; + border: 1px solid var(--divider-color); + } + + .kpi-card.warning { + border-color: #ff9800; + background: rgba(255, 152, 0, 0.1); + } + + .kpi-card.overdue { + border-color: #f44336; + background: rgba(244, 67, 54, 0.1); + } + + .kpi-label { + font-size: 11px; + color: var(--secondary-text-color); + margin-bottom: 6px; + text-transform: uppercase; + font-weight: 500; + } + + .kpi-value { + font-size: 16px; + font-weight: 500; + color: var(--primary-text-color); + } + + .kpi-value-large { + font-size: 22px; + font-weight: 600; + color: var(--primary-text-color); + } + + .kpi-subtext { + font-size: 10px; + color: var(--secondary-text-color); + margin-top: 4px; + } + + .two-column-layout { + display: grid; + grid-template-columns: 40% 60%; + gap: 16px; + margin-bottom: 24px; + } + + .two-column-layout.single-column { + grid-template-columns: 1fr; + } + + .left-column, + .right-column { + display: flex; + flex-direction: column; + gap: 16px; + } + + .recent-activities { + background: var(--card-background-color, #fff); + border-radius: 8px; + padding: 16px; + border: 1px solid var(--divider-color); + } + + .recent-activities h3 { + margin: 0 0 12px 0; + } + + .activity-item { + display: flex; + align-items: center; + gap: 12px; + padding: 8px 0; + border-bottom: 1px solid var(--divider-color); + } + + .activity-item:last-of-type { + border-bottom: none; + } + + .activity-icon { + font-size: 18px; + width: 24px; + text-align: center; + } + + .activity-date { + font-size: 12px; + color: var(--secondary-text-color); + min-width: 120px; + } + + .activity-note { + flex: 1; + font-size: 14px; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .activity-badge { + font-size: 12px; + padding: 2px 8px; + background: var(--primary-color); + color: white; + border-radius: 12px; + } + + .activity-show-all { + margin-top: 12px; + text-align: center; + } + + .history-filters-new { + display: flex; + justify-content: space-between; + gap: 12px; + margin-bottom: 16px; + flex-wrap: wrap; + } + + .filter-chips { + display: flex; + gap: 8px; + flex-wrap: wrap; + } + + .filter-controls { + display: flex; + gap: 8px; + } + + .search-input { + padding: 8px 12px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 14px; + min-width: 200px; + } + + .search-input:focus { + outline: none; + border-color: var(--primary-color); + } + + /* Checklist preview card (read-only display in task overview) */ + .checklist-preview-card { + background: var(--card-background-color, #fff); + border-radius: 8px; + padding: 12px 16px; + border: 1px solid var(--divider-color); + margin-top: 8px; + } + .checklist-preview-header { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 500; + color: var(--secondary-text-color); + margin-bottom: 8px; + } + .checklist-preview-header ha-icon { + --mdc-icon-size: 18px; + } + .checklist-preview-list { + margin: 0; + padding-left: 20px; + color: var(--primary-text-color); + font-size: 14px; + line-height: 1.6; + } + .checklist-preview-list li { + padding: 1px 0; + } + + /* Recommendation Card */ + .recommendation-card { + background: var(--card-background-color, #fff); + border-radius: 8px; + padding: 16px; + border: 1px solid var(--divider-color); + } + + .recommendation-card h4 { + margin: 0 0 12px 0; + font-size: 14px; + } + + .interval-comparison { + margin-bottom: 16px; + } + + .interval-bar { + margin-bottom: 12px; + } + + .interval-label { + font-size: 12px; + margin-bottom: 4px; + display: flex; + align-items: center; + gap: 8px; + } + + .interval-visual { + height: 24px; + border-radius: 4px; + transition: width 0.3s; + } + + .interval-visual.current { + background: var(--secondary-text-color); + opacity: 0.5; + } + + .interval-visual.suggested { + background: var(--primary-color); + } + + .confidence-badge { + font-size: 10px; + padding: 2px 6px; + border-radius: 3px; + background: var(--divider-color); + } + + .confidence-badge.high { + background: #4caf50; + color: white; + } + + .confidence-badge.medium { + background: #ff9800; + color: white; + } + + .confidence-badge.low { + background: var(--secondary-text-color); + color: white; + } + + .recommendation-actions { + display: flex; + gap: 8px; + } + + /* Seasonal Card Compact */ + .seasonal-card-compact { + background: var(--card-background-color, #fff); + border-radius: 8px; + padding: 16px; + border: 1px solid var(--divider-color); + } + + .seasonal-card-compact h4 { + margin: 0 0 12px 0; + font-size: 14px; + } + + .seasonal-mini-chart { + display: flex; + align-items: flex-end; + gap: 4px; + height: 60px; + margin-bottom: 12px; + } + + .seasonal-bar { + flex: 1; + border-radius: 2px 2px 0 0; + transition: all 0.2s; + cursor: pointer; + } + + .seasonal-bar.low { + background: #2196f3; + } + + .seasonal-bar.normal { + background: var(--secondary-text-color); + opacity: 0.5; + } + + .seasonal-bar.high { + background: #ff9800; + } + + .seasonal-bar.current { + border: 2px solid var(--primary-color); + box-sizing: border-box; + } + + .seasonal-legend { + display: flex; + gap: 12px; + font-size: 11px; + } + + .legend-item { + display: flex; + align-items: center; + gap: 4px; + } + + .legend-item .dot { + width: 8px; + height: 8px; + border-radius: 50%; + } + + .legend-item .dot.low { + background: #2196f3; + } + + .legend-item .dot.normal { + background: var(--secondary-text-color); + opacity: 0.5; + } + + .legend-item .dot.high { + background: #ff9800; + } + + /* Task meta card (notes + documentation URL) */ + .task-meta-card { + background: var(--card-background-color, #fff); + border: 1px solid var(--divider-color); + border-radius: 12px; + padding: 12px 16px; + margin-bottom: 16px; + display: flex; + flex-direction: column; + gap: 10px; + } + + .task-meta-row { + display: flex; + align-items: flex-start; + gap: 8px; + font-size: 14px; + color: var(--primary-text-color); + } + + .task-meta-row ha-icon { + --mdc-icon-size: 18px; + color: var(--secondary-text-color); + flex-shrink: 0; + margin-top: 2px; + } + + .task-meta-notes { + white-space: pre-wrap; + word-break: break-word; + } + + .task-meta-link a { + color: var(--primary-color); + text-decoration: none; + } + + .task-meta-link a:hover { + text-decoration: underline; + } + + /* ── Responsive: :host([narrow]) (HA sets narrow on mobile/companion) ── */ + + :host([narrow]) .content { + padding: 0 8px 8px; + } + + :host([narrow]) .header { + padding: 8px 12px; + font-size: 14px; + } + + :host([narrow]) .kpi-bar { + grid-template-columns: repeat(2, 1fr); + gap: 8px; + margin-bottom: 16px; + } + + :host([narrow]) .kpi-card { + padding: 12px 8px; + } + + :host([narrow]) .kpi-label { + font-size: 10px; + } + + :host([narrow]) .kpi-value { + font-size: 14px; + } + + :host([narrow]) .kpi-value-large { + font-size: 18px; + } + + :host([narrow]) .two-column-layout { + grid-template-columns: 1fr; + } + + :host([narrow]) .tab { + padding: 12px 16px; + font-size: 14px; + } + + :host([narrow]) .task-header { + flex-direction: column; + align-items: flex-start; + } + + :host([narrow]) .task-header-actions { + width: 100%; + justify-content: flex-start; + } + + :host([narrow]) .filter-bar { + flex-wrap: wrap; + } + + :host([narrow]) .filter-field { + flex: 1; + min-width: 48%; + } + + :host([narrow]) .filter-bar select { + flex: 1; + min-width: 0; + width: 100%; + } + + :host([narrow]) .task-table { display: block; } + + :host([narrow]) .task-row { + /* Mobile: 4-column grid keeps due-cell + actions at deterministic + X-positions across rows regardless of content (sparkline, bar, %). + Earlier flex-wrap-based layouts let the row wrap unpredictably so + "X days" sometimes sat near the middle, sometimes at the right edge. + Grid template: + [badges auto | task-name 1fr | due-cell 100px | actions auto] + Task-name spans the full top row (own row above), chips span the + full bottom row. */ + display: grid; + grid-column: auto; + grid-template-columns: auto minmax(0, 1fr) 100px auto; + grid-template-rows: auto auto auto; + column-gap: 8px; + row-gap: 4px; + padding: 12px; + } + + :host([narrow]) .cell.type { display: none; } + :host([narrow]) .cell.task-name { + grid-column: 1 / -1; + grid-row: 1; + min-width: 0; + } + :host([narrow]) .cell-badges { + grid-column: 1; + grid-row: 2; + } + :host([narrow]) .cell.object-name { + grid-column: 2; + grid-row: 2; + min-width: 0; + /* Cap long object names at 2 lines with ellipsis instead of growing + unbounded vertically. The full name is still readable via the panel + object-detail view (one tap on the object). */ + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + overflow: hidden; + line-height: 1.2; + } + :host([narrow]) .due-cell { + grid-column: 3; + grid-row: 2; + align-items: flex-end; + min-width: 0; + } + :host([narrow]) .row-actions { + grid-column: 4; + grid-row: 2; + } + :host([narrow]) .task-sub { + grid-column: 1 / -1; + grid-row: 3; + font-size: 11px; + gap: 6px; + justify-content: flex-start; + flex-wrap: wrap; + } + :host([narrow]) .task-sub-empty { display: none; } + :host([narrow]) .mini-sparkline { width: 50px; } + + :host([narrow]) .detail-header { + flex-direction: column; + align-items: flex-start; + } + + :host([narrow]) .info-grid { + grid-template-columns: 1fr; + } + + :host([narrow]) .history-filters-new { + flex-direction: column; + } + + :host([narrow]) .search-input { + min-width: 0; + width: 100%; + } + + :host([narrow]) .cost-duration-card { + padding: 12px; + } + + :host([narrow]) .card-header { + flex-direction: column; + align-items: flex-start; + gap: 8px; + } + + :host([narrow]) .toggle-buttons { + width: 100%; + } + + :host([narrow]) .toggle-btn { + flex: 1; + padding: 8px; + font-size: 12px; + } + + :host([narrow]) .activity-item { + flex-wrap: wrap; + } + + :host([narrow]) .activity-date { + min-width: auto; + } + + :host([narrow]) .activity-note { + flex-basis: 100%; + white-space: normal; + } + + :host([narrow]) .popup-menu { + right: auto; + left: 0; + min-width: 160px; + } + + /* Cost/Duration Card with Toggle */ + .cost-duration-card { + background: var(--card-background-color, #fff); + border-radius: 8px; + padding: 16px; + border: 1px solid var(--divider-color); + } + + .card-header { + display: flex; + justify-content: space-between; + align-items: center; + margin-bottom: 16px; + } + + .card-header h3 { + margin: 0; + font-size: 16px; + } + + .toggle-buttons { + display: flex; + gap: 4px; + background: var(--divider-color); + border-radius: 4px; + padding: 2px; + } + + .toggle-btn { + padding: 6px 12px; + border: none; + background: transparent; + color: var(--primary-text-color); + cursor: pointer; + border-radius: 3px; + font-size: 13px; + transition: all 0.2s; + } + + .toggle-btn:hover { + background: rgba(0, 0, 0, 0.05); + } + + .toggle-btn.active { + background: var(--primary-color); + color: white; + } + + /* ── Responsive: @media fallback (when narrow attr not set) ── */ + @media (max-width: 768px) { + .content { padding: 0 8px 8px; } + .header { padding: 8px 12px; font-size: 14px; } + .kpi-bar { grid-template-columns: repeat(2, 1fr); gap: 8px; margin-bottom: 16px; } + .kpi-card { padding: 12px 8px; } + .kpi-label { font-size: 10px; } + .kpi-value { font-size: 14px; } + .kpi-value-large { font-size: 18px; } + .two-column-layout { grid-template-columns: 1fr; } + .tab { padding: 12px 16px; font-size: 14px; } + .task-header { flex-direction: column; align-items: flex-start; } + .task-header-actions { width: 100%; justify-content: flex-start; } + .filter-bar { flex-wrap: wrap; } + .filter-bar select { flex: 1; min-width: 0; } + /* Mirror the :host([narrow]) grid layout for narrow desktop windows */ + .task-row { + display: grid; + grid-template-columns: auto minmax(0, 1fr) 100px auto; + grid-template-rows: auto auto auto; + column-gap: 8px; + row-gap: 4px; + padding: 12px; + } + .cell.type { display: none; } + .cell.task-name { grid-column: 1 / -1; grid-row: 1; min-width: 0; } + .cell-badges { grid-column: 1; grid-row: 2; } + .cell.object-name { grid-column: 2; grid-row: 2; min-width: 0; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; line-height: 1.2; } + .due-cell { grid-column: 3; grid-row: 2; align-items: flex-end; min-width: 0; } + .row-actions { grid-column: 4; grid-row: 2; } + .task-sub { grid-column: 1 / -1; grid-row: 3; font-size: 11px; gap: 6px; justify-content: flex-start; flex-wrap: wrap; } + .task-sub-empty { display: none; } + .mini-sparkline { width: 50px; } + .detail-header { flex-direction: column; align-items: flex-start; } + .info-grid { grid-template-columns: 1fr; } + .history-filters-new { flex-direction: column; } + .search-input { min-width: 0; width: 100%; } + .cost-duration-card { padding: 12px; } + .card-header { flex-direction: column; align-items: flex-start; gap: 8px; } + .toggle-buttons { width: 100%; } + .toggle-btn { flex: 1; padding: 8px; font-size: 12px; } + .activity-item { flex-wrap: wrap; } + .activity-date { min-width: auto; } + .activity-note { flex-basis: 100%; white-space: normal; } + .popup-menu { right: auto; left: 0; min-width: 160px; } + } + + /* ha-button handles variant="danger" natively */ + + .toast { + position: fixed; + bottom: 24px; + left: 50%; + transform: translateX(-50%); + background: var(--error-color, #f44336); + color: #fff; + padding: 12px 24px; + border-radius: 8px; + font-size: 14px; + z-index: 1000; + box-shadow: 0 2px 8px rgba(0,0,0,.3); + animation: toast-in .3s ease; + display: flex; + align-items: center; + gap: 16px; + } + .toast-undo { + font: inherit; font-weight: 600; color: #fff; cursor: pointer; + background: transparent; border: 1px solid rgba(255,255,255,.6); + border-radius: 6px; padding: 4px 12px; text-transform: uppercase; font-size: 12.5px; + } + .toast-undo:hover { background: rgba(255,255,255,.15); } + @keyframes toast-in { + from { opacity: 0; transform: translateX(-50%) translateY(16px); } + to { opacity: 1; transform: translateX(-50%) translateY(0); } + } +`; diff --git a/custom_components/maintenance_supporter/frontend-src/qr_test.mjs b/custom_components/maintenance_supporter/frontend-src/qr_test.mjs new file mode 100644 index 0000000..73399ec --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/qr_test.mjs @@ -0,0 +1,106 @@ +import { chromium } from "playwright"; + +const HA = "http://homeassistant-dev:8123"; +const REFRESH = process.env.HA_REFRESH_TOKEN; +if (!REFRESH) { console.error("Set HA_REFRESH_TOKEN env var"); process.exit(1); } + +const browser = await chromium.connect("ws://localhost:3000"); +const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 }, locale: "de" }); +const page = await ctx.newPage(); + +await page.goto(HA); +await page.waitForTimeout(1000); +await page.evaluate((refresh) => { + localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: "http://homeassistant-dev:8123", + clientId: "http://homeassistant-dev:8123/", + refresh_token: refresh, + access_token: "", + token_type: "Bearer", + expires_in: 1800, + expires: 0, + })); +}, REFRESH); +await page.goto(HA + "/maintenance-supporter"); +await page.waitForTimeout(6000); + +function getPanel() { + return "var ha = document.querySelector('home-assistant'); " + + "var main = ha && ha.shadowRoot && ha.shadowRoot.querySelector('home-assistant-main'); " + + "var drawer = main && main.shadowRoot && main.shadowRoot.querySelector('ha-drawer'); " + + "var resolver = drawer && drawer.querySelector('partial-panel-resolver'); " + + "var custom = resolver && resolver.querySelector('ha-panel-custom'); " + + "var p = custom && custom.querySelector('maintenance-supporter-panel'); "; +} + +var info = await page.evaluate("{ " + getPanel() + + "var result = []; " + + "if (p && p._objects) { " + + " for (var i = 0; i < p._objects.length; i++) { " + + " var obj = p._objects[i]; " + + " var tasks = []; " + + " if (obj.tasks) for (var j = 0; j < obj.tasks.length; j++) tasks.push({ id: obj.tasks[j].id, name: obj.tasks[j].name }); " + + " result.push({ entry_id: obj.entry_id, name: obj.object.name, tasks: tasks }); " + + " } " + + "} " + + "JSON.stringify(result); " + + "}"); +var data = JSON.parse(info); +var entryId = data[0].entry_id; +var taskId = data[0].tasks[0].id; +var objName = data[0].name; +var taskName = data[0].tasks[0].name; + +await page.evaluate("{ " + getPanel() + "if (p) p._showTask('" + entryId + "', '" + taskId + "'); }"); +await page.waitForTimeout(2000); + +await page.evaluate("{ " + getPanel() + + "if (p) { var dlg = p.shadowRoot.querySelector('maintenance-qr-dialog'); " + + "dlg.openForTask('" + entryId + "', '" + taskId + "', '" + objName + "', '" + taskName + "'); } }"); +await page.waitForTimeout(3000); + +await page.screenshot({ path: "screenshots/qr_dialog_view.png" }); +console.log("QR dialog (view) screenshot done"); + +var s1 = await page.evaluate("{ " + getPanel() + + "var dlg = p && p.shadowRoot && p.shadowRoot.querySelector('maintenance-qr-dialog'); " + + "var sr = dlg && dlg.shadowRoot; " + + "var result = { toggleButtons: [], url: '', error: '' }; " + + "if (sr) { " + + " var btns = sr.querySelectorAll('.toggle-btn'); " + + " for (var i = 0; i < btns.length; i++) result.toggleButtons.push({ text: btns[i].textContent.trim(), active: btns[i].classList.contains('active') }); " + + " var urlEl = sr.querySelector('.url-display'); " + + " if (urlEl) result.url = urlEl.textContent.trim(); " + + " var errEl = sr.querySelector('.error'); " + + " if (errEl) result.error = errEl.textContent.trim(); " + + "} " + + "JSON.stringify(result); }"); +console.log("State (view):", s1); + +await page.evaluate("{ " + getPanel() + + "var dlg = p && p.shadowRoot && p.shadowRoot.querySelector('maintenance-qr-dialog'); " + + "var sr = dlg && dlg.shadowRoot; " + + "var btns = sr && sr.querySelectorAll('.toggle-btn'); " + + "if (btns && btns.length > 1) btns[1].click(); }"); +await page.waitForTimeout(3000); + +await page.screenshot({ path: "screenshots/qr_dialog_complete.png" }); + +var s2 = await page.evaluate("{ " + getPanel() + + "var dlg = p && p.shadowRoot && p.shadowRoot.querySelector('maintenance-qr-dialog'); " + + "var sr = dlg && dlg.shadowRoot; " + + "var result = { toggleButtons: [], url: '', error: '' }; " + + "if (sr) { " + + " var btns = sr.querySelectorAll('.toggle-btn'); " + + " for (var i = 0; i < btns.length; i++) result.toggleButtons.push({ text: btns[i].textContent.trim(), active: btns[i].classList.contains('active') }); " + + " var urlEl = sr.querySelector('.url-display'); " + + " if (urlEl) result.url = urlEl.textContent.trim(); " + + " var errEl = sr.querySelector('.error'); " + + " if (errEl) result.error = errEl.textContent.trim(); " + + "} " + + "JSON.stringify(result); }"); +console.log("State (complete):", s2); + +await ctx.close(); +await browser.close(); +console.log("Done!"); diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/chart-utils.ts b/custom_components/maintenance_supporter/frontend-src/renderers/chart-utils.ts new file mode 100644 index 0000000..b5223f7 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/renderers/chart-utils.ts @@ -0,0 +1,90 @@ +/** Shared chart helpers: nice axis ticks and consistent number/date formats. */ + +/** Round axis ticks covering [min, max] — the classic "nice numbers" loop. + * Returns the tick values plus the (possibly widened) nice domain, so the + * chart can plot against the same bounds the labels describe. */ +export function niceTicks( + min: number, + max: number, + targetCount = 4, +): { ticks: number[]; niceMin: number; niceMax: number } { + if (!isFinite(min) || !isFinite(max)) return { ticks: [], niceMin: 0, niceMax: 1 }; + if (min === max) { + // Degenerate domain: pad around the value so the line isn't glued to an edge. + const pad = Math.abs(min) * 0.1 || 1; + min -= pad; + max += pad; + } + const span = max - min; + const step0 = Math.pow(10, Math.floor(Math.log10(span / Math.max(1, targetCount)))); + let step = step0; + for (const m of [1, 2, 5, 10]) { + step = step0 * m; + if (span / step <= targetCount + 0.5) break; + } + const niceMin = Math.floor(min / step) * step; + const niceMax = Math.ceil(max / step) * step; + const ticks: number[] = []; + // Epsilon guards float drift (0.30000000000000004 style) at tick boundaries. + for (let v = niceMin; v <= niceMax + step * 1e-6; v += step) { + ticks.push(Math.abs(v) < step * 1e-9 ? 0 : v); + } + return { ticks, niceMin, niceMax }; +} + +/** Compact, consistent number format: 1.2M / 88k / 730 / 7.5 / 0.42 */ +export function fmtNum(v: number): string { + const a = Math.abs(v); + if (a >= 1_000_000) return trimZero((v / 1_000_000).toFixed(a >= 10_000_000 ? 0 : 1)) + "M"; + if (a >= 10_000) return trimZero((v / 1000).toFixed(0)) + "k"; + if (a >= 1000) return trimZero((v / 1000).toFixed(1)) + "k"; + if (a >= 100) return v.toFixed(0); + if (a >= 10) return trimZero(v.toFixed(1)); + if (a >= 1) return trimZero(v.toFixed(1)); + if (a === 0) return "0"; + return trimZero(v.toFixed(2)); +} + +function trimZero(s: string): string { + return s.replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1"); +} + +/** Full-precision value + unit for tooltips (locale-aware grouping). */ +export function fmtVal(v: number, unit: string, lang: string): string { + const s = v.toLocaleString(lang, { maximumFractionDigits: Math.abs(v) >= 100 ? 0 : 1 }); + return unit ? `${s} ${unit}` : s; +} + +/** Short date tick: "3. Juli" / "Jul 3", with year appended when the plotted + * range spans more than ~10 months (disambiguates "Jun … Feb" sequences). */ +export function fmtDateTick(ts: number, lang: string, withYear: boolean): string { + const d = new Date(ts); + const opts: Intl.DateTimeFormatOptions = withYear + ? { month: "short", day: "numeric", year: "2-digit" } + : { month: "short", day: "numeric" }; + return d.toLocaleDateString(lang, opts); +} + +/** Date+time for crosshair/tooltip labels. */ +export function fmtDateTime(ts: number, lang: string): string { + return new Date(ts).toLocaleDateString(lang, { + month: "short", + day: "numeric", + hour: "2-digit", + minute: "2-digit", + }); +} + +/** Whether date ticks need the year: the range crosses a calendar-year + * boundary (a "Jun … Feb" sequence would otherwise read backwards). */ +export function needsYear(tsMin: number, tsMax: number): boolean { + return new Date(tsMin).getFullYear() !== new Date(tsMax).getFullYear(); +} + +/** Evenly-spaced x-axis tick timestamps (first/last inclusive). */ +export function timeTicks(tsMin: number, tsMax: number, count: number): number[] { + if (count < 2 || tsMax <= tsMin) return [tsMin, tsMax]; + const out: number[] = []; + for (let i = 0; i < count; i++) out.push(tsMin + ((tsMax - tsMin) * i) / (count - 1)); + return out; +} diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/charts.ts b/custom_components/maintenance_supporter/frontend-src/renderers/charts.ts new file mode 100644 index 0000000..cbffe7a --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/renderers/charts.ts @@ -0,0 +1,149 @@ +/** Cost/duration chart renderers (task detail). + * + * Completions are plotted on a true time axis (a completion 8 months ago sits + * visibly further away than one from last week), with round y-ticks per axis + * and year-aware date labels once the span crosses years — "Jun 3 '25" cannot + * be misread as coming after "Feb 21 '26". + */ + +import { html, svg, nothing } from "lit"; +import { t } from "../styles"; +import { niceTicks, fmtNum, fmtDateTick, timeTicks, needsYear } from "./chart-utils"; +import type { MaintenanceTask } from "../types"; + +const COST_CHART_H = 200; +const PAD_T = 10; +const PAD_B = 22; + +export function renderCostDurationCard( + task: MaintenanceTask, + lang: string, + toggle: "cost" | "duration" | "both", + setToggle: (val: "cost" | "duration" | "both") => void, +) { + const completedEntries = task.history.filter((h) => h.type === "completed" && (h.cost != null || h.duration != null)); + if (completedEntries.length < 2) return nothing; + + const anyCost = completedEntries.some((h) => (h.cost ?? 0) > 0); + const anyDuration = completedEntries.some((h) => (h.duration ?? 0) > 0); + if (!anyCost && !anyDuration) return nothing; + + return html` +
    +
    +

    ${t("cost_duration_chart", lang)}

    +
    + ${anyCost ? html`` : nothing} + ${anyCost && anyDuration ? html`` : nothing} + ${anyDuration ? html`` : nothing} +
    +
    + ${renderHistoryChart(task, lang, toggle)} +
    + `; +} + +function renderHistoryChart(task: MaintenanceTask, lang: string, toggle: "cost" | "duration" | "both") { + const entries = task.history + .filter((h) => h.type === "completed" && (h.cost != null || h.duration != null)) + .map((h) => ({ ts: new Date(h.timestamp).getTime(), cost: h.cost ?? 0, duration: h.duration ?? 0 })) + .sort((a, b) => a.ts - b.ts); + + if (entries.length < 2) return nothing; + + const dataCost = entries.some((e) => e.cost > 0); + const dataDuration = entries.some((e) => e.duration > 0); + if (!dataCost && !dataDuration) return nothing; + + const hasCost = toggle !== "duration" && dataCost; + const hasDuration = toggle !== "cost" && dataDuration; + const showCost = hasCost || (!hasDuration && dataCost); + const showDuration = hasDuration || (!hasCost && dataDuration); + + const W = 640; // wide viewBox; the container scales it to full card width + const H = COST_CHART_H; + const PAD_L = showCost ? 44 : 12; + const PAD_R = showDuration ? 44 : 12; + const plotW = W - PAD_L - PAD_R; + const plotB = H - PAD_B; + const plotH = plotB - PAD_T; + + // True time axis with a padded domain so edge bars don't clip. + const tsMin = entries[0].ts; + const tsMax = entries[entries.length - 1].ts; + const tsPad = (tsMax - tsMin || 86400000) * 0.05; + const t0 = tsMin - tsPad; + const t1 = tsMax + tsPad; + const withYear = needsYear(tsMin, tsMax); + const toX = (ts: number) => PAD_L + ((ts - t0) / (t1 - t0)) * plotW; + + const costAxis = niceTicks(0, Math.max(...entries.map((e) => e.cost)) || 1, 3); + const durAxis = niceTicks(0, Math.max(...entries.map((e) => e.duration)) || 1, 3); + const costY = (v: number) => PAD_T + (1 - v / (costAxis.niceMax || 1)) * plotH; + const durY = (v: number) => PAD_T + (1 - v / (durAxis.niceMax || 1)) * plotH; + + // Bars keep a readable width even when completions crowd together. + const minGap = entries.length > 1 + ? Math.min(...entries.slice(1).map((e, i) => toX(e.ts) - toX(entries[i].ts))) + : plotW; + const barW = Math.max(6, Math.min(22, minGap * 0.55)); + + const xTicks = timeTicks(tsMin, tsMax, Math.max(2, Math.min(4, entries.length))); + + return html` +
    + + ${showCost ? costAxis.ticks.map((v) => { + const y = costY(v); + if (y < PAD_T - 1 || y > plotB + 1) return nothing; + return svg` + + ${fmtNum(v)}€`; + }) : nothing} + ${showDuration ? durAxis.ticks.map((v) => { + const y = durY(v); + if (y < PAD_T - 1 || y > plotB + 1) return nothing; + return svg`${fmtNum(v)}m`; + }) : nothing} + + ${showCost ? entries.filter((e) => e.cost > 0).map((e) => svg` + + ${fmtDateTick(e.ts, lang, true)}: ${e.cost.toLocaleString(lang)}€${e.duration ? ` · ${e.duration}m` : ""} + + `) : nothing} + ${showDuration ? svg` + + ${entries.map((e) => svg` + + ${fmtDateTick(e.ts, lang, true)}: ${e.duration}m${e.cost ? ` · ${e.cost.toLocaleString(lang)}€` : ""} + + `)} + ` : nothing} + + + ${xTicks.map((ts, i) => { + const anchor = i === 0 ? "start" : i === xTicks.length - 1 ? "end" : "middle"; + return svg`${fmtDateTick(ts, lang, withYear)}`; + })} + +
    +
    + ${showCost ? html`${t("cost", lang)}` : nothing} + ${showDuration ? html`${t("duration", lang)}` : nothing} +
    + `; +} diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/history.ts b/custom_components/maintenance_supporter/frontend-src/renderers/history.ts new file mode 100644 index 0000000..c65eb22 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/renderers/history.ts @@ -0,0 +1,122 @@ +/** Task history sub-view: filter chips + search + the timeline of entries. + * + * Extracted from the panel (renderers/ pattern). State the panel owns — + * the active filter, the search text, the currency symbol — plus the + * callbacks that mutate it or open the edit dialog are passed in via + * HistoryContext, keeping these functions pure of component internals. + */ + +import { html, nothing } from "lit"; +import { t, formatDateTime, STATUS_ICONS } from "../styles"; +import type { MaintenanceTask, HistoryEntry, HomeAssistant } from "../types"; +import "../components/history-photo"; + +export interface HistoryContext { + lang: string; + /** Home Assistant object — used to sign completion-photo URLs. */ + hass: HomeAssistant; + /** Active type filter, or null for "all". */ + filter: string | null; + /** Free-text search over entry notes. */ + search: string; + /** Currency symbol for cost display (defaults to €). */ + currencySymbol: string; + setFilter: (filter: string | null) => void; + setSearch: (search: string) => void; + openEdit: (entry: HistoryEntry) => void; + /** v2.20 (#83): unit + delta-vs-previous for reading-task entries. */ + readingUnit?: string | null; + readingDelta?: (entry: HistoryEntry) => number | null; +} + +const _FILTER_TYPES = ["completed", "skipped", "missed", "reset", "triggered"] as const; + +export function renderHistoryFilters(task: MaintenanceTask, ctx: HistoryContext) { + const L = ctx.lang; + return html` +
    +
    + ${_FILTER_TYPES.map((type) => { + const count = task.history.filter((h) => h.type === type).length; + if (count === 0) return nothing; + return html` + ctx.setFilter(ctx.filter === type ? null : type)}> + ${t(type, L)} (${count}) + + `; + })} + ${ctx.filter ? html` ctx.setFilter(null)}>${t("show_all", L)}` : nothing} +
    +
    + ctx.setSearch((e.target as HTMLInputElement).value)} /> +
    +
    + `; +} + +export function renderHistoryList(task: MaintenanceTask, ctx: HistoryContext) { + const L = ctx.lang; + let filtered = ctx.filter + ? task.history.filter((h) => h.type === ctx.filter) + : task.history; + + // Apply search filter + if (ctx.search) { + const search = ctx.search.toLowerCase(); + filtered = filtered.filter((h) => h.notes?.toLowerCase().includes(search)); + } + + if (filtered.length === 0) { + return html`

    ${t("no_history", L)}

    `; + } + + return html` +
    + ${[...filtered].reverse().map((entry: HistoryEntry) => renderHistoryEntry(entry, ctx))} +
    + `; +} + +export function renderHistoryEntry(entry: HistoryEntry, ctx: HistoryContext) { + const L = ctx.lang; + // v2.2.0: only "lifecycle + cost-bearing" entries are user-editable. + // Triggers are auto-generated by sensors and shouldn't be retroactively + // rewritten. Allow edits for completed / reset / skipped entries. + const editable = ["completed", "reset", "skipped"].includes(entry.type); + return html` +
    +
    + +
    +
    +
    + ${t(entry.type, L)} + ${editable + ? html`` + : nothing} +
    +
    ${formatDateTime(entry.timestamp, L)}
    + ${entry.notes ? html`
    ${entry.notes}
    ` : nothing} + ${entry.photo_doc_id + ? html`` + : nothing} +
    + ${entry.cost != null ? html`${t("cost", L)}: ${entry.cost.toFixed(2)} ${ctx.currencySymbol}` : nothing} + ${entry.duration != null ? html`${t("duration", L)}: ${entry.duration} min` : nothing} + ${entry.trigger_value != null ? html`${t("trigger_val", L)}: ${entry.trigger_value}` : nothing} + ${entry.reading_value != null + ? html`${t("reading_label", L)}: ${entry.reading_value}${ctx.readingUnit ? ` ${ctx.readingUnit}` : ""}${(() => { + const d = ctx.readingDelta?.(entry); + return d == null ? "" : ` (${d >= 0 ? "+" : ""}${Number(d.toFixed(3))})`; + })()}` + : nothing} +
    +
    +
    + `; +} diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/prediction.ts b/custom_components/maintenance_supporter/frontend-src/renderers/prediction.ts new file mode 100644 index 0000000..3a5b2d7 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/renderers/prediction.ts @@ -0,0 +1,60 @@ +/** Sensor prediction section renderer. */ + +import { html, nothing } from "lit"; +import { t, formatDate, fireMoreInfo } from "../styles"; +import type { MaintenanceTask, AdvancedFeatures } from "../types"; + +export function renderPredictionSection(task: MaintenanceTask, lang: string, features: AdvancedFeatures) { + const hasDegradation = task.degradation_trend != null && task.degradation_trend !== "insufficient_data"; + const hasThreshold = task.days_until_threshold != null; + const hasEnv = task.environmental_factor != null && task.environmental_factor !== 1.0; + if (!hasDegradation && !hasThreshold && !hasEnv) return nothing; + + const trendIcon = task.degradation_trend === "rising" + ? "M16,6L18.29,8.29L13.41,13.17L9.41,9.17L2,16.59L3.41,18L9.41,12L13.41,16L19.71,9.71L22,12V6H16Z" + : task.degradation_trend === "falling" + ? "M16,18L18.29,15.71L13.41,10.83L9.41,14.83L2,7.41L3.41,6L9.41,12L13.41,8L19.71,14.29L22,12V18H16Z" + : "M22,12L18,8V11H3V13H18V16L22,12Z"; + + return html` +
    + ${task.sensor_prediction_urgency ? html` +
    + + ${t("sensor_prediction_urgency", lang).replace("{days}", String(Math.round(task.days_until_threshold || 0)))} +
    + ` : nothing} +
    + + ${t("sensor_prediction", lang)} +
    +
    + ${hasDegradation ? html` +
    + + ${t("degradation_trend", lang)} + ${t("trend_" + task.degradation_trend, lang)} + ${task.degradation_rate != null ? html`${task.degradation_rate > 0 ? "+" : ""}${Math.abs(task.degradation_rate) >= 10 ? Math.round(task.degradation_rate).toLocaleString() : task.degradation_rate.toFixed(1)} ${task.trigger_entity_info?.unit_of_measurement || ""}/${t("day_short", lang)}` : nothing} +
    + ` : nothing} + ${hasThreshold ? html` +
    + + ${t("days_until_threshold", lang)} + ${task.days_until_threshold === 0 ? t("threshold_exceeded", lang) : "~" + Math.round(task.days_until_threshold!) + " " + t("days", lang)} + ${task.threshold_prediction_date ? html`${formatDate(task.threshold_prediction_date, lang)}` : nothing} + ${task.threshold_prediction_confidence ? html`` : nothing} +
    + ` : nothing} + ${hasEnv && features.environmental ? html` +
    + + ${t("environmental_adjustment", lang)} + ${task.environmental_factor!.toFixed(2)}x + ${task.environmental_entity ? html` fireMoreInfo(ev, task.environmental_entity!)}>${task.environmental_entity}` : nothing} +
    + ` : nothing} +
    +
    + `; +} diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/progress.ts b/custom_components/maintenance_supporter/frontend-src/renderers/progress.ts new file mode 100644 index 0000000..4f0676a --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/renderers/progress.ts @@ -0,0 +1,189 @@ +/** Progress renderers shared by the dashboard rows, the object-detail view and + * the task-detail overview tab. + * + * These are pure of component state — everything they need is passed in — so + * they live outside the panel (which keeps the 2.5k-line panel focused on + * routing/data) yet still render into its shadow root, where the CSS lives. + * + * - renderTriggerProgress: a "current / target" bar for a sensor trigger + * (threshold / counter / state_change / runtime / compound). + * - renderMiniSparkline: a tiny 60x20 trend line for overview rows. + * - renderDaysProgress: the detailed last→next due bar for the detail view. + */ + +import { html, nothing } from "lit"; +import { t, formatDate, formatDueDays } from "../styles"; +import { daysProgress } from "../helpers/interval"; +import type { MaintenanceTask, TaskRow, StatisticsPoint } from "../types"; + +const MINI_SPARKLINE_W = 60; +const MINI_SPARKLINE_H = 20; +const MAX_MINI_POINTS = 30; + +export function renderTriggerProgress(row: TaskRow | MaintenanceTask) { + const tc = row.trigger_config ?? null; + if (!tc) return nothing; + + const triggerType = tc.type || "threshold"; + const unit = row.trigger_entity_info?.unit_of_measurement ?? ""; + + let pct = 0; + let label = ""; + + if (triggerType === "threshold") { + const val = row.trigger_current_value ?? null; + if (val == null) return nothing; + const above = tc.trigger_above; + const below = tc.trigger_below; + if (above != null) { + // Progress toward upper limit + const low = below ?? 0; + const range = above - low || 1; + pct = Math.min(100, Math.max(0, ((val - low) / range) * 100)); + label = `${val.toFixed(1)} / ${above} ${unit}`; + } else if (below != null) { + // Progress toward lower limit (inverted: lower is worse) + // Use entity max, or 2x the threshold as a stable "safe" reference. + // Using val*2 caused a dynamic ceiling that distorted the bar. + const entityMax = row.trigger_entity_info?.max; + const high = entityMax ?? ((below * 2) || 100); + const range = high - below || 1; + pct = Math.min(100, Math.max(0, ((high - val) / range) * 100)); + label = `${val.toFixed(1)} / ${below} ${unit}`; + } else { + return nothing; + } + } else if (triggerType === "counter") { + const target = tc.trigger_target_value || 1; + // Use delta if available, otherwise current value + const delta = row.trigger_current_delta ?? null; + const val = delta ?? (row.trigger_current_value ?? null); + if (val == null) return nothing; + pct = Math.min(100, Math.max(0, (val / target) * 100)); + label = `${val.toFixed(1)} / ${target} ${unit}`; + } else if (triggerType === "state_change") { + const target = tc.trigger_target_changes || 1; + const val = row.trigger_current_value ?? null; + if (val == null) return nothing; + pct = Math.min(100, Math.max(0, (val / target) * 100)); + label = `${Math.round(val)} / ${target}`; + } else if (triggerType === "runtime") { + const target = tc.trigger_runtime_hours || 100; + const val = row.trigger_current_value ?? null; + if (val == null) return nothing; + pct = Math.min(100, Math.max(0, (val / target) * 100)); + label = `${val.toFixed(1)}h / ${target}h`; + } else if (triggerType === "compound") { + const logic = tc.compound_logic || (tc as any).operator || "AND"; + const condCount = tc.conditions?.length || 0; + label = `${logic} (${condCount})`; + pct = row.trigger_active ? 100 : 0; + } else { + return nothing; + } + + const triggerOverflow = pct >= 100; + const barColor = pct > 90 ? "var(--error-color, #f44336)" + : pct > 70 ? "var(--warning-color, #ff9800)" + : "var(--primary-color)"; + + return html` +
    +
    +
    +
    + ${label} +
    + `; +} + +/** A mini sparkline for overview rows (tiny trend line). */ +export function renderMiniSparkline( + row: TaskRow | MaintenanceTask, + miniStatsData: Map, + lang: string, +) { + if (!row.trigger_config?.entity_id) return nothing; + const entityId = row.trigger_config.entity_id; + + // PRIMARY: HA recorder statistics (daily, last 14 days) + const statsPoints = miniStatsData.get(entityId) || []; + + let points: { ts: number; val: number }[] = []; + + if (statsPoints.length >= 2) { + points = statsPoints.map((p) => ({ ts: p.ts, val: p.val })); + } else { + // FALLBACK: original behavior from task history + if (!row.history) return nothing; + for (const h of row.history) { + if (h.trigger_value != null) { + points.push({ ts: new Date(h.timestamp).getTime(), val: h.trigger_value }); + } + } + } + + if (row.trigger_current_value != null) { + points.push({ ts: Date.now(), val: row.trigger_current_value }); + } + if (points.length < 2) return nothing; + points.sort((a, b) => a.ts - b.ts); + + const W = MINI_SPARKLINE_W, H = MINI_SPARKLINE_H; + const vals = points.map((p) => p.val); + let minV = Math.min(...vals), maxV = Math.max(...vals); + const range = maxV - minV || 1; + minV -= range * 0.1; maxV += range * 0.1; + const tsMin = points[0].ts, tsMax = points[points.length - 1].ts; + const tsR = tsMax - tsMin || 1; + + const toX = (ts: number) => ((ts - tsMin) / tsR) * W; + const toY = (v: number) => 2 + (1 - (v - minV) / (maxV - minV)) * (H - 4); + + // Downsample for tiny SVG + let renderPoints = points; + if (renderPoints.length > MAX_MINI_POINTS) { + const step = Math.ceil(renderPoints.length / MAX_MINI_POINTS); + renderPoints = renderPoints.filter((_, i) => i % step === 0 || i === renderPoints.length - 1); + } + + const pts = renderPoints.map((p) => `${toX(p.ts).toFixed(1)},${toY(p.val).toFixed(1)}`).join(" "); + // Match the detail chart's semantics: an actively-triggered sensor tints red. + const stroke = row.trigger_active + ? "var(--error-color, #f44336)" + : "var(--primary-color)"; + + return html` + + + + `; +} + +/** A detailed days-progress bar (last performed → next due) for the detail view. */ +export function renderDaysProgress(task: MaintenanceTask, lang: string) { + const L = lang; + if (task.days_until_due == null || !task.interval_days || task.interval_days <= 0) return nothing; + + // Unit-aware (issue #59): a "1 year" task has span ≈365 d, not 1 d. + const { pct, overflow: daysOverflow } = daysProgress( + task.interval_days, task.days_until_due, task.interval_unit, + ); + + let barColor = "var(--success-color, #4caf50)"; + if (task.status === "overdue") barColor = "var(--error-color, #f44336)"; + else if (task.status === "due_soon") barColor = "var(--warning-color, #ff9800)"; + + return html` +
    +
    + ${task.last_performed ? `${t("last_performed", L)}: ${formatDate(task.last_performed, L)}` : ""} + ${task.next_due ? `${t("next_due", L)}: ${formatDate(task.next_due, L)}` : ""} +
    +
    +
    +
    +
    ${formatDueDays(task.days_until_due, L)}
    +
    + `; +} diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/recommendation.ts b/custom_components/maintenance_supporter/frontend-src/renderers/recommendation.ts new file mode 100644 index 0000000..3e03f52 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/renderers/recommendation.ts @@ -0,0 +1,49 @@ +/** Shared recommendation-card visuals (bars + confidence badge). + * + * The visual layout is shared between the panel's task-detail page + * (maintenance-panel._renderRecommendationCard) and the Lovelace + * task-quick-actions dialog (task-quick-actions-dialog._renderRecommendation). + * + * The two contexts disagree on: + * - Whether ha-button or native + ${collapsed ? nothing : html`
    ${body}
    `} + + `; +} + +/** Read-only preview of the configured checklist steps so users can see + * the steps without having to open the Edit or Complete dialog. Only + * rendered when the Checklists feature is enabled and steps are set. */ +function renderChecklistCard(task: MaintenanceTask, ctx: TaskDetailContext) { + if (!ctx.features.checklists) return nothing; + const items = task.checklist || []; + if (items.length === 0) return nothing; + const L = ctx.lang; + return html` +
    +
    + + ${t("checklist", L)} (${items.length}) +
    +
      + ${items.map((item) => html`
    1. ${item}
    2. `)} +
    +
    + `; +} + +/** Task notes, task documentation URL, and (since v1.4.1) the parent object's + * documentation_url for quick access to the device manual without having to + * navigate back to the object detail. */ +function renderTaskMeta(task: MaintenanceTask, ctx: TaskDetailContext) { + const safeTaskUrl = task.documentation_url && /^https?:\/\//i.test(task.documentation_url) + ? task.documentation_url : null; + const safeObjUrl = ctx.objectDocUrl && /^https?:\/\//i.test(ctx.objectDocUrl) ? ctx.objectDocUrl : null; + if (!task.notes && !safeTaskUrl && !safeObjUrl) return nothing; + const L = ctx.lang; + return html` +
    + ${task.notes ? html` +
    + + ${task.notes} +
    + ` : nothing} + ${safeTaskUrl ? html` + + ` : nothing} + ${safeObjUrl ? html` + + ` : nothing} +
    + `; +} + +/** KPI bar with 7 cards. */ +function renderKPIBar(task: MaintenanceTask, ctx: TaskDetailContext) { + const L = ctx.lang; + const avgCost = task.times_performed > 0 ? task.total_cost / task.times_performed : 0; + const daysClass = task.days_until_due !== null && task.days_until_due !== undefined + ? (task.days_until_due < 0 ? "overdue" : (task.days_until_due <= task.warning_days ? "warning" : "")) + : ""; + + return html` +
    +
    +
    ${t("next_due", L)}
    +
    ${task.next_due ? formatDate(task.next_due, L) : "—"}
    + ${ctx.features.schedule_time && task.schedule_time + ? html`
    ${t("at_time", L)} ${task.schedule_time}
    ` + : nothing} +
    +
    +
    ${t("days_until_due", L)}
    +
    ${task.days_until_due !== null && task.days_until_due !== undefined ? task.days_until_due : "—"}
    +
    +
    +
    ${t("interval", L)}
    +
    ${formatRecurrence(task, L)}
    + ${ctx.features.adaptive && task.suggested_interval && task.suggested_interval !== task.interval_days ? html` +
    ${t("recommended", L)}: ${task.suggested_interval}${task.interval_analysis?.confidence_interval_low != null ? ` (${task.interval_analysis.confidence_interval_low}–${task.interval_analysis.confidence_interval_high})` : ""}
    + ` : nothing} +
    +
    +
    ${t("warning", L)}
    +
    ${task.warning_days} ${t("days", L)}
    +
    +
    +
    ${t("last_performed", L)}
    +
    ${task.last_performed ? formatDate(task.last_performed, L) : "—"}
    +
    +
    +
    ${t("avg_cost", L)}
    +
    ${avgCost.toFixed(0)} ${ctx.currencySymbol}
    +
    +
    +
    ${t("avg_duration", L)}
    +
    ${task.average_duration ? task.average_duration.toFixed(0) : "—"} min
    +
    +
    + `; +} + +function renderRecommendationCard(task: MaintenanceTask, ctx: TaskDetailContext) { + const L = ctx.lang; + + if (!ctx.features.adaptive || !task.suggested_interval + || task.suggested_interval === task.interval_days) { + return nothing; + } + if (ctx.suggestionDismissed) return nothing; + + const suggested = task.suggested_interval; + return html` +
    +

    ${t("suggested_interval", L)}

    + ${renderRecommendationBars( + task.interval_days, suggested, + task.interval_confidence || "medium", L, + )} +
    + ctx.applySuggestion(suggested)}> + ${t("apply_suggestion", L)} + + ctx.reanalyze()}> + ${t("reanalyze", L)} + + ctx.dismissSuggestion()}> + ${t("dismiss_suggestion", L)} + +
    +
    + `; +} + +function renderRecentActivities(task: MaintenanceTask, ctx: TaskDetailContext) { + const L = ctx.lang; + const recent = task.history.slice(-3).reverse(); + + if (recent.length === 0) { + return nothing; + } + + const getIcon = (type: string) => { + switch (type) { + case "completed": return "✓"; + case "triggered": return "⊗"; + case "skipped": return "↷"; + case "reset": return "↺"; + default: return "·"; + } + }; + + return html` +
    +

    ${t("recent_activities", L)}

    + ${recent.map(entry => html` +
    + ${getIcon(entry.type)} + ${formatDateTime(entry.timestamp, L)} + ${entry.notes || "—"} + ${entry.cost ? html`${entry.cost.toFixed(0)}${ctx.currencySymbol}` : nothing} + ${entry.duration ? html`${entry.duration}min` : nothing} +
    + `)} +
    + ctx.setActiveTab("history")}>${t("show_all", L)} → +
    +
    + `; +} + +export function renderOverviewTab(task: MaintenanceTask, ctx: TaskDetailContext) { + const L = ctx.lang; + + // Check if we have recommendation / seasonal content + const hasRecommendation = ctx.features.adaptive && task.suggested_interval && task.suggested_interval !== task.interval_days; + const hasSeasonal = ctx.features.seasonal && task.seasonal_factor && task.seasonal_factor !== 1.0; + const hasLeftColumn = hasRecommendation || hasSeasonal; + + // Analysis content: Weibull/Seasonal expanded (only when data is available) + const hasWeibullData = ctx.features.adaptive + && task.interval_analysis?.weibull_beta != null + && task.interval_analysis?.weibull_eta != null; + const hasSeasonalData = ctx.features.seasonal + && (task.seasonal_factors?.length === 12 + || task.interval_analysis?.seasonal_factors?.length === 12); + + return html` +
    + ${renderKPIBar(task, ctx)} + ${renderTaskMeta(task, ctx)} + ${renderDaysProgress(task, ctx.lang)} + ${renderTriggerSection(task, ctx.sparkline)} + ${renderPredictionSection(task, L, ctx.features)} +
    + ${hasLeftColumn ? html` +
    + ${renderRecommendationCard(task, ctx)} + ${renderSeasonalCardCompact(task, L, ctx.features)} +
    + ` : nothing} +
    + ${renderCostDurationCard(task, L, ctx.costDurationToggle, (v) => ctx.setCostDurationToggle(v))} +
    +
    + ${hasWeibullData + ? collapsible("weibull", "weibull_reliability_curve", renderWeibullSection(task, L), ctx) + : nothing} + ${hasSeasonalData + ? collapsible("seasonal", "seasonal_chart_title", html` + ${renderSeasonalCardExpanded(task, L)} +
    + ctx.openSeasonalOverrides(task)}> + ${t("edit_seasonal_overrides", L)} + +
    + `, ctx) + : nothing} + ${renderChecklistCard(task, ctx)} + ${renderRecentActivities(task, ctx)} +
    + `; +} + +function renderHistoryTab(task: MaintenanceTask, ctx: TaskDetailContext) { + return html` +
    + ${renderHistoryFilters(task, ctx.history)} + ${renderHistoryList(task, ctx.history)} +
    + `; +} + +function renderTabContent(task: MaintenanceTask, ctx: TaskDetailContext) { + switch (ctx.activeTab) { + case "overview": + return renderOverviewTab(task, ctx); + case "history": + return renderHistoryTab(task, ctx); + default: + return nothing; + } +} + +/** The complete task-detail view. Renders into the PANEL's shadow root (this + * is a render function, not a component) so the panel's dialogs and styles + * keep working unchanged. */ +export function renderTaskDetail(task: MaintenanceTask, ctx: TaskDetailContext) { + return html` +
    + ${renderTaskHeader(task, ctx)} + ${renderTabBar(ctx)} + ${renderTabContent(task, ctx)} + +
    + `; +} diff --git a/custom_components/maintenance_supporter/frontend-src/renderers/weibull.ts b/custom_components/maintenance_supporter/frontend-src/renderers/weibull.ts new file mode 100644 index 0000000..f10330b --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/renderers/weibull.ts @@ -0,0 +1,187 @@ +/** Weibull reliability analysis renderers. */ + +import { html, svg, nothing } from "lit"; +import { t } from "../styles"; +import type { MaintenanceTask } from "../types"; + +export function renderWeibullSection(task: MaintenanceTask, lang: string) { + const analysis = task.interval_analysis; + const beta = analysis?.weibull_beta; + const eta = analysis?.weibull_eta; + if (beta == null || eta == null || eta <= 0) return nothing; + + const currentInterval = task.interval_days ?? 0; + const rec = task.suggested_interval ?? currentInterval; + + return html` +
    +
    + + ${t("weibull_reliability_curve", lang)} + ${renderBetaBadge(beta, lang)} +
    + ${renderWeibullChart(beta, eta, currentInterval, rec, lang)} + ${renderWeibullInfo(analysis!, lang)} + ${analysis?.confidence_interval_low != null ? renderConfidenceInterval(analysis!, task, lang) : nothing} +
    + `; +} + +function renderBetaBadge(beta: number, lang: string) { + let cls: string; + let icon: string; + let key: string; + if (beta < 0.8) { + cls = "early_failures"; + icon = "M13,14H11V10H13M13,18H11V16H13M1,21H23L12,2L1,21Z"; + key = "beta_early_failures"; + } else if (beta <= 1.2) { + cls = "random_failures"; + icon = "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M13,17H11V15H13V17M13,13H11V7H13V13Z"; + key = "beta_random_failures"; + } else if (beta <= 3.5) { + cls = "wear_out"; + icon = "M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4M12,6A6,6 0 0,1 18,12H12V6Z"; + key = "beta_wear_out"; + } else { + cls = "highly_predictable"; + icon = "M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M11,16.5L18,9.5L16.59,8.09L11,13.67L7.91,10.59L6.5,12L11,16.5Z"; + key = "beta_highly_predictable"; + } + return html` + + + ${t(key, lang)} (\u03B2=${beta.toFixed(2)}) + + `; +} + +function renderWeibullChart(beta: number, eta: number, currentInterval: number, recommended: number, lang: string) { + const W = 300, H = 160; + const PAD_L = 32, PAD_R = 8, PAD_T = 8, PAD_B = 24; + const chartW = W - PAD_L - PAD_R; + const chartH = H - PAD_T - PAD_B; + + const maxT = Math.max(currentInterval, recommended, eta, 1) * 1.3; + const N = 50; + + const points: Array<[number, number]> = []; + for (let i = 0; i <= N; i++) { + const t_val = (i / N) * maxT; + const cdf = 1.0 - Math.exp(-Math.pow(t_val / eta, beta)); + const x = PAD_L + (t_val / maxT) * chartW; + const y = PAD_T + chartH - cdf * chartH; + points.push([x, y]); + } + + const polyline = points.map(([x, y]) => `${x.toFixed(1)},${y.toFixed(1)}`).join(" "); + const areaPath = `M${PAD_L},${PAD_T + chartH} ` + + points.map(([x, y]) => `L${x.toFixed(1)},${y.toFixed(1)}`).join(" ") + + ` L${points[N][0].toFixed(1)},${PAD_T + chartH} Z`; + + const curX = PAD_L + (currentInterval / maxT) * chartW; + const curCdf = 1.0 - Math.exp(-Math.pow(currentInterval / eta, beta)); + const curY = PAD_T + chartH - curCdf * chartH; + const reliability = ((1.0 - curCdf) * 100).toFixed(0); + + const recX = PAD_L + (recommended / maxT) * chartW; + const yTicks = [0, 0.25, 0.5, 0.75, 1.0]; + + return html` +
    + + ${yTicks.map(tick => { + const y = PAD_T + chartH - tick * chartH; + return svg` + + ${(tick * 100).toFixed(0)}% + `; + })} + + 0 + ${Math.round(maxT / 2)} + ${Math.round(maxT)} + + + + + ${currentInterval > 0 ? svg` + + + R=${reliability}% + ` : nothing} + + ${recommended > 0 && recommended !== currentInterval ? svg` + + ` : nothing} + + + + +
    +
    + ${t("weibull_failure_probability", lang)} + ${currentInterval > 0 ? html` ${t("current_interval_marker", lang)}` : nothing} + ${recommended > 0 && recommended !== currentInterval ? html` ${t("recommended_marker", lang)}` : nothing} +
    + `; +} + +function renderWeibullInfo(analysis: NonNullable, lang: string) { + return html` +
    +
    + ${t("characteristic_life", lang)} + ${Math.round(analysis.weibull_eta!)} ${t("days", lang)} +
    + ${analysis.weibull_r_squared != null ? html` +
    + ${t("weibull_r_squared", lang)} + ${analysis.weibull_r_squared!.toFixed(3)} +
    + ` : nothing} +
    + `; +} + +function renderConfidenceInterval(analysis: NonNullable, task: MaintenanceTask, lang: string) { + const low = analysis.confidence_interval_low!; + const high = analysis.confidence_interval_high!; + const rec = task.suggested_interval ?? task.interval_days ?? 0; + const current = task.interval_days ?? 0; + + const barMin = Math.max(0, low - 5); + const barMax = high + 5; + const range = barMax - barMin; + + const fillLeft = ((low - barMin) / range) * 100; + const fillWidth = ((high - low) / range) * 100; + const recPos = ((rec - barMin) / range) * 100; + const curPos = current > 0 ? ((current - barMin) / range) * 100 : -1; + + return html` +
    +
    + ${t("confidence_interval", lang)}: ${rec} ${t("days", lang)} (${low}\u2013${high}) +
    +
    +
    + ${curPos >= 0 ? html`
    ` : nothing} + +
    +
    + ${t("confidence_conservative", lang)} (${low}${t("days", lang).charAt(0)}) + ${t("confidence_aggressive", lang)} (${high}${t("days", lang).charAt(0)}) +
    +
    + `; +} diff --git a/custom_components/maintenance_supporter/frontend-src/screenshot-section-cards.mjs b/custom_components/maintenance_supporter/frontend-src/screenshot-section-cards.mjs new file mode 100644 index 0000000..e3b6773 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/screenshot-section-cards.mjs @@ -0,0 +1,251 @@ +/** + * Screenshot the new interactive section cards (Vacation/Budget/Groups) + * + the existing task quick-actions dialog (after the button-row fix) + * + the complete-dialog (to investigate the user's "fields are missing" report). + * + * Creates a temporary Lovelace dashboard with all three cards, navigates, + * captures shots in light + dark mode at desktop width. + */ +import { chromium } from "playwright"; +import { promises as fs } from "fs"; +import http from "http"; +import path from "path"; + +const HA = "http://localhost:8125"; +const OUT = path.resolve("../../../screenshots-section-cards"); + +async function getRefreshToken() { + function post(p, body, ct = "application/json") { + return new Promise((resolve, reject) => { + const data = typeof body === "string" ? body : JSON.stringify(body); + const opts = { + hostname: "localhost", port: 8125, path: p, method: "POST", + headers: { "Content-Type": ct, "Content-Length": Buffer.byteLength(data) }, + }; + const req = http.request(opts, (r) => { + let d = ""; + r.on("data", (c) => (d += c)); + r.on("end", () => resolve(JSON.parse(d))); + }); + req.on("error", reject); + req.write(data); + req.end(); + }); + } + const flow = await post("/auth/login_flow", { + client_id: HA + "/", handler: ["homeassistant", null], redirect_uri: HA + "/", + }); + const auth = await post(`/auth/login_flow/${flow.flow_id}`, { + client_id: HA + "/", username: "dev", password: "dev", + }); + const tok = await post( + "/auth/token", + `grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`, + "application/x-www-form-urlencoded", + ); + return tok.refresh_token; +} + +async function ws(page, cmd) { + return page.evaluate(async (c) => { + const ha = document.querySelector("home-assistant"); + if (!ha?.hass?.connection) throw new Error("no hass connection"); + return await ha.hass.connection.sendMessagePromise(c); + }, cmd); +} + +async function ensureDashboard(page) { + const dashboards = await ws(page, { type: "lovelace/dashboards/list" }); + const slug = "section-cards-test"; + const existing = dashboards.find((d) => d.url_path === slug); + if (!existing) { + await ws(page, { + type: "lovelace/dashboards/create", + url_path: slug, + title: "Section Cards Test", + icon: "mdi:test-tube", + mode: "storage", + show_in_sidebar: false, + require_admin: false, + }); + } + // Save the lovelace config with our 3 cards + await ws(page, { + type: "lovelace/config/save", + url_path: slug, + config: { + views: [ + { + title: "Sections", + path: "default", + icon: "mdi:view-dashboard", + cards: [ + { type: "custom:maintenance-vacation-section-card" }, + { type: "custom:maintenance-budget-section-card" }, + { type: "custom:maintenance-groups-section-card" }, + ], + }, + { + title: "Card", + path: "card", + icon: "mdi:card-text", + cards: [ + { type: "custom:maintenance-supporter-card", show_actions: true, max_items: 6 }, + ], + }, + ], + }, + }); + return slug; +} + +async function shot(page, name) { + await page.waitForTimeout(800); + const file = path.join(OUT, name + ".png"); + await page.screenshot({ path: file, fullPage: false }); + console.log(` 📸 ${name}.png`); +} + +async function setColorScheme(page, scheme) { + // Force HA's theme by injecting localStorage entry + reloading + await page.evaluate((s) => { + localStorage.setItem("selectedTheme", JSON.stringify({ dark: s === "dark" })); + }, scheme); +} + +async function main() { + await fs.mkdir(OUT, { recursive: true }); + console.log("Getting refresh token…"); + const refreshToken = await getRefreshToken(); + + const browser = await chromium.launch({ headless: true }); + + for (const colorScheme of ["dark", "light"]) { + console.log(`\n=== ${colorScheme} mode ===`); + const ctx = await browser.newContext({ + viewport: { width: 1280, height: 900 }, + colorScheme, + ignoreHTTPSErrors: true, + }); + const page = await ctx.newPage(); + page.on("pageerror", (e) => console.log("PAGE ERROR:", e.message)); + page.on("console", (m) => { + if (m.type() === "error") console.log("CONSOLE:", m.text()); + }); + + await page.goto(HA); + await page.waitForTimeout(800); + await page.evaluate((args) => { + localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: args.ha, clientId: args.ha + "/", + refresh_token: args.r, access_token: "", + token_type: "Bearer", expires_in: 1800, expires: 0, + })); + localStorage.setItem("selectedTheme", JSON.stringify({ dark: args.cs === "dark" })); + }, { ha: HA, r: refreshToken, cs: colorScheme }); + + await page.goto(HA); + await page.waitForTimeout(3500); + + const slug = await ensureDashboard(page); + await page.goto(`${HA}/${slug}/default`); + await page.waitForTimeout(4000); + + await shot(page, `sections-${colorScheme}`); + + // Card view — click a task row to open quick-actions dialog + await page.goto(`${HA}/${slug}/card`); + await page.waitForTimeout(3500); + await shot(page, `card-${colorScheme}`); + + // Find a task row that's likely to have adaptive data (Oil Change has the + // longest history in the demo dataset). Falls back to first row. + const taskClicked = await page.evaluate(() => { + function findInAllShadows(root, sel) { + if (!root) return null; + if (root.querySelector?.(sel)) return root.querySelector(sel); + for (const el of root.querySelectorAll?.("*") || []) { + if (el.shadowRoot) { + const found = findInAllShadows(el.shadowRoot, sel); + if (found) return found; + } + } + return null; + } + const card = findInAllShadows(document, "maintenance-supporter-card"); + if (!card?.shadowRoot) return "no card"; + const rows = Array.from(card.shadowRoot.querySelectorAll(".task-item.clickable")); + if (rows.length === 0) return "no rows"; + // Prefer "Filter Replacement" (HVAC) — has interval recommendation, + // then "Oil Change" (Family Car) — has sensor + long history. + const filterReplace = rows.find((r) => /filter replacement|filter wechsel/i.test(r.textContent || "")); + const oilChange = rows.find((r) => /oil change|ölwechsel/i.test(r.textContent || "")); + const target = filterReplace || oilChange || rows[0]; + target.click(); + return "clicked: " + (target.querySelector(".task-name")?.textContent || "?"); + }); + + console.log(` Task row click: ${taskClicked}`); + if (taskClicked.startsWith("clicked")) { + // Wait for both /object load AND /settings (features) load — the stats + // toggle only renders after features arrive + await page.waitForTimeout(4000); + await shot(page, `quick-actions-${colorScheme}`); + + // Click the 2nd link (= "Show stats + graphs") + const statsClick = await page.evaluate(() => { + const dlg = document.body.querySelector("maintenance-task-quick-actions-dialog"); + const links = dlg?.shadowRoot?.querySelectorAll(".details-toggle button.link") || []; + if (links.length < 2) return `only ${links.length} link(s)`; + links[1].click(); + return "clicked: " + (links[1].textContent || "").trim(); + }); + console.log(` Stats toggle: ${statsClick}`); + await page.waitForTimeout(1500); + await shot(page, `quick-actions-stats-${colorScheme}`); + + // Capture the graphs section — scroll dialog and shoot again + await page.evaluate(() => { + const dlg = document.body.querySelector("maintenance-task-quick-actions-dialog"); + const inner = dlg?.shadowRoot?.querySelector(".dialog"); + if (inner) inner.scrollTop = Math.max(0, inner.scrollHeight - 700); + }).catch(() => {}); + await page.waitForTimeout(400); + await shot(page, `quick-actions-stats-bottom-${colorScheme}`); + + // Restore scroll for complete dialog test + await page.evaluate(() => { + const dlg = document.body.querySelector("maintenance-task-quick-actions-dialog"); + const inner = dlg?.shadowRoot?.querySelector(".dialog"); + if (inner) inner.scrollTop = 0; + }).catch(() => {}); + await page.waitForTimeout(300); + const completeClicked = await page.evaluate(() => { + const dlg = document.body.querySelector("maintenance-task-quick-actions-dialog"); + if (!dlg?.shadowRoot) return "no quick-actions dialog on body"; + const btns = dlg.shadowRoot.querySelectorAll(".actions.primary-row button"); + // The first button in primary-row IS Complete + if (btns.length === 0) return "no buttons in primary-row"; + btns[0].click(); + return "clicked: " + (btns[0].textContent || "").trim(); + }); + console.log(` Complete click: ${completeClicked}`); + await page.waitForTimeout(2000); + await shot(page, `complete-dialog-${colorScheme}`); + + // Close everything + await page.keyboard.press("Escape"); + await page.waitForTimeout(500); + } + + await ctx.close(); + } + + await browser.close(); + console.log(`\nDone. Screenshots in ${OUT}`); +} + +main().catch((e) => { + console.error(e); + process.exit(1); +}); diff --git a/custom_components/maintenance_supporter/frontend-src/screenshot.mjs b/custom_components/maintenance_supporter/frontend-src/screenshot.mjs new file mode 100644 index 0000000..c18f020 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/screenshot.mjs @@ -0,0 +1,88 @@ +import { chromium } from "playwright"; + +const HA = "http://homeassistant-dev:8123"; +const REFRESH = process.env.HA_REFRESH_TOKEN; +if (!REFRESH) { console.error("Set HA_REFRESH_TOKEN env var"); process.exit(1); } + +const browser = await chromium.connect("ws://localhost:3000"); + +function getPanel() { + return "var ha = document.querySelector('home-assistant'); " + + "var main = ha && ha.shadowRoot && ha.shadowRoot.querySelector('home-assistant-main'); " + + "var drawer = main && main.shadowRoot && main.shadowRoot.querySelector('ha-drawer'); " + + "var resolver = drawer && drawer.querySelector('partial-panel-resolver'); " + + "var custom = resolver && resolver.querySelector('ha-panel-custom'); " + + "var p = custom && custom.querySelector('maintenance-supporter-panel'); "; +} + +async function login(page) { + await page.goto(HA); + await page.waitForTimeout(1000); + await page.evaluate((refresh) => { + localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: "http://homeassistant-dev:8123", + clientId: "http://homeassistant-dev:8123/", + refresh_token: refresh, + access_token: "", + token_type: "Bearer", + expires_in: 1800, + expires: 0, + })); + }, REFRESH); + await page.goto(HA + "/maintenance-supporter"); + await page.waitForTimeout(6000); +} + +// Desktop screenshot (1280x900) +const desktopCtx = await browser.newContext({ viewport: { width: 1280, height: 900 }, locale: "de" }); +const desktopPage = await desktopCtx.newPage(); +await login(desktopPage); + +var info = await desktopPage.evaluate("{ " + getPanel() + + "var result = []; " + + "if (p && p._objects) { " + + " for (var i = 0; i < p._objects.length; i++) { " + + " var obj = p._objects[i]; " + + " var tasks = []; " + + " if (obj.tasks) for (var j = 0; j < obj.tasks.length; j++) tasks.push({ id: obj.tasks[j].id }); " + + " result.push({ entry_id: obj.entry_id, tasks: tasks }); " + + " } " + + "} " + + "JSON.stringify(result); " + + "}"); +var data = JSON.parse(info); +var entryId = data[0].entry_id; +var taskId = data[0].tasks[0].id; + +// Desktop: task detail +await desktopPage.evaluate("{ " + getPanel() + "if (p) p._showTask('" + entryId + "', '" + taskId + "'); }"); +await desktopPage.waitForTimeout(2000); +await desktopPage.screenshot({ path: "screenshots/desktop_task.png" }); +console.log("Desktop task detail done"); + +// Desktop: overview +await desktopPage.evaluate("{ " + getPanel() + "if (p) p._showOverview(); }"); +await desktopPage.waitForTimeout(2000); +await desktopPage.screenshot({ path: "screenshots/desktop_overview.png" }); +console.log("Desktop overview done"); + +await desktopCtx.close(); + +// Mobile screenshot (375x812, iPhone-like) +const mobileCtx = await browser.newContext({ viewport: { width: 375, height: 812 }, locale: "de", isMobile: true }); +const mobilePage = await mobileCtx.newPage(); +await login(mobilePage); + +// Mobile: overview +await mobilePage.screenshot({ path: "screenshots/mobile_overview.png" }); +console.log("Mobile overview done"); + +// Mobile: task detail +await mobilePage.evaluate("{ " + getPanel() + "if (p) p._showTask('" + entryId + "', '" + taskId + "'); }"); +await mobilePage.waitForTimeout(2000); +await mobilePage.screenshot({ path: "screenshots/mobile_task.png" }); +console.log("Mobile task detail done"); + +await mobileCtx.close(); +await browser.close(); +console.log("All screenshots done!"); diff --git a/custom_components/maintenance_supporter/frontend-src/statistics-service.ts b/custom_components/maintenance_supporter/frontend-src/statistics-service.ts new file mode 100644 index 0000000..f555514 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/statistics-service.ts @@ -0,0 +1,223 @@ +/** + * Statistics service for fetching and caching HA recorder statistics. + * Used by sparkline charts to display dense time-series data. + */ +import type { HomeAssistant, StatisticsPoint, EntityStatisticsCache, HAStatisticsRow } from "./types"; + +const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes +const DETAIL_DAYS = 30; +const MINI_DAYS = 14; + +export class StatisticsService { + private _hass: HomeAssistant; + private _cache = new Map(); + private _pending = new Map>(); + + constructor(hass: HomeAssistant) { + this._hass = hass; + } + + updateHass(hass: HomeAssistant): void { + this._hass = hass; + } + + /** + * Statistics for the detail chart. Hourly resolution for short ranges, + * daily beyond ~5 weeks (keeps a 1-year window at ~365 points). + */ + async getDetailStats( + entityId: string, + isCounter: boolean, + days: number = DETAIL_DAYS, + ): Promise { + return this._getStats(entityId, days <= 35 ? "hour" : "day", days, isCounter); + } + + /** + * Get 14 days of daily statistics for the mini-sparkline (60x20 chart). + */ + async getMiniStats(entityId: string, isCounter: boolean): Promise { + return this._getStats(entityId, "day", MINI_DAYS, isCounter); + } + + /** + * Batch-fetch mini stats for multiple entities in at most 2 WS calls + * (one for counter-type, one for non-counter-type entities). + */ + async getBatchMiniStats( + entities: Array<{ entityId: string; isCounter: boolean }>, + ): Promise> { + const result = new Map(); + const toFetch: Array<{ entityId: string; isCounter: boolean }> = []; + + // Check cache first, collect cache misses (same key shape as _getStats + // so the single-entity mini path and the batch path share the cache). + for (const e of entities) { + const cacheKey = `${e.entityId}:day:${MINI_DAYS}`; + const cached = this._cache.get(cacheKey); + if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) { + result.set(e.entityId, cached.points); + } else { + toFetch.push(e); + } + } + + if (toFetch.length === 0) return result; + + // Group by type (counter vs non-counter need different stat types) + const counterIds = toFetch.filter((e) => e.isCounter).map((e) => e.entityId); + const nonCounterIds = toFetch.filter((e) => !e.isCounter).map((e) => e.entityId); + const startTime = new Date(Date.now() - MINI_DAYS * 24 * 60 * 60 * 1000).toISOString(); + + const promises: Promise[] = []; + + if (counterIds.length > 0) { + promises.push( + this._fetchBatch(counterIds, "day", startTime, ["state", "sum", "change"], true, result), + ); + } + if (nonCounterIds.length > 0) { + promises.push( + this._fetchBatch(nonCounterIds, "day", startTime, ["mean", "min", "max"], false, result), + ); + } + + await Promise.all(promises); + return result; + } + + clearCache(): void { + this._cache.clear(); + this._pending.clear(); + } + + private async _getStats( + entityId: string, + period: "hour" | "day", + days: number, + isCounter: boolean, + ): Promise { + // days must be part of the key: the same entity/period at a different + // range (7d vs 30d) is a different dataset, not a cache hit. + const cacheKey = `${entityId}:${period}:${days}`; + + const cached = this._cache.get(cacheKey); + if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) { + return cached.points; + } + + // Deduplicate in-flight requests + if (this._pending.has(cacheKey)) { + return this._pending.get(cacheKey)!; + } + + const promise = this._fetchAndNormalize(entityId, period, days, isCounter, cacheKey); + this._pending.set(cacheKey, promise); + + try { + return await promise; + } finally { + this._pending.delete(cacheKey); + } + } + + private async _fetchAndNormalize( + entityId: string, + period: "hour" | "day", + days: number, + isCounter: boolean, + cacheKey: string, + ): Promise { + const startTime = new Date(Date.now() - days * 24 * 60 * 60 * 1000).toISOString(); + const types = isCounter ? ["state", "sum", "change"] : ["mean", "min", "max"]; + + try { + const result = (await this._hass.connection.sendMessagePromise({ + type: "recorder/statistics_during_period", + start_time: startTime, + statistic_ids: [entityId], + period, + types, + })) as Record; + + const rows = result[entityId] || []; + const points = this._normalizeRows(rows, isCounter); + + this._cache.set(cacheKey, { + entityId, + fetchedAt: Date.now(), + period, + points, + }); + + return points; + } catch (err) { + console.warn(`[maintenance-supporter] Failed to fetch statistics for ${entityId}:`, err); + return []; + } + } + + private async _fetchBatch( + entityIds: string[], + period: "hour" | "day", + startTime: string, + types: string[], + isCounter: boolean, + out: Map, + ): Promise { + try { + const response = (await this._hass.connection.sendMessagePromise({ + type: "recorder/statistics_during_period", + start_time: startTime, + statistic_ids: entityIds, + period, + types, + })) as Record; + + for (const entityId of entityIds) { + const rows = response[entityId] || []; + const points = this._normalizeRows(rows, isCounter); + out.set(entityId, points); + this._cache.set(`${entityId}:${period}:${MINI_DAYS}`, { + entityId, + fetchedAt: Date.now(), + period, + points, + }); + } + } catch (err) { + console.warn("[maintenance-supporter] Batch statistics fetch failed:", err); + } + } + + private _normalizeRows(rows: HAStatisticsRow[], isCounter: boolean): StatisticsPoint[] { + const points: StatisticsPoint[] = []; + + for (const row of rows) { + let val: number | null = null; + + if (isCounter) { + val = row.state ?? null; + } else { + val = row.mean ?? null; + } + + if (val === null) continue; + + const point: StatisticsPoint = { + ts: row.start, // HA returns epoch milliseconds + val, + }; + + if (!isCounter) { + if (row.min != null) point.min = row.min; + if (row.max != null) point.max = row.max; + } + + points.push(point); + } + + points.sort((a, b) => a.ts - b.ts); + return points; + } +} diff --git a/custom_components/maintenance_supporter/frontend-src/styles.ts b/custom_components/maintenance_supporter/frontend-src/styles.ts new file mode 100644 index 0000000..be83280 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/styles.ts @@ -0,0 +1,1393 @@ +/** Shared CSS styles and i18n for the Maintenance Supporter frontend. */ + +import { css } from "lit"; +import EN from "./locales/en.json"; + +// Display fallback when the backend hasn't sent a currency symbol. The backend +// derives the real symbol from const.BUDGET_CURRENCIES[DEFAULT_BUDGET_CURRENCY]. +export const DEFAULT_CURRENCY_SYMBOL = "€"; + +export const STATUS_COLORS: Record = { + ok: "var(--success-color, #4caf50)", + due_soon: "var(--warning-color, #ff9800)", + overdue: "var(--error-color, #f44336)", + // Theme-token first so it adapts to dark/custom themes (was a bare #ff5722). + triggered: "var(--deep-orange-color, #ff5722)", + // v2.10.0: archived is a neutral, greyed-out state (retired but retained). + archived: "var(--disabled-color, #9e9e9e)", + // v2.20 (N3): paused is frozen-but-present — info blue, clearly not urgent. + paused: "var(--info-color, #2196f3)", +}; + +export const STATUS_ICONS: Record = { + ok: "mdi:check-circle", + due_soon: "mdi:alert-circle", + overdue: "mdi:alert-octagon", + triggered: "mdi:bell-alert", + archived: "mdi:archive-outline", + paused: "mdi:pause-circle-outline", + completed: "mdi:check-circle", + skipped: "mdi:skip-next", + missed: "mdi:calendar-remove", + reset: "mdi:refresh", +}; + +/* ─── i18n ─── */ + +interface Translations { + [key: string]: string; +} + +// English is bundled (imported above) as the always-available fallback so +// the panel renders instantly with a complete table — never an +// untranslated-key flash, even if a locale fetch fails. The other 17 +// languages live in served JSON (frontend/locales/.json) and are +// fetched on demand by ensureLocale(), so a translation edit needs no bundle +// rebuild — the fix for the recurring "stale bundle ships English to non-EN +// users" pitfall. Steady-state t() is the same sync lookup as before. +const DEFAULT_LANG = "en"; + +// The locale store MUST be shared across bundle copies of this module. +// maintenance-card.js is loaded globally (extra_module_url) and defines the +// dialog custom elements first-wins — so a inside +// the panel runs the CARD bundle's copy of this module. With a module-scoped +// store, the panel's ensureLocale() fills only the PANEL copy and the dialog +// stays English while the rest of the panel is localized (v2.17.0 regression, +// invisible before the runtime-locale split because every bundle inlined all +// languages). One window-scoped store + inflight map keeps every copy reading +// and writing the same tables. +interface LocaleGlobals { + store: Record; + inflight: Record>; +} +const _localeGlobals: LocaleGlobals = (() => { + const w = window as unknown as { __msLocales?: LocaleGlobals }; + if (!w.__msLocales) w.__msLocales = { store: {}, inflight: {} }; + return w.__msLocales; +})(); + +const STORE = _localeGlobals.store; +// Each copy guarantees the bundled English fallback is present. +if (!STORE.en) STORE.en = EN as Translations; + +/** Languages available as runtime-loaded JSON. Keep in sync with locales/. */ +const SUPPORTED_LANGS = new Set([ + "de", "nl", "fr", "it", "es", "pt", "ru", "uk", "pl", "cs", "sv", "zh", + "da", "fi", "nb", "ja", "hi", +]); + +/** Served base for the runtime locale files (mirrors LOCALES_URL in const.py). */ +const LOCALES_BASE = "/maintenance_supporter_locales"; + +const _localeInflight = _localeGlobals.inflight; + +/** Normalize an HA language code to our 2-letter table key. */ +function normLang(lang?: string): string { + return (lang || DEFAULT_LANG).substring(0, 2).toLowerCase(); +} + +/** Get a localized string. Falls back to English, then to the key itself. */ +export function t(key: string, lang?: string): string { + const l = normLang(lang); + return STORE[l]?.[key] ?? STORE.en[key] ?? key; +} + +/** True when *lang*'s table is in memory (English is always bundled). */ +export function isLocaleLoaded(lang?: string): boolean { + const l = normLang(lang); + return l === DEFAULT_LANG || l in STORE; +} + +/** + * Fetch *lang*'s table once and cache it. Resolves immediately for English, + * an already-loaded language, or an unsupported one (which keeps the English + * fallback). Never rejects: a failed fetch silently leaves English in place, + * matching t()'s fall-through. Callers re-render on resolution. + */ +export function ensureLocale(lang?: string): Promise { + const l = normLang(lang); + if (l === DEFAULT_LANG || l in STORE || !SUPPORTED_LANGS.has(l)) { + return Promise.resolve(); + } + if (!(l in _localeInflight)) { + _localeInflight[l] = fetch(`${LOCALES_BASE}/${l}.json`) + .then((r) => (r.ok ? r.json() : null)) + .then((data) => { + if (data) { + STORE[l] = data as Translations; + } else { + // Don't make one failed fetch sticky for the whole page session — + // the next ensureLocale() call retries. + delete _localeInflight[l]; + } + }) + .catch(() => { + delete _localeInflight[l]; + }); + } + return _localeInflight[l]; +} + +/** + * Synchronously seed a locale table, bypassing the fetch. For tests and for + * callers that already hold a table (preload/SSR); the loader and t() share the + * same STORE, so a seeded language reads immediately. + */ +export function setLocale(lang: string, table: Translations): void { + STORE[normLang(lang)] = table; +} + +/** Map language prefix to BCP-47 locale for date formatting. */ +function langToLocale(lang?: string): string { + const l = (lang || "en").substring(0, 2).toLowerCase(); + const map: Record = { + de: "de-DE", en: "en-US", nl: "nl-NL", fr: "fr-FR", it: "it-IT", es: "es-ES", pt: "pt-PT", ru: "ru-RU", uk: "uk-UA", zh: "zh-CN", + da: "da-DK", fi: "fi-FI", nb: "nb-NO", ja: "ja-JP", hi: "hi-IN" + }; + return map[l] ?? "en-US"; +} + +/** Format a date string (ISO) in the user's locale. + * Appends T00:00:00 to date-only strings so JS parses them as local time, not UTC. */ +export function formatDate(iso: string | null | undefined, lang?: string): string { + if (!iso) return "—"; + try { + const local = iso.includes("T") ? iso : iso + "T00:00:00"; + return new Date(local).toLocaleDateString(langToLocale(lang), { day: "2-digit", month: "2-digit", year: "numeric" }); + } catch { + return iso; + } +} + +/** Format a datetime string (ISO) in the user's locale. */ +export function formatDateTime(iso: string | null | undefined, lang?: string): string { + if (!iso) return "—"; + try { + const locale = langToLocale(lang); + const d = new Date(iso); + return ( + d.toLocaleDateString(locale, { day: "2-digit", month: "2-digit", year: "numeric" }) + + " " + + d.toLocaleTimeString(locale, { hour: "2-digit", minute: "2-digit" }) + ); + } catch { + return iso; + } +} + +/** Format "days until due" in localized manner. */ +export function formatDueDays(days: number | null | undefined, lang?: string): string { + if (days === null || days === undefined) return "—"; + const l = lang || "en"; + if (days < 0) return `${Math.abs(days)} ${t("d_overdue", l)}`; + if (days === 0) return t("today", l); + return `${days} ${days === 1 ? t("day", l) : t("days", l)}`; +} + +/** Localized interval label, e.g. "3 Months" / "7 Days" (issue #59 — was + * hard-coded to days). Uses the unit_* keys from the schedule dialog. */ +export function formatInterval( + intervalDays: number | null | undefined, + unit?: string | null, + lang?: string, +): string { + if (intervalDays === null || intervalDays === undefined) return "—"; + return `${intervalDays} ${t("unit_" + (unit || "days"), lang)}`; +} + +/** Localized weekday name (0=Mon … 6=Sun) via Intl — 2024-01-01 is a Monday. + * Single source for the dialog selectors AND formatRecurrence (DRY). */ +export function weekdayName(i: number, lang?: string, style: "long" | "short" = "long"): string { + const locale = (lang || "en").substring(0, 2); + return new Date(Date.UTC(2024, 0, 1 + i)).toLocaleDateString(locale, { weekday: style, timeZone: "UTC" }); +} + +/** Recurrence shape carried on the WS payload (see types.TaskSchedule). */ +interface RecurrenceLike { + schedule?: { + kind?: string; every?: number | null; unit?: string; + weekdays?: number[]; nth?: number; weekday?: number; day?: number; + business?: boolean; offset?: number; + } | null; + interval_days?: number | null; + interval_unit?: string | null; + due_date?: string | null; + schedule_type?: string; +} + +/** A task's recurrence as a localized human label, for ANY schedule kind. + * The single recurrence formatter (DRY) — interval / weekdays / nth_weekday / + * day_of_month / one_time / manual. Used by the panel, card, and quick-actions. */ +export function formatRecurrence(task: RecurrenceLike, lang?: string): string { + const s = task.schedule; + // (#83) ±N-day shift suffix shared by the calendar kinds. + const off = s?.offset ? ` ${s.offset > 0 ? "+" : "−"}${Math.abs(s.offset)}d` : ""; + switch (s?.kind) { + case "weekdays": + return ((s.weekdays || []).map((d) => weekdayName(d, lang, "short")).join(" & ") || "—") + off; + case "nth_weekday": { + if (s.weekday == null || s.nth == null) return "—"; + const ord = s.nth === -1 ? t("ord_last", lang) : t("ord_" + s.nth, lang); + return `${ord} ${weekdayName(s.weekday, lang, "long")}${off}`; + } + case "day_of_month": { + if (s.day == null) return "—"; + // (#83) -1 = last day of the month; business rolls weekends back to Friday. + const base = s.day === -1 + ? t(s.business ? "last_business_day_month" : "last_day_month", lang) + : `${t("day_word", lang)} ${s.day}`; + return base + off; + } + case "one_time": + return task.due_date ? formatDate(task.due_date, lang) : t("one_time", lang); + case "manual": + return t("manual", lang); + case "interval": + return formatInterval(s.every, s.unit, lang); + } + // legacy / no nested schedule + if (task.schedule_type === "one_time") return task.due_date ? formatDate(task.due_date, lang) : t("one_time", lang); + if (task.schedule_type === "manual") return t("manual", lang); + // Sensor-driven tasks have a trigger, not a calendar/interval — label them as + // such instead of "—" (regression when the report moved to formatRecurrence). + if (task.schedule_type === "sensor_based") return t("sensor_based", lang); + return task.interval_days != null ? formatInterval(task.interval_days, task.interval_unit, lang) : "—"; +} + +/** Dispatch HA native "More Info" dialog for an entity. */ +export function fireMoreInfo(ev: Event, entityId: string) { + (ev.currentTarget as HTMLElement).dispatchEvent( + new CustomEvent("hass-more-info", { + detail: { entityId }, + bubbles: true, + composed: true, + }) + ); +} + +export const sharedStyles = css` + :host { + --maint-ok-color: var(--success-color, #4caf50); + --maint-due-soon-color: var(--warning-color, #ff9800); + --maint-overdue-color: var(--error-color, #f44336); + --maint-triggered-color: #ff5722; + } + + .status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + color: white; + white-space: nowrap; + /* Fixed minimum so OK / Due Soon / Overdue / Triggered pills are uniform + width in the task table — keeps the object-name column aligned. */ + min-width: 70px; + box-sizing: border-box; + } + /* Shape icon so status is not conveyed by colour alone (accessibility). */ + .status-badge ha-icon { --mdc-icon-size: 14px; margin-left: -1px; } + + .status-badge.ok { background-color: var(--maint-ok-color); } + .status-badge.due_soon { background-color: var(--maint-due-soon-color); } + .status-badge.overdue { background-color: var(--maint-overdue-color); } + .status-badge.triggered { background-color: var(--maint-triggered-color); } + /* Completed one-time task ("done") — muted blue-grey. */ + .status-badge.done { background-color: var(--maint-done-color, #78909c); } + /* v2.10.0: archived (retire-but-retain) — neutral grey, clearly inert. */ + .status-badge.archived { background-color: var(--disabled-color, #9e9e9e); } + /* v2.20 (N3): paused — frozen but present, info blue. */ + .status-badge.paused { background-color: var(--info-color, #2196f3); } + + /* v1.4.7: 5-column grid so all 5 KPIs (Objects/Tasks/Overdue/Due Soon/ + Triggered) always stay in one row. The previous flex-wrap layout was + wrapping the 5th item (Triggered, the widest label) onto its own row + on narrow viewports because the natural width of the items pushed past + the container width. Grid forces equal 1/5 distribution regardless of + label length. */ + .stats-bar { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 16px; + padding: 16px; + } + + .stat-item { + display: flex; + flex-direction: column; + align-items: center; + min-width: 0; + } + .stat-item .stat-label { + /* Allow long labels to ellipsis rather than overflow the grid cell. */ + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .stat-item.clickable { cursor: pointer; border-radius: 8px; padding: 4px 8px; transition: background 0.15s, box-shadow 0.15s; } + .stat-item.clickable:hover { background: var(--secondary-background-color); } + /* v2.1.0 — KPIs that map to a status filter highlight when active so the + user can see at a glance which filter is on, even after scrolling away. */ + .stat-item.clickable.active { + background: var(--secondary-background-color); + box-shadow: inset 0 -3px 0 var(--primary-color); + } + + .objects-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 16px; + padding: 16px 0; + } + .object-card { + padding: 16px; + background: var(--card-background-color); + border-radius: 8px; + cursor: pointer; + border: 1px solid var(--divider-color); + transition: transform 0.15s, box-shadow 0.15s; + /* Large installs (100+ objects): skip rendering off-screen cards. The + intrinsic size keeps the scrollbar stable while they're skipped. */ + content-visibility: auto; + contain-intrinsic-size: auto 120px; + } + .object-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); } + .object-card-header { display: flex; justify-content: space-between; align-items: center; } + .object-card-name { font-weight: 500; font-size: 16px; } + .object-card-count { color: var(--secondary-text-color); font-size: 13px; } + .object-card-meta { color: var(--secondary-text-color); font-size: 13px; margin-top: 4px; } + .object-card-empty { color: var(--warning-color); font-size: 13px; margin-top: 8px; font-style: italic; } + + /* Overdue indicator dot on object cards (#35) */ + .object-card { position: relative; } + .object-card-overdue { border-left: 3px solid var(--error-color); } + .overdue-dot { + position: absolute; + top: 12px; + right: 12px; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--error-color); + box-shadow: 0 0 0 2px var(--card-background-color); + } + + /* Group-by collapsible sections (#35 + #36) */ + .group-section { + margin: 12px 0; + border: 1px solid var(--divider-color); + border-radius: 8px; + background: var(--card-background-color); + } + .group-section[open] { padding-bottom: 8px; } + .group-section-header { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + cursor: pointer; + font-weight: 500; + list-style: none; + user-select: none; + } + .group-section-header::-webkit-details-marker { display: none; } + .group-section-header::before { + content: "▶"; + font-size: 10px; + color: var(--secondary-text-color); + transition: transform 0.15s; + } + .group-section[open] .group-section-header::before { transform: rotate(90deg); } + .group-section-count { + color: var(--secondary-text-color); + font-size: 13px; + font-weight: 400; + } + .group-section .objects-grid, + .group-section .task-table { + padding: 0 12px; + } + + .empty-state-centered { text-align: center; padding: 32px 16px; } + .empty-state-centered ha-button { margin-top: 16px; } + + .stat-value { + font-size: 24px; + font-weight: bold; + color: var(--primary-text-color); + } + + .stat-label { + font-size: 12px; + color: var(--secondary-text-color); + } + + .card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + } + + .card-header h1 { + margin: 0; + font-size: 20px; + font-weight: 500; + } + + .action-buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; + } + + .action-buttons ha-button { + --ha-button-font-size: 13px; + } + + .history-timeline { padding: 0 16px 16px; } + + .history-entry { + display: flex; + gap: 12px; + padding: 8px 0; + border-bottom: 1px solid var(--divider-color); + /* Long histories: skip painting off-screen entries (flex, not subgrid, so + safe — subgrid task rows can't use this without breaking alignment). */ + content-visibility: auto; + contain-intrinsic-size: auto 48px; + } + .history-entry:last-child { border-bottom: none; } + + .history-icon { + flex-shrink: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: white; + } + + .history-icon.completed { background: var(--maint-ok-color); } + .history-icon.skipped { background: var(--secondary-text-color); } + .history-icon.reset { background: var(--info-color, #2196f3); } + .history-icon.triggered { background: var(--maint-triggered-color); } + + .history-content { flex: 1; min-width: 0; } + + /* v2.2.0 — row holds the type label + the small Edit button */ + .history-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + .history-edit-btn { + background: transparent; + color: var(--secondary-text-color); + border: none; + border-radius: 4px; + padding: 4px; + cursor: pointer; + display: inline-flex; + align-items: center; + transition: background 0.15s, color 0.15s; + } + .history-edit-btn:hover { + background: var(--secondary-background-color); + color: var(--primary-color); + } + .history-edit-btn ha-icon { --mdc-icon-size: 16px; } + + .history-date { + font-size: 12px; + color: var(--secondary-text-color); + } + + .history-details { + display: flex; + gap: 12px; + font-size: 13px; + color: var(--secondary-text-color); + margin-top: 4px; + } + + /* History filter chips */ + .history-filters { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; + } + + .filter-chip { + display: inline-flex; + align-items: center; + padding: 4px 12px; + border-radius: 16px; + font-size: 12px; + cursor: pointer; + background: var(--secondary-background-color, #f5f5f5); + color: var(--primary-text-color); + border: 1px solid var(--divider-color); + transition: all 0.2s; + user-select: none; + } + + .filter-chip:hover { background: var(--divider-color); } + + .filter-chip.active { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color); + } + + .filter-chip.clear { + font-style: italic; + opacity: 0.7; + } + + /* Cost/Duration history chart */ + .history-chart { + width: 100%; + height: 200px; + display: block; + } + + .chart-legend { + display: flex; + justify-content: center; + gap: 16px; + margin-top: 4px; + font-size: 11px; + color: var(--secondary-text-color); + } + + .legend-item { + display: inline-flex; + align-items: center; + gap: 4px; + } + + .legend-swatch { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 2px; + } + + .empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 16px; + color: var(--secondary-text-color); + } + + .empty-state ha-svg-icon { + --mdc-icon-size: 48px; + margin-bottom: 16px; + } + + /* Sparkline chart */ + .sparkline-container { position: relative; margin: 8px 0; } + + .sparkline-svg { + width: 100%; + height: 140px; + display: block; + } + + /* Trigger info card */ + .trigger-card { + background: var(--card-background-color, #fff); + border-radius: 12px; + padding: 12px 16px; + margin: 8px 0; + border: 1px solid var(--divider-color); + } + + .trigger-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + } + + .trigger-entity-name { font-weight: 500; font-size: 14px; } + .trigger-entity-id { font-size: 11px; color: var(--secondary-text-color); font-family: monospace; } + + .entity-link { + cursor: pointer; + text-decoration: underline dotted; + text-underline-offset: 2px; + } + .entity-link:hover { + color: var(--primary-color); + text-decoration: underline solid; + } + + .trigger-value-row { + display: flex; + align-items: baseline; + gap: 6px; + margin: 4px 0; + } + + .trigger-current { font-size: 28px; font-weight: 700; color: var(--primary-text-color); } + .trigger-current.active { color: var(--maint-triggered-color); } + .trigger-unit { font-size: 14px; color: var(--secondary-text-color); } + + /* Counter progress ("8,507 / 15,000 km · 57 %" + bar) */ + .counter-progress { margin: 6px 0 4px; } + .counter-progress-nums { + display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; + } + .counter-progress-main { font-size: 26px; font-weight: 700; color: var(--primary-text-color); } + .counter-progress-target { font-size: 15px; font-weight: 500; color: var(--secondary-text-color); } + .counter-progress-pct { font-size: 15px; font-weight: 700; } + .counter-progress-pct.ok { color: var(--success-color, #4caf50); } + .counter-progress-pct.near { color: var(--warning-color, #ff9800); } + .counter-progress-pct.over { color: var(--error-color, #f44336); } + .counter-progress-bar { + height: 8px; border-radius: 4px; margin: 6px 0 4px; overflow: hidden; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.08)); + } + .counter-progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s ease; } + .counter-progress-fill.ok { background: var(--success-color, #4caf50); } + .counter-progress-fill.near { background: var(--warning-color, #ff9800); } + .counter-progress-fill.over { background: var(--error-color, #f44336); } + .counter-progress-caption { font-size: 12px; color: var(--secondary-text-color); } + + /* Note under a chart that fell back to sparse maintenance-event values */ + .chart-note { + display: flex; align-items: center; gap: 6px; margin-top: 2px; + font-size: 12px; color: var(--secondary-text-color); + } + .chart-note ha-icon { --mdc-icon-size: 15px; flex: none; } + + .trigger-limits { + display: flex; + gap: 16px; + font-size: 13px; + color: var(--secondary-text-color); + margin: 6px 0; + flex-wrap: wrap; + } + + .trigger-limit-item { + display: flex; + align-items: center; + gap: 4px; + } + + .trigger-limit-item .dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + .trigger-limit-item .dot.warn { background: var(--error-color, #f44336); } + .trigger-limit-item .dot.range { background: var(--secondary-text-color); } + .trigger-limit-item .dot.ok { background: var(--maint-ok-color); } + + /* Row action buttons */ + .row-actions { + display: flex; + gap: 0; + flex-shrink: 0; + margin-left: auto; + } + + .row-actions mwc-icon-button { + --mdc-icon-button-size: 32px; + --mdc-icon-size: 18px; + } + + .row-actions .btn-complete { color: var(--maint-ok-color); } + .row-actions .btn-skip { color: var(--secondary-text-color); } + + /* Days bar for overview */ + .due-cell { + display: flex; + flex-direction: column; + align-items: flex-end; + min-width: 90px; + gap: 2px; + } + + .due-text { font-size: 13px; } + + .days-bar { + width: 100%; + height: 3px; + background: var(--divider-color); + border-radius: 2px; + overflow: hidden; + } + + .days-bar-fill { + height: 100%; + border-radius: 2px; + transition: width 0.3s; + } + + /* Trigger progress bar (overview rows) */ + .trigger-progress { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 90px; + } + + .trigger-progress-bar { + width: 100%; + height: 6px; + background: var(--divider-color); + border-radius: 3px; + overflow: hidden; + } + + .trigger-progress-fill { + height: 100%; + border-radius: 2px; + transition: width 0.3s; + } + + .trigger-progress-label { + font-size: 12px; + color: var(--secondary-text-color); + text-align: right; + } + + /* Days progress bar (detail view) */ + .days-progress { + margin: 8px 0 16px; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color); + } + + .days-progress-labels { + display: flex; + justify-content: space-between; + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 6px; + } + + .days-progress-bar { + width: 100%; + height: 6px; + background: var(--divider-color); + border-radius: 3px; + overflow: hidden; + } + + .days-progress-fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s; + } + + .days-progress-text { + font-size: 13px; + font-weight: 500; + text-align: center; + margin-top: 6px; + color: var(--primary-text-color); + } + + /* Mini-sparkline in overview rows */ + .mini-sparkline { + width: 60px; + height: 20px; + display: block; + margin-top: 2px; + opacity: 0.7; + } + + /* Overflow indicator for overdue progress bars */ + .days-bar-fill.overflow, + .days-progress-fill.overflow, + .trigger-progress-fill.overflow { + background-image: repeating-linear-gradient( + -45deg, + transparent, + transparent 3px, + rgba(255,255,255,0.2) 3px, + rgba(255,255,255,0.2) 6px + ); + animation: overflow-pulse 2s ease-in-out infinite; + } + + @keyframes overflow-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } + } + + /* Budget bars */ + .budget-bars { + display: flex; + gap: 16px; + padding: 8px 16px; + flex-wrap: wrap; + } + + .budget-item { + flex: 1; + min-width: 200px; + } + + .budget-label { + display: flex; + justify-content: space-between; + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 4px; + } + + .budget-bar { + width: 100%; + height: 6px; + background: var(--divider-color); + border-radius: 3px; + overflow: hidden; + } + + .budget-bar-fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s; + } + + /* Groups section */ + .groups-section { + padding: 8px 16px 16px; + } + + .groups-section h3 { + font-size: 14px; + font-weight: 500; + color: var(--secondary-text-color); + margin: 0 0 8px; + } + + .groups-grid { + display: flex; + gap: 12px; + flex-wrap: wrap; + } + + .group-card { + background: var(--card-background-color, #fff); + border: 1px solid var(--divider-color); + border-radius: 12px; + padding: 12px 16px; + min-width: 180px; + flex: 1; + max-width: 300px; + cursor: default; + } + + .group-card-head { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; + } + + .group-card-name { + font-weight: 500; + font-size: 14px; + margin-bottom: 4px; + } + + .group-card-actions { + display: flex; + gap: 0; + } + .group-card-actions mwc-icon-button { + --mdc-icon-button-size: 28px; + --mdc-icon-size: 16px; + color: var(--secondary-text-color); + } + + .groups-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + margin-bottom: 8px; + } + .groups-header h3 { margin: 0; } + + .seasonal-actions { + display: flex; + justify-content: flex-end; + padding: 4px 0; + } + + .group-card-desc { + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 8px; + } + + .group-card-tasks { + display: flex; + gap: 6px; + flex-wrap: wrap; + } + + .group-task-chip { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + padding: 2px 8px; + border-radius: 10px; + background: var(--secondary-background-color, #f5f5f5); + color: var(--primary-text-color); + } + + /* Adaptive scheduling suggestion badge */ + .suggestion-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 16px; + font-size: 12px; + font-weight: 500; + background: var(--info-color, #2196f3); + color: white; + margin-left: 8px; + } + + .suggestion-actions { + display: flex; + gap: 8px; + margin-top: 8px; + } + + .suggestion-actions ha-button { + --ha-button-font-size: 12px; + } + + .confidence-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + + .confidence-dot.low { background: var(--secondary-text-color); } + .confidence-dot.medium { background: var(--warning-color, #ff9800); } + .confidence-dot.high { background: var(--success-color, #4caf50); } + + /* Feedback toggle buttons in complete dialog */ + .feedback-section { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 0; + border-top: 1px solid var(--divider-color); + } + + .feedback-label { + font-weight: 500; + font-size: 13px; + color: var(--secondary-text-color); + } + + .feedback-buttons { + display: flex; + gap: 8px; + } + + .feedback-btn { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--divider-color); + border-radius: 8px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 13px; + cursor: pointer; + text-align: center; + transition: all 0.2s; + } + + .feedback-btn:hover { + background: var(--secondary-background-color, #f5f5f5); + } + + .feedback-btn.selected { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color); + } + + /* Seasonal chart */ + .seasonal-chart { + padding: 12px 16px; + margin: 8px 0; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color); + } + + .seasonal-chart-title { + font-size: 13px; + font-weight: 500; + color: var(--secondary-text-color); + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 6px; + } + + .seasonal-chart-title .source-tag { + font-size: 10px; + padding: 1px 6px; + border-radius: 8px; + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + font-weight: 400; + } + + .seasonal-chart svg { + width: 100%; + height: 100px; + display: block; + } + + .seasonal-labels { + display: flex; + justify-content: space-between; + padding: 0 2px; + margin-top: 4px; + } + + .seasonal-label { + font-size: 10px; + color: var(--secondary-text-color); + text-align: center; + flex: 1; + } + + .seasonal-label.active-month { + font-weight: 700; + color: var(--primary-color); + } + + .seasonal-factor-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 500; + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + margin-left: 6px; + } + + .seasonal-factor-tag.short { + background: rgba(76, 175, 80, 0.15); + color: var(--success-color, #4caf50); + } + + .seasonal-factor-tag.long { + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + } + + /* --- Sensor Prediction Section (Phase 3) --- */ + + .prediction-section { + margin: 16px 0; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color, #e0e0e0); + } + + .prediction-urgency-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + margin-bottom: 12px; + border-radius: 8px; + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + font-size: 13px; + font-weight: 500; + } + .prediction-urgency-banner ha-svg-icon { + --mdc-icon-size: 18px; + flex-shrink: 0; + } + + .prediction-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--primary-text-color); + margin-bottom: 10px; + } + .prediction-title ha-svg-icon { + --mdc-icon-size: 16px; + color: var(--primary-color); + } + + .prediction-grid { + display: flex; + flex-wrap: wrap; + gap: 12px; + } + + .prediction-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--secondary-text-color); + } + .prediction-item ha-svg-icon { + --mdc-icon-size: 14px; + color: var(--secondary-text-color); + flex-shrink: 0; + } + + .prediction-label { + font-weight: 500; + } + + .prediction-value { + font-weight: 600; + color: var(--primary-text-color); + } + .prediction-value.rising { color: var(--error-color, #f44336); } + .prediction-value.falling { color: var(--info-color, #2196f3); } + .prediction-value.stable { color: var(--success-color, #4caf50); } + .prediction-value.exceeded { color: var(--error-color, #f44336); font-weight: 700; } + .prediction-value.urgent { color: var(--warning-color, #ff9800); font-weight: 700; } + + .prediction-rate { + font-size: 11px; + opacity: 0.7; + font-family: monospace; + } + + .prediction-date { + font-size: 11px; + opacity: 0.7; + } + + .prediction-entity { + font-size: 10px; + opacity: 0.6; + font-family: monospace; + } + + /* --- Weibull Reliability Section (Phase 4) --- */ + + .weibull-section { + margin: 16px 0; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color, #e0e0e0); + } + + .weibull-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--primary-text-color); + margin-bottom: 10px; + } + .weibull-title ha-svg-icon { + --mdc-icon-size: 16px; + color: var(--primary-color); + } + + .weibull-chart svg { + width: 100%; + height: 160px; + display: block; + } + + .weibull-info-row { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-top: 10px; + } + + .weibull-info-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--secondary-text-color); + } + + .weibull-info-value { + font-weight: 600; + color: var(--primary-text-color); + } + + /* Beta interpretation badge */ + .beta-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + white-space: nowrap; + } + .beta-badge ha-svg-icon { + --mdc-icon-size: 14px; + } + + .beta-badge.early_failures { + background: rgba(244, 67, 54, 0.15); + color: var(--error-color, #f44336); + } + .beta-badge.random_failures { + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + } + .beta-badge.wear_out { + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + } + .beta-badge.highly_predictable { + background: rgba(76, 175, 80, 0.15); + color: var(--success-color, #4caf50); + } + + /* Confidence interval range bar */ + .confidence-range { + margin-top: 12px; + } + + .confidence-range-title { + font-size: 12px; + font-weight: 500; + color: var(--secondary-text-color); + margin-bottom: 6px; + } + + .confidence-bar { + position: relative; + width: 100%; + height: 8px; + background: var(--divider-color, #e0e0e0); + border-radius: 4px; + overflow: visible; + } + + .confidence-fill { + position: absolute; + height: 100%; + border-radius: 4px; + background: var(--primary-color, #03a9f4); + opacity: 0.25; + } + + .confidence-marker { + position: absolute; + top: -4px; + width: 3px; + height: 16px; + border-radius: 1px; + transform: translateX(-50%); + } + .confidence-marker.recommended { + background: var(--success-color, #4caf50); + } + .confidence-marker.current { + background: var(--primary-color, #03a9f4); + } + + .confidence-labels { + display: flex; + justify-content: space-between; + margin-top: 4px; + } + + .confidence-text { + font-size: 10px; + color: var(--secondary-text-color); + } + .confidence-text.low { + text-align: left; + } + .confidence-text.high { + text-align: right; + } + + .task-disabled { opacity: 0.5; } + .badge-disabled { + font-size: 10px; + padding: 1px 6px; + border-radius: 8px; + background: var(--disabled-color, #9e9e9e); + color: white; + } + + /* ── Shared responsive styles (panel + card) ── */ + @media (max-width: 600px) { + .row-actions mwc-icon-button { + --mdc-icon-button-size: 44px; + --mdc-icon-size: 22px; + } + + .due-cell { min-width: 70px; } + + .trigger-card { padding: 10px 12px; } + .trigger-current { font-size: 22px; } + + .prediction-grid { flex-direction: column; gap: 8px; } + + .weibull-info-row { flex-direction: column; gap: 8px; } + + .budget-bars { flex-direction: column; } + .budget-item { min-width: 0; } + + .group-card { min-width: 0; max-width: 100%; } + + .filter-chip { padding: 6px 12px; font-size: 13px; } + + .history-details { flex-wrap: wrap; gap: 6px; } + + .sparkline-container { max-width: 100%; overflow: hidden; } + .sparkline-svg { height: 100px; } + + .stats-bar { gap: 8px; padding: 12px; } + .stat-item { min-width: 60px; } + .stat-value { font-size: 20px; } + } +`; diff --git a/custom_components/maintenance_supporter/frontend-src/test-entity-attributes.mjs b/custom_components/maintenance_supporter/frontend-src/test-entity-attributes.mjs new file mode 100644 index 0000000..4d3be33 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/test-entity-attributes.mjs @@ -0,0 +1,112 @@ +/** + * Quick Playwright test for entity attribute introspection. + * Run from frontend-src: node test-entity-attributes.mjs + */ +import { chromium } from "playwright"; + +const HA_URL = "http://homeassistant-dev:8123"; + +async function main() { + const browser = await chromium.connect("ws://localhost:3000"); + const context = await browser.newContext(); + const page = await context.newPage(); + + // Navigate to HA + await page.goto(HA_URL); + await page.waitForTimeout(2000); + + // Check if we need to login + const url = page.url(); + console.log("Current URL:", url); + + if (url.includes("/auth/authorize") || url.includes("onboarding")) { + // Try auto-login - look for login form + const loginField = await page.$('input[name="username"]'); + if (loginField) { + await loginField.fill("dev"); + const pwField = await page.$('input[name="password"]'); + if (pwField) { + await pwField.fill("dev"); + await page.click('mwc-button, button[type="submit"], ha-button'); + await page.waitForTimeout(3000); + } + } + } + + console.log("After login URL:", page.url()); + + // Test the WS endpoint via browser console + const result = await page.evaluate(async () => { + // Access hass object + const ha = document.querySelector("home-assistant"); + if (!ha || !ha.hass) { + return { error: "No hass object found" }; + } + + // Test entity/attributes endpoint + try { + const res = await ha.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/entity/attributes", + entity_id: "sensor.sun_solar_elevation", + }); + return { success: true, result: res }; + } catch (e) { + return { error: e.message || String(e) }; + } + }); + + console.log("\n=== Entity Attributes WS Test ==="); + console.log(JSON.stringify(result, null, 2)); + + // Test with a non-existent entity to verify graceful handling + const result2 = await page.evaluate(async () => { + const ha = document.querySelector("home-assistant"); + try { + const res = await ha.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/entity/attributes", + entity_id: "climate.nonexistent", + }); + return { success: true, result: res }; + } catch (e) { + return { error: e.message || String(e) }; + } + }); + + console.log("\n=== Non-existent Entity Test ==="); + console.log(JSON.stringify(result2, null, 2)); + + // Check binary_sensor entities + const binarySensors = await page.evaluate(async () => { + const ha = document.querySelector("home-assistant"); + if (!ha || !ha.hass) return { error: "No hass" }; + const states = ha.hass.states; + const ms = {}; + for (const [k, v] of Object.entries(states)) { + if (k.includes("maintenance")) { + ms[k] = { state: v.state, attributes: v.attributes }; + } + } + return ms; + }); + + console.log("\n=== Maintenance Entities ==="); + console.log(JSON.stringify(binarySensors, null, 2)); + + // Navigate to panel and test the UI + await page.goto(HA_URL + "/maintenance_supporter_panel"); + await page.waitForTimeout(3000); + + // Take screenshot + await page.screenshot({ path: "/tmp/panel-test.png", fullPage: true }); + console.log("\nScreenshot saved to /tmp/panel-test.png"); + + await context.close(); + await browser.close(); + + console.log("\n=== All tests completed ==="); +} + +main().catch((e) => { + console.error("Test failed:", e); + process.exit(1); +}); diff --git a/custom_components/maintenance_supporter/frontend-src/tsconfig.json b/custom_components/maintenance_supporter/frontend-src/tsconfig.json new file mode 100644 index 0000000..bd39eea --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ES2021", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "esModuleInterop": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "experimentalDecorators": true, + "useDefineForClassFields": false, + "declaration": false, + "sourceMap": false, + "outDir": "../frontend", + "lib": ["ES2021", "DOM", "DOM.Iterable"] + }, + "include": ["*.ts", "components/*.ts", "renderers/*.ts"] +} diff --git a/custom_components/maintenance_supporter/frontend-src/types.ts b/custom_components/maintenance_supporter/frontend-src/types.ts new file mode 100644 index 0000000..c063784 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/types.ts @@ -0,0 +1,421 @@ +/** TypeScript interfaces for the Maintenance Supporter frontend. */ + +export interface MaintenanceObject { + id: string; + name: string; + area_id?: string | null; + manufacturer?: string | null; + model?: string | null; + serial_number?: string | null; + installation_date?: string | null; + /** (#67): warranty expiry date (ISO YYYY-MM-DD) for asset tracking */ + warranty_expiry?: string | null; + // 2.19: attach to an existing HA device / nest under another object + ha_device_id?: string | null; + parent_entry_id?: string | null; + /** v1.4.0 (#43): optional link to PDF manual / vendor page for the object */ + documentation_url?: string | null; + /** v1.4.10 (#46): free-form notes — part numbers, procedures, etc. */ + notes?: string | null; + /** v2.10.0 archive: archived (retire-but-retain) state; archived = archived_at != null. */ + archived?: boolean; + archived_at?: string | null; + /** v2.20 (N3) seasonal pause: paused = paused_at != null; paused_until is + * the optional auto-resume date. */ + paused?: boolean; + paused_at?: string | null; + paused_until?: string | null; + /** v2.20 (N1) replace-flow lineage (both directions). */ + predecessor_entry_id?: string | null; + replaced_by_entry_id?: string | null; + /** (roadmap P2) number of attached documents (files + web-links); drives the + * objects-table paperclip badge. Computed server-side, not persisted. */ + document_count?: number; +} + +export interface TriggerConfig { + entity_id?: string; + entity_ids?: string[]; + entity_logic?: "any" | "all"; + attribute?: string | null; + type?: string; // "threshold" | "counter" | "state_change" | "runtime" + trigger_above?: number | null; + trigger_below?: number | null; + trigger_for_minutes?: number; + trigger_target_value?: number; + trigger_delta_mode?: boolean; + trigger_baseline_value?: number | null; + trigger_from_state?: string | null; + trigger_to_state?: string | null; + trigger_target_changes?: number; + trigger_runtime_hours?: number; + compound_logic?: "AND" | "OR"; + conditions?: Array; + /** Record a completion when the trigger clears itself (#53). */ + auto_complete_on_recovery?: boolean; +} + +export interface TriggerEntityInfo { + entity_id: string; + friendly_name: string; + unit_of_measurement?: string | null; + min?: number | null; + max?: number | null; + step?: number | null; +} + +export interface HistoryEntry { + timestamp: string; + type: string; // "completed" | "skipped" | "reset" | "triggered" + notes?: string | null; + cost?: number | null; + duration?: number | null; + trigger_value?: number | null; + checklist_state?: Record | null; + feedback?: string | null; + completed_by?: string | null; + photo_doc_id?: string | null; + /** v2.20 (#83): recorded value for `reading`-type tasks. */ + reading_value?: number | null; +} + +export interface AdaptiveConfig { + enabled: boolean; + ewa_alpha?: number; + min_interval_days?: number; + max_interval_days?: number; + smoothed_interval?: number; + feedback_count?: number; + confidence?: string; + weibull_beta?: number | null; + weibull_eta?: number | null; + current_recommendation?: number | null; + recommendation_reason?: string | null; + last_analysis_date?: string | null; + seasonal_enabled?: boolean; + seasonal_overrides?: Record | null; + // Sensor prediction (Phase 3) + sensor_prediction_enabled?: boolean; + environmental_entity?: string | null; + environmental_attribute?: string | null; +} + +export interface IntervalAnalysis { + average_actual?: number | null; + ewa_prediction?: number | null; + weibull_beta?: number | null; + weibull_eta?: number | null; + weibull_r_squared?: number | null; + data_points?: number; + reason?: string | null; + seasonal_factor?: number | null; + seasonal_factors?: number[] | null; + seasonal_reason?: string | null; // "learned" | "manual" + confidence_interval_low?: number | null; + confidence_interval_high?: number | null; +} + +/** Nested recurrence object (schedule-model v2). The flat fields above remain + * for the interval/one_time kinds; the calendar kinds (weekdays / nth_weekday / + * day_of_month) can only be expressed here. weekday: 0=Mon … 6=Sun. */ +export interface TaskSchedule { + kind: string; // interval | weekdays | nth_weekday | day_of_month | one_time | manual + every?: number | null; + unit?: string; + anchor?: string; + due_date?: string | null; + weekdays?: number[]; + nth?: number; // 1..5, or -1 = last + weekday?: number; + day?: number; // 1..31, or -1 = last day of the month (#83) + months?: number[]; + /** (#83) day_of_month only: roll a weekend date back to Friday. */ + business?: boolean; + /** (#83) shift the computed occurrence by ±N days (clamped ±15). */ + offset?: number; +} + +export interface MaintenanceTask { + id: string; + name: string; + type: string; // "cleaning" | "inspection" | "replacement" | "calibration" | "service" | "reading" | "custom" + enabled: boolean; + schedule_type: string; // "time_based" | "sensor_based" | "one_time" | "manual" | calendar kind + interval_days?: number | null; + interval_unit?: string; // "days" | "weeks" | "months" | "years" + due_date?: string | null; // one-time task due date (ISO) + interval_anchor?: "completion" | "planned"; + schedule?: TaskSchedule; // nested recurrence (calendar kinds read this) + schedule_time?: string | null; // "HH:MM" or null/undefined = midnight + warning_days: number; + last_performed?: string | null; + notes?: string | null; + documentation_url?: string | null; + checklist?: string[]; + labels?: string[]; + assignee_pool?: string[]; + rotation_strategy?: string | null; + earliest_completion_days?: number | null; + // v1.3.0: completion-action + quick-complete (gated by completion_actions feature) + on_complete_action?: { + service: string; // "domain.service" + target?: { entity_id?: string | string[]; device_id?: string | string[]; area_id?: string | string[] }; + data?: Record; + } | null; + quick_complete_defaults?: { + notes?: string; + cost?: number; + duration?: number; + feedback?: "needed" | "not_needed"; + } | null; + trigger_config?: TriggerConfig | null; + trigger_entity_info?: TriggerEntityInfo | null; + trigger_entity_infos?: TriggerEntityInfo[] | null; + history: HistoryEntry[]; + // Computed + status: string; // "ok" | "due_soon" | "overdue" | "triggered" | "archived" + /** True for a one-time task that has been completed (done; never re-arms). */ + is_done?: boolean; + /** v2.10.0 archive: archived = archived_at != null; reason is manual|auto|object. */ + archived?: boolean; + archived_at?: string | null; + archived_reason?: string | null; + days_until_due?: number | null; + next_due?: string | null; + trigger_active: boolean; + trigger_current_value?: number | null; + trigger_current_delta?: number | null; + trigger_baseline_value?: number | null; + trigger_entity_state?: string; + times_performed: number; + total_cost: number; + average_duration?: number | null; + // Adaptive scheduling + adaptive_config?: AdaptiveConfig | null; + suggested_interval?: number | null; + interval_confidence?: string | null; + interval_analysis?: IntervalAnalysis | null; + // Seasonal scheduling (top-level convenience) + seasonal_factor?: number | null; + seasonal_factors?: number[] | null; + // Sensor-driven predictions (Phase 3) + degradation_rate?: number | null; + degradation_trend?: string | null; // "rising" | "falling" | "stable" | "insufficient_data" + degradation_r_squared?: number | null; + days_until_threshold?: number | null; + threshold_prediction_date?: string | null; + threshold_prediction_confidence?: string | null; // "low" | "medium" | "high" + environmental_factor?: number | null; + environmental_entity?: string | null; + environmental_correlation?: number | null; + sensor_prediction_urgency?: boolean; + // User assignment + responsible_user_id?: string | null; + custom_icon?: string | null; + nfc_tag_id?: string | null; + /** v2.20 (#83): display unit for `reading`-type tasks ("kWh", "m³", ...). */ + reading_unit?: string | null; + priority?: string | null; + entity_slug?: string | null; + // Auto-derived sensor + binary_sensor entity_ids (since 1.0.45) + sensor_entity_id?: string | null; + binary_sensor_entity_id?: string | null; +} + +export interface MaintenanceObjectResponse { + entry_id: string; + object: MaintenanceObject; + tasks: MaintenanceTask[]; +} + +export interface StatisticsResponse { + total_objects: number; + total_tasks: number; + overdue: number; + due_soon: number; + triggered: number; + total_cost: number; + budget?: { currency_symbol?: string } | null; +} + +export interface CardConfig { + type: string; + title?: string; + show_header?: boolean; + max_items?: number; + filter_status?: string[]; + filter_objects?: string[]; + // HA-native entity_ids: pattern (since 1.0.45). When set, only tasks whose + // sensor or binary_sensor entity_id matches one of these are shown. Combines + // additively with filter_status / filter_objects. + entity_ids?: string[]; + // Range filter on task.days_until_due (since 1.7.0). Inclusive on both + // ends. Used by the dashboard strategy's group_by=due_date buckets: + // Today: min=0, max=0 + // This Week: min=1, max=7 + // This Month: min=8, max=30 + // Later: min=31 + // Overdue: max=-1 + // Tasks with null/undefined days_until_due (e.g. sensor-triggered without + // a computed next_due) are excluded when either bound is set. + filter_due_min_days?: number; + filter_due_max_days?: number; + compact?: boolean; + show_actions?: boolean; +} + +export interface GroupTaskRef { + entry_id: string; + task_id: string; +} + +export interface MaintenanceGroup { + name: string; + description: string; + task_refs: GroupTaskRef[]; +} + +export interface BudgetStatus { + monthly_budget: number; + monthly_spent: number; + yearly_budget: number; + yearly_spent: number; + alert_threshold_pct: number; + currency_symbol: string; +} + +export interface AdvancedFeatures { + adaptive: boolean; + predictions: boolean; + seasonal: boolean; + environmental: boolean; + budget: boolean; + groups: boolean; + checklists: boolean; + schedule_time: boolean; + /** v1.3.0: gates per-task on_complete_action + quick_complete_defaults UI. */ + completion_actions: boolean; +} + +/** A single point in a recorder statistics time series. */ +export interface StatisticsPoint { + ts: number; // epoch ms + val: number; // mean (threshold) or state (counter) + min?: number; + max?: number; +} + +/** Cached statistics result for a single entity. */ +export interface EntityStatisticsCache { + entityId: string; + fetchedAt: number; + period: "hour" | "day"; + points: StatisticsPoint[]; +} + +/** Shape of a single row from HA recorder/statistics_during_period response */ +export interface HAStatisticsRow { + start: number; // epoch ms + end: number; + mean?: number | null; + min?: number | null; + max?: number | null; + state?: number | null; + sum?: number | null; +} + +// Flatten task with parent object info for table display +export interface TaskRow { + entry_id: string; + task_id: string; + object_name: string; + task_name: string; + type: string; + schedule_type: string; + status: string; + days_until_due: number | null; + next_due: string | null; + trigger_active: boolean; + trigger_current_value: number | null; + trigger_current_delta: number | null; + trigger_config: TriggerConfig | null; + trigger_entity_info: TriggerEntityInfo | null; + times_performed: number; + total_cost: number; + interval_days: number | null; + interval_unit?: string | null; + interval_anchor: "completion" | "planned" | null; + is_done: boolean; + archived: boolean; + history: HistoryEntry[]; + enabled: boolean; + nfc_tag_id: string | null; + priority: string; + labels: string[]; + area_id: string | null; + responsible_user_id: string | null; + group_names: string[]; +} + +// HomeAssistant type (minimal for our needs) +export interface HassEntity { + entity_id: string; + state: string; + attributes: Record; + last_changed: string; + last_updated: string; +} + +export interface HomeAssistant { + connection: { + // Result defaults to `any` (home-assistant-js-websocket ergonomics): + // call sites annotate `` where the shape matters. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sendMessagePromise(msg: Record): Promise; + subscribeMessage( + callback: (msg: unknown) => void, + subscribeMsg: Record, + options?: Record + ): Promise<() => void>; + }; + callService( + domain: string, + service: string, + data?: Record + ): Promise; + states: Record; + areas?: Record; + /** + * HA service registry, mirroring the structure exposed to the frontend. + * Used by the task-dialog action section to drive ha-service-picker + * (autocomplete) + ha-form (schema-driven data fields). + */ + services?: Record; + fields?: Record; + }>; + }>>; + language: string; + locale?: { language: string; number_format?: string }; + localize(key: string, ...args: unknown[]): string; + user?: { id: string; name: string; is_admin: boolean; is_owner: boolean }; + /** Current access token — used for authenticated `fetch()` to our HTTP + * document views (a WebSocket frame can't carry a multipart file upload). */ + auth?: { data?: { access_token?: string } }; +} + +export interface HAUser { + id: string; + name: string; + // Only returned to admin callers of users/list; absent for non-admins. + is_admin?: boolean; + is_owner?: boolean; +} diff --git a/custom_components/maintenance_supporter/frontend-src/user-service.ts b/custom_components/maintenance_supporter/frontend-src/user-service.ts new file mode 100644 index 0000000..4f07f75 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/user-service.ts @@ -0,0 +1,125 @@ +/** + * User Service - Helper for user assignment operations + * Provides caching and convenience methods for working with HA users + */ + +import type { HomeAssistant, HAUser } from "./types"; + +export class UserService { + private hass: HomeAssistant; + private usersCache: HAUser[] | null = null; + private cacheTimestamp: number = 0; + private readonly CACHE_TTL_MS = 60000; // 1 minute cache + + constructor(hass: HomeAssistant) { + this.hass = hass; + } + + updateHass(hass: HomeAssistant): void { + this.hass = hass; + } + + /** + * Get list of all active HA users (with caching) + */ + async getUsers(forceRefresh = false): Promise { + const now = Date.now(); + + // Return cached data if valid + if ( + !forceRefresh && + this.usersCache && + now - this.cacheTimestamp < this.CACHE_TTL_MS + ) { + return this.usersCache; + } + + // Fetch from backend + try { + const response = (await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/users/list", + })) as { users: HAUser[] }; + + this.usersCache = response.users; + this.cacheTimestamp = now; + return this.usersCache; + } catch (error) { + console.error("Failed to fetch users:", error); + // Return cached data if available, even if expired + return this.usersCache || []; + } + } + + /** + * Assign a user to a task (or unassign if userId is null) + */ + async assignUser( + entryId: string, + taskId: string, + userId: string | null + ): Promise { + await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/task/assign_user", + entry_id: entryId, + task_id: taskId, + user_id: userId, + }); + } + + /** + * Get tasks assigned to a specific user + */ + async getTasksByUser(userId: string): Promise { + const response = (await this.hass.connection.sendMessagePromise({ + type: "maintenance_supporter/tasks/by_user", + user_id: userId, + })) as { tasks: unknown[] }; + return response.tasks; + } + + /** + * Get user name by ID (from cache, returns null if not found) + */ + getUserName(userId: string | null): string | null { + if (!userId || !this.usersCache) { + return null; + } + + const user = this.usersCache.find((u) => u.id === userId); + return user?.name || null; + } + + /** + * Get full user object by ID (from cache) + */ + getUser(userId: string | null): HAUser | null { + if (!userId || !this.usersCache) { + return null; + } + + return this.usersCache.find((u) => u.id === userId) || null; + } + + /** + * Get current logged-in user ID + */ + getCurrentUserId(): string | null { + return this.hass.user?.id || null; + } + + /** + * Check if a user ID is the current user + */ + isCurrentUser(userId: string | null): boolean { + if (!userId) return false; + return userId === this.getCurrentUserId(); + } + + /** + * Clear the user cache (useful when users are added/removed) + */ + clearCache(): void { + this.usersCache = null; + this.cacheTimestamp = 0; + } +} diff --git a/custom_components/maintenance_supporter/frontend-src/verify-history-edit-e2e.mjs b/custom_components/maintenance_supporter/frontend-src/verify-history-edit-e2e.mjs new file mode 100644 index 0000000..312aa6d --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/verify-history-edit-e2e.mjs @@ -0,0 +1,275 @@ +/** v2.2.0 — intensive E2E for the history-edit feature. + * + * The user (Discussion #49 follow-up) explicitly asked: "das müsste intensiv + * getestet werden ob die Änderungen korrekt gespeichert werden". This script + * exercises the full roundtrip via the live HA WebSocket: + * + * 1. find an existing maintenance object + completed task with ≥1 history entry + * 2. record the original entry payload + * 3. send maintenance_supporter/task/history/update with patched fields + * 4. fetch the object again, verify the patch landed + * 5. test edge cases: clear notes (None), edit timestamp updates last_performed + * 6. read back AGAIN after a brief wait — confirms persistence isn't lost + * + * No browser UI needed — this is pure WS roundtrip, the most reliable layer. + */ +import { chromium } from "playwright"; + +const HA = "http://localhost:8125"; +const HA_TOKEN = process.env.HA_TOKEN; +if (!HA_TOKEN) { + console.error("Set HA_TOKEN env var."); + process.exit(1); +} + +async function getRefreshToken() { + async function post(path, body, ctype = "application/json") { + const data = typeof body === "string" ? body : JSON.stringify(body); + const r = await fetch(`${HA}${path}`, { + method: "POST", + headers: { "Content-Type": ctype }, + body: data, + }); + return await r.json(); + } + const flow = await post("/auth/login_flow", { + client_id: `${HA}/`, handler: ["homeassistant", null], redirect_uri: `${HA}/`, + }); + const auth = await post(`/auth/login_flow/${flow.flow_id}`, { + client_id: `${HA}/`, username: "dev", password: "dev", + }); + const tokens = await post( + "/auth/token", + `grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`, + "application/x-www-form-urlencoded", + ); + return tokens.refresh_token; +} + +const browser = await chromium.launch({ headless: true }); +const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); +const page = await ctx.newPage(); + +const refreshToken = await getRefreshToken(); +await page.goto(HA); +await page.waitForTimeout(800); +await page.evaluate(({ ha, r }) => { + localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: ha, clientId: `${ha}/`, refresh_token: r, + access_token: "", token_type: "Bearer", expires_in: 1800, expires: 0, + })); +}, { ha: HA, r: refreshToken }); +await page.goto(HA); +await page.waitForTimeout(8000); + +// All assertions go through this — runs in browser context with hass.connection +const result = await page.evaluate(async () => { + const out = { steps: [] }; + const log = (msg, data) => out.steps.push({ msg, ...(data ? { data } : {}) }); + + const hass = document.querySelector("home-assistant")?.hass; + if (!hass) { + out.error = "no hass"; + return out; + } + const ws = (m) => hass.connection.sendMessagePromise(m); + + // 1. Find an object with a completed task that has ≥1 history entry + const { objects } = await ws({ type: "maintenance_supporter/objects" }); + let testEntry = null; + let testTask = null; + for (const obj of objects) { + for (const task of obj.tasks || []) { + if (Array.isArray(task.history) && task.history.length >= 1) { + testEntry = obj; + testTask = task; + break; + } + } + if (testEntry) break; + } + if (!testEntry) { + // None exists — create a fresh object + task + complete it once + log("no history found, creating one"); + const cr = await ws({ + type: "maintenance_supporter/object/create", + name: `histtest-${Math.random().toString(36).slice(2, 8)}`, + }); + const newEntryId = cr.entry_id; + await new Promise(r => setTimeout(r, 500)); + const tr = await ws({ + type: "maintenance_supporter/task/create", + entry_id: newEntryId, + name: "edit-test-task", + schedule_type: "time_based", + interval_days: 30, + }); + const newTaskId = tr.task_id; + await new Promise(r => setTimeout(r, 500)); + await ws({ + type: "maintenance_supporter/task/complete", + entry_id: newEntryId, + task_id: newTaskId, + notes: "initial completion", + cost: 25.0, + duration: 45, + }); + await new Promise(r => setTimeout(r, 800)); + const fresh = await ws({ + type: "maintenance_supporter/object", + entry_id: newEntryId, + }); + testEntry = { entry_id: newEntryId }; + testTask = fresh.tasks.find(t => t.id === newTaskId); + } + log("test target", { + entry_id: testEntry.entry_id, + task_id: testTask.id, + history_count: testTask.history.length, + }); + + // 2. Take the FIRST completed entry (most likely to be lifecycle-relevant) + const original = testTask.history.find(h => h.type === "completed") + || testTask.history[0]; + const originalTs = original.timestamp; + log("original entry", { ts: originalTs, notes: original.notes, cost: original.cost }); + + // 3. PATCH 1 — change notes only + const patchedNotes = `e2e-patch-${Date.now()}`; + const r1 = await ws({ + type: "maintenance_supporter/task/history/update", + entry_id: testEntry.entry_id, + task_id: testTask.id, + original_timestamp: originalTs, + notes: patchedNotes, + }); + log("patch1 result", r1); + out.patch1_ok = r1.success === true; + + // 4. Verify roundtrip + await new Promise(r => setTimeout(r, 800)); + const round1 = await ws({ + type: "maintenance_supporter/object", + entry_id: testEntry.entry_id, + }); + const taskAfter1 = round1.tasks.find(t => t.id === testTask.id); + const entryAfter1 = taskAfter1.history.find(h => h.timestamp === originalTs); + out.notes_persisted = entryAfter1?.notes === patchedNotes; + log("after patch1", { notes: entryAfter1?.notes }); + + // 5. PATCH 2 — change cost AND duration in one call + const r2 = await ws({ + type: "maintenance_supporter/task/history/update", + entry_id: testEntry.entry_id, + task_id: testTask.id, + original_timestamp: originalTs, + cost: 99.95, + duration: 123, + }); + out.patch2_ok = r2.success === true; + await new Promise(r => setTimeout(r, 800)); + const round2 = await ws({ + type: "maintenance_supporter/object", + entry_id: testEntry.entry_id, + }); + const taskAfter2 = round2.tasks.find(t => t.id === testTask.id); + const entryAfter2 = taskAfter2.history.find(h => h.timestamp === originalTs); + out.cost_persisted = entryAfter2?.cost === 99.95; + out.duration_persisted = entryAfter2?.duration === 123; + out.notes_still_there = entryAfter2?.notes === patchedNotes; // unchanged + out.type_unchanged = entryAfter2?.type === original.type; // never patchable + + // 6. PATCH 3 — clear notes via null + const r3 = await ws({ + type: "maintenance_supporter/task/history/update", + entry_id: testEntry.entry_id, + task_id: testTask.id, + original_timestamp: originalTs, + notes: null, + }); + out.patch3_ok = r3.success === true; + await new Promise(r => setTimeout(r, 800)); + const round3 = await ws({ + type: "maintenance_supporter/object", + entry_id: testEntry.entry_id, + }); + const taskAfter3 = round3.tasks.find(t => t.id === testTask.id); + const entryAfter3 = taskAfter3.history.find(h => h.timestamp === originalTs); + out.notes_cleared = entryAfter3?.notes == null; + + // 7. PATCH 4 — change timestamp; verify last_performed follows when this + // is the latest lifecycle entry + const lifecycleEntries = taskAfter3.history.filter( + h => ["completed", "reset", "skipped"].includes(h.type), + ); + const latestTs = lifecycleEntries.reduce( + (max, h) => h.timestamp > max ? h.timestamp : max, + "", + ); + const isLatest = entryAfter3.timestamp === latestTs; + out.is_latest_lifecycle = isLatest; + if (isLatest) { + // Bump timestamp by 1 hour + const dt = new Date(entryAfter3.timestamp); + dt.setHours(dt.getHours() + 1); + const newTs = dt.toISOString(); + const r4 = await ws({ + type: "maintenance_supporter/task/history/update", + entry_id: testEntry.entry_id, + task_id: testTask.id, + original_timestamp: entryAfter3.timestamp, + timestamp: newTs, + }); + out.patch4_ok = r4.success === true; + await new Promise(r => setTimeout(r, 800)); + const round4 = await ws({ + type: "maintenance_supporter/object", + entry_id: testEntry.entry_id, + }); + const taskAfter4 = round4.tasks.find(t => t.id === testTask.id); + const entryAfter4 = taskAfter4.history.find(h => h.timestamp === newTs); + out.timestamp_persisted = entryAfter4 != null; + out.last_performed_followed = + taskAfter4.last_performed === newTs.slice(0, 10); + } + + // 8. INVALID timestamp rejected + try { + await ws({ + type: "maintenance_supporter/task/history/update", + entry_id: testEntry.entry_id, + task_id: testTask.id, + original_timestamp: "nonexistent-2020-01-01", + notes: "oops", + }); + out.invalid_ts_rejected = false; + } catch (e) { + out.invalid_ts_rejected = true; + out.invalid_ts_error = String(e).substring(0, 200); + } + + return out; +}); + +console.log(JSON.stringify(result, null, 2)); + +const summary = [ + ["patch1 (notes) ", result.patch1_ok && result.notes_persisted], + ["patch2 (cost+duration) ", result.patch2_ok && result.cost_persisted && result.duration_persisted], + ["patch2 leaves notes intact ", result.notes_still_there], + ["patch2 leaves type intact ", result.type_unchanged], + ["patch3 (clear notes via null)", result.patch3_ok && result.notes_cleared], + ["patch4 (timestamp) ", result.is_latest_lifecycle ? (result.patch4_ok && result.timestamp_persisted) : true], + ["last_performed follows latest", result.is_latest_lifecycle ? result.last_performed_followed : true], + ["invalid original_ts rejected ", result.invalid_ts_rejected], +]; + +console.log("\n=== SUMMARY ==="); +let allOk = true; +for (const [label, ok] of summary) { + console.log(` ${ok ? "✓" : "✗"} ${label}`); + if (!ok) allOk = false; +} + +await browser.close(); +process.exit(allOk ? 0 : 1); diff --git a/custom_components/maintenance_supporter/frontend-src/verify-kpi-click.mjs b/custom_components/maintenance_supporter/frontend-src/verify-kpi-click.mjs new file mode 100644 index 0000000..7ee3482 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/verify-kpi-click.mjs @@ -0,0 +1,224 @@ +/** Verify v2.1.0 KPI click filtering (Discussion #49 — @byoung79). + * + * Loads the panel, clicks each clickable KPI, asserts the panel's + * filter dropdown reflects the new state and the .stat-item.active class + * lights up correctly. + */ +import { chromium } from "playwright"; + +const HA = "http://localhost:8125"; +const HA_TOKEN = process.env.HA_TOKEN; +if (!HA_TOKEN) { + console.error("Set HA_TOKEN env var."); + process.exit(1); +} + +async function getRefreshToken() { + async function post(path, body, ctype = "application/json") { + const data = typeof body === "string" ? body : JSON.stringify(body); + const r = await fetch(`${HA}${path}`, { + method: "POST", + headers: { "Content-Type": ctype }, + body: data, + }); + return await r.json(); + } + const flow = await post("/auth/login_flow", { + client_id: `${HA}/`, + handler: ["homeassistant", null], + redirect_uri: `${HA}/`, + }); + const auth = await post(`/auth/login_flow/${flow.flow_id}`, { + client_id: `${HA}/`, + username: "dev", + password: "dev", + }); + const tokens = await post( + "/auth/token", + `grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`, + "application/x-www-form-urlencoded", + ); + return tokens.refresh_token; +} + +const browser = await chromium.launch({ headless: true }); +const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); +const page = await ctx.newPage(); +const errors = []; +page.on("pageerror", (e) => errors.push(`pageerror: ${e.message}`)); + +const refreshToken = await getRefreshToken(); +await page.goto(HA); +await page.waitForTimeout(1000); +await page.evaluate(({ ha, r }) => { + localStorage.setItem( + "hassTokens", + JSON.stringify({ + hassUrl: ha, + clientId: `${ha}/`, + refresh_token: r, + access_token: "", + token_type: "Bearer", + expires_in: 1800, + expires: 0, + }), + ); +}, { ha: HA, r: refreshToken }); +await page.goto(`${HA}/maintenance-supporter`); +await page.waitForTimeout(12000); + +if (errors.length) { + console.log("Pre-evaluate page errors:"); + errors.slice(0, 5).forEach((e) => console.log(" " + e.substring(0, 300))); +} + +// Helper that drills into the panel's shadow root and finds the stat items. +const PANEL_QUERY = ` + (function () { + const ha = document.querySelector("home-assistant"); + const sr = ha?.shadowRoot + ?.querySelector("home-assistant-main")?.shadowRoot + ?.querySelector("ha-drawer") + ?.querySelector("partial-panel-resolver") + ?.querySelector("ha-panel-custom") + ?.querySelector("maintenance-supporter-panel")?.shadowRoot; + return sr; + })() +`; + +const result = await page.evaluate(async (q) => { + const out = {}; + const sr = eval(q); + if (!sr) { + out.error = "panel shadowRoot not found"; + return out; + } + // Wrap each step so a single failure doesn't abort the whole probe. + const safe = async (label, fn) => { + try { await fn(); } + catch (e) { out[`${label}_error`] = String(e).substring(0, 150); } + }; + + const queryItems = () => [...sr.querySelectorAll(".stat-item.clickable")]; + const findKpi = (label) => + queryItems().find((i) => + i.querySelector(".stat-label")?.textContent?.trim().toLowerCase() === + label.toLowerCase(), + ); + const findTab = (label) => + [...sr.querySelectorAll(".tab")].find((t) => + t.textContent?.trim().toLowerCase().includes(label.toLowerCase()), + ); + const activeTabLabel = () => + sr.querySelector(".tab.active")?.textContent?.trim(); + + out.kpiCount = queryItems().length; + out.kpiLabels = queryItems().map((i) => + i.querySelector(".stat-label")?.textContent?.trim(), + ); + + // ── A. From Dashboard tab — overdue click ───────────────────────────── + out.startTab = activeTabLabel(); + await safe("A", async () => { + const overdueItem = findKpi("Overdue") || findKpi("Überfällig"); + overdueItem.click(); + await new Promise((r) => setTimeout(r, 600)); + const select = sr.querySelector(".filter-bar select"); + out.A_afterOverdueClick_dropdownValue = select?.value; + out.A_afterOverdueClick_overdueActive = findKpi("Overdue")?.classList.contains("active"); + out.A_tabAfter = activeTabLabel(); + }); + + // ── B. Click Tasks to clear filter ──────────────────────────────────── + await safe("B", async () => { + const tasksItem = findKpi("Tasks") || findKpi("Aufgaben"); + tasksItem.click(); + await new Promise((r) => setTimeout(r, 600)); + const select = sr.querySelector(".filter-bar select"); + out.B_afterTasksClick_dropdownValue = select?.value; + out.B_afterTasksClick_overdueActive = + findKpi("Overdue")?.classList.contains("active") ?? null; + }); + + // ── C. Switch to Calendar tab — are KPIs still visible? ─────────────── + await safe("C", async () => { + const calendarTab = findTab("Calendar") || findTab("Kalender"); + calendarTab.click(); + await new Promise((r) => setTimeout(r, 600)); + out.C_tabAfterCalendarClick = activeTabLabel(); + out.C_kpisVisibleOnCalendar = queryItems().length > 0; + }); + + // ── D. From Calendar tab, click Overdue KPI ─────────────────────────── + await safe("D", async () => { + const overdueOnCalendar = findKpi("Overdue") || findKpi("Überfällig"); + if (!overdueOnCalendar) { + out.D_skipped = "Overdue KPI not visible from Calendar tab"; + return; + } + overdueOnCalendar.click(); + await new Promise((r) => setTimeout(r, 800)); + out.D_tabAfterOverdueFromCalendar = activeTabLabel(); + const select = sr.querySelector(".filter-bar select"); + out.D_dropdownValue = select?.value; + }); + + // ── E. Switch to Settings tab — KPIs still visible? ─────────────────── + await safe("E", async () => { + const dashTab = findTab("Dashboard") || findTab("Übersicht") || findTab("Overview"); + dashTab?.click(); + await new Promise((r) => setTimeout(r, 300)); + const settingsTab = findTab("Settings") || findTab("Einstellungen"); + if (!settingsTab) return; + settingsTab.click(); + await new Promise((r) => setTimeout(r, 600)); + out.E_tabAfterSettingsClick = activeTabLabel(); + out.E_kpisVisibleOnSettings = queryItems().length > 0; + }); + + return out; +}, PANEL_QUERY); + +console.log(JSON.stringify(result, null, 2)); + +const okFiveKpis = result.kpiCount === 5; +const okOverdueClick = + result.A_afterOverdueClick_dropdownValue === "overdue" && + result.A_afterOverdueClick_overdueActive === true; +const okTasksClick = + result.B_afterTasksClick_dropdownValue === "" && + result.B_afterTasksClick_overdueActive === false; +const okKpisOnCalendar = result.C_kpisVisibleOnCalendar === true; +const okFilterFromCalendar = + result.D_tabAfterOverdueFromCalendar?.toLowerCase().includes("dashboard") === + true || + result.D_tabAfterOverdueFromCalendar?.toLowerCase().includes("übersicht") === + true; +const okKpisOnSettings = result.E_kpisVisibleOnSettings === true; + +console.log( + "\n=== SUMMARY ===\n" + + ` 5 clickable KPIs (Dashboard tab): ${okFiveKpis ? "✓" : "✗"} (${result.kpiCount})\n` + + ` Overdue click → dropdown=overdue: ${okOverdueClick ? "✓" : "✗"}\n` + + ` Tasks click clears + dehighlight: ${okTasksClick ? "✓" : "✗"}\n` + + ` KPIs visible on Calendar tab: ${okKpisOnCalendar ? "✓" : "✗"} (${result.C_kpisVisibleOnCalendar})\n` + + ` KPIs visible on Settings tab: ${okKpisOnSettings ? "✓" : "✗"} (${result.E_kpisVisibleOnSettings})\n` + + ` Filter-from-Calendar tab works: ${okFilterFromCalendar ? "✓" : "✗"} (tab-after=${result.D_tabAfterOverdueFromCalendar}, dropdown=${result.D_dropdownValue})`, +); + +if (errors.length) { + console.log("\n=== Browser errors ==="); + errors.slice(0, 5).forEach((e) => console.log(` ${e.substring(0, 200)}`)); +} + +await browser.close(); +process.exit( + okFiveKpis && + okOverdueClick && + okTasksClick && + okKpisOnCalendar && + okKpisOnSettings && + okFilterFromCalendar + ? 0 + : 1, +); diff --git a/custom_components/maintenance_supporter/frontend-src/verify-section-cards-write.mjs b/custom_components/maintenance_supporter/frontend-src/verify-section-cards-write.mjs new file mode 100644 index 0000000..4babe65 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/verify-section-cards-write.mjs @@ -0,0 +1,116 @@ +/** Smoke-test the WS write paths used by the new section cards. + * + * 1. Enable vacation via vacation/update → expect enabled=true + * 2. Set start/end + buffer → expect persisted + * 3. Disable vacation → expect enabled=false + * 4. Set monthly + yearly budget via global/update → expect persisted + * 5. Create + rename + delete a group → expect roundtrip + */ +import http from "http"; +import WebSocket from "ws"; + +const HA = "http://localhost:8125"; +const WSURL = "ws://localhost:8125/api/websocket"; + +function post(p, body, ct = "application/json") { + return new Promise((resolve, reject) => { + const data = typeof body === "string" ? body : JSON.stringify(body); + const opts = { hostname: "localhost", port: 8125, path: p, method: "POST", + headers: { "Content-Type": ct, "Content-Length": Buffer.byteLength(data) } }; + const req = http.request(opts, (r) => { let d = ""; r.on("data", (c) => (d += c)); r.on("end", () => resolve(JSON.parse(d))); }); + req.on("error", reject); req.write(data); req.end(); + }); +} + +async function getToken() { + const flow = await post("/auth/login_flow", { client_id: HA + "/", handler: ["homeassistant", null], redirect_uri: HA + "/" }); + const auth = await post(`/auth/login_flow/${flow.flow_id}`, { client_id: HA + "/", username: "dev", password: "dev" }); + const tok = await post("/auth/token", `grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`, "application/x-www-form-urlencoded"); + return tok.access_token; +} + +async function main() { + const token = await getToken(); + const ws = new WebSocket(WSURL); + let id = 1; + const pending = new Map(); + + ws.on("message", (raw) => { + const m = JSON.parse(raw); + if (m.type === "auth_required") { + ws.send(JSON.stringify({ type: "auth", access_token: token })); + } else if (m.type === "auth_ok") { + pending.get("auth")?.resolve(); + } else if (m.type === "result") { + const p = pending.get(m.id); + if (p) { + if (m.success === false) p.reject(new Error(JSON.stringify(m.error))); + else p.resolve(m.result); + pending.delete(m.id); + } + } + }); + + await new Promise((resolve) => { ws.once("open", resolve); }); + await new Promise((resolve) => pending.set("auth", { resolve })); + + function call(payload) { + return new Promise((resolve, reject) => { + const i = id++; + pending.set(i, { resolve, reject }); + ws.send(JSON.stringify({ id: i, ...payload })); + }); + } + + let ok = 0, total = 0; + function check(label, cond, value) { + total++; + if (cond) ok++; + console.log(` ${cond ? "✓" : "✗"} ${label}${value !== undefined ? " — " + value : ""}`); + } + + console.log("\n=== Vacation enable + dates ==="); + await call({ type: "maintenance_supporter/vacation/update", enabled: true, start: "2026-06-01", end: "2026-06-15", buffer_days: 3 }); + let s = await call({ type: "maintenance_supporter/vacation/state" }); + check("enabled=true", s.enabled === true); + check("start=2026-06-01", s.start === "2026-06-01", s.start); + check("end=2026-06-15", s.end === "2026-06-15", s.end); + check("buffer_days=3", s.buffer_days === 3, s.buffer_days); + + console.log("\n=== Vacation disable ==="); + await call({ type: "maintenance_supporter/vacation/update", enabled: false }); + s = await call({ type: "maintenance_supporter/vacation/state" }); + check("enabled=false after disable", s.enabled === false); + check("dates preserved", s.start === "2026-06-01" && s.end === "2026-06-15"); + + console.log("\n=== Budget update via global/update ==="); + await call({ type: "maintenance_supporter/global/update", settings: { budget_monthly: 200, budget_yearly: 2000 } }); + let bs = await call({ type: "maintenance_supporter/budget_status" }); + check("monthly=200", bs.monthly_budget === 200, bs.monthly_budget); + check("yearly=2000", bs.yearly_budget === 2000, bs.yearly_budget); + + // Restore previous values + await call({ type: "maintenance_supporter/global/update", settings: { budget_monthly: 150, budget_yearly: 1500 } }); + + console.log("\n=== Groups: create + rename + delete ==="); + const created = await call({ type: "maintenance_supporter/group/create", name: "TmpTestGroup" }); + check("group created with id", typeof created.group_id === "string"); + const gid = created.group_id; + + let g = await call({ type: "maintenance_supporter/groups" }); + check("new group present", !!g.groups[gid] && g.groups[gid].name === "TmpTestGroup", g.groups[gid]?.name); + + await call({ type: "maintenance_supporter/group/update", group_id: gid, name: "RenamedGroup" }); + g = await call({ type: "maintenance_supporter/groups" }); + check("renamed", g.groups[gid]?.name === "RenamedGroup", g.groups[gid]?.name); + + await call({ type: "maintenance_supporter/group/delete", group_id: gid }); + g = await call({ type: "maintenance_supporter/groups" }); + check("deleted", !g.groups[gid]); + + console.log(`\n=== SUMMARY ===\n ${ok}/${total} checks passed`); + ws.close(); + process.exit(ok === total ? 0 : 1); +} + +main().catch((e) => { console.error(e); process.exit(1); }); diff --git a/custom_components/maintenance_supporter/frontend-src/verify-strategy-picker.mjs b/custom_components/maintenance_supporter/frontend-src/verify-strategy-picker.mjs new file mode 100644 index 0000000..b335b85 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/verify-strategy-picker.mjs @@ -0,0 +1,153 @@ +/** Diagnose: is the strategy registered in the Add-Dashboard picker context? + * + * Earlier verify-strategy.mjs checked window.customStrategies after navigating + * to /maintenance-supporter. This script navigates to /config/dashboards + * (Settings → Dashboards) instead — the actual context where the picker opens. + */ +import { chromium } from "playwright"; + +const HA = "http://localhost:8125"; +const HA_TOKEN = process.env.HA_TOKEN; +if (!HA_TOKEN) { + console.error("Set HA_TOKEN env var."); + process.exit(1); +} + +async function getRefreshToken() { + async function post(path, body, ctype = "application/json") { + const data = typeof body === "string" ? body : JSON.stringify(body); + const r = await fetch(`${HA}${path}`, { + method: "POST", + headers: { "Content-Type": ctype }, + body: data, + }); + return await r.json(); + } + const flow = await post("/auth/login_flow", { + client_id: `${HA}/`, handler: ["homeassistant", null], redirect_uri: `${HA}/`, + }); + const auth = await post(`/auth/login_flow/${flow.flow_id}`, { + client_id: `${HA}/`, username: "dev", password: "dev", + }); + const tokens = await post( + "/auth/token", + `grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`, + "application/x-www-form-urlencoded", + ); + return tokens.refresh_token; +} + +const browser = await chromium.launch({ headless: true }); +const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); +const page = await ctx.newPage(); +const errors = []; +page.on("pageerror", (e) => errors.push(`pageerror: ${e.message}`)); +page.on("console", (m) => { + if (m.type() === "error") errors.push(`console: ${m.text().substring(0, 200)}`); +}); + +const refreshToken = await getRefreshToken(); +await page.goto(HA); +await page.waitForTimeout(800); +await page.evaluate(({ ha, r }) => { + localStorage.setItem("hassTokens", JSON.stringify({ + hassUrl: ha, clientId: `${ha}/`, refresh_token: r, + access_token: "", token_type: "Bearer", expires_in: 1800, expires: 0, + })); +}, { ha: HA, r: refreshToken }); + +// 1. After hitting root only — is window.customStrategies populated? +await page.goto(HA); +await page.waitForTimeout(8000); +const atRoot = await page.evaluate(() => ({ + customStrategies: window.customStrategies || null, + customCards: (window.customCards || []).filter(c => c.type?.includes?.("maintenance")), + scriptsLoaded: [...document.scripts].filter(s => s.src.includes("maintenance")).map(s => s.src), + extraModules: [...document.querySelectorAll("script[type=module]")].filter( + s => s.src.includes("maintenance") + ).map(s => s.src), +})); +console.log("=== After root navigation ==="); +console.log(JSON.stringify(atRoot, null, 2)); + +// 2. Now navigate to Settings → Dashboards (the picker's context) +await page.goto(`${HA}/config/dashboards`); +await page.waitForTimeout(6000); +const atDashboardsConfig = await page.evaluate(() => ({ + url: window.location.href, + customStrategies: window.customStrategies || null, + customCards: (window.customCards || []).filter(c => c.type?.includes?.("maintenance")), + scriptsLoaded: [...document.scripts].filter(s => s.src.includes("maintenance")).map(s => s.src), + // Check if our specific custom element is registered + strategyTagDefined: !!customElements.get("ll-strategy-dashboard-maintenance-supporter"), +})); +console.log("\n=== After /config/dashboards navigation ==="); +console.log(JSON.stringify(atDashboardsConfig, null, 2)); + +// 3. Open the Add-Dashboard picker dialog and look at what it renders +await page.goto(`${HA}/config/dashboards`); +await page.waitForTimeout(5000); + +// Open the Add-Dashboard dialog via HA's standard "show-dialog" event — +// way more robust than chasing the FAB through nested shadow DOMs. +const dialogResult = await page.evaluate(async () => { + const ha = document.querySelector("home-assistant"); + if (!ha) return { error: "no home-assistant element" }; + // HA opens dialogs via this DOM event. The dialog is dialog-new-dashboard. + ha.dispatchEvent( + new CustomEvent("show-dialog", { + detail: { + dialogTag: "dialog-new-dashboard", + dialogImport: () => + import("/frontend_latest/dialog-new-dashboard").catch(() => ({})), + dialogParams: {}, + }, + bubbles: true, + composed: true, + }), + ); + // The standard import path may differ — wait for the dialog to materialize + for (let i = 0; i < 30; i++) { + await new Promise((r) => setTimeout(r, 200)); + if (document.querySelector("dialog-new-dashboard")) break; + } + return { tried: true }; +}); +console.log("Dialog open attempt:", dialogResult); + +await page.waitForTimeout(2000); + +// Inspect the dialog's content +const dialogContent = await page.evaluate(() => { + const out = {}; + // The dialog opens at document level + const dialog = document.querySelector("dialog-new-dashboard"); + out.dialogPresent = !!dialog; + if (dialog) { + const sr = dialog.shadowRoot; + // Look for dashboard cards (each option in the picker) + const cards = sr?.querySelectorAll("dashboard-card") || []; + out.cardCount = cards.length; + out.cardTypes = [...cards].map((c) => { + const innerSr = c.shadowRoot; + return { + strategy: c.getAttribute("strategy") || c._strategy?.type, + name: innerSr?.querySelector("[name], h3, .name")?.textContent?.trim(), + }; + }); + // Also: did the component capture any custom strategies? + out._customStrategies = dialog._customStrategies || dialog.__customStrategies; + } + // Also peek at the global registry one more time + out.windowCustomStrategies = (window.customStrategies || []).map((s) => s.type); + return out; +}); +console.log("\n=== Dashboard picker dialog content ==="); +console.log(JSON.stringify(dialogContent, null, 2)); + +if (errors.length) { + console.log("\n=== Page errors ==="); + errors.slice(0, 8).forEach(e => console.log(" " + e.substring(0, 250))); +} + +await browser.close(); diff --git a/custom_components/maintenance_supporter/frontend-src/verify-strategy.mjs b/custom_components/maintenance_supporter/frontend-src/verify-strategy.mjs new file mode 100644 index 0000000..9d06d9c --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/verify-strategy.mjs @@ -0,0 +1,382 @@ +/** Verify the dashboard strategy registers in HA 2026.5+'s frontend. + * + * Headless Chromium logs into ha-maint, then evaluates window.customStrategies + * to confirm our entry is there. Plus tries the actual generate() call to make + * sure the WS round-trip works. + * + * Run after: npm run build && docker restart ha-maint + * node verify-strategy.mjs + */ +import { chromium } from "playwright"; + +const HA = "http://localhost:8125"; +const HA_TOKEN = process.env.HA_TOKEN; +if (!HA_TOKEN) { + console.error("Set HA_TOKEN env var (long-lived access token)."); + process.exit(1); +} + +// HA expects a *refresh* token in localStorage, not an LLAT. Quick refresh-token +// dance: hit /auth/login_flow with username/password. +async function getRefreshToken() { + async function post(path, body, ctype = "application/json") { + const data = typeof body === "string" ? body : JSON.stringify(body); + const r = await fetch(`${HA}${path}`, { + method: "POST", + headers: { "Content-Type": ctype }, + body: data, + }); + return await r.json(); + } + const flow = await post("/auth/login_flow", { + client_id: `${HA}/`, + handler: ["homeassistant", null], + redirect_uri: `${HA}/`, + }); + const auth = await post(`/auth/login_flow/${flow.flow_id}`, { + client_id: `${HA}/`, + username: "dev", + password: "dev", + }); + const tokens = await post( + "/auth/token", + `grant_type=authorization_code&code=${auth.result}&client_id=${HA}/`, + "application/x-www-form-urlencoded", + ); + return tokens.refresh_token; +} + +const browser = await chromium.launch({ headless: true }); +const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 } }); +const page = await ctx.newPage(); +const consoleErrors = []; +page.on("pageerror", (err) => consoleErrors.push(`pageerror: ${err.message}`)); +page.on("console", (msg) => { + if (msg.type() === "error") consoleErrors.push(`console.error: ${msg.text()}`); +}); + +const refreshToken = await getRefreshToken(); +await page.goto(HA); +await page.waitForTimeout(1000); +await page.evaluate(({ ha, r }) => { + localStorage.setItem( + "hassTokens", + JSON.stringify({ + hassUrl: ha, + clientId: `${ha}/`, + refresh_token: r, + access_token: "", + token_type: "Bearer", + expires_in: 1800, + expires: 0, + }), + ); +}, { ha: HA, r: refreshToken }); +await page.goto(HA); +await page.waitForTimeout(8000); // let frontend extra modules load + +const result = await page.evaluate(async () => { + const out = {}; + + // 1. window.customStrategies populated? + out.customStrategies = (window.customStrategies || []).map((s) => ({ + type: s.type, + strategyType: s.strategyType, + name: s.name, + })); + + // 2. Custom element registered? + out.elementRegistered = !!customElements.get( + "ll-strategy-dashboard-maintenance-supporter", + ); + + // 3. Try the actual generate() call in ALL group_by modes + try { + const StrategyClass = customElements.get( + "ll-strategy-dashboard-maintenance-supporter", + ); + const ha = document.querySelector("home-assistant"); + if (StrategyClass?.generate && ha?.hass) { + for (const mode of ["area", "status", "floor", "due_date"]) { + const cfg = await StrategyClass.generate( + { type: "custom:maintenance-supporter", group_by: mode }, + ha.hass, + ); + out[`generated_${mode}`] = { + viewCount: cfg.views?.length ?? 0, + viewTitles: (cfg.views || []).map((v) => v.title), + }; + } + } + } catch (e) { + out.generateError = String(e); + } + + // 4. Editor element registered + setConfig works + dispatches event + try { + out.editorRegistered = !!customElements.get( + "hui-maintenance-supporter-strategy-editor", + ); + if (out.editorRegistered) { + const StrategyClass = customElements.get( + "ll-strategy-dashboard-maintenance-supporter", + ); + const editor = await StrategyClass.getConfigElement(); + document.body.appendChild(editor); + editor.setConfig({ + type: "custom:maintenance-supporter", + group_by: "status", + }); + const select = editor.querySelector("select"); + out.editorPreselected = select?.value; + let dispatchedConfig = null; + editor.addEventListener("config-changed", (e) => { + dispatchedConfig = e.detail.config; + }); + select.value = "due_date"; + select.dispatchEvent(new Event("change")); + out.editorDispatched = dispatchedConfig; + editor.remove(); + } + } catch (e) { + out.editorError = String(e); + } + + // 5. v1.8.0 — Header + Sidebar present on Overview view + try { + const StrategyClass = customElements.get( + "ll-strategy-dashboard-maintenance-supporter", + ); + const ha = document.querySelector("home-assistant"); + if (StrategyClass?.generate && ha?.hass) { + const cfg = await StrategyClass.generate( + { type: "custom:maintenance-supporter" }, + ha.hass, + ); + const overview = cfg.views?.find((v) => v.path === "overview"); + out.overviewHeader = overview?.header?.card?.type; + out.overviewSidebar = overview?.sidebar?.sections?.[0]?.cards?.length ?? 0; + out.overviewSidebarVisibility = + overview?.sidebar?.visibility?.[0]?.condition; + } + } catch (e) { + out.overviewError = String(e); + } + + // 6. v1.8.0 — Section strategy registered + generates + try { + out.sectionStrategyInRegistry = (window.customStrategies || []).some( + (s) => s.type === "maintenance-supporter-section" && s.strategyType === "section", + ); + out.sectionElementRegistered = !!customElements.get( + "ll-strategy-section-maintenance-supporter-section", + ); + const SectionClass = customElements.get( + "ll-strategy-section-maintenance-supporter-section", + ); + const ha = document.querySelector("home-assistant"); + if (SectionClass?.generate && ha?.hass) { + const sec = await SectionClass.generate( + { + type: "custom:maintenance-supporter-section", + filter_status: ["overdue", "triggered"], + title: "Hot list", + }, + ha.hass, + ); + out.sectionGenerated = { + type: sec.type, + cardCount: sec.cards?.length ?? 0, + firstCardType: sec.cards?.[0]?.type, + lastCardType: sec.cards?.[sec.cards.length - 1]?.type, + }; + } + } catch (e) { + out.sectionError = String(e); + } + + // 7. v1.9.0 — fire-dom-event handler opens dialog IN PLACE (object dialog) + // rather than navigating. Detect by checking if the dialog element was + // appended to body and is in "open" state. + try { + // Use HA's REAL fire-dom-event shape — the action config is the event + // detail and our payload sits under `.ll_custom`. Dispatching the payload + // at the top level (as this verifier used to) tested a shape that never + // occurs in production and masked issue #69. + document.dispatchEvent( + new CustomEvent("ll-custom", { + detail: { + action: "fire-dom-event", + ll_custom: { type: "maintenance-supporter:add-object" }, + }, + bubbles: true, + composed: true, + }), + ); + // Give the synchronous mount a tick to settle + await new Promise((r) => setTimeout(r, 100)); + const dlg = document.body.querySelector("maintenance-object-dialog"); + out.objectDialogMounted = !!dlg; + out.objectDialogOpen = !!dlg && dlg.shadowRoot?.querySelector("ha-dialog, dialog, .dialog") != null; + // Close it via internal state if possible (don't leave a dialog open) + if (dlg) { + dlg._open = false; + dlg.requestUpdate?.(); + } + } catch (e) { + out.fireDomError = String(e); + } + + // 8. v1.9.0 — group_by=calendar generates 4 windowed calendar views + try { + const StrategyClass = customElements.get( + "ll-strategy-dashboard-maintenance-supporter", + ); + const ha = document.querySelector("home-assistant"); + if (StrategyClass?.generate && ha?.hass) { + const cfg = await StrategyClass.generate( + { type: "custom:maintenance-supporter", group_by: "calendar" }, + ha.hass, + ); + out.generated_calendar = { + viewCount: cfg.views?.length ?? 0, + viewTitles: (cfg.views || []).map((v) => v.title), + firstCalendarCard: cfg.views?.[1]?.cards?.[0]?.type, + }; + } + } catch (e) { + out.generateCalendarError = String(e); + } + + // 9. v1.9.0 — Calendar card element registered + customCards entry present + try { + out.calendarCardRegistered = !!customElements.get( + "maintenance-supporter-calendar-card", + ); + out.calendarCardInRegistry = (window.customCards || []).some( + (c) => c.type === "maintenance-supporter-calendar-card", + ); + } catch (e) { + out.calendarError = String(e); + } + + // 10. v1.9.1 — Calendar card editor: registers, setConfig works, dispatches + // config-changed when user picks a different window_days option. + try { + out.calendarEditorRegistered = !!customElements.get( + "maintenance-supporter-calendar-card-editor", + ); + if (out.calendarEditorRegistered) { + const CardClass = customElements.get( + "maintenance-supporter-calendar-card", + ); + const editor = CardClass.getConfigElement(); + document.body.appendChild(editor); + editor.setConfig({ + type: "custom:maintenance-supporter-calendar-card", + window_days: 14, + }); + // Wait for first render + await new Promise((r) => setTimeout(r, 100)); + const sel = editor.shadowRoot?.querySelector("#window"); + out.calendarEditorPreselected = sel?.value; + let dispatchedConfig = null; + editor.addEventListener("config-changed", (e) => { + dispatchedConfig = e.detail.config; + }); + sel.value = "365"; + sel.dispatchEvent(new Event("change")); + out.calendarEditorDispatched = dispatchedConfig; + editor.remove(); + } + } catch (e) { + out.calendarEditorError = String(e); + } + + // 4. HA version + const ha = document.querySelector("home-assistant"); + out.haVersion = ha?.hass?.config?.version; + + return out; +}); + +console.log(JSON.stringify(result, null, 2)); + +const ours = result.customStrategies.find( + (s) => s.type === "maintenance-supporter", +); +const okRegister = !!ours; +const okElement = result.elementRegistered; +const okArea = (result.generated_area?.viewCount ?? 0) > 0; +const okStatus = (result.generated_status?.viewCount ?? 0) > 0; +const okFloor = (result.generated_floor?.viewCount ?? 0) > 0; +const okDueDate = (result.generated_due_date?.viewCount ?? 0) > 0; +const okEditor = + result.editorRegistered === true && + result.editorPreselected === "status" && + result.editorDispatched?.group_by === "due_date"; +const okHeader = result.overviewHeader === "markdown"; +const okSidebar = + (result.overviewSidebar ?? 0) > 0 && + result.overviewSidebarVisibility === "view_columns"; +const okSection = + result.sectionStrategyInRegistry === true && + result.sectionElementRegistered === true && + (result.sectionGenerated?.cardCount ?? 0) >= 2 && + result.sectionGenerated?.firstCardType === "heading" && + result.sectionGenerated?.lastCardType === "custom:maintenance-supporter-card"; +const okFireDom = result.objectDialogMounted === true; +const okCalendarMode = + (result.generated_calendar?.viewCount ?? 0) === 5 && // Overview + 4 windows + result.generated_calendar?.firstCalendarCard === "custom:maintenance-supporter-calendar-card"; +const okCalendarCard = + result.calendarCardRegistered === true && + result.calendarCardInRegistry === true; +const okCalendarEditor = + result.calendarEditorRegistered === true && + result.calendarEditorPreselected === "14" && + result.calendarEditorDispatched?.window_days === 365; + +console.log( + "\n=== SUMMARY ===\n" + + ` HA version: ${result.haVersion}\n` + + ` customStrategies has our entry: ${okRegister ? "✓" : "✗"}\n` + + ` custom element registered: ${okElement ? "✓" : "✗"}\n` + + ` generate(group_by=area): ${okArea ? "✓" : "✗"} (${result.generated_area?.viewCount} views)\n` + + ` generate(group_by=status): ${okStatus ? "✓" : "✗"} (${result.generated_status?.viewCount} views)\n` + + ` generate(group_by=floor): ${okFloor ? "✓" : "✗"} (${result.generated_floor?.viewCount} views)\n` + + ` generate(group_by=due_date): ${okDueDate ? "✓" : "✗"} (${result.generated_due_date?.viewCount} views)\n` + + ` editor registered + works: ${okEditor ? "✓" : "✗"}\n` + + ` Overview has KPI header: ${okHeader ? "✓" : "✗"} (${result.overviewHeader})\n` + + ` Overview has sidebar (large): ${okSidebar ? "✓" : "✗"} (${result.overviewSidebar} cards, visibility=${result.overviewSidebarVisibility})\n` + + ` Section strategy registered+works: ${okSection ? "✓" : "✗"} (${result.sectionGenerated?.cardCount} cards)\n` + + ` fire-dom-event opens in-place: ${okFireDom ? "✓" : "✗"} (object dialog mounted=${result.objectDialogMounted})\n` + + ` generate(group_by=calendar): ${okCalendarMode ? "✓" : "✗"} (${result.generated_calendar?.viewCount} views, first-card=${result.generated_calendar?.firstCalendarCard})\n` + + ` Calendar card registered+listed: ${okCalendarCard ? "✓" : "✗"} (element=${result.calendarCardRegistered}, customCards=${result.calendarCardInRegistry})\n` + + ` Calendar editor + works: ${okCalendarEditor ? "✓" : "✗"} (preselect=${result.calendarEditorPreselected}, dispatched=${result.calendarEditorDispatched?.window_days})`, +); + +if (consoleErrors.length) { + console.log("\n=== BROWSER CONSOLE ERRORS ==="); + consoleErrors.slice(0, 10).forEach((e) => console.log(` ${e.substring(0, 300)}`)); +} + +await browser.close(); +process.exit( + okRegister && + okElement && + okArea && + okStatus && + okFloor && + okDueDate && + okEditor && + okHeader && + okSidebar && + okSection && + okFireDom && + okCalendarMode && + okCalendarCard && + okCalendarEditor + ? 0 + : 1, +); diff --git a/custom_components/maintenance_supporter/frontend-src/web-test-runner.config.mjs b/custom_components/maintenance_supporter/frontend-src/web-test-runner.config.mjs new file mode 100644 index 0000000..3f349bb --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/web-test-runner.config.mjs @@ -0,0 +1,41 @@ +/** + * Lit-component test runner for the panel UI. + * + * Runs in real Chromium (headless) but mounts components in isolation + * with mocked hass — no HA shell, no shadow-DOM-deep-piercing nightmares. + * Each test is ~10-50ms, the whole suite runs in seconds. + * + * npm test — one-shot + * npm run test:watch — watch mode + */ + +import { esbuildPlugin } from "@web/dev-server-esbuild"; +import { playwrightLauncher } from "@web/test-runner-playwright"; +import { defaultReporter, summaryReporter } from "@web/test-runner"; + +export default { + files: "__tests__/**/*.test.ts", + nodeResolve: true, + plugins: [ + esbuildPlugin({ + ts: true, + json: true, + target: "es2020", + tsconfig: "./tsconfig.json", + }), + ], + browsers: [ + playwrightLauncher({ + product: "chromium", + // Channel-less; uses bundled chromium from playwright cache. + }), + ], + testFramework: { + config: { ui: "bdd", timeout: "5000" }, + }, + coverage: false, + reporters: [ + summaryReporter(), + defaultReporter({ reportTestResults: true, reportTestProgress: true }), + ], +}; diff --git a/custom_components/maintenance_supporter/frontend-src/ws-errors.ts b/custom_components/maintenance_supporter/frontend-src/ws-errors.ts new file mode 100644 index 0000000..faefd44 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend-src/ws-errors.ts @@ -0,0 +1,160 @@ +/** + * Translate raw voluptuous / Home-Assistant WS error messages into a + * localized, human-readable sentence. + * + * The HA WS layer wraps voluptuous errors in + * { code: "invalid_format", message: "" } + * The voluptuous text is consistently shaped — we parse the common + * patterns and emit a localized template string. Falls back to the raw + * message when nothing matches, so the user never sees a silent failure. + */ +import { t } from "./styles"; + +/** + * Map of internal field keys (as they appear in voluptuous error paths) + * to existing i18n label keys. When a field has no entry here, we surface + * the raw key as-is so the user can still tell which input was rejected. + */ +const FIELD_LABEL_KEYS: Record = { + name: "name", + task_type: "maintenance_type", + schedule_type: "schedule_type", + interval_days: "interval_days", + interval_anchor: "interval_anchor", + warning_days: "warning_days", + last_performed: "last_performed_optional", + notes: "notes_optional", + documentation_url: "documentation_url_optional", + custom_icon: "custom_icon_optional", + nfc_tag_id: "nfc_tag_id_optional", + responsible_user_id: "responsible_user", + entity_slug: "entity_slug", + entity_id: "entity_id", + area_id: "area_id_optional", + manufacturer: "manufacturer_optional", + model: "model_optional", + serial_number: "serial_number_optional", + installation_date: "installation_date_optional", + warranty_expiry: "warranty_expiry_optional", + checklist: "checklist_steps_optional", + reason: "reason", + feedback: "feedback", + cost: "cost", + duration: "duration", + description: "description_optional", + group_name: "name", + group_description: "description_optional", + environmental_entity: "environmental_entity_optional", + environmental_attribute: "environmental_attribute_optional", + trigger_above: "trigger_above", + trigger_below: "trigger_below", + trigger_for_minutes: "trigger_for_minutes", +}; + +function _label(field: string, lang: string): string { + const key = FIELD_LABEL_KEYS[field]; + if (!key) return field; + // t() returns the key itself when missing — we'd rather show the raw + // field name in that case than the i18n key with underscores. + const translated = t(key, lang); + return translated && translated !== key ? translated : field; +} + +interface ParsedError { + field?: string; + rule: + | "too_long" + | "too_short" + | "value_too_high" + | "value_too_low" + | "required" + | "wrong_type" + | "invalid_choice" + | "invalid_value" + | "unknown"; + param?: string; +} + +function _parse(message: string): ParsedError { + // Examples voluptuous produces: + // "length of value must be at most 64 for dictionary value @ data['entry_id']" + // "length of value must be at least 1 for dictionary value @ data['name']" + // "value must be at most 365 for dictionary value @ data['warning_days']" + // "value must be at least 0 for dictionary value @ data['warning_days']" + // "required key not provided @ data['name']" + // "expected str for dictionary value @ data['notes']" + // "expected int for dictionary value @ data['interval_days']" + // "value must be one of ['completion', 'planned'] for dictionary value @ data['interval_anchor']" + // "not a valid value for dictionary value @ data['date']" + const fieldMatch = message.match(/data\['([^']+)'\]/); + const field = fieldMatch?.[1]; + + let m: RegExpMatchArray | null; + if ((m = message.match(/length of value must be at most (\d+)/))) { + return { field, rule: "too_long", param: m[1] }; + } + if ((m = message.match(/length of value must be at least (\d+)/))) { + return { field, rule: "too_short", param: m[1] }; + } + if ((m = message.match(/value must be at most (\S+)/))) { + return { field, rule: "value_too_high", param: m[1] }; + } + if ((m = message.match(/value must be at least (\S+)/))) { + return { field, rule: "value_too_low", param: m[1] }; + } + if (/required key not provided/.test(message)) { + return { field, rule: "required" }; + } + if ((m = message.match(/expected (\w+)/))) { + return { field, rule: "wrong_type", param: m[1] }; + } + if (/value must be one of/.test(message)) { + return { field, rule: "invalid_choice" }; + } + if (/not a valid value/.test(message)) { + return { field, rule: "invalid_value" }; + } + return { field, rule: "unknown" }; +} + +/** + * Convert a thrown WS-promise rejection into a localized one-line string. + * Pass the lang code (e.g. `this._lang`) and optionally a fallback for + * non-WS errors (defaults to the localized generic "action failed"). + */ +export function describeWsError(e: unknown, lang: string, fallback?: string): string { + fallback = fallback ?? t("action_error", lang); + if (typeof e === "string") return e; + if (typeof e !== "object" || e === null) return fallback; + + const err = e as { message?: string; error?: { message?: string }; code?: string }; + const raw = err.message || err.error?.message || ""; + if (!raw) return fallback; + + const parsed = _parse(raw); + const field = parsed.field ? _label(parsed.field, lang) : ""; + + const tpl = (key: string) => t(key, lang).replace("{field}", field).replace("{n}", parsed.param ?? ""); + + switch (parsed.rule) { + case "too_long": + return tpl("err_too_long"); + case "too_short": + return tpl("err_too_short"); + case "value_too_high": + return tpl("err_value_too_high"); + case "value_too_low": + return tpl("err_value_too_low"); + case "required": + return tpl("err_required"); + case "wrong_type": + return tpl("err_wrong_type").replace("{type}", parsed.param ?? ""); + case "invalid_choice": + return tpl("err_invalid_choice"); + case "invalid_value": + return tpl("err_invalid_value"); + default: + // Unknown shape — show the raw message so debugging is possible + return raw || fallback; + } +} diff --git a/custom_components/maintenance_supporter/frontend/__init__.py b/custom_components/maintenance_supporter/frontend/__init__.py new file mode 100644 index 0000000..ff378d1 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/__init__.py @@ -0,0 +1,105 @@ +"""Frontend module registration for the Maintenance Supporter integration.""" + +from __future__ import annotations + +import logging +from pathlib import Path + +from homeassistant.components.http import StaticPathConfig +from homeassistant.core import HomeAssistant + +from ..const import ( + CALENDAR_CARD_URL, + CARD_URL, + DOMAIN, + LOCALES_URL, + STRATEGY_CHUNKS_URL, + STRATEGY_SHIM_URL, + STRATEGY_URL, + VENDOR_URL, +) + +_LOGGER = logging.getLogger(__name__) + +DATA_EXTRA_MODULE_URL = "frontend_extra_module_url" + +# Survives the hass.data[DOMAIN] pop on last-entry unload (see docstring). +_CARD_REGISTERED_KEY = f"{DOMAIN}_card_registered" + + +async def async_register_card(hass: HomeAssistant) -> None: + """Register the Lovelace card + dashboard strategy JS modules. + + Card: works on every supported HA version. + Strategy: HA 2026.5+ picks it up via ``window.customStrategies``; on older + versions the registration is a silent no-op. We ship both unconditionally + so users on any version get the card and 2026.5+ users additionally get + the auto-generated dashboard in the "Add Dashboard" picker. + + The already-registered guard lives OUTSIDE hass.data[DOMAIN]: that dict is + popped when the last entry unloads, but the static paths stay mounted on + the HTTP app — re-registering them after an uninstall → reinstall in the + same HA run would raise on the duplicate routes (journey O1). + """ + if hass.data.get(_CARD_REGISTERED_KEY): + return + + frontend_dir = Path(__file__).parent + # v2.3.4 (issue #52): the strategy is a multi-file split bundle living + # in frontend/strategy/. Mount the directory under STRATEGY_DIR_URL so + # the entry's relative ./chunks/* dynamic imports resolve without per- + # chunk URL registration. The entry file URL stays STRATEGY_URL so the + # frontend_extra_module_url registration that HA serves to the browser + # doesn't need to know about the chunk layout. + static_paths = [ + StaticPathConfig(CARD_URL, str(frontend_dir / "maintenance-card.js"), False), + # The shim (tiny, zero-import) is the frontend extra-module-url; it + # lazy-imports the full strategy bundle below on first use. + StaticPathConfig( + STRATEGY_SHIM_URL, + str(frontend_dir / "maintenance-strategy-shim.js"), + False, + ), + StaticPathConfig( + STRATEGY_URL, + str(frontend_dir / "strategy" / "maintenance-dashboard-strategy.js"), + False, + ), + StaticPathConfig( + STRATEGY_CHUNKS_URL, + str(frontend_dir / "strategy" / "chunks"), + False, + ), + StaticPathConfig( + CALENDAR_CARD_URL, + str(frontend_dir / "maintenance-calendar-card.js"), + False, + ), + # Runtime-loaded UI translations: a directory of .json fetched by + # the panel/card on demand (only EN is bundled into the JS). cache=False + # so a translation edit shows up on reload without a version bump. + StaticPathConfig(LOCALES_URL, str(frontend_dir / "locales"), False), + # v2.21: pdf.js for the work sheet's inline manual excerpt (cache=True — + # versioned by release like the JS bundles). + StaticPathConfig(VENDOR_URL, str(frontend_dir / "vendor"), True), + ] + await hass.http.async_register_static_paths(static_paths) + + # Add to extra module URLs so HA auto-loads them in the frontend. + # IMPORTANT: register the SHIM, not the heavy strategy bundle — the shim + # defines the dashboard-strategy element synchronously so it wins HA's 5 s + # whenDefined race even under heavy HACS plugin load (the bundle is loaded + # lazily by the shim). The heavy bundle's static path stays mounted above + # so the shim's import() can fetch it on demand. + extra = hass.data.setdefault(DATA_EXTRA_MODULE_URL, set()) + extra.add(CARD_URL) + extra.add(STRATEGY_SHIM_URL) + extra.add(CALENDAR_CARD_URL) + + hass.data[_CARD_REGISTERED_KEY] = True + _LOGGER.debug( + "Maintenance Supporter frontend resources registered: card=%s, strategy=%s, calendar=%s", + CARD_URL, + STRATEGY_URL, + CALENDAR_CARD_URL, + ) diff --git a/custom_components/maintenance_supporter/frontend/__pycache__/__init__.cpython-314.pyc b/custom_components/maintenance_supporter/frontend/__pycache__/__init__.cpython-314.pyc new file mode 100644 index 0000000..e6a1b5a Binary files /dev/null and b/custom_components/maintenance_supporter/frontend/__pycache__/__init__.cpython-314.pyc differ diff --git a/custom_components/maintenance_supporter/frontend/locales/cs.json b/custom_components/maintenance_supporter/frontend/locales/cs.json new file mode 100644 index 0000000..5225db4 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/cs.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Údržba", + "objects": "Objekty", + "tasks": "Úkoly", + "overdue": "Po termínu", + "due_soon": "Brzy", + "triggered": "Spuštěno", + "ok": "OK", + "all": "Vše", + "new_object": "+ Nový objekt", + "templates_from": "Ze šablony", + "templates_title": "Začít ze šablony", + "templates_task_count": "{n} úkolů", + "template_created": "Vytvořeno ze šablony", + "onboard_hint": "Přidejte první objekt a začněte sledovat údržbu.", + "edit": "Upravit", + "duplicate": "Duplikovat", + "task_duplicated": "Úkol duplikován", + "object_duplicated": "Objekt duplikován", + "delete": "Smazat", + "add_task": "+ Přidat úkol", + "complete": "Dokončit", + "completed": "Dokončeno", + "skip": "Přeskočit", + "skipped": "Přeskočeno", + "missed": "Missed", + "reset": "Reset", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Zrušit", + "bulk_select": "Vybrat", + "bulk_select_all": "Vybrat vše", + "bulk_n_selected": "{n} vybráno", + "bulk_completed": "Dokončeno {n} úkolů", + "bulk_archived": "Archivováno {n} úkolů", + "completing": "Dokončování…", + "interval": "Interval", + "warning": "Upozornění", + "last_performed": "Naposledy provedeno", + "next_due": "Další termín", + "days_until_due": "Dnů do termínu", + "avg_duration": "Prům. trvání", + "trigger": "Spouštěč", + "trigger_type": "Typ spouštěče", + "threshold_above": "Horní limit", + "threshold_below": "Dolní limit", + "threshold": "Práh", + "counter": "Čítač", + "state_change": "Změna stavu", + "runtime": "Doba běhu", + "runtime_hours": "Cílová doba běhu (hodiny)", + "target_value": "Cílová hodnota", + "baseline": "Základní hodnota", + "target_changes": "Cílový počet změn", + "for_minutes": "Po dobu (minut)", + "time_based": "Časový", + "sensor_based": "Založený na senzoru", + "manual": "Manuální", + "one_time": "Jednorázově", + "weekdays": "Dny v týdnu", + "nth_weekday": "N-tý den v týdnu v měsíci", + "day_of_month": "Den v měsíci", + "recurrence_on_days": "Opakovat v", + "recurrence_occurrence": "Výskyt", + "recurrence_weekday": "Den v týdnu", + "recurrence_day": "Den v měsíci (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Poslední", + "day_word": "Den", + "interval_value": "Interval", + "interval_unit": "Jednotka", + "unit_days": "Dny", + "unit_weeks": "Týdny", + "unit_months": "Měsíce", + "unit_years": "Roky", + "due_date": "Datum splnění", + "cleaning": "Čištění", + "inspection": "Inspekce", + "replacement": "Výměna", + "calibration": "Kalibrace", + "service": "Servis", + "reading": "Odečet", + "custom": "Vlastní", + "history": "Historie", + "cost": "Náklady", + "report_button": "Zpráva", + "report_title": "Zpráva o údržbě", + "report_generated": "Vytvořeno", + "report_times_done": "Provedeno", + "report_total_cost": "Celkové náklady", + "report_every": "každých {n} {unit}", + "report_notes": "Poznámky", + "report_col_type": "Typ", + "report_col_status": "Stav", + "report_col_schedule": "Plán", + "duration": "Doba trvání", + "both": "Obojí", + "trigger_val": "Hodnota spouštěče", + "complete_title": "Dokončit: ", + "checklist": "Kontrolní seznam", + "checklist_steps_optional": "Kroky kontrolního seznamu (volitelné)", + "checklist_placeholder": "Vyčistit filtr\nVyměnit těsnění\nOtestovat tlak", + "checklist_help": "Jeden krok na řádek. Max 100 položek.", + "err_too_long": "{field}: příliš dlouhé (max {n} znaků)", + "err_too_short": "{field}: příliš krátké (min {n} znaků)", + "err_value_too_high": "{field}: příliš velké (max {n})", + "err_value_too_low": "{field}: příliš malé (min {n})", + "err_required": "{field}: povinné", + "err_wrong_type": "{field}: špatný typ (očekáván: {type})", + "err_invalid_choice": "{field}: nepovolená hodnota", + "err_invalid_value": "{field}: neplatná hodnota", + "feat_schedule_time": "Plánování podle denní doby", + "feat_schedule_time_desc": "Úkoly se stanou po termínu v určenou denní dobu místo o půlnoci.", + "schedule_time_optional": "Termín v čase (volitelné, HH:MM)", + "schedule_time_help": "Prázdné = půlnoc (výchozí). Časové pásmo HA.", + "at_time": "v", + "notes_optional": "Poznámky (volitelné)", + "cost_optional": "Náklady (volitelné)", + "duration_minutes": "Doba trvání v minutách (volitelné)", + "days": "dní", + "day": "den", + "today": "Dnes", + "d_overdue": "d po termínu", + "no_tasks": "Zatím žádné úkoly údržby. Vytvořte objekt pro začátek.", + "no_tasks_short": "Žádné úkoly", + "no_history": "Zatím žádné záznamy historie.", + "show_all": "Zobrazit vše", + "cost_duration_chart": "Náklady a doba trvání", + "installed": "Nainstalováno", + "confirm_delete_object": "Smazat tento objekt a všechny jeho úkoly?", + "confirm_delete_task": "Smazat tento úkol?", + "min": "Min", + "max": "Max", + "save": "Uložit", + "saving": "Ukládání…", + "edit_task": "Upravit úkol", + "new_task": "Nový úkol údržby", + "task_name": "Název úkolu", + "maintenance_type": "Typ údržby", + "priority": "Priorita", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Nízká", + "priority_normal": "Normální", + "priority_high": "Vysoká", + "schedule_type": "Typ rozvrhu", + "interval_days": "Interval (dny)", + "warning_days": "Dny upozornění", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Naposledy provedeno (volitelné)", + "interval_anchor": "Ukotvení intervalu", + "anchor_completion": "Od data dokončení", + "anchor_planned": "Od plánovaného data (bez posunu)", + "edit_object": "Upravit objekt", + "name": "Název", + "manufacturer_optional": "Výrobce (volitelné)", + "model_optional": "Model (volitelné)", + "serial_number_optional": "Sériové číslo (volitelné)", + "serial_number_label": "S/N", + "documentation_url_label": "Návod", + "object_notes_label": "Poznámky", + "sort_due_date": "Termín", + "sort_object": "Název objektu", + "sort_type": "Typ", + "sort_task_name": "Název úkolu", + "all_objects": "Všechny objekty", + "tasks_lower": "úkolů", + "no_tasks_yet": "Zatím žádné úkoly", + "add_first_task": "Přidat první úkol", + "trigger_configuration": "Konfigurace spouštěče", + "entity_id": "ID entity", + "comma_separated": "oddělené čárkami", + "entity_logic": "Logika entit", + "entity_logic_any": "Spustí libovolná entita", + "entity_logic_all": "Všechny entity musí spustit", + "entities": "entity", + "attribute_optional": "Atribut (volitelný, prázdný = stav)", + "use_entity_state": "Použít stav entity (bez atributu)", + "trigger_above": "Spustit nad", + "trigger_below": "Spustit pod", + "for_at_least_minutes": "Po dobu alespoň (minut)", + "safety_interval_days": "Bezpečnostní interval (dny, volitelný)", + "safety_interval": "Bezpečnostní interval (volitelný)", + "delta_mode": "Režim delta", + "from_state_optional": "Ze stavu (volitelné)", + "to_state_optional": "Do stavu (volitelné)", + "documentation_url_optional": "URL dokumentace (volitelné)", + "object_notes_optional": "Poznámky (volitelné)", + "nfc_tag_id_optional": "ID NFC tagu (volitelné)", + "nfc_tags_empty_help": "V Home Assistant zatím nejsou registrovány žádné NFC tagy.", + "nfc_tags_open_settings": "Otevřít nastavení tagů", + "nfc_tags_refresh": "Obnovit", + "environmental_entity_optional": "Senzor prostředí (volitelný)", + "environmental_entity_helper": "např. sensor.outdoor_temperature — upravuje interval podle podmínek prostředí", + "environmental_attribute_optional": "Atribut prostředí (volitelný)", + "nfc_tag_id": "ID NFC tagu", + "nfc_linked": "NFC tag propojen", + "nfc_link_hint": "Klikněte pro propojení NFC tagu", + "responsible_user": "Zodpovědný uživatel", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Žádný uživatel přiřazen)", + "all_users": "Všichni uživatelé", + "my_tasks": "Moje úkoly", + "tab_calendar": "Kalendář", + "cal_no_events": "Bez údržby", + "cal_window_7": "7 dní", + "cal_window_14": "14 dní", + "cal_window_30": "30 dní", + "cal_window_365": "1 rok", + "cal_every_n_days": "každých {n} dní", + "cal_source_time": "Čas", + "cal_source_time_adaptive": "Čas (adaptivní)", + "cal_source_sensor": "Senzor", + "cal_predicted": "predikce", + "cal_confidence_high": "vysoká přesnost", + "cal_confidence_medium": "střední přesnost", + "cal_confidence_low": "nízká přesnost", + "budget_monthly": "Měsíční rozpočet", + "budget_yearly": "Roční rozpočet", + "groups": "Skupiny", + "new_group": "Nová skupina", + "edit_group": "Upravit skupinu", + "no_groups": "Zatím žádné skupiny", + "delete_group": "Smazat skupinu", + "delete_group_confirm": "Smazat skupinu '{name}'?", + "group_select_tasks": "Vybrat úkoly", + "group_name_required": "Název je povinný", + "description_optional": "Popis (volitelný)", + "selected": "Vybráno", + "loading_chart": "Načítání dat grafu...", + "hide_outliers": "Skrýt odlehlé hodnoty (chyby čidla)", + "was_maintenance_needed": "Byla tato údržba potřeba?", + "feedback_needed": "Potřebná", + "feedback_not_needed": "Nepotřebná", + "feedback_not_sure": "Nejsem si jistý", + "suggested_interval": "Navrhovaný interval", + "apply_suggestion": "Použít", + "reanalyze": "Znovu analyzovat", + "reanalyze_result": "Nová analýza", + "reanalyze_insufficient_data": "Nedostatek dat pro vytvoření doporučení", + "data_points": "datových bodů", + "dismiss_suggestion": "Zavřít", + "confidence_low": "Nízká", + "confidence_medium": "Střední", + "confidence_high": "Vysoká", + "recommended": "doporučeno", + "seasonal_awareness": "Sezónní povědomí", + "edit_seasonal_overrides": "Upravit sezónní faktory", + "seasonal_overrides_title": "Sezónní faktory (přepsání)", + "seasonal_overrides_hint": "Faktor na měsíc (0.1–5.0). Prázdné = naučeno automaticky.", + "seasonal_override_invalid": "Neplatná hodnota", + "seasonal_override_range": "Faktor musí být mezi 0.1 a 5.0", + "clear_all": "Vymazat vše", + "seasonal_chart_title": "Sezónní faktory", + "seasonal_learned": "Naučené", + "seasonal_manual": "Manuální", + "month_jan": "Led", + "month_feb": "Úno", + "month_mar": "Bře", + "month_apr": "Dub", + "month_may": "Kvě", + "month_jun": "Čer", + "month_jul": "Čvc", + "month_aug": "Srp", + "month_sep": "Zář", + "month_oct": "Říj", + "month_nov": "Lis", + "month_dec": "Pro", + "sensor_prediction": "Predikce senzoru", + "degradation_trend": "Trend", + "trend_rising": "Rostoucí", + "trend_falling": "Klesající", + "trend_stable": "Stabilní", + "trend_insufficient_data": "Nedostatek dat", + "days_until_threshold": "Dnů do prahu", + "threshold_exceeded": "Práh překročen", + "environmental_adjustment": "Faktor prostředí", + "sensor_prediction_urgency": "Senzor předpovídá práh za ~{days} dní", + "day_short": "d", + "weibull_reliability_curve": "Křivka spolehlivosti", + "weibull_failure_probability": "Pravděpodobnost selhání", + "weibull_r_squared": "Shoda R²", + "beta_early_failures": "Časná selhání", + "beta_random_failures": "Náhodná selhání", + "beta_wear_out": "Opotřebení", + "beta_highly_predictable": "Vysoce předvídatelné", + "confidence_interval": "Interval spolehlivosti", + "confidence_conservative": "Konzervativní", + "confidence_aggressive": "Optimistický", + "current_interval_marker": "Aktuální interval", + "recommended_marker": "Doporučený", + "characteristic_life": "Charakteristická životnost", + "chart_mini_sparkline": "Graf trendu", + "chart_history": "Historie nákladů a doby trvání", + "chart_seasonal": "Sezónní faktory, 12 měsíců", + "chart_weibull": "Weibullova křivka spolehlivosti", + "chart_sparkline": "Graf hodnoty spouštěče senzoru", + "days_progress": "Postup dnů", + "qr_code": "QR kód", + "qr_generating": "Generování QR kódu…", + "qr_error": "Nepodařilo se vygenerovat QR kód.", + "qr_error_no_url": "Není nakonfigurováno URL HA. Nastavte externí nebo interní URL v Nastavení → Systém → Síť.", + "save_error": "Nepodařilo se uložit. Zkuste to znovu.", + "qr_print": "Tisk", + "qr_download": "Stáhnout SVG", + "qr_action": "Akce při skenování", + "qr_action_view": "Zobrazit informace o údržbě", + "qr_action_complete": "Označit údržbu jako dokončenou", + "qr_url_mode": "Typ odkazu", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokální (mDNS)", + "qr_mode_server": "URL serveru", + "overview": "Přehled", + "analysis": "Analýza", + "recent_activities": "Nedávné aktivity", + "search_notes": "Hledat v poznámkách", + "avg_cost": "Prům. náklady", + "no_advanced_features": "Žádné pokročilé funkce nejsou povoleny", + "no_advanced_features_hint": "Povolte „Adaptivní intervaly” nebo „Sezónní vzory” v nastavení integrace pro zobrazení analytických dat.", + "analysis_not_enough_data": "Zatím nedostatek dat pro analýzu.", + "analysis_not_enough_data_hint": "Weibullova analýza vyžaduje alespoň 5 dokončených údržeb; sezónní vzory se stanou viditelné po 6+ datových bodech na měsíc.", + "analysis_manual_task_hint": "Manuální úkoly bez intervalu negenerují analytická data.", + "completions": "dokončení", + "current": "Aktuální", + "shorter": "Kratší", + "longer": "Delší", + "normal": "Normální", + "disabled": "Zakázáno", + "compound_logic": "Složená logika", + "compound": "Složené (více podmínek)", + "compound_logic_and": "A — musí platit všechny podmínky", + "compound_logic_or": "NEBO — stačí libovolná podmínka", + "compound_help": "Zkombinujte několik podmínek senzorů do jednoho spouštěče.", + "compound_no_conditions": "Zatím žádné podmínky — přidejte alespoň jednu.", + "compound_add_condition": "Přidat podmínku", + "compound_condition": "Podmínka", + "compound_remove_condition": "Odebrat podmínku", + "card_title": "Název", + "card_show_header": "Zobrazit záhlaví se statistikami", + "card_show_actions": "Zobrazit tlačítka akcí", + "card_compact": "Kompaktní režim", + "card_max_items": "Max položek (0 = vše)", + "card_filter_status": "Filtrovat podle stavu", + "card_filter_status_help": "Prázdné = zobrazit všechny stavy.", + "card_filter_objects": "Filtrovat podle objektů", + "card_filter_objects_help": "Prázdné = zobrazit všechny objekty.", + "card_filter_entities": "Filtrovat podle entit (entity_ids)", + "card_filter_entities_help": "Vyberte entity sensor / binary_sensor z této integrace. Prázdné = všechny.", + "card_loading_objects": "Načítání objektů…", + "card_load_error": "Nepodařilo se načíst objekty — zkontrolujte WebSocket spojení.", + "card_no_tasks_title": "Zatím žádné úkoly údržby", + "card_no_tasks_cta": "→ Vytvořte v panelu Maintenance", + "no_objects": "Zatím žádné objekty.", + "action_error": "Akce se nezdařila. Zkuste to znovu.", + "area_id_optional": "Oblast (volitelná)", + "installation_date_optional": "Datum instalace (volitelné)", + "warranty_expiry_optional": "Konec záruky (volitelné)", + "warranty": "Záruka", + "warranty_valid_until": "platná do {date}", + "warranty_expires_in": "vyprší za {days} dní", + "warranty_expired": "vypršela", + "cal_past_windows": "Minulá okna", + "cal_forward_windows": "Budoucí okna", + "history_edit_title": "Upravit záznam historie", + "history_edit_timestamp": "Časové razítko", + "manufacturer": "Výrobce", + "model": "Model", + "area": "Oblast", + "actions": "Akce", + "view_mode_label": "Zobrazení", + "view_cards": "Zobrazení karet", + "view_table": "Zobrazení tabulky", + "objects_table_columns_label": "Sloupce tabulky objektů", + "objects_table_columns_hint": "Vyberte, které sloupce se zobrazí v tabulkovém zobrazení objektů.", + "custom_icon_optional": "Ikona (volitelná, např. mdi:wrench)", + "task_enabled": "Úkol povolen", + "skip_reason_prompt": "Přeskočit tento úkol?", + "reason_optional": "Důvod (volitelný)", + "reset_date_prompt": "Označit úkol jako provedený?", + "reset_date_optional": "Datum posledního provedení (volitelné, výchozí dnes)", + "notes_label": "Poznámky", + "documentation_label": "Dokumentace", + "no_nfc_tag": "— Žádný tag —", + "dashboard": "Přehled", + "tab_today": "Dnes", + "palette_placeholder": "Hledat objekty a úkoly…", + "palette_no_results": "Žádné výsledky", + "palette_hint": "↑↓ pohyb · Enter otevřít · Esc zavřít", + "today_all_caught_up": "Vše hotovo! Tento týden nic nezbývá.", + "today_overdue": "Po termínu", + "today_due_today": "Dnes", + "today_this_week": "Tento týden", + "settings": "Nastavení", + "settings_features": "Pokročilé funkce", + "settings_features_desc": "Povolte nebo zakažte pokročilé funkce. Zakázání je skryje z UI, ale nesmaže data.", + "feat_adaptive": "Adaptivní plánování", + "feat_adaptive_desc": "Učte se optimální intervaly z historie údržby", + "feat_predictions": "Predikce senzorů", + "feat_predictions_desc": "Předpovídejte termíny spouštění z degradace senzoru", + "feat_seasonal": "Sezónní úpravy", + "feat_seasonal_desc": "Upravte intervaly podle sezónních vzorů", + "feat_environmental": "Korelace s prostředím", + "feat_environmental_desc": "Korelujte intervaly s teplotou/vlhkostí", + "feat_budget": "Sledování rozpočtu", + "feat_budget_desc": "Sledujte měsíční a roční výdaje na údržbu", + "feat_groups": "Skupiny úkolů", + "feat_groups_desc": "Organizujte úkoly do logických skupin", + "feat_checklists": "Kontrolní seznamy", + "feat_checklists_desc": "Vícestupňové procedury pro dokončení úkolu", + "settings_general": "Obecné", + "settings_default_warning": "Výchozí dny upozornění", + "settings_panel_enabled": "Boční panel", + "settings_panel_title": "Název panelu", + "settings_notifications": "Oznámení", + "settings_notify_service": "Služba oznámení", + "test_notification": "Testovací oznámení", + "send_test": "Odeslat test", + "testing": "Odesílání…", + "test_notification_success": "Testovací oznámení odesláno", + "test_notification_failed": "Testovací oznámení se nezdařilo", + "settings_notify_due_soon": "Oznámit když brzy", + "settings_notify_overdue": "Oznámit když po termínu", + "settings_notify_triggered": "Oznámit když spuštěno", + "settings_interval_hours": "Interval opakování (hodiny, 0 = jednou)", + "settings_quiet_hours": "Tiché hodiny", + "settings_quiet_start": "Začátek", + "settings_quiet_end": "Konec", + "settings_max_per_day": "Max oznámení denně (0 = bez limitu)", + "settings_bundling": "Seskupit oznámení", + "settings_bundle_threshold": "Práh seskupení", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobilní akční tlačítka", + "settings_action_complete": "Zobrazit tlačítko 'Dokončit'", + "settings_action_skip": "Zobrazit tlačítko 'Přeskočit'", + "settings_action_snooze": "Zobrazit tlačítko 'Odložit'", + "settings_weekly_digest": "Týdenní přehled", + "settings_weekly_digest_hint": "Jedno souhrnné oznámení v pondělí ráno, když jsou úkoly.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Doba odložení (hodiny)", + "settings_budget": "Rozpočet", + "settings_currency": "Měna", + "settings_budget_monthly": "Měsíční rozpočet", + "settings_budget_yearly": "Roční rozpočet", + "settings_budget_alerts": "Rozpočtová upozornění", + "settings_budget_threshold": "Práh upozornění (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Exportovat JSON", + "settings_export_yaml": "Exportovat YAML", + "settings_export_csv": "Exportovat CSV", + "settings_import_csv": "Importovat CSV", + "settings_import_placeholder": "Vložte sem obsah JSON nebo CSV…", + "settings_import_btn": "Importovat", + "settings_import_success": "{count} objektů úspěšně importováno.", + "settings_export_success": "Export stažen.", + "settings_saved": "Nastavení uloženo.", + "settings_include_history": "Zahrnout historii", + "sort_alphabetical": "Abecedně", + "sort_due_soonest": "Nejbližší termín", + "sort_task_count": "Počet úkolů", + "sort_area": "Oblast", + "sort_assigned_user": "Přiřazený uživatel", + "sort_group": "Skupina", + "groupby_none": "Bez seskupení", + "groupby_area": "Podle oblasti", + "groupby_group": "Podle skupiny", + "groupby_user": "Podle uživatele", + "filter_label": "Filtr", + "user_label": "Uživatel", + "sort_label": "Řazení", + "group_by_label": "Seskupit podle", + "state_value_help": "Použijte hodnotu stavu HA (obvykle malými písmeny, např. \"on\"/\"off\"). Velikost písmen se při uložení normalizuje.", + "target_changes_help": "Počet odpovídajících přechodů, po kterých se trigger spustí (výchozí: 1).", + "qr_print_title": "Tisk QR kódů", + "qr_print_desc": "Vygeneruj tiskovou stránku s QR kódy k vystřižení a nalepení na zařízení.", + "qr_print_load": "Načíst objekty", + "qr_print_filter": "Filtr", + "qr_print_objects": "Objekty", + "qr_print_actions": "Akce", + "qr_print_url_mode": "Typ odkazu", + "qr_print_estimate": "Odhad QR kódů", + "qr_print_over_limit": "limit 200, zužte filtr", + "qr_print_generate": "Vygenerovat QR kódy", + "qr_print_generating": "Generování…", + "qr_print_ready": "QR kódy připraveny", + "qr_print_print_button": "Tisk", + "qr_print_empty": "Nic k vygenerování", + "qr_action_skip": "Přeskočit", + "vacation_title": "Režim dovolené", + "vacation_active": "aktivní", + "vacation_ended": "ukončeno", + "vacation_desc": "Naplánuj dovolenou: oznámení jsou pozastavena během období plus dny rezervy. Lze definovat výjimky pro jednotlivé úkoly.", + "vacation_enable": "Zapnout režim dovolené", + "vacation_start": "Začátek", + "vacation_end": "Konec", + "vacation_buffer": "Rezerva (dnů)", + "vacation_exempt_title": "Upozorňovat i přes dovolenou", + "vacation_exempt_desc": "Vyber úkoly, které mají upozorňovat i během dovolené (např. kritická chemie bazénu).", + "vacation_load_tasks": "Načíst úkoly", + "vacation_preview_btn": "Zobrazit náhled", + "vacation_preview_affected": "úkolů ovlivněno", + "vacation_event_due_soon": "blíží se termín", + "vacation_event_overdue": "stane se po termínu", + "vacation_event_triggered_est": "možné spuštění senzoru", + "vacation_sensor_based": "(senzorové)", + "vacation_action_notify": "Přesto upozornit", + "vacation_action_unsilence": "Znovu ztlumit", + "vacation_marked_complete": "Označeno jako dokončené", + "vacation_marked_skip": "Přeskočeno", + "vacation_end_now": "Ukončit dovolenou nyní", + "unassigned": "Nepřiřazeno", + "no_area": "Bez oblasti", + "has_overdue": "Má úkoly po termínu", + "object": "Objekt", + "settings_panel_access": "Přístup k panelu", + "settings_panel_access_desc": "Administrátoři mají vždy plný přístup. Chcete-li delegovat vytváření, úpravy a mazání konkrétním uživatelům bez práv administrátora, zapněte tuto možnost a vyberte je níže — ostatní vidí pouze Dokončit a Přeskočit.", + "settings_operator_write": "Povolit vybraným uživatelům vytvářet, upravovat a mazat", + "settings_operator_write_desc": "Vypnuto: obsah mohou měnit pouze administrátoři. Zapnuto: plný přístup získají i vybraní uživatelé níže.", + "no_non_admin_users": "Nenalezeni žádní uživatelé bez admin práv. Přidejte je v Nastavení → Lidé.", + "owner_label": "Vlastník", + "feat_completion_actions": "Akce při dokončení", + "feat_completion_actions_desc": "Akce HA na úlohu při dokončení + QR rychlého dokončení s předvolenými hodnotami.", + "on_complete_action_title": "Při dokončení: spustit akci HA (volitelné)", + "on_complete_action_desc": "Volá službu HA, když je úloha dokončena — např. resetuje čítač na zařízení.", + "on_complete_action_service": "Služba", + "on_complete_action_target": "Cílová entita", + "on_complete_action_data": "Data (JSON, volitelné)", + "on_complete_action_test": "Testovat akci", + "on_complete_action_test_success": "Úspěch", + "on_complete_action_test_failed": "Selhalo", + "quick_complete_defaults_title": "Výchozí hodnoty rychlého dokončení (pro QR skenů, volitelné)", + "quick_complete_defaults_desc": "Předvolené hodnoty pro QR rychlého dokončení. Bez nich QR otevře dialog.", + "quick_complete_defaults_notes": "Poznámky", + "quick_complete_defaults_cost": "Cena", + "quick_complete_defaults_duration": "Trvání (minuty)", + "quick_complete_defaults_feedback_none": "Bez zpětné vazby", + "quick_complete_defaults_feedback_needed": "Bylo potřeba", + "quick_complete_defaults_feedback_not_needed": "Nebylo potřeba", + "quick_complete_success": "Rychle označeno jako hotové", + "trigger_replaced": "Spouštěč nahrazen", + "add": "Přidat", + "show_stats": "Zobrazit statistiky + grafy", + "hide_stats": "Skrýt statistiky", + "adaptive_no_data": "Zatím není dostatek historie dokončení pro adaptivní analýzu. Dokončete tento úkol ještě několikrát, abyste odemkli doporučení intervalu a grafy spolehlivosti.", + "suggestion_applied": "Navržený interval použit", + "vacation_mode": "Režim dovolené", + "vacation_status_active": "Nyní aktivní", + "vacation_status_scheduled": "Naplánováno", + "vacation_status_inactive": "Neaktivní", + "vacation_end_now_confirm": "Ukončit dovolenou okamžitě?", + "vacation_exempt_count": "vyňato", + "vacation_advanced": "Pokročilé…", + "vacation_open_panel": "Otevřít v panelu", + "enable": "Zapnout", + "saved": "Uloženo", + "budget_monthly_set": "Nastavit měsíční", + "budget_yearly_set": "Nastavit roční", + "budget_advanced": "Měna, výstrahy…", + "budget_open_panel": "Otevřít v panelu", + "groups_empty": "Zatím žádné skupiny.", + "group_new_placeholder": "Přidat skupinu…", + "group_delete_confirm": "Smazat skupinu „{name}\"?", + "groups_manage_tasks": "Spravovat přiřazení úkolů…", + "groups_open_panel": "Otevřít v panelu", + "on_complete_action_target_hint": "Poznámka: doména entity musí odpovídat službě — např. 'button.press' funguje jen na button.*, 'counter.increment' jen na counter.*, 'input_button.press' jen na input_button.* atd. Při neshodě akce tiše selže (HA zaznamená 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Zobrazit všechny objekty", + "show_all_tasks": "Vymazat filtr — zobrazit všechny úkoly", + "filter_to_overdue": "Filtrovat seznam jen na po termínu", + "filter_to_due_soon": "Filtrovat seznam jen na blížící se termín", + "filter_to_triggered": "Filtrovat seznam jen na spuštěné", + "open_task": "Otevřít úkol", + "show_details": "Zobrazit historii + statistiky", + "hide_details": "Skrýt podrobnosti", + "history_empty": "Zatím žádná historie.", + "history_edit_button": "Upravit záznam", + "total_cost": "Celkové náklady", + "times_performed": "Provedeno", + "older_entries": "starší", + "open_in_panel": "Otevřít v panelu Údržba", + "skip_reason": "Důvod přeskočení (volitelné)", + "reset_to_date": "Resetovat poslední provedení na", + "delete_task_confirm": "Smazat tento úkol a jeho historii?", + "delete_object_confirm": "Smazat tento objekt a všechny jeho úkoly?", + "loading": "Načítání…", + "archive": "Archivovat", + "undo": "Zpět", + "task_archived": "Úkol archivován", + "object_archived": "Objekt archivován", + "unarchive": "Obnovit z archivu", + "archived": "Archivováno", + "show_archived": "Zobrazit archivované", + "hide_archived": "Skrýt archivované", + "confirm_archive_object": "Archivovat tento objekt a jeho úkoly? Historie zůstane zachována a lze ji později obnovit.", + "settings_archive": "Archiv a uchovávání", + "settings_archive_desc": "Odkládejte dokončené jednorázové úkoly bez mazání. Archivované položky jsou skryté a neaktivní, ale zachovávají historii a náklady.", + "settings_archive_oneoff_days": "Automaticky archivovat dokončené jednorázové úkoly po (dny, 0 = vypnuto)", + "settings_delete_archived_oneoff_days": "Automaticky mazat archivované jednorázové úkoly po (dny, 0 = nikdy)", + "archive_object": "Archivovat objekt", + "unarchive_object": "Obnovit objekt z archivu", + "documents": "Dokumenty", + "documents_empty": "Zatím žádné dokumenty.", + "doc_upload": "Nahrát soubor", + "doc_uploading": "Nahrávání…", + "doc_add_link": "Přidat odkaz", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Název (nepovinné)", + "doc_open": "Otevřít", + "doc_delete_confirm": "Smazat „{name}“?", + "doc_too_large": "Soubor je příliš velký (max. 25 MB).", + "doc_upload_failed": "Nahrání selhalo.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Již uloženo jinde — sdíleno, bez dalšího místa.", + "doc_dup_in_object": "Tento soubor je již připojen k tomuto objektu.", + "doc_link_invalid": "Povoleny jsou pouze odkazy http/https.", + "doc_cat_manual": "Návod", + "doc_cat_warranty": "Záruka", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Náhradní díly", + "doc_cat_photo": "Fotografie", + "doc_cat_other": "Ostatní", + "doc_link_badge": "Odkaz", + "doc_storage_title": "Úložiště dokumentů", + "doc_storage_saved": "Ušetřeno deduplikací", + "doc_storage_refresh": "Obnovit", + "doc_download": "Stáhnout", + "doc_close": "Zavřít", + "doc_camera": "Vyfotit", + "doc_drop_hint": "Přetáhněte soubory sem", + "doc_task_none": "Žádné dokumenty spojené s tímto úkolem.", + "doc_link_existing": "Připojit dokument…", + "doc_attach": "Připojit", + "doc_unlink": "Odpojit", + "doc_page": "Strana", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1r", + "chart_since_service": "od poslední údržby", + "chart_no_stats": "Pro tuto entitu nejsou dlouhodobé statistiky — zobrazují se pouze hodnoty z údržbových záznamů", + "auto_complete_on_recovery": "Automaticky dokončit při zotavení senzoru", + "auto_complete_on_recovery_help": "Zaznamená dokončení (nastaví poslední provedení), jakmile se spouštěč sám vyřeší — např. doplněná sůl, vyměněný filtr.", + "doc_search": "Hledat dokumenty…", + "doc_search_none": "Žádné odpovídající dokumenty", + "link_device_optional": "Propojit s existujícím zařízením (volitelné)", + "parent_object_optional": "Nadřazený objekt (volitelné)", + "parent_none": "(Žádný nadřazený)", + "paused": "Pozastaveno", + "pause_object": "Pozastavit", + "resume_object": "Pokračovat", + "pause_until_prompt": "Zmrazit plány tohoto objektu — nic není splatné a nic neupozorňuje, dokud nebude obnoven. Volitelně nastavte datum automatického obnovení.", + "pause_until_label": "Pokračovat dne (volitelné)", + "object_paused": "Objekt pozastaven", + "object_resumed": "Objekt obnoven — plány restartovány", + "object_paused_badge": "Pozastaveno", + "paused_until_label": "do", + "replace_object": "Nahradit…", + "replace_object_prompt": "Vyřadit tento objekt a vytvořit nástupce. Historie a náklady zůstávají archivované u starého; úkoly a dokumenty přecházejí na nový, počítadla začínají od nuly.", + "replace_name_label": "Název nástupce", + "object_replaced": "Objekt nahrazen — nástupce vytvořen", + "reading_unit_label": "Jednotka odečtu (např. kWh, m³)", + "reading_unit_help": "Zobrazí se vedle zaznamenané hodnoty při dokončení tohoto úkolu.", + "reading_value_label": "Hodnota odečtu", + "reading_label": "Odečet", + "settings_templates_label": "Galerie šablon", + "settings_templates_hint": "Odškrtněte šablony, které nikdy nevyužijete — zmizí z výběru „Ze šablony\" (panel i config flow). Nic jiného se nemění; kdykoli je lze znovu zapnout.", + "worksheet": "Pracovní list", + "worksheet_scan_view": "Naskenujte pro otevření úkolu", + "worksheet_scan_complete": "Naskenujte pro dokončení", + "worksheet_manual_excerpt": "Výňatek z manuálu", + "worksheet_pages": "strany", + "worksheet_printed": "Vytištěno", + "worksheet_never": "Nikdy" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/da.json b/custom_components/maintenance_supporter/frontend/locales/da.json new file mode 100644 index 0000000..648bfc0 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/da.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Vedligeholdelse", + "objects": "Objekter", + "tasks": "Opgaver", + "overdue": "Forfalden", + "due_soon": "Snart forfalden", + "triggered": "Udløst", + "trigger_replaced": "Udløser erstattet", + "ok": "OK", + "all": "Alle", + "new_object": "+ Nyt objekt", + "templates_from": "Fra skabelon", + "templates_title": "Start fra en skabelon", + "templates_task_count": "{n} opgaver", + "template_created": "Oprettet fra skabelon", + "onboard_hint": "Tilføj dit første objekt for at spore vedligeholdelse.", + "edit": "Rediger", + "duplicate": "Dupliker", + "task_duplicated": "Opgave duplikeret", + "object_duplicated": "Objekt duplikeret", + "delete": "Slet", + "add_task": "+ Tilføj opgave", + "complete": "Fuldfør", + "completed": "Fuldført", + "skip": "Spring over", + "skipped": "Sprunget over", + "missed": "Missed", + "reset": "Nulstil", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Annuller", + "bulk_select": "Vælg", + "bulk_select_all": "Vælg alle", + "bulk_n_selected": "{n} valgt", + "bulk_completed": "{n} opgaver fuldført", + "bulk_archived": "{n} opgaver arkiveret", + "completing": "Fuldfører…", + "interval": "Interval", + "warning": "Advarsel", + "last_performed": "Senest udført", + "next_due": "Næste forfald", + "days_until_due": "Dage til forfald", + "avg_duration": "Gns. varighed", + "trigger": "Udløser", + "trigger_type": "Udløsertype", + "threshold_above": "Øvre grænse", + "threshold_below": "Nedre grænse", + "threshold": "Tærskel", + "counter": "Tæller", + "state_change": "Tilstandsændring", + "runtime": "Driftstid", + "runtime_hours": "Måldriftstid (timer)", + "target_value": "Målværdi", + "baseline": "Referenceværdi", + "target_changes": "Antal målændringer", + "for_minutes": "I (minutter)", + "time_based": "Tidsbaseret", + "sensor_based": "Sensorbaseret", + "manual": "Manuel", + "one_time": "Engangs", + "weekdays": "Ugedage", + "nth_weekday": "N'te ugedag i måneden", + "day_of_month": "Dag i måneden", + "recurrence_on_days": "Gentag på", + "recurrence_occurrence": "Forekomst", + "recurrence_weekday": "Ugedag", + "recurrence_day": "Dag i måneden (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Sidste", + "day_word": "Dag", + "interval_value": "Interval", + "interval_unit": "Enhed", + "unit_days": "Dage", + "unit_weeks": "Uger", + "unit_months": "Måneder", + "unit_years": "År", + "due_date": "Forfaldsdato", + "cleaning": "Rengøring", + "inspection": "Inspektion", + "replacement": "Udskiftning", + "calibration": "Kalibrering", + "service": "Service", + "reading": "Aflæsning", + "custom": "Tilpasset", + "history": "Historik", + "cost": "Omkostning", + "report_button": "Rapport", + "report_title": "Vedligeholdelsesrapport", + "report_generated": "Genereret", + "report_times_done": "Udført", + "report_total_cost": "Samlede omkostninger", + "report_every": "hver {n}. {unit}", + "report_notes": "Noter", + "report_col_type": "Type", + "report_col_status": "Status", + "report_col_schedule": "Tidsplan", + "duration": "Varighed", + "both": "Begge", + "trigger_val": "Udløserværdi", + "complete_title": "Fuldfør: ", + "checklist": "Tjekliste", + "checklist_steps_optional": "Tjeklistetrin (valgfrit)", + "checklist_placeholder": "Rengør filter\nUdskift pakning\nTest tryk", + "checklist_help": "Ét trin pr. linje. Maks. 100 elementer.", + "err_too_long": "{field}: for lang (maks. {n} tegn)", + "err_too_short": "{field}: for kort (min. {n} tegn)", + "err_value_too_high": "{field}: for stor (maks. {n})", + "err_value_too_low": "{field}: for lille (min. {n})", + "err_required": "{field}: påkrævet", + "err_wrong_type": "{field}: forkert type (forventet: {type})", + "err_invalid_choice": "{field}: ikke en tilladt værdi", + "err_invalid_value": "{field}: ugyldig værdi", + "feat_schedule_time": "Planlægning efter tidspunkt på dagen", + "feat_schedule_time_desc": "Opgaver bliver forfaldne på et bestemt tidspunkt på dagen i stedet for midnat.", + "schedule_time_optional": "Forfalder kl. (valgfrit, TT:MM)", + "schedule_time_help": "Tom = midnat (standard). HA-tidszone.", + "at_time": "kl.", + "notes_optional": "Noter (valgfrit)", + "cost_optional": "Omkostning (valgfrit)", + "duration_minutes": "Varighed i minutter (valgfrit)", + "days": "dage", + "day": "dag", + "today": "I dag", + "d_overdue": "d forsinket", + "no_tasks": "Ingen vedligeholdelsesopgaver endnu. Opret et objekt for at komme i gang.", + "no_tasks_short": "Ingen opgaver", + "no_history": "Ingen historikposter endnu.", + "show_all": "Vis alle", + "cost_duration_chart": "Omkostning og varighed", + "installed": "Installeret", + "confirm_delete_object": "Slet dette objekt og alle dets opgaver?", + "confirm_delete_task": "Slet denne opgave?", + "min": "Min", + "max": "Maks", + "save": "Gem", + "saving": "Gemmer…", + "edit_task": "Rediger opgave", + "new_task": "Ny vedligeholdelsesopgave", + "task_name": "Opgavenavn", + "maintenance_type": "Vedligeholdelsestype", + "priority": "Prioritet", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Lav", + "priority_normal": "Normal", + "priority_high": "Høj", + "schedule_type": "Planlægningstype", + "interval_days": "Interval (dage)", + "warning_days": "Advarselsdage", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Senest udført (valgfrit)", + "interval_anchor": "Intervalanker", + "anchor_completion": "Fra fuldførelsesdato", + "anchor_planned": "Fra planlagt dato (ingen drift)", + "edit_object": "Rediger objekt", + "name": "Navn", + "manufacturer_optional": "Producent (valgfrit)", + "model_optional": "Model (valgfrit)", + "serial_number_optional": "Serienummer (valgfrit)", + "serial_number_label": "S/N", + "documentation_url_label": "Manual", + "object_notes_label": "Noter", + "sort_due_date": "Forfaldsdato", + "sort_object": "Objektnavn", + "sort_type": "Type", + "sort_task_name": "Opgavenavn", + "all_objects": "Alle objekter", + "tasks_lower": "opgaver", + "no_tasks_yet": "Ingen opgaver endnu", + "add_first_task": "Tilføj første opgave", + "trigger_configuration": "Udløserkonfiguration", + "entity_id": "Enheds-ID", + "comma_separated": "kommasepareret", + "entity_logic": "Enhedslogik", + "entity_logic_any": "Enhver enhed udløser", + "entity_logic_all": "Alle enheder skal udløse", + "entities": "enheder", + "attribute_optional": "Attribut (valgfrit, tom = tilstand)", + "use_entity_state": "Brug enhedstilstand (ingen attribut)", + "trigger_above": "Udløs over", + "trigger_below": "Udløs under", + "for_at_least_minutes": "I mindst (minutter)", + "safety_interval_days": "Sikkerhedsinterval (dage, valgfrit)", + "safety_interval": "Sikkerhedsinterval (valgfrit)", + "delta_mode": "Delta-tilstand", + "from_state_optional": "Fra tilstand (valgfrit)", + "to_state_optional": "Til tilstand (valgfrit)", + "documentation_url_optional": "Dokumentations-URL (valgfrit)", + "object_notes_optional": "Noter (valgfrit)", + "nfc_tag_id_optional": "NFC-tag-ID (valgfrit)", + "nfc_tags_empty_help": "Ingen NFC-tags registreret i Home Assistant endnu.", + "nfc_tags_open_settings": "Åbn tag-indstillinger", + "nfc_tags_refresh": "Opdater", + "environmental_entity_optional": "Miljøsensor (valgfrit)", + "environmental_entity_helper": "f.eks. sensor.outdoor_temperature — justerer intervallet baseret på miljøforhold", + "environmental_attribute_optional": "Miljøattribut (valgfrit)", + "nfc_tag_id": "NFC-tag-ID", + "nfc_linked": "NFC-tag tilknyttet", + "nfc_link_hint": "Klik for at tilknytte NFC-tag", + "responsible_user": "Ansvarlig bruger", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ingen bruger tildelt)", + "all_users": "Alle brugere", + "my_tasks": "Mine opgaver", + "tab_calendar": "Kalender", + "cal_no_events": "Ingen vedligeholdelse", + "cal_window_7": "7 dage", + "cal_window_14": "14 dage", + "cal_window_30": "30 dage", + "cal_window_365": "1 år", + "cal_every_n_days": "hver {n}. dag", + "cal_source_time": "Tidsbaseret", + "cal_source_time_adaptive": "Tidsbaseret (adaptiv)", + "cal_source_sensor": "Sensorbaseret", + "cal_predicted": "forudsagt", + "cal_confidence_high": "høj sikkerhed", + "cal_confidence_medium": "mellem sikkerhed", + "cal_confidence_low": "lav sikkerhed", + "budget_monthly": "Månedligt budget", + "budget_yearly": "Årligt budget", + "groups": "Grupper", + "new_group": "Ny gruppe", + "edit_group": "Rediger gruppe", + "no_groups": "Ingen grupper endnu", + "delete_group": "Slet gruppe", + "delete_group_confirm": "Slet gruppen '{name}'?", + "group_select_tasks": "Vælg opgaver", + "group_name_required": "Navn er påkrævet", + "description_optional": "Beskrivelse (valgfrit)", + "selected": "Valgte", + "loading_chart": "Indlæser diagramdata...", + "hide_outliers": "Skjul outliers (sensorfejl)", + "was_maintenance_needed": "Var denne vedligeholdelse nødvendig?", + "feedback_needed": "Nødvendig", + "feedback_not_needed": "Ikke nødvendig", + "feedback_not_sure": "Usikker", + "suggested_interval": "Foreslået interval", + "apply_suggestion": "Anvend", + "reanalyze": "Analyser igen", + "reanalyze_result": "Ny analyse", + "reanalyze_insufficient_data": "Ikke nok data til at give en anbefaling", + "data_points": "datapunkter", + "dismiss_suggestion": "Afvis", + "confidence_low": "Lav", + "confidence_medium": "Mellem", + "confidence_high": "Høj", + "recommended": "anbefalet", + "seasonal_awareness": "Sæsonbevidsthed", + "edit_seasonal_overrides": "Rediger sæsonfaktorer", + "seasonal_overrides_title": "Sæsonfaktorer (tilsidesæt)", + "seasonal_overrides_hint": "Faktor pr. måned (0.1–5.0). Tom = lært automatisk.", + "seasonal_override_invalid": "Ugyldig værdi", + "seasonal_override_range": "Faktor skal være mellem 0.1 og 5.0", + "clear_all": "Ryd alle", + "seasonal_chart_title": "Sæsonfaktorer", + "seasonal_learned": "Lært", + "seasonal_manual": "Manuel", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "Maj", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Dec", + "sensor_prediction": "Sensorforudsigelse", + "degradation_trend": "Tendens", + "trend_rising": "Stigende", + "trend_falling": "Faldende", + "trend_stable": "Stabil", + "trend_insufficient_data": "Utilstrækkelige data", + "days_until_threshold": "Dage til tærskel", + "threshold_exceeded": "Tærskel overskredet", + "environmental_adjustment": "Miljøfaktor", + "sensor_prediction_urgency": "Sensor forudsiger tærskel om ~{days} dage", + "day_short": "d", + "weibull_reliability_curve": "Pålidelighedskurve", + "weibull_failure_probability": "Fejlsandsynlighed", + "weibull_r_squared": "Tilpasning R²", + "beta_early_failures": "Tidlige fejl", + "beta_random_failures": "Tilfældige fejl", + "beta_wear_out": "Slitage", + "beta_highly_predictable": "Meget forudsigelig", + "confidence_interval": "Konfidensinterval", + "confidence_conservative": "Konservativ", + "confidence_aggressive": "Optimistisk", + "current_interval_marker": "Nuværende interval", + "recommended_marker": "Anbefalet", + "characteristic_life": "Karakteristisk levetid", + "chart_mini_sparkline": "Tendensdiagram", + "chart_history": "Omkostnings- og varighedshistorik", + "chart_seasonal": "Sæsonfaktorer, 12 måneder", + "chart_weibull": "Weibull-pålidelighedskurve", + "chart_sparkline": "Diagram over sensorudløserværdi", + "days_progress": "Dagsforløb", + "qr_code": "QR-kode", + "qr_generating": "Genererer QR-kode…", + "qr_error": "Kunne ikke generere QR-kode.", + "qr_error_no_url": "Ingen HA-URL konfigureret. Angiv en ekstern eller intern URL under Indstillinger → System → Netværk.", + "save_error": "Kunne ikke gemme. Prøv igen.", + "qr_print": "Udskriv", + "qr_download": "Download SVG", + "qr_action": "Handling ved scanning", + "qr_action_view": "Vis vedligeholdelsesoplysninger", + "qr_action_complete": "Marker vedligeholdelse som fuldført", + "qr_url_mode": "Linktype", + "qr_mode_companion": "Companion-app", + "qr_mode_local": "Lokal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Oversigt", + "analysis": "Analyse", + "recent_activities": "Seneste aktiviteter", + "search_notes": "Søg i noter", + "avg_cost": "Gns. omkostning", + "no_advanced_features": "Ingen avancerede funktioner aktiveret", + "no_advanced_features_hint": "Aktiver „Adaptive intervaller” eller „Sæsonmønstre” i integrationsindstillingerne for at se analysedata her.", + "analysis_not_enough_data": "Ikke nok data til analyse endnu.", + "analysis_not_enough_data_hint": "Weibull-analyse kræver mindst 5 fuldførte vedligeholdelser; sæsonmønstre bliver synlige efter 6+ datapunkter pr. måned.", + "analysis_manual_task_hint": "Manuelle opgaver uden et interval genererer ikke analysedata.", + "completions": "fuldførelser", + "current": "Nuværende", + "shorter": "Kortere", + "longer": "Længere", + "normal": "Normal", + "disabled": "Deaktiveret", + "compound_logic": "Sammensat logik", + "compound": "Sammensat (flere betingelser)", + "compound_logic_and": "OG — alle betingelser skal udløses", + "compound_logic_or": "ELLER — en betingelse er nok", + "compound_help": "Kombiner flere sensorbetingelser til én udløser.", + "compound_no_conditions": "Ingen betingelser endnu — tilføj mindst én.", + "compound_add_condition": "Tilføj betingelse", + "compound_condition": "Betingelse", + "compound_remove_condition": "Fjern betingelse", + "card_title": "Titel", + "card_show_header": "Vis overskrift med statistik", + "card_show_actions": "Vis handlingsknapper", + "card_compact": "Kompakt tilstand", + "card_max_items": "Maks. elementer (0 = alle)", + "card_filter_status": "Filtrer efter status", + "card_filter_status_help": "Tom = vis alle statusser.", + "card_filter_objects": "Filtrer efter objekter", + "card_filter_objects_help": "Tom = vis alle objekter.", + "card_filter_entities": "Filtrer efter enheder (entity_ids)", + "card_filter_entities_help": "Vælg sensor- / binary_sensor-enheder fra denne integration. Tom = alle.", + "card_loading_objects": "Indlæser objekter…", + "card_load_error": "Kunne ikke indlæse objekter — tjek WebSocket-forbindelsen.", + "card_no_tasks_title": "Ingen vedligeholdelsesopgaver endnu", + "card_no_tasks_cta": "→ Opret en i Vedligeholdelse-panelet", + "no_objects": "Ingen objekter endnu.", + "action_error": "Handlingen mislykkedes. Prøv igen.", + "area_id_optional": "Område (valgfrit)", + "installation_date_optional": "Installationsdato (valgfrit)", + "warranty_expiry_optional": "Garantiens udløb (valgfrit)", + "warranty": "Garanti", + "warranty_valid_until": "gyldig indtil {date}", + "warranty_expires_in": "udløber om {days} dage", + "warranty_expired": "udløbet", + "cal_past_windows": "Tidligere vinduer", + "cal_forward_windows": "Kommende vinduer", + "history_edit_title": "Rediger historikpost", + "history_edit_timestamp": "Tidsstempel", + "manufacturer": "Producent", + "model": "Model", + "area": "Område", + "actions": "Handlinger", + "view_mode_label": "Visning", + "view_cards": "Kortvisning", + "view_table": "Tabelvisning", + "objects_table_columns_label": "Kolonner i objekttabel", + "objects_table_columns_hint": "Vælg, hvilke kolonner der vises i objekttabelvisningen.", + "custom_icon_optional": "Ikon (valgfrit, f.eks. mdi:wrench)", + "task_enabled": "Opgave aktiveret", + "skip_reason_prompt": "Spring denne opgave over?", + "reason_optional": "Årsag (valgfrit)", + "reset_date_prompt": "Marker opgave som udført?", + "reset_date_optional": "Dato for seneste udførelse (valgfrit, standard er i dag)", + "notes_label": "Noter", + "documentation_label": "Dokumentation", + "no_nfc_tag": "— Intet tag —", + "dashboard": "Oversigt", + "tab_today": "I dag", + "palette_placeholder": "Søg objekter og opgaver…", + "palette_no_results": "Ingen match", + "palette_hint": "↑↓ naviger · Enter åbn · Esc luk", + "today_all_caught_up": "Alt er klaret! Intet forfalder i denne uge.", + "today_overdue": "Forfaldne", + "today_due_today": "Forfalder i dag", + "today_this_week": "Denne uge", + "settings": "Indstillinger", + "settings_features": "Avancerede funktioner", + "settings_features_desc": "Aktiver eller deaktiver avancerede funktioner. Deaktivering skjuler dem fra brugerfladen, men sletter ikke data.", + "feat_adaptive": "Adaptiv planlægning", + "feat_adaptive_desc": "Lær optimale intervaller fra vedligeholdelseshistorik", + "feat_predictions": "Sensorforudsigelser", + "feat_predictions_desc": "Forudsig udløsningsdatoer fra sensornedbrydning", + "feat_seasonal": "Sæsonjusteringer", + "feat_seasonal_desc": "Juster intervaller baseret på sæsonmønstre", + "feat_environmental": "Miljøkorrelation", + "feat_environmental_desc": "Korreler intervaller med temperatur/luftfugtighed", + "feat_budget": "Budgetopfølgning", + "feat_budget_desc": "Følg månedlige og årlige vedligeholdelsesudgifter", + "feat_groups": "Opgavegrupper", + "feat_groups_desc": "Organiser opgaver i logiske grupper", + "feat_checklists": "Tjeklister", + "feat_checklists_desc": "Procedurer i flere trin til fuldførelse af opgaver", + "settings_general": "Generelt", + "settings_default_warning": "Standard advarselsdage", + "settings_panel_enabled": "Sidepanel", + "settings_panel_title": "Sidepanelets titel", + "settings_notifications": "Notifikationer", + "settings_notify_service": "Notifikationstjeneste", + "test_notification": "Testnotifikation", + "send_test": "Send test", + "testing": "Sender…", + "test_notification_success": "Testnotifikation sendt", + "test_notification_failed": "Testnotifikation mislykkedes", + "settings_notify_due_soon": "Notificer ved snart forfalden", + "settings_notify_overdue": "Notificer ved forfalden", + "settings_notify_triggered": "Notificer ved udløst", + "settings_interval_hours": "Gentagelsesinterval (timer, 0 = én gang)", + "settings_quiet_hours": "Stilletimer", + "settings_quiet_start": "Start", + "settings_quiet_end": "Slut", + "settings_max_per_day": "Maks. notifikationer pr. dag (0 = ubegrænset)", + "settings_bundling": "Saml notifikationer", + "settings_bundle_threshold": "Samlingstærskel", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobile handlingsknapper", + "settings_action_complete": "Vis 'Fuldfør'-knap", + "settings_action_skip": "Vis 'Spring over'-knap", + "settings_action_snooze": "Vis 'Udsæt'-knap", + "settings_weekly_digest": "Ugentlig oversigt", + "settings_weekly_digest_hint": "Én samlet notifikation mandag morgen, når opgaver forfalder.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Udsættelsesvarighed (timer)", + "settings_budget": "Budget", + "settings_currency": "Valuta", + "settings_budget_monthly": "Månedligt budget", + "settings_budget_yearly": "Årligt budget", + "settings_budget_alerts": "Budgetadvarsler", + "settings_budget_threshold": "Advarselstærskel (%)", + "settings_import_export": "Import / Eksport", + "settings_export_json": "Eksporter JSON", + "settings_export_yaml": "Eksporter YAML", + "settings_export_csv": "Eksporter CSV", + "settings_import_csv": "Importer CSV", + "settings_import_placeholder": "Indsæt JSON- eller CSV-indhold her…", + "settings_import_btn": "Importer", + "settings_import_success": "{count} objekter importeret.", + "settings_export_success": "Eksport downloadet.", + "settings_saved": "Indstilling gemt.", + "settings_include_history": "Inkluder historik", + "sort_alphabetical": "Alfabetisk", + "sort_due_soonest": "Forfalder først", + "sort_task_count": "Antal opgaver", + "sort_area": "Område", + "sort_assigned_user": "Tildelt bruger", + "sort_group": "Gruppe", + "groupby_none": "Ingen gruppering", + "groupby_area": "Efter område", + "groupby_group": "Efter gruppe", + "groupby_user": "Efter bruger", + "filter_label": "Filter", + "user_label": "Bruger", + "sort_label": "Sortering", + "group_by_label": "Grupper efter", + "state_value_help": "Brug HA-tilstandsværdien (normalt med små bogstaver, f.eks. \"on\"/\"off\"). Store/små bogstaver normaliseres ved lagring.", + "target_changes_help": "Antal matchende overgange, før udløseren aktiveres (standard: 1).", + "qr_print_title": "Udskriv QR-koder", + "qr_print_desc": "Generer en udskriftsklar side med QR-koder, der kan klippes ud og sættes på dit udstyr.", + "qr_print_load": "Indlæs objekter", + "qr_print_filter": "Filter", + "qr_print_objects": "Objekter", + "qr_print_actions": "Handlinger", + "qr_print_url_mode": "Linktype", + "qr_print_estimate": "Anslåede QR-koder", + "qr_print_over_limit": "grænsen er 200, indsnævr filteret", + "qr_print_generate": "Generer QR-koder", + "qr_print_generating": "Genererer…", + "qr_print_ready": "QR-koder klar", + "qr_print_print_button": "Udskriv", + "qr_print_empty": "Intet at generere", + "qr_action_skip": "Spring over", + "vacation_title": "Ferietilstand", + "vacation_active": "aktiv", + "vacation_ended": "afsluttet", + "vacation_desc": "Planlæg en ferie: notifikationer sættes på pause i perioden plus et bufferantal dage. Du kan vælge bestemte opgaver til igen.", + "vacation_enable": "Aktiver ferietilstand", + "vacation_start": "Start", + "vacation_end": "Slut", + "vacation_buffer": "Buffer (dage)", + "vacation_exempt_title": "Notificer alligevel under ferie", + "vacation_exempt_desc": "Vælg opgaver, der stadig skal notificere under ferien (f.eks. kritisk poolkemi).", + "vacation_load_tasks": "Indlæs opgaver", + "vacation_preview_btn": "Vis forhåndsvisning", + "vacation_preview_affected": "opgaver berørt", + "vacation_event_due_soon": "bliver snart forfalden", + "vacation_event_overdue": "bliver forfalden", + "vacation_event_triggered_est": "sensorudløsning mulig", + "vacation_sensor_based": "(sensorbaseret)", + "vacation_action_notify": "Notificer alligevel", + "vacation_action_unsilence": "Dæmp igen", + "vacation_marked_complete": "Markeret som fuldført", + "vacation_marked_skip": "Sprunget over", + "vacation_end_now": "Afslut ferie nu", + "add": "Tilføj", + "show_stats": "Vis statistik + grafer", + "hide_stats": "Skjul statistik", + "adaptive_no_data": "Endnu ikke nok fuldførelseshistorik til adaptiv analyse. Fuldfør denne opgave et par gange mere for at låse op for intervalanbefalinger og pålidelighedsdiagrammer.", + "suggestion_applied": "Foreslået interval anvendt", + "vacation_mode": "Ferietilstand", + "vacation_status_active": "Aktiv nu", + "vacation_status_scheduled": "Planlagt", + "vacation_status_inactive": "Inaktiv", + "vacation_end_now_confirm": "Afslut ferie med det samme?", + "vacation_exempt_count": "undtaget", + "vacation_advanced": "Avanceret…", + "vacation_open_panel": "Åbn i panel", + "enable": "Aktiver", + "saved": "Gemt", + "budget_monthly_set": "Angiv måned", + "budget_yearly_set": "Angiv år", + "budget_advanced": "Valuta, advarsler…", + "budget_open_panel": "Åbn i panel", + "groups_empty": "Ingen grupper endnu.", + "group_new_placeholder": "Tilføj gruppe…", + "group_delete_confirm": "Slet gruppen \"{name}\"?", + "groups_manage_tasks": "Administrer opgavetildelinger…", + "groups_open_panel": "Åbn i panel", + "unassigned": "Ikke tildelt", + "no_area": "Intet område", + "has_overdue": "Har forfaldne opgaver", + "object": "Objekt", + "settings_panel_access": "Paneladgang", + "settings_panel_access_desc": "Administratorer har altid fuld adgang. For at delegere oprettelse, redigering og sletning til bestemte ikke-administratorer skal du slå dette til og vælge dem nedenfor — alle andre ser kun Fuldfør og Spring over.", + "settings_operator_write": "Tillad valgte brugere at oprette, redigere og slette", + "settings_operator_write_desc": "Fra: kun administratorer kan ændre indhold. Til: de valgte brugere nedenfor får også fuld adgang.", + "no_non_admin_users": "Ingen ikke-administratorbrugere fundet. Tilføj nogle under Indstillinger → Personer.", + "owner_label": "Ejer", + "feat_completion_actions": "Fuldførelseshandlinger", + "feat_completion_actions_desc": "HA-handling pr. opgave ved fuldførelse + hurtig-fuldfør-QR med forudindstillede værdier.", + "on_complete_action_title": "Ved fuldførelse: udløs HA-handling (valgfrit)", + "on_complete_action_desc": "Kalder en HA-tjeneste, når opgaven fuldføres — f.eks. nulstil en tæller på enheden.", + "on_complete_action_service": "Tjeneste", + "on_complete_action_target": "Målenhed", + "on_complete_action_target_hint": "Bemærk: enhedens domæne skal matche tjenesten — f.eks. virker 'button.press' kun på button.*, 'counter.increment' kun på counter.*, 'input_button.press' kun på input_button.* osv. Ved et misforhold mislykkes handlingen lydløst (HA logger 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Data (JSON, valgfrit)", + "on_complete_action_test": "Valider konfiguration", + "on_complete_action_test_success": "✓ Konfiguration gyldig (handlingen udløses kun ved fuldførelse af opgaven)", + "on_complete_action_test_failed": "Mislykkedes", + "quick_complete_defaults_title": "Standardværdier for hurtig-fuldfør (til QR-scanninger, valgfrit)", + "quick_complete_defaults_desc": "Forudindstillede værdier til hurtig-fuldfør-QR-scanninger. Uden disse åbner QR-koden fuldfør-dialogen.", + "quick_complete_defaults_notes": "Noter", + "quick_complete_defaults_cost": "Omkostning", + "quick_complete_defaults_duration": "Varighed (minutter)", + "quick_complete_defaults_feedback_none": "Ingen feedback", + "quick_complete_defaults_feedback_needed": "Var nødvendig", + "quick_complete_defaults_feedback_not_needed": "Ikke nødvendig", + "quick_complete_success": "Hurtigt markeret som fuldført", + "show_all_objects": "Vis alle objekter", + "show_all_tasks": "Ryd filter — vis alle opgaver", + "filter_to_overdue": "Filtrer opgavelisten til kun forfaldne", + "filter_to_due_soon": "Filtrer opgavelisten til kun snart forfaldne", + "filter_to_triggered": "Filtrer opgavelisten til kun udløste", + "open_task": "Åbn opgave", + "show_details": "Vis historik + statistik", + "hide_details": "Skjul detaljer", + "history_empty": "Ingen historik endnu.", + "history_edit_button": "Rediger post", + "total_cost": "Samlet omkostning", + "times_performed": "Udført", + "older_entries": "ældre", + "open_in_panel": "Åbn i Vedligeholdelse-panelet", + "skip_reason": "Årsag til at springe over (valgfrit)", + "reset_to_date": "Nulstil senest udført til", + "delete_task_confirm": "Slet denne opgave og dens historik?", + "delete_object_confirm": "Slet dette objekt og alle dets opgaver?", + "loading": "Indlæser…", + "archive": "Arkivér", + "undo": "Fortryd", + "task_archived": "Opgave arkiveret", + "object_archived": "Objekt arkiveret", + "unarchive": "Gendan", + "archived": "Arkiveret", + "show_archived": "Vis arkiverede", + "hide_archived": "Skjul arkiverede", + "confirm_archive_object": "Arkivér dette objekt og dets opgaver? Historikken bevares og kan gendannes senere.", + "settings_archive": "Arkiv og opbevaring", + "settings_archive_desc": "Læg færdige engangsopgaver væk uden at slette dem. Arkiverede elementer er skjult og inaktive, men bevarer historik og omkostninger.", + "settings_archive_oneoff_days": "Arkivér automatisk færdige engangsopgaver efter (dage, 0 = fra)", + "settings_delete_archived_oneoff_days": "Slet automatisk arkiverede engangsopgaver efter (dage, 0 = aldrig)", + "archive_object": "Arkivér objekt", + "unarchive_object": "Gendan objekt", + "documents": "Dokumenter", + "documents_empty": "Ingen dokumenter endnu.", + "doc_upload": "Upload fil", + "doc_uploading": "Uploader…", + "doc_add_link": "Tilføj link", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titel (valgfri)", + "doc_open": "Åbn", + "doc_delete_confirm": "Slet \"{name}\"?", + "doc_too_large": "Filen er for stor (maks. 25 MB).", + "doc_upload_failed": "Upload mislykkedes.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Allerede gemt et andet sted — delt, ingen ekstra plads.", + "doc_dup_in_object": "Denne fil er allerede vedhæftet dette objekt.", + "doc_link_invalid": "Kun http/https-links er tilladt.", + "doc_cat_manual": "Vejledning", + "doc_cat_warranty": "Garanti", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Reservedele", + "doc_cat_photo": "Foto", + "doc_cat_other": "Andet", + "doc_link_badge": "Link", + "doc_storage_title": "Dokumentlager", + "doc_storage_saved": "Sparet ved deduplikering", + "doc_storage_refresh": "Opdater", + "doc_download": "Download", + "doc_close": "Luk", + "doc_camera": "Tag billede", + "doc_drop_hint": "Slip filer her", + "doc_task_none": "Ingen dokumenter knyttet til denne opgave.", + "doc_link_existing": "Knyt et dokument…", + "doc_attach": "Knyt", + "doc_unlink": "Fjern kobling", + "doc_page": "Side", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1å", + "chart_since_service": "siden sidste vedligeholdelse", + "chart_no_stats": "Ingen langtidsstatistik for denne enhed — viser kun værdier fra vedligeholdelsesposter", + "auto_complete_on_recovery": "Fuldfør automatisk når sensoren retter sig", + "auto_complete_on_recovery_help": "Registrerer en fuldførelse (sætter senest udført), når udløseren løser sig selv — f.eks. salt påfyldt, filter udskiftet.", + "doc_search": "Søg dokumenter…", + "doc_search_none": "Ingen matchende dokumenter", + "link_device_optional": "Tilknyt eksisterende enhed (valgfrit)", + "parent_object_optional": "Overordnet objekt (valgfrit)", + "parent_none": "(Intet overordnet)", + "paused": "Sat på pause", + "pause_object": "Sæt på pause", + "resume_object": "Genoptag", + "pause_until_prompt": "Frys objektets planer — intet forfalder og intet notificerer, før det genoptages. Angiv eventuelt en dato for automatisk genoptagelse.", + "pause_until_label": "Genoptag den (valgfrit)", + "object_paused": "Objekt sat på pause", + "object_resumed": "Objekt genoptaget — planer genstartet", + "object_paused_badge": "På pause", + "paused_until_label": "indtil", + "replace_object": "Udskift…", + "replace_object_prompt": "Pensioner dette objekt og opret en efterfølger. Historik og omkostninger forbliver arkiveret på det gamle; opgaver og dokumenter følger med til det nye, tællere starter forfra.", + "replace_name_label": "Efterfølgerens navn", + "object_replaced": "Objekt udskiftet — efterfølger oprettet", + "reading_unit_label": "Aflæsningsenhed (f.eks. kWh, m³)", + "reading_unit_help": "Vises ved siden af den registrerede værdi, når opgaven fuldføres.", + "reading_value_label": "Aflæst værdi", + "reading_label": "Aflæsning", + "settings_templates_label": "Skabelongalleri", + "settings_templates_hint": "Fravælg skabeloner, du aldrig får brug for — de forsvinder fra \"Fra skabelon\"-vælgerne (panel og config flow). Intet andet ændres; kan genaktiveres når som helst.", + "worksheet": "Arbejdsark", + "worksheet_scan_view": "Scan for at åbne opgaven", + "worksheet_scan_complete": "Scan for at fuldføre", + "worksheet_manual_excerpt": "Uddrag af manualen", + "worksheet_pages": "sider", + "worksheet_printed": "Udskrevet", + "worksheet_never": "Aldrig" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/de.json b/custom_components/maintenance_supporter/frontend/locales/de.json new file mode 100644 index 0000000..72ec8e6 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/de.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Wartung", + "objects": "Objekte", + "tasks": "Aufgaben", + "overdue": "Überfällig", + "due_soon": "Bald fällig", + "triggered": "Ausgelöst", + "trigger_replaced": "Auslöser ersetzt", + "ok": "OK", + "all": "Alle", + "new_object": "+ Neues Objekt", + "templates_from": "Aus Vorlage", + "templates_title": "Mit einer Vorlage starten", + "templates_task_count": "{n} Aufgaben", + "template_created": "Aus Vorlage erstellt", + "onboard_hint": "Füge dein erstes Objekt hinzu, um mit der Wartung zu starten.", + "edit": "Bearbeiten", + "duplicate": "Duplizieren", + "task_duplicated": "Aufgabe dupliziert", + "object_duplicated": "Objekt dupliziert", + "delete": "Löschen", + "add_task": "+ Aufgabe", + "complete": "Erledigt", + "completed": "Abgeschlossen", + "skip": "Überspringen", + "skipped": "Übersprungen", + "missed": "Verpasst", + "reset": "Zurücksetzen", + "snooze": "Schlummern", + "snoozed": "Schlummert", + "cancel": "Abbrechen", + "bulk_select": "Auswählen", + "bulk_select_all": "Alle auswählen", + "bulk_n_selected": "{n} ausgewählt", + "bulk_completed": "{n} Aufgaben erledigt", + "bulk_archived": "{n} Aufgaben archiviert", + "completing": "Wird erledigt…", + "interval": "Intervall", + "warning": "Vorwarnung", + "last_performed": "Zuletzt durchgeführt", + "next_due": "Nächste Fälligkeit", + "days_until_due": "Tage bis fällig", + "avg_duration": "Ø Dauer", + "trigger": "Trigger", + "trigger_type": "Trigger-Typ", + "threshold_above": "Obergrenze", + "threshold_below": "Untergrenze", + "threshold": "Schwellwert", + "counter": "Zähler", + "state_change": "Zustandsänderung", + "runtime": "Laufzeit", + "runtime_hours": "Ziel-Laufzeit (Stunden)", + "target_value": "Zielwert", + "baseline": "Nulllinie", + "target_changes": "Ziel-Änderungen", + "for_minutes": "Für (Minuten)", + "time_based": "Zeitbasiert", + "sensor_based": "Sensorbasiert", + "manual": "Manuell", + "one_time": "Einmalig", + "weekdays": "Wochentage", + "nth_weekday": "N-ter Wochentag im Monat", + "day_of_month": "Tag im Monat", + "recurrence_on_days": "Wiederholen an", + "recurrence_occurrence": "Vorkommen", + "recurrence_weekday": "Wochentag", + "recurrence_day": "Tag im Monat (1–31)", + "recurrence_last_day": "Letzter Tag des Monats", + "recurrence_business_day": "Nur Werktage (vom Wochenende vorziehen)", + "recurrence_offset": "Versatz (Tage, ±)", + "recurrence_offset_help": "Verschiebt das Datum um ±N Tage, z. B. -2 = zwei Tage davor.", + "last_day_month": "Letzter Tag des Monats", + "last_business_day_month": "Letzter Werktag", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Letzter", + "day_word": "Tag", + "interval_value": "Intervall", + "interval_unit": "Einheit", + "unit_days": "Tage", + "unit_weeks": "Wochen", + "unit_months": "Monate", + "unit_years": "Jahre", + "due_date": "Fälligkeitsdatum", + "cleaning": "Reinigung", + "inspection": "Inspektion", + "replacement": "Austausch", + "calibration": "Kalibrierung", + "service": "Service", + "reading": "Ablesung", + "custom": "Benutzerdefiniert", + "history": "Verlauf", + "cost": "Kosten", + "report_button": "Bericht", + "report_title": "Wartungsbericht", + "report_generated": "Erstellt", + "report_times_done": "Erledigt", + "report_total_cost": "Gesamtkosten", + "report_every": "alle {n} {unit}", + "report_notes": "Notizen", + "report_col_type": "Typ", + "report_col_status": "Status", + "report_col_schedule": "Zeitplan", + "duration": "Dauer", + "both": "Beides", + "trigger_val": "Trigger-Wert", + "complete_title": "Erledigt: ", + "checklist": "Checkliste", + "checklist_steps_optional": "Checkliste-Schritte (optional)", + "checklist_placeholder": "Filter reinigen\nDichtung ersetzen\nDruck testen", + "checklist_help": "Ein Schritt pro Zeile. Max. 100 Einträge.", + "err_too_long": "{field}: zu lang (max. {n} Zeichen)", + "err_too_short": "{field}: zu kurz (min. {n} Zeichen)", + "err_value_too_high": "{field}: zu groß (max. {n})", + "err_value_too_low": "{field}: zu klein (min. {n})", + "err_required": "{field}: Pflichtfeld", + "err_wrong_type": "{field}: falscher Typ (erwartet: {type})", + "err_invalid_choice": "{field}: nicht erlaubter Wert", + "err_invalid_value": "{field}: ungültiger Wert", + "feat_schedule_time": "Uhrzeit-Scheduling", + "feat_schedule_time_desc": "Tasks werden zu einer festen Uhrzeit fällig statt um Mitternacht.", + "schedule_time_optional": "Fällig um (optional, HH:MM)", + "schedule_time_help": "Leer = Mitternacht (Default). HA-Zeitzone.", + "at_time": "um", + "notes_optional": "Notizen (optional)", + "cost_optional": "Kosten (optional)", + "duration_minutes": "Dauer in Minuten (optional)", + "days": "Tage", + "day": "Tag", + "today": "Heute", + "d_overdue": "T überfällig", + "no_tasks": "Keine Wartungsaufgaben vorhanden. Erstellen Sie ein Objekt um zu beginnen.", + "no_tasks_short": "Keine Aufgaben", + "no_history": "Noch keine Verlaufseinträge.", + "show_all": "Alle anzeigen", + "cost_duration_chart": "Kosten & Dauer", + "installed": "Installiert", + "confirm_delete_object": "Dieses Objekt und alle zugehörigen Aufgaben löschen?", + "confirm_delete_task": "Diese Aufgabe wirklich löschen?", + "min": "Min", + "max": "Max", + "save": "Speichern", + "saving": "Speichern…", + "edit_task": "Aufgabe bearbeiten", + "new_task": "Neue Wartungsaufgabe", + "task_name": "Aufgabenname", + "maintenance_type": "Wartungstyp", + "priority": "Priorität", + "labels": "Labels", + "labels_placeholder": "z. B. Sicherheit, Saison, Mieter-sichtbar", + "labels_help": "Kommagetrennte Tags zum Filtern und für Berichte.", + "priority_low": "Niedrig", + "priority_normal": "Normal", + "priority_high": "Hoch", + "schedule_type": "Planungsart", + "interval_days": "Intervall (Tage)", + "warning_days": "Warntage", + "earliest_completion_days": "Frühester Abschluss (Tage vor Fälligkeit)", + "earliest_completion_days_help": "Leer lassen, um jederzeit abschließen zu können. 0 = erst am/nach dem Fälligkeitstag.", + "last_performed_optional": "Zuletzt durchgeführt (optional)", + "interval_anchor": "Intervall-Anker", + "anchor_completion": "Ab Erledigung", + "anchor_planned": "Ab geplantem Datum (kein Drift)", + "edit_object": "Objekt bearbeiten", + "name": "Name", + "manufacturer_optional": "Hersteller (optional)", + "model_optional": "Modell (optional)", + "serial_number_optional": "Seriennummer (optional)", + "serial_number_label": "S/N", + "documentation_url_label": "Handbuch", + "object_notes_label": "Notizen", + "sort_due_date": "Fälligkeit", + "sort_object": "Objekt-Name", + "sort_type": "Typ", + "sort_task_name": "Aufgaben-Name", + "all_objects": "Alle Objekte", + "tasks_lower": "Aufgaben", + "no_tasks_yet": "Noch keine Aufgaben", + "add_first_task": "Erste Aufgabe hinzufügen", + "trigger_configuration": "Trigger-Konfiguration", + "entity_id": "Entitäts-ID", + "comma_separated": "kommagetrennt", + "entity_logic": "Entitäts-Logik", + "entity_logic_any": "Beliebige Entität löst aus", + "entity_logic_all": "Alle Entitäten müssen auslösen", + "entities": "Entitäten", + "attribute_optional": "Attribut (optional, leer = Zustand)", + "use_entity_state": "Entitäts-Zustand verwenden (kein Attribut)", + "trigger_above": "Auslösen wenn über", + "trigger_below": "Auslösen wenn unter", + "for_at_least_minutes": "Für mindestens (Minuten)", + "safety_interval_days": "Sicherheitsintervall (Tage, optional)", + "safety_interval": "Sicherheitsintervall (optional)", + "delta_mode": "Delta-Modus", + "from_state_optional": "Von Zustand (optional)", + "to_state_optional": "Zu Zustand (optional)", + "documentation_url_optional": "Dokumentation URL (optional)", + "object_notes_optional": "Notizen (optional)", + "nfc_tag_id_optional": "NFC-Tag-ID (optional)", + "nfc_tags_empty_help": "Noch keine NFC-Tags in Home Assistant registriert.", + "nfc_tags_open_settings": "Tag-Einstellungen öffnen", + "nfc_tags_refresh": "Aktualisieren", + "environmental_entity_optional": "Umgebungs-Sensor (optional)", + "environmental_entity_helper": "z.B. sensor.aussentemperatur — passt das Intervall an Umgebungswerte an", + "environmental_attribute_optional": "Umgebungs-Attribut (optional)", + "nfc_tag_id": "NFC-Tag-ID", + "nfc_linked": "NFC-Tag verknüpft", + "nfc_link_hint": "Klicken um NFC-Tag zu verknüpfen", + "responsible_user": "Verantwortlicher Benutzer", + "shared_with": "Geteilt mit (Rotation)", + "shared_with_help": "Mehrere Personen auswählen, die sich diese Aufgabe teilen; der/die Verantwortliche wechselt bei jedem Abschluss.", + "rotation_strategy": "Rotation", + "rotation_none": "Keine Rotation", + "rotation_round_robin": "Reihum", + "rotation_least_completed": "Am wenigsten erledigt", + "rotation_random": "Zufällig", + "no_user_assigned": "(Kein Benutzer zugewiesen)", + "all_users": "Alle Benutzer", + "my_tasks": "Meine Aufgaben", + "tab_calendar": "Kalender", + "cal_no_events": "Keine Wartung", + "cal_window_7": "7 Tage", + "cal_window_14": "14 Tage", + "cal_window_30": "30 Tage", + "cal_window_365": "1 Jahr", + "cal_every_n_days": "alle {n} Tage", + "cal_source_time": "Zeit-basiert", + "cal_source_time_adaptive": "Zeit-basiert (adaptiv)", + "cal_source_sensor": "Sensor-basiert", + "cal_predicted": "vorhergesagt", + "cal_confidence_high": "hohe Genauigkeit", + "cal_confidence_medium": "mittlere Genauigkeit", + "cal_confidence_low": "niedrige Genauigkeit", + "budget_monthly": "Monatsbudget", + "budget_yearly": "Jahresbudget", + "groups": "Gruppen", + "new_group": "Neue Gruppe", + "edit_group": "Gruppe bearbeiten", + "no_groups": "Keine Gruppen vorhanden", + "delete_group": "Gruppe löschen", + "delete_group_confirm": "Gruppe '{name}' wirklich löschen?", + "group_select_tasks": "Aufgaben auswählen", + "group_name_required": "Name erforderlich", + "description_optional": "Beschreibung (optional)", + "selected": "Ausgewählt", + "loading_chart": "Daten werden geladen...", + "hide_outliers": "Ausreißer ausblenden (Messfehler)", + "was_maintenance_needed": "War diese Wartung nötig?", + "feedback_needed": "Nötig", + "feedback_not_needed": "Nicht nötig", + "feedback_not_sure": "Unsicher", + "suggested_interval": "Empfohlenes Intervall", + "apply_suggestion": "Übernehmen", + "reanalyze": "Neu analysieren", + "reanalyze_result": "Neue Analyse", + "reanalyze_insufficient_data": "Nicht genügend Daten für eine Empfehlung", + "data_points": "Datenpunkte", + "dismiss_suggestion": "Verwerfen", + "confidence_low": "Niedrig", + "confidence_medium": "Mittel", + "confidence_high": "Hoch", + "recommended": "empfohlen", + "seasonal_awareness": "Saisonale Anpassung", + "edit_seasonal_overrides": "Saison-Faktoren bearbeiten", + "seasonal_overrides_title": "Saisonale Faktoren (Override)", + "seasonal_overrides_hint": "Faktor pro Monat (0.1–5.0). Leer = automatisch gelernt.", + "seasonal_override_invalid": "Ungültiger Wert", + "seasonal_override_range": "Faktor muss zwischen 0.1 und 5.0 liegen", + "clear_all": "Alle zurücksetzen", + "seasonal_chart_title": "Saisonale Faktoren", + "seasonal_learned": "Gelernt", + "seasonal_manual": "Manuell", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mär", + "month_apr": "Apr", + "month_may": "Mai", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Dez", + "sensor_prediction": "Sensorvorhersage", + "degradation_trend": "Trend", + "trend_rising": "Steigend", + "trend_falling": "Fallend", + "trend_stable": "Stabil", + "trend_insufficient_data": "Unzureichende Daten", + "days_until_threshold": "Tage bis Schwellwert", + "threshold_exceeded": "Schwellwert überschritten", + "environmental_adjustment": "Umgebungsfaktor", + "sensor_prediction_urgency": "Sensor prognostiziert Schwellwert in ~{days} Tagen", + "day_short": "Tag", + "weibull_reliability_curve": "Zuverlässigkeitskurve", + "weibull_failure_probability": "Ausfallwahrscheinlichkeit", + "weibull_r_squared": "Güte R²", + "beta_early_failures": "Frühausfälle", + "beta_random_failures": "Zufällige Ausfälle", + "beta_wear_out": "Verschleiß", + "beta_highly_predictable": "Hochvorhersagbar", + "confidence_interval": "Konfidenzintervall", + "confidence_conservative": "Konservativ", + "confidence_aggressive": "Optimistisch", + "current_interval_marker": "Aktuelles Intervall", + "recommended_marker": "Empfohlen", + "characteristic_life": "Charakteristische Lebensdauer", + "chart_mini_sparkline": "Trend-Sparkline", + "chart_history": "Kosten- und Dauer-Verlauf", + "chart_seasonal": "Saisonfaktoren, 12 Monate", + "chart_weibull": "Weibull-Zuverlässigkeitskurve", + "chart_sparkline": "Sensor-Triggerwert-Verlauf", + "days_progress": "Tagesfortschritt", + "qr_code": "QR-Code", + "qr_generating": "QR-Code wird generiert…", + "qr_error": "QR-Code konnte nicht generiert werden.", + "qr_error_no_url": "Keine HA-URL konfiguriert. Bitte unter Einstellungen → System → Netzwerk eine externe oder interne URL setzen.", + "save_error": "Fehler beim Speichern. Bitte erneut versuchen.", + "qr_print": "Drucken", + "qr_download": "SVG herunterladen", + "qr_action": "Aktion beim Scannen", + "qr_action_view": "Wartungsinfo anzeigen", + "qr_action_complete": "Wartung als erledigt markieren", + "qr_url_mode": "Link-Typ", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Übersicht", + "analysis": "Analyse", + "recent_activities": "Letzte Aktivitäten", + "search_notes": "Notizen durchsuchen", + "avg_cost": "Ø Kosten", + "no_advanced_features": "Keine erweiterten Funktionen aktiviert", + "no_advanced_features_hint": "Aktiviere „Adaptive Intervalle“ oder „Saisonale Muster“ in den Integrationseinstellungen, um hier Analysedaten zu sehen.", + "analysis_not_enough_data": "Noch nicht genügend Daten für die Analyse vorhanden.", + "analysis_not_enough_data_hint": "Die Weibull-Analyse benötigt mindestens 5 abgeschlossene Wartungen, saisonale Muster werden nach 6+ Datenpunkten pro Monat sichtbar.", + "analysis_manual_task_hint": "Manuelle Aufgaben ohne Intervall erzeugen keine Analysedaten.", + "completions": "Abschlüsse", + "current": "Aktuell", + "shorter": "Kürzer", + "longer": "Länger", + "normal": "Normal", + "disabled": "Deaktiviert", + "compound_logic": "Verknüpfungslogik", + "compound": "Verknüpft (mehrere Bedingungen)", + "compound_logic_and": "UND — alle Bedingungen müssen auslösen", + "compound_logic_or": "ODER — eine Bedingung genügt", + "compound_help": "Mehrere Sensor-Bedingungen zu einem Auslöser kombinieren.", + "compound_no_conditions": "Noch keine Bedingungen — mindestens eine hinzufügen.", + "compound_add_condition": "Bedingung hinzufügen", + "compound_condition": "Bedingung", + "compound_remove_condition": "Bedingung entfernen", + "card_title": "Titel", + "card_show_header": "Kopfzeile mit Statistiken anzeigen", + "card_show_actions": "Aktionsbuttons anzeigen", + "card_compact": "Kompaktmodus", + "card_max_items": "Max. Einträge (0 = alle)", + "card_filter_status": "Nach Status filtern", + "card_filter_status_help": "Leer = alle Status zeigen.", + "card_filter_objects": "Nach Objekten filtern", + "card_filter_objects_help": "Leer = alle Objekte zeigen.", + "card_filter_entities": "Nach Entitäten filtern (entity_ids)", + "card_filter_entities_help": "Wähle Sensor-/Binary-Sensor-Entitäten dieser Integration. Leer = alle.", + "card_loading_objects": "Lade Objekte…", + "card_load_error": "Objekte konnten nicht geladen werden — bitte WS-Verbindung prüfen.", + "card_no_tasks_title": "Noch keine Wartungsaufgaben", + "card_no_tasks_cta": "→ Im Maintenance-Panel anlegen", + "no_objects": "Keine Objekte vorhanden.", + "action_error": "Aktion fehlgeschlagen. Bitte erneut versuchen.", + "area_id_optional": "Bereich (optional)", + "installation_date_optional": "Installationsdatum (optional)", + "warranty_expiry_optional": "Garantie-Ablauf (optional)", + "warranty": "Garantie", + "warranty_valid_until": "gültig bis {date}", + "warranty_expires_in": "läuft in {days} Tagen ab", + "warranty_expired": "abgelaufen", + "cal_past_windows": "Vergangene Fenster", + "cal_forward_windows": "Zukünftige Fenster", + "history_edit_title": "Verlaufseintrag bearbeiten", + "history_edit_timestamp": "Zeitstempel", + "manufacturer": "Hersteller", + "model": "Modell", + "area": "Bereich", + "actions": "Aktionen", + "view_mode_label": "Ansicht", + "view_cards": "Kartenansicht", + "view_table": "Tabellenansicht", + "objects_table_columns_label": "Spalten der Objekt-Tabelle", + "objects_table_columns_hint": "Wähle die Spalten für die Tabellenansicht der Objektübersicht.", + "custom_icon_optional": "Icon (optional, z.B. mdi:wrench)", + "task_enabled": "Aufgabe aktiviert", + "skip_reason_prompt": "Aufgabe überspringen?", + "reason_optional": "Grund (optional)", + "reset_date_prompt": "Aufgabe als ausgeführt markieren?", + "reset_date_optional": "Letztes Erledigungs-Datum (optional, Standard: heute)", + "notes_label": "Notizen", + "documentation_label": "Dokumentation", + "no_nfc_tag": "— Kein Tag —", + "dashboard": "Dashboard", + "tab_today": "Heute", + "palette_placeholder": "Objekte und Aufgaben suchen…", + "palette_no_results": "Keine Treffer", + "palette_hint": "↑↓ Navigieren · Enter Öffnen · Esc Schließen", + "today_all_caught_up": "Alles erledigt! Diese Woche steht nichts an.", + "today_overdue": "Überfällig", + "today_due_today": "Heute fällig", + "today_this_week": "Diese Woche", + "settings": "Einstellungen", + "settings_features": "Erweiterte Funktionen", + "settings_features_desc": "Erweiterte Funktionen ein- oder ausschalten. Deaktivieren blendet sie in der Oberfläche aus, löscht aber keine Daten.", + "feat_adaptive": "Adaptive Intervalle", + "feat_adaptive_desc": "Optimale Intervalle aus Wartungshistorie lernen", + "feat_predictions": "Sensorvorhersagen", + "feat_predictions_desc": "Trigger-Datum anhand von Sensordegradation vorhersagen", + "feat_seasonal": "Saisonale Anpassungen", + "feat_seasonal_desc": "Intervalle basierend auf saisonalen Mustern anpassen", + "feat_environmental": "Umgebungskorrelation", + "feat_environmental_desc": "Intervalle mit Temperatur/Luftfeuchtigkeit korrelieren", + "feat_budget": "Budgetverfolgung", + "feat_budget_desc": "Monatliche und jährliche Wartungsausgaben verfolgen", + "feat_groups": "Aufgabengruppen", + "feat_groups_desc": "Aufgaben in logische Gruppen organisieren", + "feat_checklists": "Checklisten", + "feat_checklists_desc": "Mehrstufige Verfahren zur Aufgabenerlediung", + "settings_general": "Allgemein", + "settings_default_warning": "Standard-Warntage", + "settings_panel_enabled": "Seitenleisten-Panel", + "settings_panel_title": "Panel-Titel", + "settings_notifications": "Benachrichtigungen", + "settings_notify_service": "Benachrichtigungsdienst", + "test_notification": "Test-Benachrichtigung", + "send_test": "Test senden", + "testing": "Sende…", + "test_notification_success": "Test-Benachrichtigung gesendet", + "test_notification_failed": "Test-Benachrichtigung fehlgeschlagen", + "settings_notify_due_soon": "Bei baldiger Fälligkeit benachrichtigen", + "settings_notify_overdue": "Bei Überfälligkeit benachrichtigen", + "settings_notify_triggered": "Bei Auslösung benachrichtigen", + "settings_interval_hours": "Wiederholungsintervall (Stunden, 0 = einmalig)", + "settings_quiet_hours": "Ruhezeiten", + "settings_quiet_start": "Beginn", + "settings_quiet_end": "Ende", + "settings_max_per_day": "Max. Benachrichtigungen pro Tag (0 = unbegrenzt)", + "settings_bundling": "Benachrichtigungen bündeln", + "settings_bundle_threshold": "Bündelungsschwelle", + "settings_reminder_leads": "Zusätzliche Erinnerungen (Tage vor Fälligkeit)", + "settings_reminder_leads_hint": "Kommagetrennte Vorlaufzeiten, z. B. 14, 3, 0 — am jeweiligen Tag wird eine zusätzliche Erinnerung gesendet. Leer = aus.", + "settings_actions": "Mobile Aktionsbuttons", + "settings_action_complete": "\"Erledigt\"-Button anzeigen", + "settings_action_skip": "\"Überspringen\"-Button anzeigen", + "settings_action_snooze": "\"Schlummern\"-Button anzeigen", + "settings_weekly_digest": "Wöchentliche Zusammenfassung", + "settings_weekly_digest_hint": "Eine Zusammenfassungs-Benachrichtigung am Montagmorgen, wenn Aufgaben anstehen.", + "settings_warranty_reminder": "Garantie-Ablauf-Erinnerung", + "settings_warranty_reminder_days": "Tage vor Ablauf", + "settings_warranty_reminder_hint": "Einmal benachrichtigen, wenn die Garantie eines Objekts so viele Tage vor Ablauf steht.", + "settings_snooze_hours": "Schlummerdauer (Stunden)", + "settings_budget": "Budget", + "settings_currency": "Währung", + "settings_budget_monthly": "Monatsbudget", + "settings_budget_yearly": "Jahresbudget", + "settings_budget_alerts": "Budget-Warnungen", + "settings_budget_threshold": "Warnschwelle (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "JSON exportieren", + "settings_export_yaml": "YAML exportieren", + "settings_export_csv": "CSV exportieren", + "settings_import_csv": "CSV importieren", + "settings_import_placeholder": "JSON- oder CSV-Inhalt hier einfügen…", + "settings_import_btn": "Importieren", + "settings_import_success": "{count} Objekte erfolgreich importiert.", + "settings_export_success": "Export heruntergeladen.", + "settings_saved": "Einstellung gespeichert.", + "settings_include_history": "Verlauf einbeziehen", + "sort_alphabetical": "Alphabetisch", + "sort_due_soonest": "Frühestens fällig", + "sort_task_count": "Aufgaben-Anzahl", + "sort_area": "Bereich", + "sort_assigned_user": "Verantwortlicher", + "sort_group": "Gruppe", + "groupby_none": "Keine Gruppierung", + "groupby_area": "Nach Bereich", + "groupby_group": "Nach Gruppe", + "groupby_user": "Nach Verantwortlichem", + "filter_label": "Filter", + "user_label": "Benutzer", + "sort_label": "Sortierung", + "group_by_label": "Gruppieren nach", + "state_value_help": "Verwende den HA-Zustandswert (meist kleingeschrieben, z. B. \"on\"/\"off\"). Groß-/Kleinschreibung wird beim Speichern normalisiert.", + "target_changes_help": "Anzahl der passenden Übergänge, nach denen der Trigger auslöst (Standard: 1).", + "qr_print_title": "QR-Codes drucken", + "qr_print_desc": "Erzeuge eine Druckseite mit QR-Codes zum Ausschneiden und Anbringen an den Geräten.", + "qr_print_load": "Objekte laden", + "qr_print_filter": "Filter", + "qr_print_objects": "Objekte", + "qr_print_actions": "Aktionen", + "qr_print_url_mode": "Link-Typ", + "qr_print_estimate": "Geschätzte QR-Codes", + "qr_print_over_limit": "Obergrenze ist 200, bitte Filter eingrenzen", + "qr_print_generate": "QR-Codes erzeugen", + "qr_print_generating": "Erzeuge…", + "qr_print_ready": "QR-Codes bereit", + "qr_print_print_button": "Drucken", + "qr_print_empty": "Keine QR-Codes zu erzeugen", + "qr_action_skip": "Überspringen", + "vacation_title": "Urlaubsmodus", + "vacation_active": "aktiv", + "vacation_ended": "beendet", + "vacation_desc": "Plane deinen Urlaub: Benachrichtigungen werden während des Zeitraums plus Puffer-Tagen pausiert. Du kannst pro Aufgabe Ausnahmen festlegen.", + "vacation_enable": "Urlaubsmodus aktivieren", + "vacation_start": "Beginn", + "vacation_end": "Ende", + "vacation_buffer": "Puffer (Tage)", + "vacation_exempt_title": "Trotz Urlaubsmodus benachrichtigen", + "vacation_exempt_desc": "Wähle Aufgaben aus, für die auch im Urlaub Benachrichtigungen kommen sollen (z. B. kritische Pool-Chemie).", + "vacation_load_tasks": "Aufgaben laden", + "vacation_preview_btn": "Vorschau anzeigen", + "vacation_preview_affected": "Aufgaben betroffen", + "vacation_event_due_soon": "wird bald fällig", + "vacation_event_overdue": "wird überfällig", + "vacation_event_triggered_est": "Sensor-Trigger möglich", + "vacation_sensor_based": "(sensorbasiert)", + "vacation_action_notify": "Trotzdem benachrichtigen", + "vacation_action_unsilence": "Wieder stummschalten", + "vacation_marked_complete": "Als erledigt markiert", + "vacation_marked_skip": "Übersprungen", + "vacation_end_now": "Urlaub jetzt beenden", + "add": "Hinzufügen", + "show_stats": "Statistiken + Diagramme", + "hide_stats": "Statistiken ausblenden", + "adaptive_no_data": "Noch nicht genug Completion-Historie für die adaptive Auswertung. Schließe diese Aufgabe ein paar Mal mehr ab, um Intervall-Empfehlungen und Zuverlässigkeitskurven freizuschalten.", + "suggestion_applied": "Vorgeschlagenes Intervall übernommen", + "vacation_mode": "Urlaubsmodus", + "vacation_status_active": "Aktiv", + "vacation_status_scheduled": "Geplant", + "vacation_status_inactive": "Inaktiv", + "vacation_end_now_confirm": "Urlaub sofort beenden?", + "vacation_exempt_count": "ausgenommen", + "vacation_advanced": "Erweitert…", + "vacation_open_panel": "Im Panel öffnen", + "enable": "Aktivieren", + "saved": "Gespeichert", + "budget_monthly_set": "Monatsbudget setzen", + "budget_yearly_set": "Jahresbudget setzen", + "budget_advanced": "Währung, Alarme…", + "budget_open_panel": "Im Panel öffnen", + "groups_empty": "Keine Gruppen vorhanden.", + "group_new_placeholder": "Gruppe hinzufügen…", + "group_delete_confirm": "Gruppe \"{name}\" löschen?", + "groups_manage_tasks": "Aufgaben-Zuordnungen verwalten…", + "groups_open_panel": "Im Panel öffnen", + "unassigned": "Nicht zugewiesen", + "no_area": "Kein Bereich", + "has_overdue": "Überfällige Aufgaben", + "object": "Objekt", + "settings_panel_access": "Panel-Zugriff", + "settings_panel_access_desc": "Admins haben immer Vollzugriff. Um Erstellen, Bearbeiten und Löschen an bestimmte Non-Admins zu delegieren, aktiviere dies und wähle sie unten aus — alle anderen sehen nur Abhaken/Überspringen.", + "settings_operator_write": "Ausgewählten Nutzern Erstellen, Bearbeiten & Löschen erlauben", + "settings_operator_write_desc": "Aus: nur Admins können Inhalte ändern. Ein: die ausgewählten Nutzer unten erhalten ebenfalls Vollzugriff.", + "no_non_admin_users": "Keine Non-Admin-User gefunden. Lege welche unter Einstellungen → Personen an.", + "owner_label": "Owner", + "feat_completion_actions": "Completion-Actions", + "feat_completion_actions_desc": "Pro Aufgabe HA-Action beim Abschluss konfigurieren + Quick-Complete-QR mit voreingestellten Werten.", + "on_complete_action_title": "Beim Abschluss: HA-Action auslösen (optional)", + "on_complete_action_desc": "Ruft beim Erledigen der Aufgabe einen HA-Service auf — z. B. einen Zähler am Gerät zurücksetzen.", + "on_complete_action_service": "Service", + "on_complete_action_target": "Ziel-Entität", + "on_complete_action_target_hint": "Achtung: Domain der Entität muss zum Service passen — z. B. 'button.press' nur für button.*, 'counter.increment' nur für counter.*, 'input_button.press' nur für input_button.* etc. Bei Mismatch fired die Aktion nicht (HA loggt 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Daten (JSON, optional)", + "on_complete_action_test": "Konfiguration prüfen", + "on_complete_action_test_success": "✓ Konfiguration gültig (Aktion wird erst beim Abschluss ausgeführt)", + "on_complete_action_test_failed": "Konfigurationsfehler", + "quick_complete_defaults_title": "Schnell-Abschluss-Standardwerte (für QR-Scans, optional)", + "quick_complete_defaults_desc": "Voreingestellte Werte für Schnell-Abschluss-QR. Ohne Werte öffnet der QR den Abschluss-Dialog.", + "quick_complete_defaults_notes": "Notizen", + "quick_complete_defaults_cost": "Kosten", + "quick_complete_defaults_duration": "Dauer (Minuten)", + "quick_complete_defaults_feedback_none": "Kein Feedback", + "quick_complete_defaults_feedback_needed": "War notwendig", + "quick_complete_defaults_feedback_not_needed": "War nicht notwendig", + "quick_complete_success": "Schnell als erledigt markiert", + "show_all_objects": "Alle Objekte anzeigen", + "show_all_tasks": "Filter zurücksetzen — alle Aufgaben anzeigen", + "filter_to_overdue": "Auf überfällige Aufgaben filtern", + "filter_to_due_soon": "Auf bald fällige Aufgaben filtern", + "filter_to_triggered": "Auf ausgelöste Aufgaben filtern", + "open_task": "Aufgabe öffnen", + "show_details": "Verlauf + Statistik anzeigen", + "hide_details": "Details ausblenden", + "history_empty": "Noch keine Einträge.", + "history_edit_button": "Eintrag bearbeiten", + "total_cost": "Gesamtkosten", + "times_performed": "Erledigt", + "older_entries": "ältere", + "open_in_panel": "Im Wartungspanel öffnen", + "skip_reason": "Übersprungen-Grund (optional)", + "reset_to_date": "last_performed setzen auf", + "delete_task_confirm": "Diese Aufgabe und ihren Verlauf löschen?", + "delete_object_confirm": "Dieses Objekt und alle seine Aufgaben löschen?", + "loading": "Laden…", + "archive": "Archivieren", + "undo": "Rückgängig", + "task_archived": "Aufgabe archiviert", + "object_archived": "Objekt archiviert", + "unarchive": "Wiederherstellen", + "archived": "Archiviert", + "show_archived": "Archivierte anzeigen", + "hide_archived": "Archivierte ausblenden", + "confirm_archive_object": "Dieses Objekt und seine Aufgaben archivieren? Verlauf bleibt erhalten, kann später wiederhergestellt werden.", + "settings_archive": "Archiv & Aufbewahrung", + "settings_archive_desc": "Erledigte einmalige Aufgaben archivieren statt löschen. Archivierte Einträge sind ausgeblendet und inaktiv, behalten aber Verlauf und Kosten.", + "settings_archive_oneoff_days": "Erledigte einmalige Aufgaben automatisch archivieren nach (Tage, 0 = aus)", + "settings_delete_archived_oneoff_days": "Archivierte einmalige Aufgaben automatisch löschen nach (Tage, 0 = nie)", + "archive_object": "Objekt archivieren", + "unarchive_object": "Objekt wiederherstellen", + "documents": "Dokumente", + "documents_empty": "Noch keine Dokumente.", + "doc_upload": "Datei hochladen", + "doc_uploading": "Wird hochgeladen…", + "doc_add_link": "Link hinzufügen", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titel (optional)", + "doc_open": "Öffnen", + "doc_delete_confirm": "„{name}“ löschen?", + "doc_too_large": "Datei ist zu groß (max. 25 MB).", + "doc_upload_failed": "Upload fehlgeschlagen.", + "completion_photo_optional": "Abschlussfoto (optional)", + "add_photo": "Foto hinzufügen", + "uploading": "Wird hochgeladen…", + "remove": "Entfernen", + "doc_deduped": "Bereits anderswo gespeichert — geteilt, kein zusätzlicher Speicher.", + "doc_dup_in_object": "Diese Datei ist bereits an diesem Objekt angehängt.", + "doc_link_invalid": "Nur http/https-Links sind erlaubt.", + "doc_cat_manual": "Anleitung", + "doc_cat_warranty": "Garantie", + "doc_cat_invoice": "Rechnung", + "doc_cat_spare_parts": "Ersatzteile", + "doc_cat_photo": "Foto", + "doc_cat_other": "Sonstiges", + "doc_link_badge": "Link", + "doc_storage_title": "Dokumentenspeicher", + "doc_storage_saved": "Durch Deduplizierung gespart", + "doc_storage_refresh": "Aktualisieren", + "doc_download": "Herunterladen", + "doc_close": "Schließen", + "doc_camera": "Foto aufnehmen", + "doc_drop_hint": "Dateien hier ablegen", + "doc_task_none": "Keine Dokumente mit dieser Aufgabe verknüpft.", + "doc_link_existing": "Dokument verknüpfen…", + "doc_attach": "Verknüpfen", + "doc_unlink": "Verknüpfung lösen", + "doc_page": "Seite", + "chart_range_7d": "7T", + "chart_range_30d": "30T", + "chart_range_90d": "90T", + "chart_range_1y": "1J", + "chart_since_service": "seit letzter Wartung", + "chart_no_stats": "Keine Langzeit-Statistik für diese Entity — es werden nur Werte aus Wartungseinträgen angezeigt", + "auto_complete_on_recovery": "Bei Sensor-Erholung automatisch abschließen", + "auto_complete_on_recovery_help": "Verbucht eine Wartung (setzt „zuletzt durchgeführt“), sobald sich der Auslöser von selbst auflöst — z. B. Salz nachgefüllt, Filter getauscht.", + "doc_search": "Dokumente durchsuchen…", + "doc_search_none": "Keine passenden Dokumente", + "link_device_optional": "Mit bestehendem Gerät verknüpfen (optional)", + "parent_object_optional": "Übergeordnetes Objekt (optional)", + "parent_none": "(Kein übergeordnetes Objekt)", + "paused": "Pausiert", + "pause_object": "Pausieren", + "resume_object": "Fortsetzen", + "pause_until_prompt": "Zeitpläne dieses Objekts einfrieren — nichts wird fällig und nichts benachrichtigt, bis es fortgesetzt wird. Optional ein Datum für die automatische Fortsetzung festlegen.", + "pause_until_label": "Fortsetzen am (optional)", + "object_paused": "Objekt pausiert", + "object_resumed": "Objekt fortgesetzt — Zeitpläne neu gestartet", + "object_paused_badge": "Pausiert", + "paused_until_label": "bis", + "replace_object": "Ersetzen…", + "replace_object_prompt": "Dieses Objekt ausmustern und einen Nachfolger anlegen. Historie und Kosten bleiben am alten Objekt archiviert; Aufgaben und Dokumente wandern zum neuen, Zähler starten frisch.", + "replace_name_label": "Name des Nachfolgers", + "object_replaced": "Objekt ersetzt — Nachfolger angelegt", + "reading_unit_label": "Ableseeinheit (z. B. kWh, m³)", + "reading_unit_help": "Wird beim Erledigen dieser Aufgabe neben dem erfassten Wert angezeigt.", + "reading_value_label": "Ablesewert", + "reading_label": "Ablesung", + "settings_templates_label": "Vorlagen-Galerie", + "settings_templates_hint": "Vorlagen abwählen, die du nie brauchst — sie verschwinden aus den \"Aus Vorlage\"-Auswahlen (Panel und Config-Flow). Sonst ändert sich nichts; jederzeit wieder aktivierbar.", + "worksheet": "Arbeitsblatt", + "worksheet_scan_view": "Scannen, um die Aufgabe zu öffnen", + "worksheet_scan_complete": "Scannen zum Erledigen", + "worksheet_manual_excerpt": "Handbuch-Auszug", + "worksheet_pages": "Seiten", + "worksheet_printed": "Gedruckt", + "worksheet_never": "Nie" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/en.json b/custom_components/maintenance_supporter/frontend/locales/en.json new file mode 100644 index 0000000..2b373cb --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/en.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Maintenance", + "objects": "Objects", + "tasks": "Tasks", + "overdue": "Overdue", + "due_soon": "Due Soon", + "triggered": "Triggered", + "trigger_replaced": "Trigger replaced", + "ok": "OK", + "all": "All", + "new_object": "+ New Object", + "templates_from": "From template", + "templates_title": "Start from a template", + "templates_task_count": "{n} tasks", + "template_created": "Created from template", + "onboard_hint": "Add your first object to start tracking maintenance.", + "edit": "Edit", + "duplicate": "Duplicate", + "task_duplicated": "Task duplicated", + "object_duplicated": "Object duplicated", + "delete": "Delete", + "add_task": "+ Add Task", + "complete": "Complete", + "completed": "Completed", + "skip": "Skip", + "skipped": "Skipped", + "missed": "Missed", + "reset": "Reset", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Cancel", + "bulk_select": "Select", + "bulk_select_all": "Select all", + "bulk_n_selected": "{n} selected", + "bulk_completed": "{n} tasks completed", + "bulk_archived": "{n} tasks archived", + "completing": "Completing…", + "interval": "Interval", + "warning": "Warning", + "last_performed": "Last performed", + "next_due": "Next due", + "days_until_due": "Days until due", + "avg_duration": "Avg duration", + "trigger": "Trigger", + "trigger_type": "Trigger type", + "threshold_above": "Upper limit", + "threshold_below": "Lower limit", + "threshold": "Threshold", + "counter": "Counter", + "state_change": "State change", + "runtime": "Runtime", + "runtime_hours": "Target runtime (hours)", + "target_value": "Target value", + "baseline": "Baseline", + "target_changes": "Target changes", + "for_minutes": "For (minutes)", + "time_based": "Time-based", + "sensor_based": "Sensor-based", + "manual": "Manual", + "one_time": "One-time", + "weekdays": "Weekdays", + "nth_weekday": "Nth weekday of month", + "day_of_month": "Day of month", + "recurrence_on_days": "Repeat on", + "recurrence_occurrence": "Occurrence", + "recurrence_weekday": "Weekday", + "recurrence_day": "Day of month (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1st", + "ord_2": "2nd", + "ord_3": "3rd", + "ord_4": "4th", + "ord_5": "5th", + "ord_last": "Last", + "day_word": "Day", + "interval_value": "Interval", + "interval_unit": "Unit", + "unit_days": "Days", + "unit_weeks": "Weeks", + "unit_months": "Months", + "unit_years": "Years", + "due_date": "Due date", + "cleaning": "Cleaning", + "inspection": "Inspection", + "replacement": "Replacement", + "calibration": "Calibration", + "service": "Service", + "reading": "Reading", + "custom": "Custom", + "history": "History", + "cost": "Cost", + "report_button": "Report", + "report_title": "Maintenance report", + "report_generated": "Generated", + "report_times_done": "Done", + "report_total_cost": "Total cost", + "report_every": "every {n} {unit}", + "report_notes": "Notes", + "report_col_type": "Type", + "report_col_status": "Status", + "report_col_schedule": "Schedule", + "duration": "Duration", + "both": "Both", + "trigger_val": "Trigger value", + "complete_title": "Complete: ", + "checklist": "Checklist", + "checklist_steps_optional": "Checklist steps (optional)", + "checklist_placeholder": "Clean filter\nReplace seal\nTest pressure", + "checklist_help": "One step per line. Max 100 items.", + "err_too_long": "{field}: too long (max {n} characters)", + "err_too_short": "{field}: too short (min {n} characters)", + "err_value_too_high": "{field}: too large (max {n})", + "err_value_too_low": "{field}: too small (min {n})", + "err_required": "{field}: required", + "err_wrong_type": "{field}: wrong type (expected: {type})", + "err_invalid_choice": "{field}: not an allowed value", + "err_invalid_value": "{field}: invalid value", + "feat_schedule_time": "Time-of-day scheduling", + "feat_schedule_time_desc": "Tasks become overdue at a specific time of day instead of midnight.", + "schedule_time_optional": "Due at time (optional, HH:MM)", + "schedule_time_help": "Empty = midnight (default). HA timezone.", + "at_time": "at", + "notes_optional": "Notes (optional)", + "cost_optional": "Cost (optional)", + "duration_minutes": "Duration in minutes (optional)", + "days": "days", + "day": "day", + "today": "Today", + "d_overdue": "d overdue", + "no_tasks": "No maintenance tasks yet. Create an object to get started.", + "no_tasks_short": "No tasks", + "no_history": "No history entries yet.", + "show_all": "Show all", + "cost_duration_chart": "Cost & Duration", + "installed": "Installed", + "confirm_delete_object": "Delete this object and all its tasks?", + "confirm_delete_task": "Delete this task?", + "min": "Min", + "max": "Max", + "save": "Save", + "saving": "Saving…", + "edit_task": "Edit Task", + "new_task": "New Maintenance Task", + "task_name": "Task name", + "maintenance_type": "Maintenance type", + "priority": "Priority", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Low", + "priority_normal": "Normal", + "priority_high": "High", + "schedule_type": "Schedule type", + "interval_days": "Interval (days)", + "warning_days": "Warning days", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Last performed (optional)", + "interval_anchor": "Interval anchor", + "anchor_completion": "From completion date", + "anchor_planned": "From planned date (no drift)", + "edit_object": "Edit Object", + "name": "Name", + "manufacturer_optional": "Manufacturer (optional)", + "model_optional": "Model (optional)", + "serial_number_optional": "Serial number (optional)", + "serial_number_label": "S/N", + "documentation_url_label": "Manual", + "object_notes_label": "Notes", + "sort_due_date": "Due date", + "sort_object": "Object name", + "sort_type": "Type", + "sort_task_name": "Task name", + "all_objects": "All objects", + "tasks_lower": "tasks", + "no_tasks_yet": "No tasks yet", + "add_first_task": "Add first task", + "trigger_configuration": "Trigger Configuration", + "entity_id": "Entity ID", + "comma_separated": "comma-separated", + "entity_logic": "Entity logic", + "entity_logic_any": "Any entity triggers", + "entity_logic_all": "All entities must trigger", + "entities": "entities", + "attribute_optional": "Attribute (optional, blank = state)", + "use_entity_state": "Use entity state (no attribute)", + "trigger_above": "Trigger above", + "trigger_below": "Trigger below", + "for_at_least_minutes": "For at least (minutes)", + "safety_interval_days": "Safety interval (days, optional)", + "safety_interval": "Safety interval (optional)", + "delta_mode": "Delta mode", + "from_state_optional": "From state (optional)", + "to_state_optional": "To state (optional)", + "documentation_url_optional": "Documentation URL (optional)", + "object_notes_optional": "Notes (optional)", + "nfc_tag_id_optional": "NFC Tag ID (optional)", + "nfc_tags_empty_help": "No NFC tags registered in Home Assistant yet.", + "nfc_tags_open_settings": "Open Tags settings", + "nfc_tags_refresh": "Refresh", + "environmental_entity_optional": "Environmental sensor (optional)", + "environmental_entity_helper": "e.g. sensor.outdoor_temperature — adjusts the interval based on environmental conditions", + "environmental_attribute_optional": "Environmental attribute (optional)", + "nfc_tag_id": "NFC Tag ID", + "nfc_linked": "NFC tag linked", + "nfc_link_hint": "Click to link NFC tag", + "responsible_user": "Responsible User", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(No user assigned)", + "all_users": "All Users", + "my_tasks": "My Tasks", + "tab_calendar": "Calendar", + "cal_no_events": "No maintenance", + "cal_window_7": "7 days", + "cal_window_14": "14 days", + "cal_window_30": "30 days", + "cal_window_365": "1 year", + "cal_every_n_days": "every {n} days", + "cal_source_time": "Time-based", + "cal_source_time_adaptive": "Time-based (adaptive)", + "cal_source_sensor": "Sensor-based", + "cal_predicted": "predicted", + "cal_confidence_high": "high confidence", + "cal_confidence_medium": "medium confidence", + "cal_confidence_low": "low confidence", + "budget_monthly": "Monthly budget", + "budget_yearly": "Yearly budget", + "groups": "Groups", + "new_group": "New group", + "edit_group": "Edit group", + "no_groups": "No groups yet", + "delete_group": "Delete group", + "delete_group_confirm": "Delete group '{name}'?", + "group_select_tasks": "Select tasks", + "group_name_required": "Name is required", + "description_optional": "Description (optional)", + "selected": "Selected", + "loading_chart": "Loading chart data...", + "hide_outliers": "Hide outliers (sensor glitches)", + "was_maintenance_needed": "Was this maintenance needed?", + "feedback_needed": "Needed", + "feedback_not_needed": "Not needed", + "feedback_not_sure": "Not sure", + "suggested_interval": "Suggested interval", + "apply_suggestion": "Apply", + "reanalyze": "Re-analyze", + "reanalyze_result": "New analysis", + "reanalyze_insufficient_data": "Not enough data to produce a recommendation", + "data_points": "data points", + "dismiss_suggestion": "Dismiss", + "confidence_low": "Low", + "confidence_medium": "Medium", + "confidence_high": "High", + "recommended": "recommended", + "seasonal_awareness": "Seasonal Awareness", + "edit_seasonal_overrides": "Edit seasonal factors", + "seasonal_overrides_title": "Seasonal factors (override)", + "seasonal_overrides_hint": "Factor per month (0.1–5.0). Empty = learned automatically.", + "seasonal_override_invalid": "Invalid value", + "seasonal_override_range": "Factor must be between 0.1 and 5.0", + "clear_all": "Clear all", + "seasonal_chart_title": "Seasonal Factors", + "seasonal_learned": "Learned", + "seasonal_manual": "Manual", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "May", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Oct", + "month_nov": "Nov", + "month_dec": "Dec", + "sensor_prediction": "Sensor Prediction", + "degradation_trend": "Trend", + "trend_rising": "Rising", + "trend_falling": "Falling", + "trend_stable": "Stable", + "trend_insufficient_data": "Insufficient data", + "days_until_threshold": "Days until threshold", + "threshold_exceeded": "Threshold exceeded", + "environmental_adjustment": "Environmental factor", + "sensor_prediction_urgency": "Sensor predicts threshold in ~{days} days", + "day_short": "day", + "weibull_reliability_curve": "Reliability Curve", + "weibull_failure_probability": "Failure Probability", + "weibull_r_squared": "Fit R²", + "beta_early_failures": "Early Failures", + "beta_random_failures": "Random Failures", + "beta_wear_out": "Wear-out", + "beta_highly_predictable": "Highly Predictable", + "confidence_interval": "Confidence Interval", + "confidence_conservative": "Conservative", + "confidence_aggressive": "Optimistic", + "current_interval_marker": "Current interval", + "recommended_marker": "Recommended", + "characteristic_life": "Characteristic life", + "chart_mini_sparkline": "Trend sparkline", + "chart_history": "Cost and duration history", + "chart_seasonal": "Seasonal factors, 12 months", + "chart_weibull": "Weibull reliability curve", + "chart_sparkline": "Sensor trigger value chart", + "days_progress": "Days progress", + "qr_code": "QR Code", + "qr_generating": "Generating QR code…", + "qr_error": "Failed to generate QR code.", + "qr_error_no_url": "No HA URL configured. Please set an external or internal URL in Settings → System → Network.", + "save_error": "Failed to save. Please try again.", + "qr_print": "Print", + "qr_download": "Download SVG", + "qr_action": "Action on scan", + "qr_action_view": "View maintenance info", + "qr_action_complete": "Mark maintenance as complete", + "qr_url_mode": "Link type", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Local (mDNS)", + "qr_mode_server": "Server URL", + "overview": "Overview", + "analysis": "Analysis", + "recent_activities": "Recent Activities", + "search_notes": "Search notes", + "avg_cost": "Avg Cost", + "no_advanced_features": "No advanced features enabled", + "no_advanced_features_hint": "Enable “Adaptive Intervals” or “Seasonal Patterns” in the integration settings to see analysis data here.", + "analysis_not_enough_data": "Not enough data for analysis yet.", + "analysis_not_enough_data_hint": "Weibull analysis requires at least 5 completed maintenances; seasonal patterns become visible after 6+ data points per month.", + "analysis_manual_task_hint": "Manual tasks without an interval do not generate analysis data.", + "completions": "completions", + "current": "Current", + "shorter": "Shorter", + "longer": "Longer", + "normal": "Normal", + "disabled": "Disabled", + "compound_logic": "Compound logic", + "compound": "Compound (multiple conditions)", + "compound_logic_and": "AND — all conditions must trigger", + "compound_logic_or": "OR — any condition triggers", + "compound_help": "Combine several sensor conditions into one trigger.", + "compound_no_conditions": "No conditions yet — add at least one.", + "compound_add_condition": "Add condition", + "compound_condition": "Condition", + "compound_remove_condition": "Remove condition", + "card_title": "Title", + "card_show_header": "Show header with statistics", + "card_show_actions": "Show action buttons", + "card_compact": "Compact mode", + "card_max_items": "Max items (0 = all)", + "card_filter_status": "Filter by status", + "card_filter_status_help": "Empty = show all statuses.", + "card_filter_objects": "Filter by objects", + "card_filter_objects_help": "Empty = show all objects.", + "card_filter_entities": "Filter by entities (entity_ids)", + "card_filter_entities_help": "Pick sensor / binary_sensor entities from this integration. Empty = all.", + "card_loading_objects": "Loading objects…", + "card_load_error": "Could not load objects — check the WebSocket connection.", + "card_no_tasks_title": "No maintenance tasks yet", + "card_no_tasks_cta": "→ Create one in the Maintenance panel", + "no_objects": "No objects yet.", + "action_error": "Action failed. Please try again.", + "area_id_optional": "Area (optional)", + "installation_date_optional": "Installation date (optional)", + "warranty_expiry_optional": "Warranty expiry (optional)", + "warranty": "Warranty", + "warranty_valid_until": "valid until {date}", + "warranty_expires_in": "expires in {days} days", + "warranty_expired": "expired", + "cal_past_windows": "Past windows", + "cal_forward_windows": "Forward windows", + "history_edit_title": "Edit history entry", + "history_edit_timestamp": "Timestamp", + "manufacturer": "Manufacturer", + "model": "Model", + "area": "Area", + "actions": "Actions", + "view_mode_label": "View", + "view_cards": "Card view", + "view_table": "Table view", + "objects_table_columns_label": "Objects table columns", + "objects_table_columns_hint": "Choose which columns appear in the objects table view.", + "custom_icon_optional": "Icon (optional, e.g. mdi:wrench)", + "task_enabled": "Task enabled", + "skip_reason_prompt": "Skip this task?", + "reason_optional": "Reason (optional)", + "reset_date_prompt": "Mark task as performed?", + "reset_date_optional": "Last performed date (optional, defaults to today)", + "notes_label": "Notes", + "documentation_label": "Documentation", + "no_nfc_tag": "— No tag —", + "dashboard": "Dashboard", + "tab_today": "Today", + "palette_placeholder": "Search objects and tasks…", + "palette_no_results": "No matches", + "palette_hint": "↑↓ to navigate · Enter to open · Esc to close", + "today_all_caught_up": "All caught up! Nothing due this week.", + "today_overdue": "Overdue", + "today_due_today": "Due today", + "today_this_week": "This week", + "settings": "Settings", + "settings_features": "Advanced Features", + "settings_features_desc": "Enable or disable advanced features. Disabling hides them from the UI but does not delete data.", + "feat_adaptive": "Adaptive Scheduling", + "feat_adaptive_desc": "Learn optimal intervals from maintenance history", + "feat_predictions": "Sensor Predictions", + "feat_predictions_desc": "Predict trigger dates from sensor degradation", + "feat_seasonal": "Seasonal Adjustments", + "feat_seasonal_desc": "Adjust intervals based on seasonal patterns", + "feat_environmental": "Environmental Correlation", + "feat_environmental_desc": "Correlate intervals with temperature/humidity", + "feat_budget": "Budget Tracking", + "feat_budget_desc": "Track monthly and yearly maintenance spending", + "feat_groups": "Task Groups", + "feat_groups_desc": "Organize tasks into logical groups", + "feat_checklists": "Checklists", + "feat_checklists_desc": "Multi-step procedures for task completion", + "settings_general": "General", + "settings_default_warning": "Default warning days", + "settings_panel_enabled": "Sidebar panel", + "settings_panel_title": "Sidebar panel title", + "settings_notifications": "Notifications", + "settings_notify_service": "Notification service", + "test_notification": "Test notification", + "send_test": "Send test", + "testing": "Sending…", + "test_notification_success": "Test notification sent", + "test_notification_failed": "Test notification failed", + "settings_notify_due_soon": "Notify when due soon", + "settings_notify_overdue": "Notify when overdue", + "settings_notify_triggered": "Notify when triggered", + "settings_interval_hours": "Repeat interval (hours, 0 = once)", + "settings_quiet_hours": "Quiet hours", + "settings_quiet_start": "Start", + "settings_quiet_end": "End", + "settings_max_per_day": "Max notifications per day (0 = unlimited)", + "settings_bundling": "Bundle notifications", + "settings_bundle_threshold": "Bundle threshold", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobile Action Buttons", + "settings_action_complete": "Show 'Complete' button", + "settings_action_skip": "Show 'Skip' button", + "settings_action_snooze": "Show 'Snooze' button", + "settings_weekly_digest": "Weekly digest", + "settings_weekly_digest_hint": "A single summary notification on Monday morning when tasks are due.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Snooze duration (hours)", + "settings_budget": "Budget", + "settings_currency": "Currency", + "settings_budget_monthly": "Monthly budget", + "settings_budget_yearly": "Yearly budget", + "settings_budget_alerts": "Budget alerts", + "settings_budget_threshold": "Alert threshold (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Export JSON", + "settings_export_yaml": "Export YAML", + "settings_export_csv": "Export CSV", + "settings_import_csv": "Import CSV", + "settings_import_placeholder": "Paste JSON or CSV content here…", + "settings_import_btn": "Import", + "settings_import_success": "{count} objects imported successfully.", + "settings_export_success": "Export downloaded.", + "settings_saved": "Setting saved.", + "settings_include_history": "Include history", + "sort_alphabetical": "Alphabetical", + "sort_due_soonest": "Due soonest", + "sort_task_count": "Task count", + "sort_area": "Area", + "sort_assigned_user": "Assigned user", + "sort_group": "Group", + "groupby_none": "No grouping", + "groupby_area": "By area", + "groupby_group": "By group", + "groupby_user": "By user", + "filter_label": "Filter", + "user_label": "User", + "sort_label": "Sort", + "group_by_label": "Group by", + "state_value_help": "Use the HA state value (usually lowercase, e.g. \"on\"/\"off\"). Case is normalised on save.", + "target_changes_help": "Number of matching transitions before the trigger fires (default: 1).", + "qr_print_title": "Print QR codes", + "qr_print_desc": "Generate a printable page of QR codes to cut out and stick on your equipment.", + "qr_print_load": "Load objects", + "qr_print_filter": "Filter", + "qr_print_objects": "Objects", + "qr_print_actions": "Actions", + "qr_print_url_mode": "Link type", + "qr_print_estimate": "Estimated QR codes", + "qr_print_over_limit": "cap is 200, narrow the filter", + "qr_print_generate": "Generate QR codes", + "qr_print_generating": "Generating…", + "qr_print_ready": "QR codes ready", + "qr_print_print_button": "Print", + "qr_print_empty": "Nothing to generate", + "qr_action_skip": "Skip", + "vacation_title": "Vacation mode", + "vacation_active": "active", + "vacation_ended": "ended", + "vacation_desc": "Plan a vacation: notifications are paused during the period plus a buffer of days. You can opt specific tasks back in.", + "vacation_enable": "Enable vacation mode", + "vacation_start": "Start", + "vacation_end": "End", + "vacation_buffer": "Buffer (days)", + "vacation_exempt_title": "Notify anyway during vacation", + "vacation_exempt_desc": "Pick tasks that should still notify during vacation (e.g. critical pool chemistry).", + "vacation_load_tasks": "Load tasks", + "vacation_preview_btn": "Show preview", + "vacation_preview_affected": "tasks affected", + "vacation_event_due_soon": "becomes due soon", + "vacation_event_overdue": "becomes overdue", + "vacation_event_triggered_est": "sensor trigger possible", + "vacation_sensor_based": "(sensor-based)", + "vacation_action_notify": "Notify anyway", + "vacation_action_unsilence": "Silence again", + "vacation_marked_complete": "Marked complete", + "vacation_marked_skip": "Skipped", + "vacation_end_now": "End vacation now", + "add": "Add", + "show_stats": "Show stats + graphs", + "hide_stats": "Hide stats", + "adaptive_no_data": "Not enough completion history yet for adaptive analysis. Complete this task a few more times to unlock interval recommendations and reliability charts.", + "suggestion_applied": "Suggested interval applied", + "vacation_mode": "Vacation mode", + "vacation_status_active": "Active now", + "vacation_status_scheduled": "Scheduled", + "vacation_status_inactive": "Inactive", + "vacation_end_now_confirm": "End vacation immediately?", + "vacation_exempt_count": "exempt", + "vacation_advanced": "Advanced…", + "vacation_open_panel": "Open in panel", + "enable": "Enable", + "saved": "Saved", + "budget_monthly_set": "Set monthly", + "budget_yearly_set": "Set yearly", + "budget_advanced": "Currency, alerts…", + "budget_open_panel": "Open in panel", + "groups_empty": "No groups yet.", + "group_new_placeholder": "Add group…", + "group_delete_confirm": "Delete group \"{name}\"?", + "groups_manage_tasks": "Manage task assignments…", + "groups_open_panel": "Open in panel", + "unassigned": "Unassigned", + "no_area": "No area", + "has_overdue": "Has overdue tasks", + "object": "Object", + "settings_panel_access": "Panel access", + "settings_panel_access_desc": "Admins always have full access. To delegate create, edit and delete to specific non-admins, switch this on and pick them below — everyone else sees only Complete and Skip.", + "settings_operator_write": "Allow selected users to create, edit & delete", + "settings_operator_write_desc": "Off: only admins can change content. On: the selected users below get full access too.", + "no_non_admin_users": "No non-admin users found. Add some in Settings → People.", + "owner_label": "Owner", + "feat_completion_actions": "Completion actions", + "feat_completion_actions_desc": "Per-task HA action on complete + quick-complete QR with pre-set values.", + "on_complete_action_title": "On complete: trigger HA action (optional)", + "on_complete_action_desc": "Calls an HA service when the task is completed — e.g. reset a counter on the device.", + "on_complete_action_service": "Service", + "on_complete_action_target": "Target entity", + "on_complete_action_target_hint": "Note: the entity domain must match the service — e.g. 'button.press' only works on button.*, 'counter.increment' only on counter.*, 'input_button.press' only on input_button.* etc. On a mismatch the action will silently fail (HA logs 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Data (JSON, optional)", + "on_complete_action_test": "Validate configuration", + "on_complete_action_test_success": "✓ Configuration valid (action will fire only on task completion)", + "on_complete_action_test_failed": "Failed", + "quick_complete_defaults_title": "Quick-complete defaults (for QR scans, optional)", + "quick_complete_defaults_desc": "Pre-set values for quick-complete QR scans. Without these, the QR opens the complete dialog.", + "quick_complete_defaults_notes": "Notes", + "quick_complete_defaults_cost": "Cost", + "quick_complete_defaults_duration": "Duration (minutes)", + "quick_complete_defaults_feedback_none": "No feedback", + "quick_complete_defaults_feedback_needed": "Was needed", + "quick_complete_defaults_feedback_not_needed": "Not needed", + "quick_complete_success": "Quickly marked complete", + "show_all_objects": "Show all objects", + "show_all_tasks": "Clear filter — show all tasks", + "filter_to_overdue": "Filter task list to overdue only", + "filter_to_due_soon": "Filter task list to due-soon only", + "filter_to_triggered": "Filter task list to triggered only", + "open_task": "Open task", + "show_details": "Show history + stats", + "hide_details": "Hide details", + "history_empty": "No history yet.", + "history_edit_button": "Edit entry", + "total_cost": "Total cost", + "times_performed": "Performed", + "older_entries": "older", + "open_in_panel": "Open in Maintenance panel", + "skip_reason": "Skip reason (optional)", + "reset_to_date": "Reset last_performed to", + "delete_task_confirm": "Delete this task and its history?", + "delete_object_confirm": "Delete this object and all its tasks?", + "loading": "Loading…", + "archive": "Archive", + "undo": "Undo", + "task_archived": "Task archived", + "object_archived": "Object archived", + "unarchive": "Unarchive", + "archived": "Archived", + "show_archived": "Show archived", + "hide_archived": "Hide archived", + "confirm_archive_object": "Archive this object and its tasks? They keep their history and can be unarchived later.", + "settings_archive": "Archive & Retention", + "settings_archive_desc": "Retire completed one-off tasks without deleting them. Archived items are hidden and inert but keep their history and cost.", + "settings_archive_oneoff_days": "Auto-archive completed one-off tasks after (days, 0 = off)", + "settings_delete_archived_oneoff_days": "Auto-delete archived one-off tasks after (days, 0 = never)", + "archive_object": "Archive object", + "unarchive_object": "Unarchive object", + "documents": "Documents", + "documents_empty": "No documents yet.", + "doc_upload": "Upload file", + "doc_uploading": "Uploading…", + "doc_add_link": "Add link", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Title (optional)", + "doc_open": "Open", + "doc_delete_confirm": "Delete \"{name}\"?", + "doc_too_large": "File is too large (max 25 MB).", + "doc_upload_failed": "Upload failed.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Already stored elsewhere — shared, no extra space used.", + "doc_dup_in_object": "This file is already attached to this object.", + "doc_link_invalid": "Only http/https links are allowed.", + "doc_cat_manual": "Manual", + "doc_cat_warranty": "Warranty", + "doc_cat_invoice": "Invoice", + "doc_cat_spare_parts": "Spare parts", + "doc_cat_photo": "Photo", + "doc_cat_other": "Other", + "doc_link_badge": "Link", + "doc_storage_title": "Document storage", + "doc_storage_saved": "Saved via deduplication", + "doc_storage_refresh": "Refresh", + "doc_download": "Download", + "doc_close": "Close", + "doc_camera": "Take photo", + "doc_drop_hint": "Drop files here", + "doc_task_none": "No documents linked to this task.", + "doc_link_existing": "Link a document…", + "doc_attach": "Link", + "doc_unlink": "Unlink", + "doc_page": "Page", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1y", + "chart_since_service": "since last service", + "chart_no_stats": "No long-term statistics for this entity — showing maintenance-event values only", + "auto_complete_on_recovery": "Auto-complete when the sensor recovers", + "auto_complete_on_recovery_help": "Records a completion (sets last performed) when the trigger clears itself — e.g. salt refilled, filter replaced.", + "doc_search": "Search documents…", + "doc_search_none": "No matching documents", + "link_device_optional": "Link to existing device (optional)", + "parent_object_optional": "Parent object (optional)", + "parent_none": "(No parent)", + "paused": "Paused", + "pause_object": "Pause", + "resume_object": "Resume", + "pause_until_prompt": "Freeze this object's schedules — nothing becomes due and nothing notifies until it is resumed. Optionally set an auto-resume date.", + "pause_until_label": "Resume on (optional)", + "object_paused": "Object paused", + "object_resumed": "Object resumed — schedules restarted", + "object_paused_badge": "Paused", + "paused_until_label": "until", + "replace_object": "Replace…", + "replace_object_prompt": "Retire this object and create a successor. History and costs stay archived on the old one; tasks and documents carry over to the new one, counters start fresh.", + "replace_name_label": "Successor name", + "object_replaced": "Object replaced — successor created", + "reading_unit_label": "Reading unit (e.g. kWh, m³)", + "reading_unit_help": "Shown next to the recorded value when completing this task.", + "reading_value_label": "Reading value", + "reading_label": "Reading", + "settings_templates_label": "Template gallery", + "settings_templates_hint": "Untick templates you'll never need — they disappear from the \"From template\" pickers (panel and config flow). Nothing else changes; you can re-enable them any time.", + "worksheet": "Work sheet", + "worksheet_scan_view": "Scan to open the task", + "worksheet_scan_complete": "Scan to complete", + "worksheet_manual_excerpt": "Manual excerpt", + "worksheet_pages": "pages", + "worksheet_printed": "Printed", + "worksheet_never": "Never" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/es.json b/custom_components/maintenance_supporter/frontend/locales/es.json new file mode 100644 index 0000000..bd44e4b --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/es.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Mantenimiento", + "objects": "Objetos", + "tasks": "Tareas", + "overdue": "Vencida", + "due_soon": "Próxima", + "triggered": "Activada", + "ok": "OK", + "all": "Todos", + "new_object": "+ Nuevo objeto", + "templates_from": "Desde plantilla", + "templates_title": "Empezar desde una plantilla", + "templates_task_count": "{n} tareas", + "template_created": "Creado desde plantilla", + "onboard_hint": "Añade tu primer objeto para empezar a seguir el mantenimiento.", + "edit": "Editar", + "duplicate": "Duplicar", + "task_duplicated": "Tarea duplicada", + "object_duplicated": "Objeto duplicado", + "delete": "Eliminar", + "add_task": "+ Tarea", + "complete": "Completada", + "completed": "Completada", + "skip": "Omitir", + "skipped": "Omitida", + "missed": "Missed", + "reset": "Restablecer", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Cancelar", + "bulk_select": "Seleccionar", + "bulk_select_all": "Seleccionar todo", + "bulk_n_selected": "{n} seleccionados", + "bulk_completed": "{n} tareas completadas", + "bulk_archived": "{n} tareas archivadas", + "completing": "Completando…", + "interval": "Intervalo", + "warning": "Aviso", + "last_performed": "Última ejecución", + "next_due": "Próximo vencimiento", + "days_until_due": "Días hasta vencimiento", + "avg_duration": "Ø Duración", + "trigger": "Disparador", + "trigger_type": "Tipo de disparador", + "threshold_above": "Límite superior", + "threshold_below": "Límite inferior", + "threshold": "Umbral", + "counter": "Contador", + "state_change": "Cambio de estado", + "runtime": "Tiempo de funcionamiento", + "runtime_hours": "Duración objetivo (horas)", + "target_value": "Valor objetivo", + "baseline": "Línea base", + "target_changes": "Cambios objetivo", + "for_minutes": "Durante (minutos)", + "time_based": "Temporal", + "sensor_based": "Sensor", + "manual": "Manual", + "one_time": "Una vez", + "weekdays": "Días de la semana", + "nth_weekday": "N-ésimo día de la semana del mes", + "day_of_month": "Día del mes", + "recurrence_on_days": "Repetir los", + "recurrence_occurrence": "Aparición", + "recurrence_weekday": "Día de la semana", + "recurrence_day": "Día del mes (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.º", + "ord_2": "2.º", + "ord_3": "3.º", + "ord_4": "4.º", + "ord_5": "5.º", + "ord_last": "Último", + "day_word": "Día", + "interval_value": "Intervalo", + "interval_unit": "Unidad", + "unit_days": "Días", + "unit_weeks": "Semanas", + "unit_months": "Meses", + "unit_years": "Años", + "due_date": "Fecha de vencimiento", + "cleaning": "Limpieza", + "inspection": "Inspección", + "replacement": "Sustitución", + "calibration": "Calibración", + "service": "Servicio", + "reading": "Lectura", + "custom": "Personalizado", + "history": "Historial", + "cost": "Coste", + "report_button": "Informe", + "report_title": "Informe de mantenimiento", + "report_generated": "Generado", + "report_times_done": "Hecho", + "report_total_cost": "Coste total", + "report_every": "cada {n} {unit}", + "report_notes": "Notas", + "report_col_type": "Tipo", + "report_col_status": "Estado", + "report_col_schedule": "Programación", + "duration": "Duración", + "both": "Ambos", + "trigger_val": "Valor del disparador", + "complete_title": "Completada: ", + "checklist": "Lista de verificación", + "checklist_steps_optional": "Pasos de la lista de verificación (opcional)", + "checklist_placeholder": "Limpiar filtro\nReemplazar junta\nProbar presión", + "checklist_help": "Un paso por línea. Máx. 100 elementos.", + "err_too_long": "{field}: demasiado largo (máx. {n} caracteres)", + "err_too_short": "{field}: demasiado corto (mín. {n} caracteres)", + "err_value_too_high": "{field}: demasiado grande (máx. {n})", + "err_value_too_low": "{field}: demasiado pequeño (mín. {n})", + "err_required": "{field}: campo obligatorio", + "err_wrong_type": "{field}: tipo incorrecto (esperado: {type})", + "err_invalid_choice": "{field}: valor no permitido", + "err_invalid_value": "{field}: valor inválido", + "feat_schedule_time": "Programación por hora", + "feat_schedule_time_desc": "Las tareas vencen a una hora específica en lugar de medianoche.", + "schedule_time_optional": "Vence a las (opcional, HH:MM)", + "schedule_time_help": "Vacío = medianoche (predeterminado). Zona horaria HA.", + "at_time": "a las", + "notes_optional": "Notas (opcional)", + "cost_optional": "Coste (opcional)", + "duration_minutes": "Duración en minutos (opcional)", + "days": "días", + "day": "día", + "today": "Hoy", + "d_overdue": "d vencida", + "no_tasks": "No hay tareas de mantenimiento. Cree un objeto para empezar.", + "no_tasks_short": "Sin tareas", + "no_history": "Sin entradas en el historial.", + "show_all": "Mostrar todo", + "cost_duration_chart": "Costes & Duración", + "installed": "Instalado", + "confirm_delete_object": "¿Eliminar este objeto y todas sus tareas?", + "confirm_delete_task": "¿Eliminar esta tarea?", + "min": "Mín", + "max": "Máx", + "save": "Guardar", + "saving": "Guardando…", + "edit_task": "Editar tarea", + "new_task": "Nueva tarea de mantenimiento", + "task_name": "Nombre de la tarea", + "maintenance_type": "Tipo de mantenimiento", + "priority": "Prioridad", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Baja", + "priority_normal": "Normal", + "priority_high": "Alta", + "schedule_type": "Tipo de planificación", + "interval_days": "Intervalo (días)", + "warning_days": "Días de aviso", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Última ejecución (opcional)", + "interval_anchor": "Anclaje del intervalo", + "anchor_completion": "Desde la fecha de finalización", + "anchor_planned": "Desde la fecha planificada (sin desviación)", + "edit_object": "Editar objeto", + "name": "Nombre", + "manufacturer_optional": "Fabricante (opcional)", + "model_optional": "Modelo (opcional)", + "serial_number_optional": "Número de serie (opcional)", + "serial_number_label": "N/S", + "documentation_url_label": "Manual", + "object_notes_label": "Notas", + "sort_due_date": "Vencimiento", + "sort_object": "Nombre del objeto", + "sort_type": "Tipo", + "sort_task_name": "Nombre de la tarea", + "all_objects": "Todos los objetos", + "tasks_lower": "tareas", + "no_tasks_yet": "Aún no hay tareas", + "add_first_task": "Agregar primera tarea", + "trigger_configuration": "Configuración del disparador", + "entity_id": "ID de entidad", + "comma_separated": "separados por comas", + "entity_logic": "Lógica de entidad", + "entity_logic_any": "Cualquier entidad activa", + "entity_logic_all": "Todas las entidades deben activar", + "entities": "entidades", + "attribute_optional": "Atributo (opcional, vacío = estado)", + "use_entity_state": "Usar estado de la entidad (sin atributo)", + "trigger_above": "Activar por encima de", + "trigger_below": "Activar por debajo de", + "for_at_least_minutes": "Durante al menos (minutos)", + "safety_interval_days": "Intervalo de seguridad (días, opcional)", + "safety_interval": "Intervalo de seguridad (opcional)", + "delta_mode": "Modo delta", + "from_state_optional": "Desde estado (opcional)", + "to_state_optional": "Hasta estado (opcional)", + "documentation_url_optional": "URL de documentación (opcional)", + "object_notes_optional": "Notas (opcional)", + "nfc_tag_id_optional": "ID de etiqueta NFC (opcional)", + "nfc_tags_empty_help": "Aún no hay tags NFC registrados en Home Assistant.", + "nfc_tags_open_settings": "Abrir configuración de tags", + "nfc_tags_refresh": "Actualizar", + "environmental_entity_optional": "Sensor ambiental (opcional)", + "environmental_entity_helper": "p.ej. sensor.temperatura_exterior — ajusta el intervalo según las condiciones ambientales", + "environmental_attribute_optional": "Atributo ambiental (opcional)", + "nfc_tag_id": "ID de etiqueta NFC", + "nfc_linked": "Etiqueta NFC vinculada", + "nfc_link_hint": "Clic para vincular etiqueta NFC", + "responsible_user": "Usuario responsable", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ningún usuario asignado)", + "all_users": "Todos los usuarios", + "my_tasks": "Mis tareas", + "tab_calendar": "Calendario", + "cal_no_events": "Sin mantenimiento", + "cal_window_7": "7 días", + "cal_window_14": "14 días", + "cal_window_30": "30 días", + "cal_window_365": "1 año", + "cal_every_n_days": "cada {n} días", + "cal_source_time": "Basado en tiempo", + "cal_source_time_adaptive": "Basado en tiempo (adaptativo)", + "cal_source_sensor": "Basado en sensor", + "cal_predicted": "predicho", + "cal_confidence_high": "alta confianza", + "cal_confidence_medium": "confianza media", + "cal_confidence_low": "baja confianza", + "budget_monthly": "Presupuesto mensual", + "budget_yearly": "Presupuesto anual", + "groups": "Grupos", + "new_group": "Nuevo grupo", + "edit_group": "Editar grupo", + "no_groups": "Sin grupos todavía", + "delete_group": "Eliminar grupo", + "delete_group_confirm": "¿Eliminar el grupo '{name}'?", + "group_select_tasks": "Seleccionar tareas", + "group_name_required": "Nombre requerido", + "description_optional": "Descripción (opcional)", + "selected": "Seleccionado", + "loading_chart": "Cargando datos...", + "hide_outliers": "Ocultar valores atípicos (fallos del sensor)", + "was_maintenance_needed": "¿Era necesario este mantenimiento?", + "feedback_needed": "Necesario", + "feedback_not_needed": "No necesario", + "feedback_not_sure": "No seguro", + "suggested_interval": "Intervalo sugerido", + "apply_suggestion": "Aplicar", + "reanalyze": "Reanalizar", + "reanalyze_result": "Nuevo análisis", + "reanalyze_insufficient_data": "Datos insuficientes para una recomendación", + "data_points": "puntos de datos", + "dismiss_suggestion": "Descartar", + "confidence_low": "Baja", + "confidence_medium": "Media", + "confidence_high": "Alta", + "recommended": "recomendado", + "seasonal_awareness": "Conciencia estacional", + "edit_seasonal_overrides": "Editar factores estacionales", + "seasonal_overrides_title": "Factores estacionales (override)", + "seasonal_overrides_hint": "Factor por mes (0.1–5.0). Vacío = aprendido automáticamente.", + "seasonal_override_invalid": "Valor no válido", + "seasonal_override_range": "El factor debe estar entre 0.1 y 5.0", + "clear_all": "Borrar todo", + "seasonal_chart_title": "Factores estacionales", + "seasonal_learned": "Aprendido", + "seasonal_manual": "Manual", + "month_jan": "Ene", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Abr", + "month_may": "May", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Ago", + "month_sep": "Sep", + "month_oct": "Oct", + "month_nov": "Nov", + "month_dec": "Dic", + "sensor_prediction": "Predicción del sensor", + "degradation_trend": "Tendencia", + "trend_rising": "En aumento", + "trend_falling": "En descenso", + "trend_stable": "Estable", + "trend_insufficient_data": "Datos insuficientes", + "days_until_threshold": "Días hasta el umbral", + "threshold_exceeded": "Umbral superado", + "environmental_adjustment": "Factor ambiental", + "sensor_prediction_urgency": "El sensor predice el umbral en ~{days} días", + "day_short": "día", + "weibull_reliability_curve": "Curva de fiabilidad", + "weibull_failure_probability": "Probabilidad de fallo", + "weibull_r_squared": "Ajuste R²", + "beta_early_failures": "Fallos tempranos", + "beta_random_failures": "Fallos aleatorios", + "beta_wear_out": "Desgaste", + "beta_highly_predictable": "Altamente predecible", + "confidence_interval": "Intervalo de confianza", + "confidence_conservative": "Conservador", + "confidence_aggressive": "Optimista", + "current_interval_marker": "Intervalo actual", + "recommended_marker": "Recomendado", + "characteristic_life": "Vida característica", + "chart_mini_sparkline": "Sparkline de tendencia", + "chart_history": "Historial de costes y duración", + "chart_seasonal": "Factores estacionales, 12 meses", + "chart_weibull": "Curva de fiabilidad Weibull", + "chart_sparkline": "Gráfico de valor del disparador", + "days_progress": "Progreso en días", + "qr_code": "Código QR", + "qr_generating": "Generando código QR…", + "qr_error": "No se pudo generar el código QR.", + "qr_error_no_url": "No hay URL de HA configurada. Establezca una URL externa o interna en Ajustes → Sistema → Red.", + "save_error": "Error al guardar. Inténtelo de nuevo.", + "qr_print": "Imprimir", + "qr_download": "Descargar SVG", + "qr_action": "Acción al escanear", + "qr_action_view": "Ver info de mantenimiento", + "qr_action_complete": "Marcar mantenimiento como completado", + "qr_url_mode": "Tipo de enlace", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Local (mDNS)", + "qr_mode_server": "URL del servidor", + "overview": "Resumen", + "analysis": "Análisis", + "recent_activities": "Actividades recientes", + "search_notes": "Buscar en notas", + "avg_cost": "Ø Coste", + "no_advanced_features": "Sin funciones avanzadas activadas", + "no_advanced_features_hint": "Active “Intervalos Adaptativos” o “Patrones Estacionales” en la configuración de la integración para ver datos de análisis aquí.", + "analysis_not_enough_data": "Aún no hay suficientes datos para el análisis.", + "analysis_not_enough_data_hint": "El análisis Weibull requiere al menos 5 mantenimientos completados; los patrones estacionales son visibles tras 6+ puntos de datos por mes.", + "analysis_manual_task_hint": "Las tareas manuales sin intervalo no generan datos de análisis.", + "completions": "finalizaciones", + "current": "Actual", + "shorter": "Más corto", + "longer": "Más largo", + "normal": "Normal", + "disabled": "Desactivado", + "compound_logic": "Lógica compuesta", + "compound": "Compuesto (varias condiciones)", + "compound_logic_and": "Y — todas las condiciones deben activarse", + "compound_logic_or": "O — basta con una condición", + "compound_help": "Combina varias condiciones de sensores en un disparador.", + "compound_no_conditions": "Aún no hay condiciones — añade al menos una.", + "compound_add_condition": "Añadir condición", + "compound_condition": "Condición", + "compound_remove_condition": "Eliminar condición", + "card_title": "Título", + "card_show_header": "Mostrar encabezado con estadísticas", + "card_show_actions": "Mostrar botones de acción", + "card_compact": "Modo compacto", + "card_max_items": "Máx. elementos (0 = todos)", + "card_filter_status": "Filtrar por estado", + "card_filter_status_help": "Vacío = mostrar todos los estados.", + "card_filter_objects": "Filtrar por objetos", + "card_filter_objects_help": "Vacío = mostrar todos los objetos.", + "card_filter_entities": "Filtrar por entidades (entity_ids)", + "card_filter_entities_help": "Selecciona entidades sensor / binary_sensor de esta integración. Vacío = todas.", + "card_loading_objects": "Cargando objetos…", + "card_load_error": "No se pudieron cargar los objetos — verifica la conexión WebSocket.", + "card_no_tasks_title": "Aún no hay tareas de mantenimiento", + "card_no_tasks_cta": "→ Crea una en el panel Mantenimiento", + "no_objects": "Aún no hay objetos.", + "action_error": "Acción fallida. Inténtelo de nuevo.", + "area_id_optional": "Área (opcional)", + "installation_date_optional": "Fecha de instalación (opcional)", + "warranty_expiry_optional": "Vencimiento de garantía (opcional)", + "warranty": "Garantía", + "warranty_valid_until": "válida hasta {date}", + "warranty_expires_in": "vence en {days} días", + "warranty_expired": "vencida", + "cal_past_windows": "Ventanas pasadas", + "cal_forward_windows": "Ventanas futuras", + "history_edit_title": "Editar entrada del historial", + "history_edit_timestamp": "Marca de tiempo", + "manufacturer": "Fabricante", + "model": "Modelo", + "area": "Área", + "actions": "Acciones", + "view_mode_label": "Vista", + "view_cards": "Vista de tarjetas", + "view_table": "Vista de tabla", + "objects_table_columns_label": "Columnas de la tabla de objetos", + "objects_table_columns_hint": "Elige qué columnas aparecen en la vista de tabla de objetos.", + "custom_icon_optional": "Icono (opcional, ej. mdi:wrench)", + "task_enabled": "Tarea habilitada", + "skip_reason_prompt": "¿Omitir esta tarea?", + "reason_optional": "Motivo (opcional)", + "reset_date_prompt": "¿Marcar la tarea como realizada?", + "reset_date_optional": "Fecha de última ejecución (opcional, por defecto: hoy)", + "notes_label": "Notas", + "documentation_label": "Documentación", + "no_nfc_tag": "— Sin etiqueta —", + "dashboard": "Panel", + "tab_today": "Hoy", + "palette_placeholder": "Buscar objetos y tareas…", + "palette_no_results": "Sin resultados", + "palette_hint": "↑↓ navegar · Enter abrir · Esc cerrar", + "today_all_caught_up": "¡Todo al día! Nada pendiente esta semana.", + "today_overdue": "Vencidas", + "today_due_today": "Vence hoy", + "today_this_week": "Esta semana", + "settings": "Ajustes", + "settings_features": "Funciones avanzadas", + "settings_features_desc": "Active o desactive funciones avanzadas. Desactivar las oculta de la interfaz pero no elimina datos.", + "feat_adaptive": "Planificación adaptativa", + "feat_adaptive_desc": "Aprender intervalos óptimos del historial de mantenimiento", + "feat_predictions": "Predicciones de sensor", + "feat_predictions_desc": "Predecir fechas de activación por degradación del sensor", + "feat_seasonal": "Ajustes estacionales", + "feat_seasonal_desc": "Ajustar intervalos según patrones estacionales", + "feat_environmental": "Correlación ambiental", + "feat_environmental_desc": "Correlacionar intervalos con temperatura/humedad", + "feat_budget": "Seguimiento de presupuesto", + "feat_budget_desc": "Seguir los gastos de mantenimiento mensuales y anuales", + "feat_groups": "Grupos de tareas", + "feat_groups_desc": "Organizar tareas en grupos lógicos", + "feat_checklists": "Listas de verificación", + "feat_checklists_desc": "Procedimientos de varios pasos para completar tareas", + "settings_general": "General", + "settings_default_warning": "Días de aviso predeterminados", + "settings_panel_enabled": "Panel lateral", + "settings_panel_title": "Título del panel lateral", + "settings_notifications": "Notificaciones", + "settings_notify_service": "Servicio de notificación", + "test_notification": "Notificación de prueba", + "send_test": "Enviar prueba", + "testing": "Enviando…", + "test_notification_success": "Notificación de prueba enviada", + "test_notification_failed": "La notificación de prueba falló", + "settings_notify_due_soon": "Notificar cuando esté próxima", + "settings_notify_overdue": "Notificar cuando esté vencida", + "settings_notify_triggered": "Notificar cuando se active", + "settings_interval_hours": "Intervalo de repetición (horas, 0 = una vez)", + "settings_quiet_hours": "Horas de silencio", + "settings_quiet_start": "Inicio", + "settings_quiet_end": "Fin", + "settings_max_per_day": "Máx. notificaciones por día (0 = ilimitado)", + "settings_bundling": "Agrupar notificaciones", + "settings_bundle_threshold": "Umbral de agrupación", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Botones de acción móviles", + "settings_action_complete": "Mostrar botón 'Completada'", + "settings_action_skip": "Mostrar botón 'Omitir'", + "settings_action_snooze": "Mostrar botón 'Posponer'", + "settings_weekly_digest": "Resumen semanal", + "settings_weekly_digest_hint": "Una notificación de resumen el lunes por la mañana cuando hay tareas.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Duración de posposición (horas)", + "settings_budget": "Presupuesto", + "settings_currency": "Moneda", + "settings_budget_monthly": "Presupuesto mensual", + "settings_budget_yearly": "Presupuesto anual", + "settings_budget_alerts": "Alertas de presupuesto", + "settings_budget_threshold": "Umbral de alerta (%)", + "settings_import_export": "Importar / Exportar", + "settings_export_json": "Exportar JSON", + "settings_export_yaml": "Exportar YAML", + "settings_export_csv": "Exportar CSV", + "settings_import_csv": "Importar CSV", + "settings_import_placeholder": "Pegue el contenido JSON o CSV aquí…", + "settings_import_btn": "Importar", + "settings_import_success": "{count} objetos importados correctamente.", + "settings_export_success": "Exportación descargada.", + "settings_saved": "Ajuste guardado.", + "settings_include_history": "Incluir historial", + "sort_alphabetical": "Alfabético", + "sort_due_soonest": "Próximo a vencer", + "sort_task_count": "Cantidad de tareas", + "sort_area": "Área", + "sort_assigned_user": "Usuario asignado", + "sort_group": "Grupo", + "groupby_none": "Sin agrupación", + "groupby_area": "Por área", + "groupby_group": "Por grupo", + "groupby_user": "Por usuario", + "filter_label": "Filtro", + "user_label": "Usuario", + "sort_label": "Ordenar", + "group_by_label": "Agrupar por", + "state_value_help": "Usa el valor de estado de HA (normalmente en minúsculas, p. ej. \"on\"/\"off\"). Las mayúsculas se normalizan al guardar.", + "target_changes_help": "Número de transiciones coincidentes antes de que se dispare el trigger (predeterminado: 1).", + "qr_print_title": "Imprimir códigos QR", + "qr_print_desc": "Genera una página imprimible de códigos QR para recortar y pegar en tus equipos.", + "qr_print_load": "Cargar objetos", + "qr_print_filter": "Filtro", + "qr_print_objects": "Objetos", + "qr_print_actions": "Acciones", + "qr_print_url_mode": "Tipo de enlace", + "qr_print_estimate": "Códigos QR estimados", + "qr_print_over_limit": "máximo 200, reduce el filtro", + "qr_print_generate": "Generar códigos QR", + "qr_print_generating": "Generando…", + "qr_print_ready": "Códigos QR listos", + "qr_print_print_button": "Imprimir", + "qr_print_empty": "Nada que generar", + "qr_action_skip": "Omitir", + "vacation_title": "Modo vacaciones", + "vacation_active": "activo", + "vacation_ended": "terminado", + "vacation_desc": "Planifica tus vacaciones: las notificaciones se pausan durante el período más unos días de margen. Puedes excluir tareas concretas.", + "vacation_enable": "Activar modo vacaciones", + "vacation_start": "Inicio", + "vacation_end": "Fin", + "vacation_buffer": "Margen (días)", + "vacation_exempt_title": "Notificar igual durante vacaciones", + "vacation_exempt_desc": "Selecciona tareas que deben notificar también en vacaciones (p. ej. química crítica de piscina).", + "vacation_load_tasks": "Cargar tareas", + "vacation_preview_btn": "Mostrar vista previa", + "vacation_preview_affected": "tareas afectadas", + "vacation_event_due_soon": "vencerá pronto", + "vacation_event_overdue": "se volverá vencida", + "vacation_event_triggered_est": "posible activación de sensor", + "vacation_sensor_based": "(basado en sensor)", + "vacation_action_notify": "Notificar igual", + "vacation_action_unsilence": "Silenciar de nuevo", + "vacation_marked_complete": "Marcado como completado", + "vacation_marked_skip": "Omitido", + "vacation_end_now": "Terminar vacaciones ahora", + "unassigned": "Sin asignar", + "no_area": "Sin área", + "has_overdue": "Tareas vencidas", + "object": "Objeto", + "settings_panel_access": "Acceso al panel", + "settings_panel_access_desc": "Los administradores siempre tienen acceso completo. Para delegar crear, editar y eliminar a no administradores específicos, activa esto y selecciónalos abajo — los demás solo ven Completar y Omitir.", + "settings_operator_write": "Permitir a los usuarios seleccionados crear, editar y eliminar", + "settings_operator_write_desc": "Desactivado: solo los administradores pueden cambiar el contenido. Activado: los usuarios seleccionados abajo también tienen acceso completo.", + "no_non_admin_users": "No se encontraron usuarios no administradores. Añade alguno en Ajustes → Personas.", + "owner_label": "Propietario", + "feat_completion_actions": "Acciones de finalización", + "feat_completion_actions_desc": "Acción HA por tarea al completar + QR de finalización rápida con valores prefijados.", + "on_complete_action_title": "Al completar: ejecutar acción HA (opcional)", + "on_complete_action_desc": "Llama un servicio HA cuando se completa la tarea — p. ej. reiniciar un contador del dispositivo.", + "on_complete_action_service": "Servicio", + "on_complete_action_target": "Entidad objetivo", + "on_complete_action_data": "Datos (JSON, opcional)", + "on_complete_action_test": "Probar acción", + "on_complete_action_test_success": "Éxito", + "on_complete_action_test_failed": "Fallido", + "quick_complete_defaults_title": "Valores predeterminados de finalización rápida (para escaneos QR, opcional)", + "quick_complete_defaults_desc": "Valores prefijados para QR de finalización rápida. Sin ellos, el QR abre el diálogo de completar.", + "quick_complete_defaults_notes": "Notas", + "quick_complete_defaults_cost": "Coste", + "quick_complete_defaults_duration": "Duración (minutos)", + "quick_complete_defaults_feedback_none": "Sin feedback", + "quick_complete_defaults_feedback_needed": "Era necesario", + "quick_complete_defaults_feedback_not_needed": "No era necesario", + "quick_complete_success": "Completado rápido", + "trigger_replaced": "Disparador reemplazado", + "add": "Añadir", + "show_stats": "Mostrar estadísticas + gráficos", + "hide_stats": "Ocultar estadísticas", + "adaptive_no_data": "Aún no hay suficiente historial de finalizaciones para el análisis adaptativo. Completa esta tarea unas cuantas veces más para desbloquear las recomendaciones de intervalo y los gráficos de fiabilidad.", + "suggestion_applied": "Intervalo sugerido aplicado", + "vacation_mode": "Modo vacaciones", + "vacation_status_active": "Activo ahora", + "vacation_status_scheduled": "Programado", + "vacation_status_inactive": "Inactivo", + "vacation_end_now_confirm": "¿Finalizar las vacaciones de inmediato?", + "vacation_exempt_count": "exentas", + "vacation_advanced": "Avanzado…", + "vacation_open_panel": "Abrir en el panel", + "enable": "Activar", + "saved": "Guardado", + "budget_monthly_set": "Definir mensual", + "budget_yearly_set": "Definir anual", + "budget_advanced": "Moneda, alertas…", + "budget_open_panel": "Abrir en el panel", + "groups_empty": "Aún no hay grupos.", + "group_new_placeholder": "Añadir grupo…", + "group_delete_confirm": "¿Eliminar el grupo \"{name}\"?", + "groups_manage_tasks": "Gestionar asignaciones de tareas…", + "groups_open_panel": "Abrir en el panel", + "on_complete_action_target_hint": "Nota: el dominio de la entidad debe coincidir con el servicio — p. ej. 'button.press' solo funciona en button.*, 'counter.increment' solo en counter.*, 'input_button.press' solo en input_button.* etc. Si no coinciden, la acción falla silenciosamente (HA registra 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Mostrar todos los objetos", + "show_all_tasks": "Quitar filtro — mostrar todas las tareas", + "filter_to_overdue": "Filtrar la lista solo a vencidas", + "filter_to_due_soon": "Filtrar la lista solo a próximas a vencer", + "filter_to_triggered": "Filtrar la lista solo a activadas", + "open_task": "Abrir tarea", + "show_details": "Mostrar historial + estadísticas", + "hide_details": "Ocultar detalles", + "history_empty": "Aún no hay historial.", + "history_edit_button": "Editar entrada", + "total_cost": "Coste total", + "times_performed": "Realizada", + "older_entries": "anteriores", + "open_in_panel": "Abrir en el panel de Mantenimiento", + "skip_reason": "Motivo de omisión (opcional)", + "reset_to_date": "Restablecer última ejecución a", + "delete_task_confirm": "¿Eliminar esta tarea y su historial?", + "delete_object_confirm": "¿Eliminar este objeto y todas sus tareas?", + "loading": "Cargando…", + "archive": "Archivar", + "undo": "Deshacer", + "task_archived": "Tarea archivada", + "object_archived": "Objeto archivado", + "unarchive": "Desarchivar", + "archived": "Archivado", + "show_archived": "Mostrar archivados", + "hide_archived": "Ocultar archivados", + "confirm_archive_object": "¿Archivar este objeto y sus tareas? Se conserva el historial y se puede restaurar más tarde.", + "settings_archive": "Archivado y retención", + "settings_archive_desc": "Retira las tareas únicas completadas sin eliminarlas. Los elementos archivados se ocultan y quedan inactivos, pero conservan su historial y coste.", + "settings_archive_oneoff_days": "Archivar automáticamente las tareas únicas completadas tras (días, 0 = desactivado)", + "settings_delete_archived_oneoff_days": "Eliminar automáticamente las tareas únicas archivadas tras (días, 0 = nunca)", + "archive_object": "Archivar objeto", + "unarchive_object": "Desarchivar objeto", + "documents": "Documentos", + "documents_empty": "Aún no hay documentos.", + "doc_upload": "Subir archivo", + "doc_uploading": "Subiendo…", + "doc_add_link": "Añadir enlace", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Título (opcional)", + "doc_open": "Abrir", + "doc_delete_confirm": "¿Eliminar «{name}»?", + "doc_too_large": "El archivo es demasiado grande (máx. 25 MB).", + "doc_upload_failed": "Error al subir.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Ya almacenado en otro lugar — compartido, sin espacio adicional.", + "doc_dup_in_object": "Este archivo ya está adjunto a este objeto.", + "doc_link_invalid": "Solo se permiten enlaces http/https.", + "doc_cat_manual": "Manual", + "doc_cat_warranty": "Garantía", + "doc_cat_invoice": "Factura", + "doc_cat_spare_parts": "Repuestos", + "doc_cat_photo": "Foto", + "doc_cat_other": "Otro", + "doc_link_badge": "Enlace", + "doc_storage_title": "Almacenamiento de documentos", + "doc_storage_saved": "Ahorrado por deduplicación", + "doc_storage_refresh": "Actualizar", + "doc_download": "Descargar", + "doc_close": "Cerrar", + "doc_camera": "Tomar foto", + "doc_drop_hint": "Suelta los archivos aquí", + "doc_task_none": "Ningún documento vinculado a esta tarea.", + "doc_link_existing": "Vincular un documento…", + "doc_attach": "Vincular", + "doc_unlink": "Desvincular", + "doc_page": "Página", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1a", + "chart_since_service": "desde el último mantenimiento", + "chart_no_stats": "No hay estadísticas a largo plazo para esta entidad — solo se muestran valores de eventos de mantenimiento", + "auto_complete_on_recovery": "Completar automáticamente cuando el sensor se recupere", + "auto_complete_on_recovery_help": "Registra una finalización (fija la última realización) cuando el disparador se resuelve solo — p. ej., sal rellenada, filtro cambiado.", + "doc_search": "Buscar documentos…", + "doc_search_none": "Ningún documento coincidente", + "link_device_optional": "Vincular a dispositivo existente (opcional)", + "parent_object_optional": "Objeto principal (opcional)", + "parent_none": "(Sin objeto principal)", + "paused": "Pausado", + "pause_object": "Pausar", + "resume_object": "Reanudar", + "pause_until_prompt": "Congela los calendarios de este objeto: nada vence ni notifica hasta reanudarlo. Opcionalmente, fija una fecha de reanudación automática.", + "pause_until_label": "Reanudar el (opcional)", + "object_paused": "Objeto pausado", + "object_resumed": "Objeto reanudado — calendarios reiniciados", + "object_paused_badge": "Pausado", + "paused_until_label": "hasta", + "replace_object": "Sustituir…", + "replace_object_prompt": "Retira este objeto y crea un sucesor. El historial y los costes quedan archivados en el antiguo; las tareas y los documentos pasan al nuevo, los contadores empiezan de cero.", + "replace_name_label": "Nombre del sucesor", + "object_replaced": "Objeto sustituido — sucesor creado", + "reading_unit_label": "Unidad de lectura (p. ej. kWh, m³)", + "reading_unit_help": "Se muestra junto al valor registrado al completar esta tarea.", + "reading_value_label": "Valor de lectura", + "reading_label": "Lectura", + "settings_templates_label": "Galería de plantillas", + "settings_templates_hint": "Desmarca las plantillas que nunca necesitarás: desaparecen de los selectores \"Desde plantilla\" (panel y flujo de configuración). Nada más cambia; puedes reactivarlas cuando quieras.", + "worksheet": "Hoja de trabajo", + "worksheet_scan_view": "Escanea para abrir la tarea", + "worksheet_scan_complete": "Escanea para completar", + "worksheet_manual_excerpt": "Extracto del manual", + "worksheet_pages": "páginas", + "worksheet_printed": "Impreso", + "worksheet_never": "Nunca" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/fi.json b/custom_components/maintenance_supporter/frontend/locales/fi.json new file mode 100644 index 0000000..e641728 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/fi.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Huolto", + "objects": "Kohteet", + "tasks": "Tehtävät", + "overdue": "Myöhässä", + "due_soon": "Pian erääntyy", + "triggered": "Laukaistu", + "trigger_replaced": "Laukaisin korvattu", + "ok": "OK", + "all": "Kaikki", + "new_object": "+ Uusi kohde", + "templates_from": "Mallista", + "templates_title": "Aloita mallista", + "templates_task_count": "{n} tehtävää", + "template_created": "Luotu mallista", + "onboard_hint": "Lisää ensimmäinen kohde ja aloita huollon seuranta.", + "edit": "Muokkaa", + "duplicate": "Monista", + "task_duplicated": "Tehtävä monistettu", + "object_duplicated": "Kohde monistettu", + "delete": "Poista", + "add_task": "+ Lisää tehtävä", + "complete": "Valmis", + "completed": "Suoritettu", + "skip": "Ohita", + "skipped": "Ohitettu", + "missed": "Missed", + "reset": "Nollaa", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Peruuta", + "bulk_select": "Valitse", + "bulk_select_all": "Valitse kaikki", + "bulk_n_selected": "{n} valittu", + "bulk_completed": "{n} tehtävää valmiina", + "bulk_archived": "{n} tehtävää arkistoitu", + "completing": "Suoritetaan…", + "interval": "Väli", + "warning": "Varoitus", + "last_performed": "Viimeksi suoritettu", + "next_due": "Seuraava eräpäivä", + "days_until_due": "Päiviä eräpäivään", + "avg_duration": "Keskim. kesto", + "trigger": "Laukaisin", + "trigger_type": "Laukaisimen tyyppi", + "threshold_above": "Yläraja", + "threshold_below": "Alaraja", + "threshold": "Kynnysarvo", + "counter": "Laskuri", + "state_change": "Tilan muutos", + "runtime": "Käyttöaika", + "runtime_hours": "Tavoitekäyttöaika (tuntia)", + "target_value": "Tavoitearvo", + "baseline": "Lähtötaso", + "target_changes": "Tavoitemuutokset", + "for_minutes": "Vähintään (minuuttia)", + "time_based": "Aikaperusteinen", + "sensor_based": "Anturiperusteinen", + "manual": "Manuaalinen", + "one_time": "Kertaluonteinen", + "weekdays": "Viikonpäivät", + "nth_weekday": "Kuukauden n:s viikonpäivä", + "day_of_month": "Kuukauden päivä", + "recurrence_on_days": "Toista päivinä", + "recurrence_occurrence": "Esiintymä", + "recurrence_weekday": "Viikonpäivä", + "recurrence_day": "Kuukauden päivä (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Viimeinen", + "day_word": "Päivä", + "interval_value": "Väli", + "interval_unit": "Yksikkö", + "unit_days": "Päivää", + "unit_weeks": "Viikkoa", + "unit_months": "Kuukautta", + "unit_years": "Vuotta", + "due_date": "Eräpäivä", + "cleaning": "Puhdistus", + "inspection": "Tarkastus", + "replacement": "Vaihto", + "calibration": "Kalibrointi", + "service": "Huolto", + "reading": "Lukema", + "custom": "Mukautettu", + "history": "Historia", + "cost": "Kustannus", + "report_button": "Raportti", + "report_title": "Huoltoraportti", + "report_generated": "Luotu", + "report_times_done": "Tehty", + "report_total_cost": "Kokonaiskustannus", + "report_every": "joka {n}. {unit}", + "report_notes": "Muistiinpanot", + "report_col_type": "Tyyppi", + "report_col_status": "Tila", + "report_col_schedule": "Aikataulu", + "duration": "Kesto", + "both": "Molemmat", + "trigger_val": "Laukaisuarvo", + "complete_title": "Suorita: ", + "checklist": "Tarkistuslista", + "checklist_steps_optional": "Tarkistuslistan vaiheet (valinnainen)", + "checklist_placeholder": "Puhdista suodatin\nVaihda tiiviste\nTarkista paine", + "checklist_help": "Yksi vaihe riviä kohden. Enintään 100 kohdetta.", + "err_too_long": "{field}: liian pitkä (enint. {n} merkkiä)", + "err_too_short": "{field}: liian lyhyt (väh. {n} merkkiä)", + "err_value_too_high": "{field}: liian suuri (enint. {n})", + "err_value_too_low": "{field}: liian pieni (väh. {n})", + "err_required": "{field}: pakollinen", + "err_wrong_type": "{field}: väärä tyyppi (odotettiin: {type})", + "err_invalid_choice": "{field}: ei sallittu arvo", + "err_invalid_value": "{field}: virheellinen arvo", + "feat_schedule_time": "Kellonajan ajoitus", + "feat_schedule_time_desc": "Tehtävät erääntyvät tiettyyn kellonaikaan keskiyön sijaan.", + "schedule_time_optional": "Erääntyy klo (valinnainen, HH:MM)", + "schedule_time_help": "Tyhjä = keskiyö (oletus). HA:n aikavyöhyke.", + "at_time": "klo", + "notes_optional": "Muistiinpanot (valinnainen)", + "cost_optional": "Kustannus (valinnainen)", + "duration_minutes": "Kesto minuutteina (valinnainen)", + "days": "päivää", + "day": "päivä", + "today": "Tänään", + "d_overdue": "pv myöhässä", + "no_tasks": "Ei vielä huoltotehtäviä. Luo kohde aloittaaksesi.", + "no_tasks_short": "Ei tehtäviä", + "no_history": "Ei vielä historiatietoja.", + "show_all": "Näytä kaikki", + "cost_duration_chart": "Kustannus ja kesto", + "installed": "Asennettu", + "confirm_delete_object": "Poistetaanko tämä kohde ja kaikki sen tehtävät?", + "confirm_delete_task": "Poistetaanko tämä tehtävä?", + "min": "Min", + "max": "Maks", + "save": "Tallenna", + "saving": "Tallennetaan…", + "edit_task": "Muokkaa tehtävää", + "new_task": "Uusi huoltotehtävä", + "task_name": "Tehtävän nimi", + "maintenance_type": "Huoltotyyppi", + "priority": "Prioriteetti", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Matala", + "priority_normal": "Normaali", + "priority_high": "Korkea", + "schedule_type": "Ajoitustyyppi", + "interval_days": "Väli (päivää)", + "warning_days": "Varoituspäivät", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Viimeksi suoritettu (valinnainen)", + "interval_anchor": "Välin ankkuri", + "anchor_completion": "Suorituspäivästä", + "anchor_planned": "Suunnitellusta päivästä (ei poikkeamaa)", + "edit_object": "Muokkaa kohdetta", + "name": "Nimi", + "manufacturer_optional": "Valmistaja (valinnainen)", + "model_optional": "Malli (valinnainen)", + "serial_number_optional": "Sarjanumero (valinnainen)", + "serial_number_label": "S/N", + "documentation_url_label": "Käyttöohje", + "object_notes_label": "Muistiinpanot", + "sort_due_date": "Eräpäivä", + "sort_object": "Kohteen nimi", + "sort_type": "Tyyppi", + "sort_task_name": "Tehtävän nimi", + "all_objects": "Kaikki kohteet", + "tasks_lower": "tehtävää", + "no_tasks_yet": "Ei vielä tehtäviä", + "add_first_task": "Lisää ensimmäinen tehtävä", + "trigger_configuration": "Laukaisimen asetukset", + "entity_id": "Entiteetin tunnus", + "comma_separated": "pilkuilla eroteltu", + "entity_logic": "Entiteettilogiikka", + "entity_logic_any": "Mikä tahansa entiteetti laukaisee", + "entity_logic_all": "Kaikkien entiteettien on laukaistava", + "entities": "entiteettiä", + "attribute_optional": "Attribuutti (valinnainen, tyhjä = tila)", + "use_entity_state": "Käytä entiteetin tilaa (ei attribuuttia)", + "trigger_above": "Laukaise yli", + "trigger_below": "Laukaise alle", + "for_at_least_minutes": "Vähintään (minuuttia)", + "safety_interval_days": "Turvaväli (päivää, valinnainen)", + "safety_interval": "Turvaväli (valinnainen)", + "delta_mode": "Delta-tila", + "from_state_optional": "Lähtötilasta (valinnainen)", + "to_state_optional": "Kohdetilaan (valinnainen)", + "documentation_url_optional": "Dokumentaation URL (valinnainen)", + "object_notes_optional": "Muistiinpanot (valinnainen)", + "nfc_tag_id_optional": "NFC-tunnisteen tunnus (valinnainen)", + "nfc_tags_empty_help": "Home Assistantiin ei ole vielä rekisteröity NFC-tunnisteita.", + "nfc_tags_open_settings": "Avaa tunnisteiden asetukset", + "nfc_tags_refresh": "Päivitä", + "environmental_entity_optional": "Ympäristöanturi (valinnainen)", + "environmental_entity_helper": "esim. sensor.outdoor_temperature — säätää väliä ympäristöolosuhteiden mukaan", + "environmental_attribute_optional": "Ympäristöattribuutti (valinnainen)", + "nfc_tag_id": "NFC-tunnisteen tunnus", + "nfc_linked": "NFC-tunniste linkitetty", + "nfc_link_hint": "Linkitä NFC-tunniste napsauttamalla", + "responsible_user": "Vastuukäyttäjä", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ei käyttäjää määritetty)", + "all_users": "Kaikki käyttäjät", + "my_tasks": "Omat tehtävät", + "tab_calendar": "Kalenteri", + "cal_no_events": "Ei huoltoja", + "cal_window_7": "7 päivää", + "cal_window_14": "14 päivää", + "cal_window_30": "30 päivää", + "cal_window_365": "1 vuosi", + "cal_every_n_days": "{n} päivän välein", + "cal_source_time": "Aikaperusteinen", + "cal_source_time_adaptive": "Aikaperusteinen (mukautuva)", + "cal_source_sensor": "Anturiperusteinen", + "cal_predicted": "ennustettu", + "cal_confidence_high": "korkea luottamus", + "cal_confidence_medium": "keskitason luottamus", + "cal_confidence_low": "matala luottamus", + "budget_monthly": "Kuukausibudjetti", + "budget_yearly": "Vuosibudjetti", + "groups": "Ryhmät", + "new_group": "Uusi ryhmä", + "edit_group": "Muokkaa ryhmää", + "no_groups": "Ei vielä ryhmiä", + "delete_group": "Poista ryhmä", + "delete_group_confirm": "Poistetaanko ryhmä '{name}'?", + "group_select_tasks": "Valitse tehtävät", + "group_name_required": "Nimi on pakollinen", + "description_optional": "Kuvaus (valinnainen)", + "selected": "Valittu", + "loading_chart": "Ladataan kaaviotietoja...", + "hide_outliers": "Piilota poikkeamat (anturivirheet)", + "was_maintenance_needed": "Oliko tämä huolto tarpeen?", + "feedback_needed": "Tarpeen", + "feedback_not_needed": "Ei tarpeen", + "feedback_not_sure": "Ei varma", + "suggested_interval": "Ehdotettu väli", + "apply_suggestion": "Käytä", + "reanalyze": "Analysoi uudelleen", + "reanalyze_result": "Uusi analyysi", + "reanalyze_insufficient_data": "Ei tarpeeksi tietoja suosituksen tekemiseen", + "data_points": "tietopistettä", + "dismiss_suggestion": "Hylkää", + "confidence_low": "Matala", + "confidence_medium": "Keskitaso", + "confidence_high": "Korkea", + "recommended": "suositeltu", + "seasonal_awareness": "Kausivaihtelun huomiointi", + "edit_seasonal_overrides": "Muokkaa kausikertoimia", + "seasonal_overrides_title": "Kausikertoimet (ohitus)", + "seasonal_overrides_hint": "Kerroin kuukautta kohden (0,1–5,0). Tyhjä = opitaan automaattisesti.", + "seasonal_override_invalid": "Virheellinen arvo", + "seasonal_override_range": "Kertoimen on oltava välillä 0,1–5,0", + "clear_all": "Tyhjennä kaikki", + "seasonal_chart_title": "Kausikertoimet", + "seasonal_learned": "Opittu", + "seasonal_manual": "Manuaalinen", + "month_jan": "Tammi", + "month_feb": "Helmi", + "month_mar": "Maalis", + "month_apr": "Huhti", + "month_may": "Touko", + "month_jun": "Kesä", + "month_jul": "Heinä", + "month_aug": "Elo", + "month_sep": "Syys", + "month_oct": "Loka", + "month_nov": "Marras", + "month_dec": "Joulu", + "sensor_prediction": "Anturiennuste", + "degradation_trend": "Trendi", + "trend_rising": "Nouseva", + "trend_falling": "Laskeva", + "trend_stable": "Vakaa", + "trend_insufficient_data": "Riittämättömät tiedot", + "days_until_threshold": "Päiviä kynnysarvoon", + "threshold_exceeded": "Kynnysarvo ylitetty", + "environmental_adjustment": "Ympäristökerroin", + "sensor_prediction_urgency": "Anturi ennustaa kynnysarvon noin {days} päivässä", + "day_short": "pv", + "weibull_reliability_curve": "Luotettavuuskäyrä", + "weibull_failure_probability": "Vikaantumistodennäköisyys", + "weibull_r_squared": "Sovitus R²", + "beta_early_failures": "Varhaiset viat", + "beta_random_failures": "Satunnaiset viat", + "beta_wear_out": "Kuluminen", + "beta_highly_predictable": "Erittäin ennustettava", + "confidence_interval": "Luottamusväli", + "confidence_conservative": "Varovainen", + "confidence_aggressive": "Optimistinen", + "current_interval_marker": "Nykyinen väli", + "recommended_marker": "Suositeltu", + "characteristic_life": "Ominaiselinikä", + "chart_mini_sparkline": "Trendikäyrä", + "chart_history": "Kustannus- ja kestohistoria", + "chart_seasonal": "Kausikertoimet, 12 kuukautta", + "chart_weibull": "Weibull-luotettavuuskäyrä", + "chart_sparkline": "Anturin laukaisuarvon kaavio", + "days_progress": "Päivien edistyminen", + "qr_code": "QR-koodi", + "qr_generating": "Luodaan QR-koodia…", + "qr_error": "QR-koodin luonti epäonnistui.", + "qr_error_no_url": "HA:n URL-osoitetta ei ole määritetty. Aseta ulkoinen tai sisäinen URL kohdassa Asetukset → Järjestelmä → Verkko.", + "save_error": "Tallennus epäonnistui. Yritä uudelleen.", + "qr_print": "Tulosta", + "qr_download": "Lataa SVG", + "qr_action": "Toiminto skannattaessa", + "qr_action_view": "Näytä huoltotiedot", + "qr_action_complete": "Merkitse huolto valmiiksi", + "qr_url_mode": "Linkin tyyppi", + "qr_mode_companion": "Companion-sovellus", + "qr_mode_local": "Paikallinen (mDNS)", + "qr_mode_server": "Palvelimen URL", + "overview": "Yleiskatsaus", + "analysis": "Analyysi", + "recent_activities": "Viimeaikaiset toimet", + "search_notes": "Hae muistiinpanoja", + "avg_cost": "Keskim. kustannus", + "no_advanced_features": "Lisäominaisuuksia ei ole otettu käyttöön", + "no_advanced_features_hint": "Ota käyttöön ”Mukautuvat välit” tai ”Kausivaihtelut” integraation asetuksissa nähdäksesi analyysitiedot täällä.", + "analysis_not_enough_data": "Ei vielä tarpeeksi tietoja analyysiin.", + "analysis_not_enough_data_hint": "Weibull-analyysi vaatii vähintään 5 suoritettua huoltoa; kausivaihtelut näkyvät kun kuukautta kohden on yli 6 tietopistettä.", + "analysis_manual_task_hint": "Manuaaliset tehtävät ilman väliä eivät tuota analyysitietoja.", + "completions": "suoritusta", + "current": "Nykyinen", + "shorter": "Lyhyempi", + "longer": "Pidempi", + "normal": "Normaali", + "disabled": "Pois käytöstä", + "compound_logic": "Yhdistelmälogiikka", + "compound": "Yhdistetty (useita ehtoja)", + "compound_logic_and": "JA — kaikkien ehtojen on täytyttävä", + "compound_logic_or": "TAI — mikä tahansa ehto riittää", + "compound_help": "Yhdistä useita anturiehtoja yhdeksi laukaisimeksi.", + "compound_no_conditions": "Ei vielä ehtoja — lisää vähintään yksi.", + "compound_add_condition": "Lisää ehto", + "compound_condition": "Ehto", + "compound_remove_condition": "Poista ehto", + "card_title": "Otsikko", + "card_show_header": "Näytä otsikko tilastoineen", + "card_show_actions": "Näytä toimintopainikkeet", + "card_compact": "Tiivis tila", + "card_max_items": "Enimmäismäärä (0 = kaikki)", + "card_filter_status": "Suodata tilan mukaan", + "card_filter_status_help": "Tyhjä = näytä kaikki tilat.", + "card_filter_objects": "Suodata kohteiden mukaan", + "card_filter_objects_help": "Tyhjä = näytä kaikki kohteet.", + "card_filter_entities": "Suodata entiteettien mukaan (entity_id)", + "card_filter_entities_help": "Valitse sensor- / binary_sensor-entiteetit tästä integraatiosta. Tyhjä = kaikki.", + "card_loading_objects": "Ladataan kohteita…", + "card_load_error": "Kohteita ei voitu ladata — tarkista WebSocket-yhteys.", + "card_no_tasks_title": "Ei vielä huoltotehtäviä", + "card_no_tasks_cta": "→ Luo sellainen Huolto-paneelissa", + "no_objects": "Ei vielä kohteita.", + "action_error": "Toiminto epäonnistui. Yritä uudelleen.", + "area_id_optional": "Alue (valinnainen)", + "installation_date_optional": "Asennuspäivä (valinnainen)", + "warranty_expiry_optional": "Takuun päättyminen (valinnainen)", + "warranty": "Takuu", + "warranty_valid_until": "voimassa {date} asti", + "warranty_expires_in": "päättyy {days} päivän kuluttua", + "warranty_expired": "päättynyt", + "cal_past_windows": "Menneet jaksot", + "cal_forward_windows": "Tulevat jaksot", + "history_edit_title": "Muokkaa historiamerkintää", + "history_edit_timestamp": "Aikaleima", + "manufacturer": "Valmistaja", + "model": "Malli", + "area": "Alue", + "actions": "Toiminnot", + "view_mode_label": "Näkymä", + "view_cards": "Korttinäkymä", + "view_table": "Taulukkonäkymä", + "objects_table_columns_label": "Kohdetaulukon sarakkeet", + "objects_table_columns_hint": "Valitse, mitkä sarakkeet näkyvät kohdetaulukkonäkymässä.", + "custom_icon_optional": "Kuvake (valinnainen, esim. mdi:wrench)", + "task_enabled": "Tehtävä käytössä", + "skip_reason_prompt": "Ohitetaanko tämä tehtävä?", + "reason_optional": "Syy (valinnainen)", + "reset_date_prompt": "Merkitäänkö tehtävä suoritetuksi?", + "reset_date_optional": "Viimeksi suoritettu -päivä (valinnainen, oletus tänään)", + "notes_label": "Muistiinpanot", + "documentation_label": "Dokumentaatio", + "no_nfc_tag": "— Ei tunnistetta —", + "dashboard": "Kojelauta", + "tab_today": "Tänään", + "palette_placeholder": "Hae kohteita ja tehtäviä…", + "palette_no_results": "Ei osumia", + "palette_hint": "↑↓ liiku · Enter avaa · Esc sulje", + "today_all_caught_up": "Kaikki hoidettu! Tällä viikolla ei ole mitään.", + "today_overdue": "Myöhässä", + "today_due_today": "Erääntyy tänään", + "today_this_week": "Tällä viikolla", + "settings": "Asetukset", + "settings_features": "Lisäominaisuudet", + "settings_features_desc": "Ota lisäominaisuudet käyttöön tai pois käytöstä. Poistaminen piilottaa ne käyttöliittymästä mutta ei poista tietoja.", + "feat_adaptive": "Mukautuva ajoitus", + "feat_adaptive_desc": "Opi optimaaliset välit huoltohistoriasta", + "feat_predictions": "Anturiennusteet", + "feat_predictions_desc": "Ennusta laukaisupäivät anturin kulumisesta", + "feat_seasonal": "Kausisäädöt", + "feat_seasonal_desc": "Säädä välejä kausivaihteluiden mukaan", + "feat_environmental": "Ympäristökorrelaatio", + "feat_environmental_desc": "Korreloi välit lämpötilan/kosteuden kanssa", + "feat_budget": "Budjettiseuranta", + "feat_budget_desc": "Seuraa kuukausittaisia ja vuosittaisia huoltokuluja", + "feat_groups": "Tehtäväryhmät", + "feat_groups_desc": "Järjestä tehtävät loogisiin ryhmiin", + "feat_checklists": "Tarkistuslistat", + "feat_checklists_desc": "Monivaiheiset menettelyt tehtävän suorittamiseen", + "settings_general": "Yleiset", + "settings_default_warning": "Oletusvaroituspäivät", + "settings_panel_enabled": "Sivupalkin paneeli", + "settings_panel_title": "Sivupalkin paneelin otsikko", + "settings_notifications": "Ilmoitukset", + "settings_notify_service": "Ilmoituspalvelu", + "test_notification": "Testi-ilmoitus", + "send_test": "Lähetä testi", + "testing": "Lähetetään…", + "test_notification_success": "Testi-ilmoitus lähetetty", + "test_notification_failed": "Testi-ilmoitus epäonnistui", + "settings_notify_due_soon": "Ilmoita, kun erääntyy pian", + "settings_notify_overdue": "Ilmoita, kun myöhässä", + "settings_notify_triggered": "Ilmoita, kun laukaistu", + "settings_interval_hours": "Toistoväli (tuntia, 0 = kerran)", + "settings_quiet_hours": "Hiljaiset tunnit", + "settings_quiet_start": "Alku", + "settings_quiet_end": "Loppu", + "settings_max_per_day": "Ilmoituksia enintään päivässä (0 = rajoittamaton)", + "settings_bundling": "Niputa ilmoitukset", + "settings_bundle_threshold": "Niputuksen kynnys", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobiilitoimintopainikkeet", + "settings_action_complete": "Näytä Valmis-painike", + "settings_action_skip": "Näytä Ohita-painike", + "settings_action_snooze": "Näytä Torkku-painike", + "settings_weekly_digest": "Viikkokooste", + "settings_weekly_digest_hint": "Yksi kooste-ilmoitus maanantaiaamuna, kun tehtäviä on.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Torkun kesto (tuntia)", + "settings_budget": "Budjetti", + "settings_currency": "Valuutta", + "settings_budget_monthly": "Kuukausibudjetti", + "settings_budget_yearly": "Vuosibudjetti", + "settings_budget_alerts": "Budjettihälytykset", + "settings_budget_threshold": "Hälytyskynnys (%)", + "settings_import_export": "Tuonti / Vienti", + "settings_export_json": "Vie JSON", + "settings_export_yaml": "Vie YAML", + "settings_export_csv": "Vie CSV", + "settings_import_csv": "Tuo CSV", + "settings_import_placeholder": "Liitä JSON- tai CSV-sisältö tähän…", + "settings_import_btn": "Tuo", + "settings_import_success": "{count} kohdetta tuotu onnistuneesti.", + "settings_export_success": "Vienti ladattu.", + "settings_saved": "Asetus tallennettu.", + "settings_include_history": "Sisällytä historia", + "sort_alphabetical": "Aakkosjärjestys", + "sort_due_soonest": "Erääntyy pian", + "sort_task_count": "Tehtävien määrä", + "sort_area": "Alue", + "sort_assigned_user": "Määritetty käyttäjä", + "sort_group": "Ryhmä", + "groupby_none": "Ei ryhmittelyä", + "groupby_area": "Alueen mukaan", + "groupby_group": "Ryhmän mukaan", + "groupby_user": "Käyttäjän mukaan", + "filter_label": "Suodata", + "user_label": "Käyttäjä", + "sort_label": "Lajittele", + "group_by_label": "Ryhmittele", + "state_value_help": "Käytä HA:n tila-arvoa (yleensä pienillä kirjaimilla, esim. \"on\"/\"off\"). Kirjainkoko normalisoidaan tallennettaessa.", + "target_changes_help": "Vastaavien siirtymien määrä ennen laukaisua (oletus: 1).", + "qr_print_title": "Tulosta QR-koodit", + "qr_print_desc": "Luo tulostettava sivu QR-koodeja, jotka voit leikata ja kiinnittää laitteisiin.", + "qr_print_load": "Lataa kohteet", + "qr_print_filter": "Suodata", + "qr_print_objects": "Kohteet", + "qr_print_actions": "Toiminnot", + "qr_print_url_mode": "Linkin tyyppi", + "qr_print_estimate": "Arvioitu QR-koodien määrä", + "qr_print_over_limit": "raja on 200, rajaa suodatinta", + "qr_print_generate": "Luo QR-koodit", + "qr_print_generating": "Luodaan…", + "qr_print_ready": "QR-koodit valmiina", + "qr_print_print_button": "Tulosta", + "qr_print_empty": "Ei mitään luotavaa", + "qr_action_skip": "Ohita", + "vacation_title": "Lomatila", + "vacation_active": "aktiivinen", + "vacation_ended": "päättynyt", + "vacation_desc": "Suunnittele loma: ilmoitukset keskeytetään jakson ajaksi plus puskuripäivien ajaksi. Voit ottaa tietyt tehtävät takaisin käyttöön.", + "vacation_enable": "Ota lomatila käyttöön", + "vacation_start": "Alku", + "vacation_end": "Loppu", + "vacation_buffer": "Puskuri (päivää)", + "vacation_exempt_title": "Ilmoita silti loman aikana", + "vacation_exempt_desc": "Valitse tehtävät, joista ilmoitetaan silti loman aikana (esim. kriittinen altaan kemia).", + "vacation_load_tasks": "Lataa tehtävät", + "vacation_preview_btn": "Näytä esikatselu", + "vacation_preview_affected": "tehtävää koskee", + "vacation_event_due_soon": "erääntyy pian", + "vacation_event_overdue": "menee myöhässä", + "vacation_event_triggered_est": "anturin laukaisu mahdollinen", + "vacation_sensor_based": "(anturiperusteinen)", + "vacation_action_notify": "Ilmoita silti", + "vacation_action_unsilence": "Vaimenna uudelleen", + "vacation_marked_complete": "Merkitty valmiiksi", + "vacation_marked_skip": "Ohitettu", + "vacation_end_now": "Lopeta loma nyt", + "add": "Lisää", + "show_stats": "Näytä tilastot ja kaaviot", + "hide_stats": "Piilota tilastot", + "adaptive_no_data": "Ei vielä tarpeeksi suoritushistoriaa mukautuvaan analyysiin. Suorita tämä tehtävä muutaman kerran lisää avataksesi väliehdotukset ja luotettavuuskaaviot.", + "suggestion_applied": "Ehdotettu väli otettu käyttöön", + "vacation_mode": "Lomatila", + "vacation_status_active": "Aktiivinen nyt", + "vacation_status_scheduled": "Ajoitettu", + "vacation_status_inactive": "Ei aktiivinen", + "vacation_end_now_confirm": "Lopetetaanko loma heti?", + "vacation_exempt_count": "poikkeusta", + "vacation_advanced": "Lisäasetukset…", + "vacation_open_panel": "Avaa paneelissa", + "enable": "Ota käyttöön", + "saved": "Tallennettu", + "budget_monthly_set": "Aseta kuukausittain", + "budget_yearly_set": "Aseta vuosittain", + "budget_advanced": "Valuutta, hälytykset…", + "budget_open_panel": "Avaa paneelissa", + "groups_empty": "Ei vielä ryhmiä.", + "group_new_placeholder": "Lisää ryhmä…", + "group_delete_confirm": "Poistetaanko ryhmä \"{name}\"?", + "groups_manage_tasks": "Hallinnoi tehtävien määrityksiä…", + "groups_open_panel": "Avaa paneelissa", + "unassigned": "Ei määritetty", + "no_area": "Ei aluetta", + "has_overdue": "Sisältää myöhässä olevia tehtäviä", + "object": "Kohde", + "settings_panel_access": "Paneelin käyttöoikeus", + "settings_panel_access_desc": "Ylläpitäjillä on aina täydet oikeudet. Delegoidaksesi luonnin, muokkauksen ja poiston tietyille ei-ylläpitäjille, kytke tämä päälle ja valitse heidät alta — muut näkevät vain Valmis ja Ohita.", + "settings_operator_write": "Salli valittujen käyttäjien luoda, muokata ja poistaa", + "settings_operator_write_desc": "Pois: vain ylläpitäjät voivat muuttaa sisältöä. Päällä: myös alla valitut käyttäjät saavat täydet oikeudet.", + "no_non_admin_users": "Ei-ylläpitäjäkäyttäjiä ei löytynyt. Lisää heitä kohdassa Asetukset → Henkilöt.", + "owner_label": "Omistaja", + "feat_completion_actions": "Suoritustoiminnot", + "feat_completion_actions_desc": "Tehtäväkohtainen HA-toiminto suorituksen yhteydessä + pikasuoritus-QR esiasetetuilla arvoilla.", + "on_complete_action_title": "Suorituksen yhteydessä: laukaise HA-toiminto (valinnainen)", + "on_complete_action_desc": "Kutsuu HA-palvelua, kun tehtävä suoritetaan — esim. nollaa laitteen laskuri.", + "on_complete_action_service": "Palvelu", + "on_complete_action_target": "Kohde-entiteetti", + "on_complete_action_target_hint": "Huom: entiteetin toimialueen on vastattava palvelua — esim. 'button.press' toimii vain button.*-entiteeteillä, 'counter.increment' vain counter.*-entiteeteillä, 'input_button.press' vain input_button.*-entiteeteillä jne. Epäsopivuuden sattuessa toiminto epäonnistuu hiljaisesti (HA kirjaa 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Tiedot (JSON, valinnainen)", + "on_complete_action_test": "Tarkista asetukset", + "on_complete_action_test_success": "✓ Asetukset kelvolliset (toiminto laukeaa vain tehtävän suorituksen yhteydessä)", + "on_complete_action_test_failed": "Epäonnistui", + "quick_complete_defaults_title": "Pikasuorituksen oletukset (QR-skannauksille, valinnainen)", + "quick_complete_defaults_desc": "Esiasetetut arvot pikasuoritus-QR-skannauksille. Ilman näitä QR avaa suoritusikkunan.", + "quick_complete_defaults_notes": "Muistiinpanot", + "quick_complete_defaults_cost": "Kustannus", + "quick_complete_defaults_duration": "Kesto (minuuttia)", + "quick_complete_defaults_feedback_none": "Ei palautetta", + "quick_complete_defaults_feedback_needed": "Oli tarpeen", + "quick_complete_defaults_feedback_not_needed": "Ei tarpeen", + "quick_complete_success": "Merkitty nopeasti valmiiksi", + "show_all_objects": "Näytä kaikki kohteet", + "show_all_tasks": "Tyhjennä suodatin — näytä kaikki tehtävät", + "filter_to_overdue": "Suodata tehtävälista vain myöhässä oleviin", + "filter_to_due_soon": "Suodata tehtävälista vain pian erääntyviin", + "filter_to_triggered": "Suodata tehtävälista vain laukaistuihin", + "open_task": "Avaa tehtävä", + "show_details": "Näytä historia ja tilastot", + "hide_details": "Piilota tiedot", + "history_empty": "Ei vielä historiaa.", + "history_edit_button": "Muokkaa merkintää", + "total_cost": "Kokonaiskustannus", + "times_performed": "Suoritettu", + "older_entries": "vanhempaa", + "open_in_panel": "Avaa Huolto-paneelissa", + "skip_reason": "Ohituksen syy (valinnainen)", + "reset_to_date": "Nollaa viimeksi suoritettu -päivä", + "delete_task_confirm": "Poistetaanko tämä tehtävä ja sen historia?", + "delete_object_confirm": "Poistetaanko tämä kohde ja kaikki sen tehtävät?", + "loading": "Ladataan…", + "archive": "Arkistoi", + "undo": "Kumoa", + "task_archived": "Tehtävä arkistoitu", + "object_archived": "Kohde arkistoitu", + "unarchive": "Palauta arkistosta", + "archived": "Arkistoitu", + "show_archived": "Näytä arkistoidut", + "hide_archived": "Piilota arkistoidut", + "confirm_archive_object": "Arkistoidaanko tämä kohde ja sen tehtävät? Historia säilyy ja sen voi palauttaa myöhemmin.", + "settings_archive": "Arkistointi ja säilytys", + "settings_archive_desc": "Siirrä valmiit kertatehtävät arkistoon poistamatta niitä. Arkistoidut kohteet ovat piilossa ja passiivisia, mutta säilyttävät historian ja kustannukset.", + "settings_archive_oneoff_days": "Arkistoi valmiit kertatehtävät automaattisesti (päivää, 0 = pois)", + "settings_delete_archived_oneoff_days": "Poista arkistoidut kertatehtävät automaattisesti (päivää, 0 = ei koskaan)", + "archive_object": "Arkistoi kohde", + "unarchive_object": "Palauta kohde arkistosta", + "documents": "Asiakirjat", + "documents_empty": "Ei vielä asiakirjoja.", + "doc_upload": "Lataa tiedosto", + "doc_uploading": "Ladataan…", + "doc_add_link": "Lisää linkki", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Otsikko (valinnainen)", + "doc_open": "Avaa", + "doc_delete_confirm": "Poistetaanko \"{name}\"?", + "doc_too_large": "Tiedosto on liian suuri (enint. 25 Mt).", + "doc_upload_failed": "Lataus epäonnistui.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Jo tallennettu muualle — jaettu, ei lisätilaa.", + "doc_dup_in_object": "Tämä tiedosto on jo liitetty tähän kohteeseen.", + "doc_link_invalid": "Vain http/https-linkit ovat sallittuja.", + "doc_cat_manual": "Käyttöohje", + "doc_cat_warranty": "Takuu", + "doc_cat_invoice": "Lasku", + "doc_cat_spare_parts": "Varaosat", + "doc_cat_photo": "Valokuva", + "doc_cat_other": "Muu", + "doc_link_badge": "Linkki", + "doc_storage_title": "Asiakirjojen tallennustila", + "doc_storage_saved": "Säästetty deduplikoinnilla", + "doc_storage_refresh": "Päivitä", + "doc_download": "Lataa", + "doc_close": "Sulje", + "doc_camera": "Ota kuva", + "doc_drop_hint": "Pudota tiedostot tähän", + "doc_task_none": "Tähän tehtävään ei ole liitetty asiakirjoja.", + "doc_link_existing": "Liitä asiakirja…", + "doc_attach": "Liitä", + "doc_unlink": "Poista liitos", + "doc_page": "Sivu", + "chart_range_7d": "7pv", + "chart_range_30d": "30pv", + "chart_range_90d": "90pv", + "chart_range_1y": "1v", + "chart_since_service": "edellisen huollon jälkeen", + "chart_no_stats": "Tälle entiteetille ei ole pitkän aikavälin tilastoja — näytetään vain huoltotapahtumien arvot", + "auto_complete_on_recovery": "Suorita automaattisesti, kun anturi palautuu", + "auto_complete_on_recovery_help": "Kirjaa suorituksen (asettaa viimeksi tehdyn), kun laukaisin poistuu itsestään — esim. suola lisätty, suodatin vaihdettu.", + "doc_search": "Hae asiakirjoja…", + "doc_search_none": "Ei vastaavia asiakirjoja", + "link_device_optional": "Liitä olemassa olevaan laitteeseen (valinnainen)", + "parent_object_optional": "Yläobjekti (valinnainen)", + "parent_none": "(Ei yläobjektia)", + "paused": "Keskeytetty", + "pause_object": "Keskeytä", + "resume_object": "Jatka", + "pause_until_prompt": "Jäädytä tämän kohteen aikataulut — mikään ei eräänny eikä ilmoita ennen jatkamista. Aseta halutessasi automaattisen jatkamisen päivämäärä.", + "pause_until_label": "Jatka (valinnainen)", + "object_paused": "Kohde keskeytetty", + "object_resumed": "Kohde jatkettu — aikataulut käynnistetty uudelleen", + "object_paused_badge": "Keskeytetty", + "paused_until_label": "asti", + "replace_object": "Korvaa…", + "replace_object_prompt": "Poista tämä kohde käytöstä ja luo seuraaja. Historia ja kustannukset pysyvät arkistoituina vanhassa; tehtävät ja asiakirjat siirtyvät uuteen, laskurit alkavat alusta.", + "replace_name_label": "Seuraajan nimi", + "object_replaced": "Kohde korvattu — seuraaja luotu", + "reading_unit_label": "Lukeman yksikkö (esim. kWh, m³)", + "reading_unit_help": "Näytetään kirjatun arvon vieressä tehtävää suoritettaessa.", + "reading_value_label": "Lukema", + "reading_label": "Lukema", + "settings_templates_label": "Mallipohjagalleria", + "settings_templates_hint": "Poista valinta mallipohjista, joita et koskaan tarvitse — ne katoavat \"Mallipohjasta\"-valitsimista (paneeli ja config flow). Mikään muu ei muutu; voi ottaa käyttöön uudelleen milloin tahansa.", + "worksheet": "Työlomake", + "worksheet_scan_view": "Skannaa avataksesi tehtävän", + "worksheet_scan_complete": "Skannaa suorittaaksesi", + "worksheet_manual_excerpt": "Ote käyttöohjeesta", + "worksheet_pages": "sivut", + "worksheet_printed": "Tulostettu", + "worksheet_never": "Ei koskaan" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/fr.json b/custom_components/maintenance_supporter/frontend/locales/fr.json new file mode 100644 index 0000000..46cfd17 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/fr.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Maintenance", + "objects": "Objets", + "tasks": "Tâches", + "overdue": "En retard", + "due_soon": "Bientôt dû", + "triggered": "Déclenché", + "ok": "OK", + "all": "Tous", + "new_object": "+ Nouvel objet", + "templates_from": "Depuis un modèle", + "templates_title": "Partir d'un modèle", + "templates_task_count": "{n} tâches", + "template_created": "Créé depuis un modèle", + "onboard_hint": "Ajoutez votre premier objet pour suivre l'entretien.", + "edit": "Modifier", + "duplicate": "Dupliquer", + "task_duplicated": "Tâche dupliquée", + "object_duplicated": "Objet dupliqué", + "delete": "Supprimer", + "add_task": "+ Tâche", + "complete": "Terminé", + "completed": "Terminé", + "skip": "Passer", + "skipped": "Ignoré", + "missed": "Missed", + "reset": "Réinitialiser", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Annuler", + "bulk_select": "Sélectionner", + "bulk_select_all": "Tout sélectionner", + "bulk_n_selected": "{n} sélectionné(s)", + "bulk_completed": "{n} tâches terminées", + "bulk_archived": "{n} tâches archivées", + "completing": "En cours…", + "interval": "Intervalle", + "warning": "Avertissement", + "last_performed": "Dernière exécution", + "next_due": "Prochaine échéance", + "days_until_due": "Jours restants", + "avg_duration": "Ø Durée", + "trigger": "Déclencheur", + "trigger_type": "Type de déclencheur", + "threshold_above": "Limite supérieure", + "threshold_below": "Limite inférieure", + "threshold": "Seuil", + "counter": "Compteur", + "state_change": "Changement d'état", + "runtime": "Durée de fonctionnement", + "runtime_hours": "Durée cible (heures)", + "target_value": "Valeur cible", + "baseline": "Ligne de base", + "target_changes": "Changements cibles", + "for_minutes": "Pendant (minutes)", + "time_based": "Temporel", + "sensor_based": "Capteur", + "manual": "Manuel", + "one_time": "Unique", + "weekdays": "Jours de semaine", + "nth_weekday": "N-ième jour de la semaine du mois", + "day_of_month": "Jour du mois", + "recurrence_on_days": "Répéter le", + "recurrence_occurrence": "Occurrence", + "recurrence_weekday": "Jour de semaine", + "recurrence_day": "Jour du mois (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1er", + "ord_2": "2e", + "ord_3": "3e", + "ord_4": "4e", + "ord_5": "5e", + "ord_last": "Dernier", + "day_word": "Jour", + "interval_value": "Intervalle", + "interval_unit": "Unité", + "unit_days": "Jours", + "unit_weeks": "Semaines", + "unit_months": "Mois", + "unit_years": "Années", + "due_date": "Date d'échéance", + "cleaning": "Nettoyage", + "inspection": "Inspection", + "replacement": "Remplacement", + "calibration": "Étalonnage", + "service": "Service", + "reading": "Relevé", + "custom": "Personnalisé", + "history": "Historique", + "cost": "Coût", + "report_button": "Rapport", + "report_title": "Rapport d'entretien", + "report_generated": "Généré", + "report_times_done": "Effectué", + "report_total_cost": "Coût total", + "report_every": "tous les {n} {unit}", + "report_notes": "Notes", + "report_col_type": "Type", + "report_col_status": "Statut", + "report_col_schedule": "Planning", + "duration": "Durée", + "both": "Les deux", + "trigger_val": "Valeur du déclencheur", + "complete_title": "Terminé : ", + "checklist": "Checklist", + "checklist_steps_optional": "Étapes de la checklist (optionnel)", + "checklist_placeholder": "Nettoyer le filtre\nRemplacer le joint\nTester la pression", + "checklist_help": "Une étape par ligne. Max 100 éléments.", + "err_too_long": "{field} : trop long (max {n} caractères)", + "err_too_short": "{field} : trop court (min {n} caractères)", + "err_value_too_high": "{field} : trop grand (max {n})", + "err_value_too_low": "{field} : trop petit (min {n})", + "err_required": "{field} : champ obligatoire", + "err_wrong_type": "{field} : mauvais type (attendu : {type})", + "err_invalid_choice": "{field} : valeur non autorisée", + "err_invalid_value": "{field} : valeur invalide", + "feat_schedule_time": "Planification à l'heure", + "feat_schedule_time_desc": "Les tâches arrivent à échéance à une heure précise plutôt qu'à minuit.", + "schedule_time_optional": "Échéance à l'heure (optionnel, HH:MM)", + "schedule_time_help": "Vide = minuit (défaut). Fuseau horaire HA.", + "at_time": "à", + "notes_optional": "Notes (optionnel)", + "cost_optional": "Coût (optionnel)", + "duration_minutes": "Durée en minutes (optionnel)", + "days": "jours", + "day": "jour", + "today": "Aujourd'hui", + "d_overdue": "j en retard", + "no_tasks": "Aucune tâche de maintenance. Créez un objet pour commencer.", + "no_tasks_short": "Aucune tâche", + "no_history": "Aucun historique.", + "show_all": "Tout afficher", + "cost_duration_chart": "Coûts & Durée", + "installed": "Installé", + "confirm_delete_object": "Supprimer cet objet et toutes ses tâches ?", + "confirm_delete_task": "Supprimer cette tâche ?", + "min": "Min", + "max": "Max", + "save": "Enregistrer", + "saving": "Enregistrement…", + "edit_task": "Modifier la tâche", + "new_task": "Nouvelle tâche de maintenance", + "task_name": "Nom de la tâche", + "maintenance_type": "Type de maintenance", + "priority": "Priorité", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Basse", + "priority_normal": "Normale", + "priority_high": "Haute", + "schedule_type": "Type de planification", + "interval_days": "Intervalle (jours)", + "warning_days": "Jours d'avertissement", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Dernière exécution (optionnel)", + "interval_anchor": "Ancrage de l'intervalle", + "anchor_completion": "Depuis la date de réalisation", + "anchor_planned": "Depuis la date prévue (sans dérive)", + "edit_object": "Modifier l'objet", + "name": "Nom", + "manufacturer_optional": "Fabricant (optionnel)", + "model_optional": "Modèle (optionnel)", + "serial_number_optional": "Numéro de série (optionnel)", + "serial_number_label": "N/S", + "documentation_url_label": "Manuel", + "object_notes_label": "Notes", + "sort_due_date": "Échéance", + "sort_object": "Nom de l'objet", + "sort_type": "Type", + "sort_task_name": "Nom de la tâche", + "all_objects": "Tous les objets", + "tasks_lower": "tâches", + "no_tasks_yet": "Pas encore de tâches", + "add_first_task": "Ajouter la première tâche", + "trigger_configuration": "Configuration du déclencheur", + "entity_id": "ID d'entité", + "comma_separated": "séparé par des virgules", + "entity_logic": "Logique d'entité", + "entity_logic_any": "N'importe quelle entité déclenche", + "entity_logic_all": "Toutes les entités doivent déclencher", + "entities": "entités", + "attribute_optional": "Attribut (optionnel, vide = état)", + "use_entity_state": "Utiliser l'état de l'entité (pas d'attribut)", + "trigger_above": "Déclencher au-dessus de", + "trigger_below": "Déclencher en dessous de", + "for_at_least_minutes": "Pendant au moins (minutes)", + "safety_interval_days": "Intervalle de sécurité (jours, optionnel)", + "safety_interval": "Intervalle de sécurité (optionnel)", + "delta_mode": "Mode delta", + "from_state_optional": "État source (optionnel)", + "to_state_optional": "État cible (optionnel)", + "documentation_url_optional": "URL de documentation (optionnel)", + "object_notes_optional": "Notes (facultatif)", + "nfc_tag_id_optional": "ID tag NFC (optionnel)", + "nfc_tags_empty_help": "Aucun tag NFC enregistré dans Home Assistant pour le moment.", + "nfc_tags_open_settings": "Ouvrir les réglages des tags", + "nfc_tags_refresh": "Actualiser", + "environmental_entity_optional": "Capteur d'environnement (optionnel)", + "environmental_entity_helper": "ex. sensor.temperature_exterieure — ajuste l'intervalle selon les conditions environnementales", + "environmental_attribute_optional": "Attribut d'environnement (optionnel)", + "nfc_tag_id": "ID tag NFC", + "nfc_linked": "Tag NFC lié", + "nfc_link_hint": "Cliquer pour associer un tag NFC", + "responsible_user": "Utilisateur responsable", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Aucun utilisateur assigné)", + "all_users": "Tous les utilisateurs", + "my_tasks": "Mes tâches", + "tab_calendar": "Calendrier", + "cal_no_events": "Aucun entretien", + "cal_window_7": "7 jours", + "cal_window_14": "14 jours", + "cal_window_30": "30 jours", + "cal_window_365": "1 an", + "cal_every_n_days": "tous les {n} jours", + "cal_source_time": "Basé sur le temps", + "cal_source_time_adaptive": "Basé sur le temps (adaptatif)", + "cal_source_sensor": "Basé sur capteur", + "cal_predicted": "prédit", + "cal_confidence_high": "haute confiance", + "cal_confidence_medium": "confiance moyenne", + "cal_confidence_low": "faible confiance", + "budget_monthly": "Budget mensuel", + "budget_yearly": "Budget annuel", + "groups": "Groupes", + "new_group": "Nouveau groupe", + "edit_group": "Modifier le groupe", + "no_groups": "Aucun groupe pour l'instant", + "delete_group": "Supprimer le groupe", + "delete_group_confirm": "Supprimer le groupe '{name}' ?", + "group_select_tasks": "Sélectionner les tâches", + "group_name_required": "Nom requis", + "description_optional": "Description (optionnel)", + "selected": "Sélectionné", + "loading_chart": "Chargement des données...", + "hide_outliers": "Masquer les valeurs aberrantes (erreurs capteur)", + "was_maintenance_needed": "Cette maintenance était-elle nécessaire ?", + "feedback_needed": "Nécessaire", + "feedback_not_needed": "Pas nécessaire", + "feedback_not_sure": "Pas sûr", + "suggested_interval": "Intervalle suggéré", + "apply_suggestion": "Appliquer", + "reanalyze": "Réanalyser", + "reanalyze_result": "Nouvelle analyse", + "reanalyze_insufficient_data": "Données insuffisantes pour une recommandation", + "data_points": "points de données", + "dismiss_suggestion": "Ignorer", + "confidence_low": "Faible", + "confidence_medium": "Moyen", + "confidence_high": "Élevé", + "recommended": "recommandé", + "seasonal_awareness": "Conscience saisonnière", + "edit_seasonal_overrides": "Modifier les facteurs saisonniers", + "seasonal_overrides_title": "Facteurs saisonniers (override)", + "seasonal_overrides_hint": "Facteur par mois (0.1–5.0). Vide = appris automatiquement.", + "seasonal_override_invalid": "Valeur invalide", + "seasonal_override_range": "Le facteur doit être entre 0.1 et 5.0", + "clear_all": "Tout effacer", + "seasonal_chart_title": "Facteurs saisonniers", + "seasonal_learned": "Appris", + "seasonal_manual": "Manuel", + "month_jan": "Jan", + "month_feb": "Fév", + "month_mar": "Mar", + "month_apr": "Avr", + "month_may": "Mai", + "month_jun": "Juin", + "month_jul": "Juil", + "month_aug": "Août", + "month_sep": "Sep", + "month_oct": "Oct", + "month_nov": "Nov", + "month_dec": "Déc", + "sensor_prediction": "Prédiction capteur", + "degradation_trend": "Tendance", + "trend_rising": "En hausse", + "trend_falling": "En baisse", + "trend_stable": "Stable", + "trend_insufficient_data": "Données insuffisantes", + "days_until_threshold": "Jours avant le seuil", + "threshold_exceeded": "Seuil dépassé", + "environmental_adjustment": "Facteur environnemental", + "sensor_prediction_urgency": "Le capteur prévoit le seuil dans ~{days} jours", + "day_short": "jour", + "weibull_reliability_curve": "Courbe de fiabilité", + "weibull_failure_probability": "Probabilité de défaillance", + "weibull_r_squared": "Ajustement R²", + "beta_early_failures": "Défaillances précoces", + "beta_random_failures": "Défaillances aléatoires", + "beta_wear_out": "Usure", + "beta_highly_predictable": "Très prévisible", + "confidence_interval": "Intervalle de confiance", + "confidence_conservative": "Conservateur", + "confidence_aggressive": "Optimiste", + "current_interval_marker": "Intervalle actuel", + "recommended_marker": "Recommandé", + "characteristic_life": "Durée de vie caractéristique", + "chart_mini_sparkline": "Sparkline de tendance", + "chart_history": "Historique coûts et durée", + "chart_seasonal": "Facteurs saisonniers, 12 mois", + "chart_weibull": "Courbe de fiabilité Weibull", + "chart_sparkline": "Graphique valeur déclencheur", + "days_progress": "Progression en jours", + "qr_code": "QR Code", + "qr_generating": "Génération du QR code…", + "qr_error": "Impossible de générer le QR code.", + "qr_error_no_url": "Aucune URL HA configurée. Veuillez définir une URL externe ou interne dans Paramètres → Système → Réseau.", + "save_error": "Échec de l'enregistrement. Veuillez réessayer.", + "qr_print": "Imprimer", + "qr_download": "Télécharger SVG", + "qr_action": "Action au scan", + "qr_action_view": "Afficher les infos de maintenance", + "qr_action_complete": "Marquer la maintenance comme terminée", + "qr_url_mode": "Type de lien", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Local (mDNS)", + "qr_mode_server": "URL serveur", + "overview": "Aperçu", + "analysis": "Analyse", + "recent_activities": "Activités récentes", + "search_notes": "Rechercher dans les notes", + "avg_cost": "Ø Coût", + "no_advanced_features": "Aucune fonction avancée activée", + "no_advanced_features_hint": "Activez « Intervalles adaptatifs » ou « Tendances saisonnières » dans les paramètres de l'intégration pour voir les données d'analyse ici.", + "analysis_not_enough_data": "Pas encore assez de données pour l'analyse.", + "analysis_not_enough_data_hint": "L'analyse Weibull nécessite au moins 5 maintenances terminées ; les tendances saisonnières apparaissent après 6+ points par mois.", + "analysis_manual_task_hint": "Les tâches manuelles sans intervalle ne génèrent pas de données d'analyse.", + "completions": "réalisations", + "current": "Actuel", + "shorter": "Plus court", + "longer": "Plus long", + "normal": "Normal", + "disabled": "Désactivé", + "compound_logic": "Logique composée", + "compound": "Composé (plusieurs conditions)", + "compound_logic_and": "ET — toutes les conditions doivent se déclencher", + "compound_logic_or": "OU — une seule condition suffit", + "compound_help": "Combinez plusieurs conditions de capteurs en un déclencheur.", + "compound_no_conditions": "Aucune condition — ajoutez-en au moins une.", + "compound_add_condition": "Ajouter une condition", + "compound_condition": "Condition", + "compound_remove_condition": "Supprimer la condition", + "card_title": "Titre", + "card_show_header": "Afficher l'en-tête avec statistiques", + "card_show_actions": "Afficher les boutons d'action", + "card_compact": "Mode compact", + "card_max_items": "Nombre max (0 = tous)", + "card_filter_status": "Filtrer par statut", + "card_filter_status_help": "Vide = afficher tous les statuts.", + "card_filter_objects": "Filtrer par objets", + "card_filter_objects_help": "Vide = afficher tous les objets.", + "card_filter_entities": "Filtrer par entités (entity_ids)", + "card_filter_entities_help": "Choisissez des entités sensor / binary_sensor de cette intégration. Vide = toutes.", + "card_loading_objects": "Chargement des objets…", + "card_load_error": "Impossible de charger les objets — vérifiez la connexion WebSocket.", + "card_no_tasks_title": "Aucune tâche de maintenance pour l'instant", + "card_no_tasks_cta": "→ Créez-en une dans le panneau Maintenance", + "no_objects": "Aucun objet pour l'instant.", + "action_error": "Action échouée. Veuillez réessayer.", + "area_id_optional": "Zone (optionnel)", + "installation_date_optional": "Date d'installation (optionnel)", + "warranty_expiry_optional": "Fin de garantie (optionnel)", + "warranty": "Garantie", + "warranty_valid_until": "valable jusqu'au {date}", + "warranty_expires_in": "expire dans {days} jours", + "warranty_expired": "expirée", + "cal_past_windows": "Fenêtres passées", + "cal_forward_windows": "Fenêtres à venir", + "history_edit_title": "Modifier l'entrée d'historique", + "history_edit_timestamp": "Horodatage", + "manufacturer": "Fabricant", + "model": "Modèle", + "area": "Zone", + "actions": "Actions", + "view_mode_label": "Vue", + "view_cards": "Vue cartes", + "view_table": "Vue tableau", + "objects_table_columns_label": "Colonnes du tableau d'objets", + "objects_table_columns_hint": "Choisissez les colonnes affichées dans la vue tableau des objets.", + "custom_icon_optional": "Icône (optionnel, ex. mdi:wrench)", + "task_enabled": "Tâche activée", + "skip_reason_prompt": "Ignorer cette tâche ?", + "reason_optional": "Raison (optionnel)", + "reset_date_prompt": "Marquer la tâche comme effectuée ?", + "reset_date_optional": "Date de dernière exécution (optionnel, défaut : aujourd'hui)", + "notes_label": "Notes", + "documentation_label": "Documentation", + "no_nfc_tag": "— Aucun tag —", + "dashboard": "Tableau de bord", + "tab_today": "Aujourd'hui", + "palette_placeholder": "Rechercher objets et tâches…", + "palette_no_results": "Aucun résultat", + "palette_hint": "↑↓ naviguer · Entrée ouvrir · Échap fermer", + "today_all_caught_up": "Tout est à jour ! Rien cette semaine.", + "today_overdue": "En retard", + "today_due_today": "À faire aujourd'hui", + "today_this_week": "Cette semaine", + "settings": "Paramètres", + "settings_features": "Fonctions avancées", + "settings_features_desc": "Activez ou désactivez les fonctions avancées. La désactivation les masque dans l'interface mais ne supprime pas les données.", + "feat_adaptive": "Planification adaptative", + "feat_adaptive_desc": "Apprendre les intervalles optimaux à partir de l'historique", + "feat_predictions": "Prédictions capteurs", + "feat_predictions_desc": "Prédire les dates de déclenchement par dégradation des capteurs", + "feat_seasonal": "Ajustements saisonniers", + "feat_seasonal_desc": "Ajuster les intervalles selon les tendances saisonnières", + "feat_environmental": "Corrélation environnementale", + "feat_environmental_desc": "Corréler les intervalles avec la température/humidité", + "feat_budget": "Suivi budgétaire", + "feat_budget_desc": "Suivre les dépenses de maintenance mensuelles et annuelles", + "feat_groups": "Groupes de tâches", + "feat_groups_desc": "Organiser les tâches en groupes logiques", + "feat_checklists": "Checklists", + "feat_checklists_desc": "Procédures multi-étapes pour la réalisation des tâches", + "settings_general": "Général", + "settings_default_warning": "Jours d'avertissement par défaut", + "settings_panel_enabled": "Panneau latéral", + "settings_panel_title": "Titre du panneau latéral", + "settings_notifications": "Notifications", + "settings_notify_service": "Service de notification", + "test_notification": "Notification de test", + "send_test": "Envoyer le test", + "testing": "Envoi en cours…", + "test_notification_success": "Notification de test envoyée", + "test_notification_failed": "Échec de la notification de test", + "settings_notify_due_soon": "Notifier quand bientôt dû", + "settings_notify_overdue": "Notifier quand en retard", + "settings_notify_triggered": "Notifier quand déclenché", + "settings_interval_hours": "Intervalle de répétition (heures, 0 = une fois)", + "settings_quiet_hours": "Heures de silence", + "settings_quiet_start": "Début", + "settings_quiet_end": "Fin", + "settings_max_per_day": "Max notifications par jour (0 = illimité)", + "settings_bundling": "Regrouper les notifications", + "settings_bundle_threshold": "Seuil de regroupement", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Boutons d'action mobiles", + "settings_action_complete": "Afficher le bouton 'Terminé'", + "settings_action_skip": "Afficher le bouton 'Passer'", + "settings_action_snooze": "Afficher le bouton 'Reporter'", + "settings_weekly_digest": "Récapitulatif hebdomadaire", + "settings_weekly_digest_hint": "Une notification récapitulative le lundi matin quand des tâches sont dues.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Durée de report (heures)", + "settings_budget": "Budget", + "settings_currency": "Devise", + "settings_budget_monthly": "Budget mensuel", + "settings_budget_yearly": "Budget annuel", + "settings_budget_alerts": "Alertes budgétaires", + "settings_budget_threshold": "Seuil d'alerte (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Exporter JSON", + "settings_export_yaml": "Exporter YAML", + "settings_export_csv": "Exporter CSV", + "settings_import_csv": "Importer CSV", + "settings_import_placeholder": "Collez le contenu JSON ou CSV ici…", + "settings_import_btn": "Importer", + "settings_import_success": "{count} objets importés avec succès.", + "settings_export_success": "Export téléchargé.", + "settings_saved": "Paramètre enregistré.", + "settings_include_history": "Inclure l'historique", + "sort_alphabetical": "Alphabétique", + "sort_due_soonest": "Échéance la plus proche", + "sort_task_count": "Nombre de tâches", + "sort_area": "Zone", + "sort_assigned_user": "Utilisateur affecté", + "sort_group": "Groupe", + "groupby_none": "Aucun groupement", + "groupby_area": "Par zone", + "groupby_group": "Par groupe", + "groupby_user": "Par utilisateur", + "filter_label": "Filtre", + "user_label": "Utilisateur", + "sort_label": "Tri", + "group_by_label": "Grouper par", + "state_value_help": "Utilisez la valeur d'état HA (généralement en minuscules, p. ex. \"on\"/\"off\"). La casse est normalisée à l'enregistrement.", + "target_changes_help": "Nombre de transitions correspondantes avant le déclenchement (par défaut : 1).", + "qr_print_title": "Imprimer les QR codes", + "qr_print_desc": "Générer une page imprimable de QR codes à découper et coller sur votre équipement.", + "qr_print_load": "Charger les objets", + "qr_print_filter": "Filtre", + "qr_print_objects": "Objets", + "qr_print_actions": "Actions", + "qr_print_url_mode": "Type de lien", + "qr_print_estimate": "QR codes estimés", + "qr_print_over_limit": "limite à 200, affinez le filtre", + "qr_print_generate": "Générer les QR codes", + "qr_print_generating": "Génération…", + "qr_print_ready": "QR codes prêts", + "qr_print_print_button": "Imprimer", + "qr_print_empty": "Rien à générer", + "qr_action_skip": "Passer", + "vacation_title": "Mode vacances", + "vacation_active": "actif", + "vacation_ended": "terminé", + "vacation_desc": "Planifiez vos vacances : les notifications sont mises en pause pendant la période plus quelques jours tampon. Vous pouvez réactiver certaines tâches.", + "vacation_enable": "Activer le mode vacances", + "vacation_start": "Début", + "vacation_end": "Fin", + "vacation_buffer": "Tampon (jours)", + "vacation_exempt_title": "Notifier malgré les vacances", + "vacation_exempt_desc": "Choisissez les tâches qui doivent quand même notifier pendant les vacances (p. ex. chimie de piscine critique).", + "vacation_load_tasks": "Charger les tâches", + "vacation_preview_btn": "Afficher l'aperçu", + "vacation_preview_affected": "tâches concernées", + "vacation_event_due_soon": "bientôt dû", + "vacation_event_overdue": "deviendra en retard", + "vacation_event_triggered_est": "déclencheur capteur possible", + "vacation_sensor_based": "(basé sur capteur)", + "vacation_action_notify": "Notifier quand même", + "vacation_action_unsilence": "Mettre en silence", + "vacation_marked_complete": "Marqué comme terminé", + "vacation_marked_skip": "Passé", + "vacation_end_now": "Terminer les vacances maintenant", + "unassigned": "Non assigné", + "no_area": "Aucune zone", + "has_overdue": "Tâches en retard", + "object": "Objet", + "settings_panel_access": "Accès au panneau", + "settings_panel_access_desc": "Les administrateurs ont toujours un accès complet. Pour déléguer la création, la modification et la suppression à des non-administrateurs spécifiques, activez ceci et sélectionnez-les ci-dessous — les autres ne voient que Terminer et Ignorer.", + "settings_operator_write": "Autoriser les utilisateurs sélectionnés à créer, modifier et supprimer", + "settings_operator_write_desc": "Désactivé : seuls les administrateurs peuvent modifier le contenu. Activé : les utilisateurs ci-dessous ont aussi un accès complet.", + "no_non_admin_users": "Aucun utilisateur non administrateur trouvé. Ajoutez-en dans Paramètres → Personnes.", + "owner_label": "Propriétaire", + "feat_completion_actions": "Actions de finalisation", + "feat_completion_actions_desc": "Action HA par tâche lors de la finalisation + QR de finalisation rapide avec valeurs pré-définies.", + "on_complete_action_title": "À la finalisation : déclencher une action HA (optionnel)", + "on_complete_action_desc": "Appelle un service HA quand la tâche est terminée — p. ex. réinitialiser un compteur sur l'appareil.", + "on_complete_action_service": "Service", + "on_complete_action_target": "Entité cible", + "on_complete_action_data": "Données (JSON, optionnel)", + "on_complete_action_test": "Tester l'action", + "on_complete_action_test_success": "Réussi", + "on_complete_action_test_failed": "Échoué", + "quick_complete_defaults_title": "Valeurs par défaut pour finalisation rapide (scans QR, optionnel)", + "quick_complete_defaults_desc": "Valeurs pré-définies pour les scans QR de finalisation rapide. Sans ces valeurs, le QR ouvre la boîte de dialogue.", + "quick_complete_defaults_notes": "Notes", + "quick_complete_defaults_cost": "Coût", + "quick_complete_defaults_duration": "Durée (minutes)", + "quick_complete_defaults_feedback_none": "Aucun feedback", + "quick_complete_defaults_feedback_needed": "Était nécessaire", + "quick_complete_defaults_feedback_not_needed": "Non nécessaire", + "quick_complete_success": "Terminé rapidement", + "trigger_replaced": "Déclencheur remplacé", + "add": "Ajouter", + "show_stats": "Afficher les statistiques + graphiques", + "hide_stats": "Masquer les statistiques", + "adaptive_no_data": "Pas encore assez d'historique de réalisations pour l'analyse adaptative. Réalisez cette tâche encore quelques fois pour débloquer les recommandations d'intervalle et les courbes de fiabilité.", + "suggestion_applied": "Intervalle suggéré appliqué", + "vacation_mode": "Mode vacances", + "vacation_status_active": "Actif maintenant", + "vacation_status_scheduled": "Planifié", + "vacation_status_inactive": "Inactif", + "vacation_end_now_confirm": "Terminer les vacances immédiatement ?", + "vacation_exempt_count": "exemptées", + "vacation_advanced": "Avancé…", + "vacation_open_panel": "Ouvrir dans le panneau", + "enable": "Activer", + "saved": "Enregistré", + "budget_monthly_set": "Définir mensuel", + "budget_yearly_set": "Définir annuel", + "budget_advanced": "Devise, alertes…", + "budget_open_panel": "Ouvrir dans le panneau", + "groups_empty": "Aucun groupe pour le moment.", + "group_new_placeholder": "Ajouter un groupe…", + "group_delete_confirm": "Supprimer le groupe « {name} » ?", + "groups_manage_tasks": "Gérer les affectations de tâches…", + "groups_open_panel": "Ouvrir dans le panneau", + "on_complete_action_target_hint": "Note : le domaine de l'entité doit correspondre au service — p. ex. 'button.press' ne fonctionne que sur button.*, 'counter.increment' seulement sur counter.*, 'input_button.press' seulement sur input_button.* etc. En cas de non-correspondance, l'action échoue silencieusement (HA journalise 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Afficher tous les objets", + "show_all_tasks": "Effacer le filtre — afficher toutes les tâches", + "filter_to_overdue": "Filtrer la liste sur les tâches en retard uniquement", + "filter_to_due_soon": "Filtrer la liste sur les tâches bientôt dues uniquement", + "filter_to_triggered": "Filtrer la liste sur les tâches déclenchées uniquement", + "open_task": "Ouvrir la tâche", + "show_details": "Afficher l'historique + statistiques", + "hide_details": "Masquer les détails", + "history_empty": "Aucun historique pour le moment.", + "history_edit_button": "Modifier l'entrée", + "total_cost": "Coût total", + "times_performed": "Réalisée", + "older_entries": "plus anciennes", + "open_in_panel": "Ouvrir dans le panneau Maintenance", + "skip_reason": "Raison du saut (facultatif)", + "reset_to_date": "Réinitialiser la dernière réalisation au", + "delete_task_confirm": "Supprimer cette tâche et son historique ?", + "delete_object_confirm": "Supprimer cet objet et toutes ses tâches ?", + "loading": "Chargement…", + "archive": "Archiver", + "undo": "Annuler", + "task_archived": "Tâche archivée", + "object_archived": "Objet archivé", + "unarchive": "Désarchiver", + "archived": "Archivé", + "show_archived": "Afficher les archivés", + "hide_archived": "Masquer les archivés", + "confirm_archive_object": "Archiver cet objet et ses tâches ? L'historique est conservé et peut être restauré plus tard.", + "settings_archive": "Archivage et conservation", + "settings_archive_desc": "Mettez de côté les tâches ponctuelles terminées sans les supprimer. Les éléments archivés sont masqués et inactifs, mais conservent leur historique et leurs coûts.", + "settings_archive_oneoff_days": "Archiver automatiquement les tâches ponctuelles terminées après (jours, 0 = désactivé)", + "settings_delete_archived_oneoff_days": "Supprimer automatiquement les tâches ponctuelles archivées après (jours, 0 = jamais)", + "archive_object": "Archiver l'objet", + "unarchive_object": "Désarchiver l'objet", + "documents": "Documents", + "documents_empty": "Aucun document pour l'instant.", + "doc_upload": "Téléverser un fichier", + "doc_uploading": "Téléversement…", + "doc_add_link": "Ajouter un lien", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titre (facultatif)", + "doc_open": "Ouvrir", + "doc_delete_confirm": "Supprimer « {name} » ?", + "doc_too_large": "Fichier trop volumineux (max. 25 Mo).", + "doc_upload_failed": "Échec du téléversement.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Déjà stocké ailleurs — partagé, aucun espace supplémentaire.", + "doc_dup_in_object": "Ce fichier est déjà attaché à cet objet.", + "doc_link_invalid": "Seuls les liens http/https sont autorisés.", + "doc_cat_manual": "Manuel", + "doc_cat_warranty": "Garantie", + "doc_cat_invoice": "Facture", + "doc_cat_spare_parts": "Pièces détachées", + "doc_cat_photo": "Photo", + "doc_cat_other": "Autre", + "doc_link_badge": "Lien", + "doc_storage_title": "Stockage des documents", + "doc_storage_saved": "Économisé par déduplication", + "doc_storage_refresh": "Actualiser", + "doc_download": "Télécharger", + "doc_close": "Fermer", + "doc_camera": "Prendre une photo", + "doc_drop_hint": "Déposez les fichiers ici", + "doc_task_none": "Aucun document lié à cette tâche.", + "doc_link_existing": "Lier un document…", + "doc_attach": "Lier", + "doc_unlink": "Dissocier", + "doc_page": "Page", + "chart_range_7d": "7j", + "chart_range_30d": "30j", + "chart_range_90d": "90j", + "chart_range_1y": "1a", + "chart_since_service": "depuis le dernier entretien", + "chart_no_stats": "Pas de statistiques à long terme pour cette entité — seules les valeurs des entretiens sont affichées", + "auto_complete_on_recovery": "Terminer automatiquement quand le capteur se rétablit", + "auto_complete_on_recovery_help": "Enregistre une exécution (met à jour la dernière réalisation) lorsque le déclencheur se résout de lui-même — p. ex. sel rechargé, filtre remplacé.", + "doc_search": "Rechercher des documents…", + "doc_search_none": "Aucun document correspondant", + "link_device_optional": "Lier à un appareil existant (optionnel)", + "parent_object_optional": "Objet parent (optionnel)", + "parent_none": "(Aucun parent)", + "paused": "En pause", + "pause_object": "Mettre en pause", + "resume_object": "Reprendre", + "pause_until_prompt": "Gèle les calendriers de cet objet — rien n'arrive à échéance et rien ne notifie jusqu'à la reprise. Optionnellement, fixez une date de reprise automatique.", + "pause_until_label": "Reprendre le (optionnel)", + "object_paused": "Objet mis en pause", + "object_resumed": "Objet repris — calendriers relancés", + "object_paused_badge": "En pause", + "paused_until_label": "jusqu'au", + "replace_object": "Remplacer…", + "replace_object_prompt": "Retire cet objet et crée un successeur. L'historique et les coûts restent archivés sur l'ancien ; les tâches et documents passent au nouveau, les compteurs repartent de zéro.", + "replace_name_label": "Nom du successeur", + "object_replaced": "Objet remplacé — successeur créé", + "reading_unit_label": "Unité de relevé (p. ex. kWh, m³)", + "reading_unit_help": "Affichée à côté de la valeur enregistrée lors de l'exécution de cette tâche.", + "reading_value_label": "Valeur relevée", + "reading_label": "Relevé", + "settings_templates_label": "Galerie de modèles", + "settings_templates_hint": "Décochez les modèles dont vous n'aurez jamais besoin — ils disparaissent des sélecteurs « Depuis un modèle » (panneau et config flow). Rien d'autre ne change ; réactivables à tout moment.", + "worksheet": "Fiche de travail", + "worksheet_scan_view": "Scanner pour ouvrir la tâche", + "worksheet_scan_complete": "Scanner pour terminer", + "worksheet_manual_excerpt": "Extrait du manuel", + "worksheet_pages": "pages", + "worksheet_printed": "Imprimé", + "worksheet_never": "Jamais" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/hi.json b/custom_components/maintenance_supporter/frontend/locales/hi.json new file mode 100644 index 0000000..0b0ca9d --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/hi.json @@ -0,0 +1,696 @@ +{ + "maintenance": "रखरखाव", + "objects": "वस्तुएँ", + "tasks": "कार्य", + "overdue": "अतिदेय", + "due_soon": "जल्द देय", + "triggered": "ट्रिगर हुआ", + "trigger_replaced": "ट्रिगर बदला गया", + "ok": "ठीक", + "all": "सभी", + "new_object": "+ नई वस्तु", + "templates_from": "टेम्पलेट से", + "templates_title": "टेम्पलेट से शुरू करें", + "templates_task_count": "{n} कार्य", + "template_created": "टेम्पलेट से बनाया गया", + "onboard_hint": "रखरखाव ट्रैक करने के लिए अपना पहला ऑब्जेक्ट जोड़ें।", + "edit": "संपादित करें", + "duplicate": "डुप्लिकेट करें", + "task_duplicated": "कार्य डुप्लिकेट किया गया", + "object_duplicated": "ऑब्जेक्ट डुप्लिकेट किया गया", + "delete": "हटाएँ", + "add_task": "+ कार्य जोड़ें", + "complete": "पूर्ण करें", + "completed": "पूर्ण हुआ", + "skip": "छोड़ें", + "skipped": "छोड़ा गया", + "missed": "Missed", + "reset": "रीसेट करें", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "रद्द करें", + "bulk_select": "चुनें", + "bulk_select_all": "सभी चुनें", + "bulk_n_selected": "{n} चयनित", + "bulk_completed": "{n} कार्य पूर्ण", + "bulk_archived": "{n} कार्य संग्रहित", + "completing": "पूर्ण किया जा रहा है…", + "interval": "अंतराल", + "warning": "चेतावनी", + "last_performed": "अंतिम बार किया गया", + "next_due": "अगली देय", + "days_until_due": "देय तक दिन", + "avg_duration": "औसत अवधि", + "trigger": "ट्रिगर", + "trigger_type": "ट्रिगर प्रकार", + "threshold_above": "ऊपरी सीमा", + "threshold_below": "निचली सीमा", + "threshold": "सीमा", + "counter": "काउंटर", + "state_change": "स्थिति परिवर्तन", + "runtime": "रनटाइम", + "runtime_hours": "लक्ष्य रनटाइम (घंटे)", + "target_value": "लक्ष्य मान", + "baseline": "आधार रेखा", + "target_changes": "लक्ष्य परिवर्तन", + "for_minutes": "के लिए (मिनट)", + "time_based": "समय-आधारित", + "sensor_based": "सेंसर-आधारित", + "manual": "मैनुअल", + "one_time": "एक बार", + "weekdays": "सप्ताह के दिन", + "nth_weekday": "महीने का N-वाँ सप्ताह-दिन", + "day_of_month": "महीने का दिन", + "recurrence_on_days": "दोहराएँ", + "recurrence_occurrence": "घटना", + "recurrence_weekday": "सप्ताह-दिन", + "recurrence_day": "महीने का दिन (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "पहला", + "ord_2": "दूसरा", + "ord_3": "तीसरा", + "ord_4": "चौथा", + "ord_5": "पाँचवाँ", + "ord_last": "अंतिम", + "day_word": "दिन", + "interval_value": "अंतराल", + "interval_unit": "इकाई", + "unit_days": "दिन", + "unit_weeks": "सप्ताह", + "unit_months": "महीने", + "unit_years": "वर्ष", + "due_date": "देय तिथि", + "cleaning": "सफ़ाई", + "inspection": "निरीक्षण", + "replacement": "प्रतिस्थापन", + "calibration": "अंशांकन", + "service": "सर्विस", + "reading": "रीडिंग", + "custom": "कस्टम", + "history": "इतिहास", + "cost": "लागत", + "report_button": "रिपोर्ट", + "report_title": "रखरखाव रिपोर्ट", + "report_generated": "जनरेट किया गया", + "report_times_done": "पूर्ण", + "report_total_cost": "कुल लागत", + "report_every": "हर {n} {unit}", + "report_notes": "नोट्स", + "report_col_type": "प्रकार", + "report_col_status": "स्थिति", + "report_col_schedule": "शेड्यूल", + "duration": "अवधि", + "both": "दोनों", + "trigger_val": "ट्रिगर मान", + "complete_title": "पूर्ण करें: ", + "checklist": "चेकलिस्ट", + "checklist_steps_optional": "चेकलिस्ट चरण (वैकल्पिक)", + "checklist_placeholder": "फ़िल्टर साफ़ करें\nसील बदलें\nदबाव जाँचें", + "checklist_help": "प्रति पंक्ति एक चरण। अधिकतम 100 आइटम।", + "err_too_long": "{field}: बहुत लंबा (अधिकतम {n} अक्षर)", + "err_too_short": "{field}: बहुत छोटा (न्यूनतम {n} अक्षर)", + "err_value_too_high": "{field}: बहुत बड़ा (अधिकतम {n})", + "err_value_too_low": "{field}: बहुत छोटा (न्यूनतम {n})", + "err_required": "{field}: आवश्यक", + "err_wrong_type": "{field}: गलत प्रकार (अपेक्षित: {type})", + "err_invalid_choice": "{field}: अनुमत मान नहीं है", + "err_invalid_value": "{field}: अमान्य मान", + "feat_schedule_time": "दिन का समय निर्धारण", + "feat_schedule_time_desc": "कार्य मध्यरात्रि के बजाय दिन के एक विशिष्ट समय पर अतिदेय हो जाते हैं।", + "schedule_time_optional": "इस समय देय (वैकल्पिक, HH:MM)", + "schedule_time_help": "खाली = मध्यरात्रि (डिफ़ॉल्ट)। HA समय क्षेत्र।", + "at_time": "बजे", + "notes_optional": "टिप्पणियाँ (वैकल्पिक)", + "cost_optional": "लागत (वैकल्पिक)", + "duration_minutes": "मिनटों में अवधि (वैकल्पिक)", + "days": "दिन", + "day": "दिन", + "today": "आज", + "d_overdue": "दिन अतिदेय", + "no_tasks": "अभी कोई रखरखाव कार्य नहीं। शुरू करने के लिए एक वस्तु बनाएँ।", + "no_tasks_short": "कोई कार्य नहीं", + "no_history": "अभी कोई इतिहास प्रविष्टि नहीं।", + "show_all": "सभी दिखाएँ", + "cost_duration_chart": "लागत और अवधि", + "installed": "स्थापित", + "confirm_delete_object": "इस वस्तु और इसके सभी कार्यों को हटाएँ?", + "confirm_delete_task": "इस कार्य को हटाएँ?", + "min": "न्यूनतम", + "max": "अधिकतम", + "save": "सहेजें", + "saving": "सहेजा जा रहा है…", + "edit_task": "कार्य संपादित करें", + "new_task": "नया रखरखाव कार्य", + "task_name": "कार्य का नाम", + "maintenance_type": "रखरखाव प्रकार", + "priority": "प्राथमिकता", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "कम", + "priority_normal": "सामान्य", + "priority_high": "उच्च", + "schedule_type": "अनुसूची प्रकार", + "interval_days": "अंतराल (दिन)", + "warning_days": "चेतावनी दिन", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "अंतिम बार किया गया (वैकल्पिक)", + "interval_anchor": "अंतराल आधार", + "anchor_completion": "पूर्ण तिथि से", + "anchor_planned": "नियोजित तिथि से (कोई विचलन नहीं)", + "edit_object": "वस्तु संपादित करें", + "name": "नाम", + "manufacturer_optional": "निर्माता (वैकल्पिक)", + "model_optional": "मॉडल (वैकल्पिक)", + "serial_number_optional": "सीरियल नंबर (वैकल्पिक)", + "serial_number_label": "S/N", + "documentation_url_label": "मैनुअल", + "object_notes_label": "टिप्पणियाँ", + "sort_due_date": "देय तिथि", + "sort_object": "वस्तु का नाम", + "sort_type": "प्रकार", + "sort_task_name": "कार्य का नाम", + "all_objects": "सभी वस्तुएँ", + "tasks_lower": "कार्य", + "no_tasks_yet": "अभी कोई कार्य नहीं", + "add_first_task": "पहला कार्य जोड़ें", + "trigger_configuration": "ट्रिगर कॉन्फ़िगरेशन", + "entity_id": "एंटिटी ID", + "comma_separated": "अल्पविराम से अलग", + "entity_logic": "एंटिटी लॉजिक", + "entity_logic_any": "कोई भी एंटिटी ट्रिगर करती है", + "entity_logic_all": "सभी एंटिटी ट्रिगर होनी चाहिए", + "entities": "एंटिटी", + "attribute_optional": "विशेषता (वैकल्पिक, खाली = स्थिति)", + "use_entity_state": "एंटिटी स्थिति का उपयोग करें (कोई विशेषता नहीं)", + "trigger_above": "इससे ऊपर ट्रिगर करें", + "trigger_below": "इससे नीचे ट्रिगर करें", + "for_at_least_minutes": "कम से कम (मिनट)", + "safety_interval_days": "सुरक्षा अंतराल (दिन, वैकल्पिक)", + "safety_interval": "सुरक्षा अंतराल (वैकल्पिक)", + "delta_mode": "डेल्टा मोड", + "from_state_optional": "किस स्थिति से (वैकल्पिक)", + "to_state_optional": "किस स्थिति तक (वैकल्पिक)", + "documentation_url_optional": "दस्तावेज़ URL (वैकल्पिक)", + "object_notes_optional": "टिप्पणियाँ (वैकल्पिक)", + "nfc_tag_id_optional": "NFC टैग ID (वैकल्पिक)", + "nfc_tags_empty_help": "Home Assistant में अभी कोई NFC टैग पंजीकृत नहीं है।", + "nfc_tags_open_settings": "टैग सेटिंग्स खोलें", + "nfc_tags_refresh": "रिफ़्रेश करें", + "environmental_entity_optional": "पर्यावरण सेंसर (वैकल्पिक)", + "environmental_entity_helper": "उदा. sensor.outdoor_temperature — पर्यावरणीय स्थितियों के आधार पर अंतराल समायोजित करता है", + "environmental_attribute_optional": "पर्यावरण विशेषता (वैकल्पिक)", + "nfc_tag_id": "NFC टैग ID", + "nfc_linked": "NFC टैग लिंक किया गया", + "nfc_link_hint": "NFC टैग लिंक करने के लिए क्लिक करें", + "responsible_user": "जिम्मेदार उपयोगकर्ता", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(कोई उपयोगकर्ता असाइन नहीं)", + "all_users": "सभी उपयोगकर्ता", + "my_tasks": "मेरे कार्य", + "tab_calendar": "कैलेंडर", + "cal_no_events": "कोई रखरखाव नहीं", + "cal_window_7": "7 दिन", + "cal_window_14": "14 दिन", + "cal_window_30": "30 दिन", + "cal_window_365": "1 वर्ष", + "cal_every_n_days": "हर {n} दिन", + "cal_source_time": "समय-आधारित", + "cal_source_time_adaptive": "समय-आधारित (अनुकूली)", + "cal_source_sensor": "सेंसर-आधारित", + "cal_predicted": "अनुमानित", + "cal_confidence_high": "उच्च विश्वास", + "cal_confidence_medium": "मध्यम विश्वास", + "cal_confidence_low": "निम्न विश्वास", + "budget_monthly": "मासिक बजट", + "budget_yearly": "वार्षिक बजट", + "groups": "समूह", + "new_group": "नया समूह", + "edit_group": "समूह संपादित करें", + "no_groups": "अभी कोई समूह नहीं", + "delete_group": "समूह हटाएँ", + "delete_group_confirm": "समूह '{name}' हटाएँ?", + "group_select_tasks": "कार्य चुनें", + "group_name_required": "नाम आवश्यक है", + "description_optional": "विवरण (वैकल्पिक)", + "selected": "चयनित", + "loading_chart": "चार्ट डेटा लोड हो रहा है...", + "hide_outliers": "आउटलायर छिपाएँ (सेंसर गड़बड़)", + "was_maintenance_needed": "क्या यह रखरखाव आवश्यक था?", + "feedback_needed": "आवश्यक था", + "feedback_not_needed": "आवश्यक नहीं", + "feedback_not_sure": "निश्चित नहीं", + "suggested_interval": "सुझाया गया अंतराल", + "apply_suggestion": "लागू करें", + "reanalyze": "पुनः विश्लेषण करें", + "reanalyze_result": "नया विश्लेषण", + "reanalyze_insufficient_data": "अनुशंसा देने के लिए पर्याप्त डेटा नहीं", + "data_points": "डेटा बिंदु", + "dismiss_suggestion": "खारिज करें", + "confidence_low": "निम्न", + "confidence_medium": "मध्यम", + "confidence_high": "उच्च", + "recommended": "अनुशंसित", + "seasonal_awareness": "मौसमी जागरूकता", + "edit_seasonal_overrides": "मौसमी कारक संपादित करें", + "seasonal_overrides_title": "मौसमी कारक (ओवरराइड)", + "seasonal_overrides_hint": "प्रति माह कारक (0.1–5.0)। खाली = स्वचालित रूप से सीखा गया।", + "seasonal_override_invalid": "अमान्य मान", + "seasonal_override_range": "कारक 0.1 और 5.0 के बीच होना चाहिए", + "clear_all": "सभी साफ़ करें", + "seasonal_chart_title": "मौसमी कारक", + "seasonal_learned": "सीखा गया", + "seasonal_manual": "मैनुअल", + "month_jan": "जन", + "month_feb": "फ़र", + "month_mar": "मार्च", + "month_apr": "अप्रैल", + "month_may": "मई", + "month_jun": "जून", + "month_jul": "जुल", + "month_aug": "अग", + "month_sep": "सित", + "month_oct": "अक्टू", + "month_nov": "नव", + "month_dec": "दिस", + "sensor_prediction": "सेंसर पूर्वानुमान", + "degradation_trend": "रुझान", + "trend_rising": "बढ़ता हुआ", + "trend_falling": "घटता हुआ", + "trend_stable": "स्थिर", + "trend_insufficient_data": "अपर्याप्त डेटा", + "days_until_threshold": "सीमा तक दिन", + "threshold_exceeded": "सीमा पार हुई", + "environmental_adjustment": "पर्यावरण कारक", + "sensor_prediction_urgency": "सेंसर ~{days} दिनों में सीमा का अनुमान लगाता है", + "day_short": "दिन", + "weibull_reliability_curve": "विश्वसनीयता वक्र", + "weibull_failure_probability": "विफलता संभावना", + "weibull_r_squared": "फ़िट R²", + "beta_early_failures": "प्रारंभिक विफलताएँ", + "beta_random_failures": "यादृच्छिक विफलताएँ", + "beta_wear_out": "घिसाव", + "beta_highly_predictable": "अत्यधिक पूर्वानुमेय", + "confidence_interval": "विश्वास अंतराल", + "confidence_conservative": "रूढ़िवादी", + "confidence_aggressive": "आशावादी", + "current_interval_marker": "वर्तमान अंतराल", + "recommended_marker": "अनुशंसित", + "characteristic_life": "विशेषता आयु", + "chart_mini_sparkline": "रुझान स्पार्कलाइन", + "chart_history": "लागत और अवधि इतिहास", + "chart_seasonal": "मौसमी कारक, 12 महीने", + "chart_weibull": "वीबुल विश्वसनीयता वक्र", + "chart_sparkline": "सेंसर ट्रिगर मान चार्ट", + "days_progress": "दिनों की प्रगति", + "qr_code": "QR कोड", + "qr_generating": "QR कोड बनाया जा रहा है…", + "qr_error": "QR कोड बनाने में विफल।", + "qr_error_no_url": "कोई HA URL कॉन्फ़िगर नहीं है। कृपया सेटिंग्स → सिस्टम → नेटवर्क में एक बाहरी या आंतरिक URL सेट करें।", + "save_error": "सहेजने में विफल। कृपया पुनः प्रयास करें।", + "qr_print": "प्रिंट करें", + "qr_download": "SVG डाउनलोड करें", + "qr_action": "स्कैन पर क्रिया", + "qr_action_view": "रखरखाव जानकारी देखें", + "qr_action_complete": "रखरखाव को पूर्ण के रूप में चिह्नित करें", + "qr_url_mode": "लिंक प्रकार", + "qr_mode_companion": "Companion ऐप", + "qr_mode_local": "लोकल (mDNS)", + "qr_mode_server": "सर्वर URL", + "overview": "अवलोकन", + "analysis": "विश्लेषण", + "recent_activities": "हाल की गतिविधियाँ", + "search_notes": "टिप्पणियाँ खोजें", + "avg_cost": "औसत लागत", + "no_advanced_features": "कोई उन्नत सुविधा सक्षम नहीं", + "no_advanced_features_hint": "यहाँ विश्लेषण डेटा देखने के लिए इंटीग्रेशन सेटिंग्स में “अनुकूली अंतराल” या “मौसमी पैटर्न” सक्षम करें।", + "analysis_not_enough_data": "अभी विश्लेषण के लिए पर्याप्त डेटा नहीं।", + "analysis_not_enough_data_hint": "वीबुल विश्लेषण के लिए कम से कम 5 पूर्ण रखरखाव आवश्यक हैं; मौसमी पैटर्न प्रति माह 6+ डेटा बिंदुओं के बाद दिखाई देते हैं।", + "analysis_manual_task_hint": "अंतराल के बिना मैनुअल कार्य विश्लेषण डेटा उत्पन्न नहीं करते।", + "completions": "पूर्णताएँ", + "current": "वर्तमान", + "shorter": "छोटा", + "longer": "लंबा", + "normal": "सामान्य", + "disabled": "अक्षम", + "compound_logic": "संयुक्त लॉजिक", + "compound": "संयुक्त (कई शर्तें)", + "compound_logic_and": "और — सभी शर्तें पूरी हों", + "compound_logic_or": "या — कोई एक शर्त पर्याप्त", + "compound_help": "कई सेंसर शर्तों को एक ट्रिगर में मिलाएँ।", + "compound_no_conditions": "अभी कोई शर्त नहीं — कम से कम एक जोड़ें।", + "compound_add_condition": "शर्त जोड़ें", + "compound_condition": "शर्त", + "compound_remove_condition": "शर्त हटाएँ", + "card_title": "शीर्षक", + "card_show_header": "सांख्यिकी के साथ हेडर दिखाएँ", + "card_show_actions": "क्रिया बटन दिखाएँ", + "card_compact": "संक्षिप्त मोड", + "card_max_items": "अधिकतम आइटम (0 = सभी)", + "card_filter_status": "स्थिति के अनुसार फ़िल्टर करें", + "card_filter_status_help": "खाली = सभी स्थितियाँ दिखाएँ।", + "card_filter_objects": "वस्तुओं के अनुसार फ़िल्टर करें", + "card_filter_objects_help": "खाली = सभी वस्तुएँ दिखाएँ।", + "card_filter_entities": "एंटिटी के अनुसार फ़िल्टर करें (entity_ids)", + "card_filter_entities_help": "इस इंटीग्रेशन से sensor / binary_sensor एंटिटी चुनें। खाली = सभी।", + "card_loading_objects": "वस्तुएँ लोड हो रही हैं…", + "card_load_error": "वस्तुएँ लोड नहीं हो सकीं — WebSocket कनेक्शन जाँचें।", + "card_no_tasks_title": "अभी कोई रखरखाव कार्य नहीं", + "card_no_tasks_cta": "→ रखरखाव पैनल में एक बनाएँ", + "no_objects": "अभी कोई वस्तु नहीं।", + "action_error": "क्रिया विफल। कृपया पुनः प्रयास करें।", + "area_id_optional": "क्षेत्र (वैकल्पिक)", + "installation_date_optional": "स्थापना तिथि (वैकल्पिक)", + "warranty_expiry_optional": "वारंटी समाप्ति (वैकल्पिक)", + "warranty": "वारंटी", + "warranty_valid_until": "{date} तक वैध", + "warranty_expires_in": "{days} दिनों में समाप्त", + "warranty_expired": "समाप्त", + "cal_past_windows": "पिछली अवधियाँ", + "cal_forward_windows": "आगामी अवधियाँ", + "history_edit_title": "इतिहास प्रविष्टि संपादित करें", + "history_edit_timestamp": "टाइमस्टैम्प", + "manufacturer": "निर्माता", + "model": "मॉडल", + "area": "क्षेत्र", + "actions": "क्रियाएँ", + "view_mode_label": "दृश्य", + "view_cards": "कार्ड दृश्य", + "view_table": "तालिका दृश्य", + "objects_table_columns_label": "वस्तु तालिका कॉलम", + "objects_table_columns_hint": "चुनें कि वस्तु तालिका दृश्य में कौन से कॉलम दिखाई दें।", + "custom_icon_optional": "आइकन (वैकल्पिक, उदा. mdi:wrench)", + "task_enabled": "कार्य सक्षम", + "skip_reason_prompt": "इस कार्य को छोड़ें?", + "reason_optional": "कारण (वैकल्पिक)", + "reset_date_prompt": "कार्य को किया गया के रूप में चिह्नित करें?", + "reset_date_optional": "अंतिम बार किया गया तिथि (वैकल्पिक, डिफ़ॉल्ट आज)", + "notes_label": "टिप्पणियाँ", + "documentation_label": "दस्तावेज़", + "no_nfc_tag": "— कोई टैग नहीं —", + "dashboard": "डैशबोर्ड", + "tab_today": "आज", + "palette_placeholder": "ऑब्जेक्ट और कार्य खोजें…", + "palette_no_results": "कोई मिलान नहीं", + "palette_hint": "↑↓ नेविगेट · Enter खोलें · Esc बंद", + "today_all_caught_up": "सब पूरा! इस सप्ताह कुछ भी बाकी नहीं।", + "today_overdue": "अतिदेय", + "today_due_today": "आज देय", + "today_this_week": "इस सप्ताह", + "settings": "सेटिंग्स", + "settings_features": "उन्नत सुविधाएँ", + "settings_features_desc": "उन्नत सुविधाओं को सक्षम या अक्षम करें। अक्षम करने से वे UI से छिप जाती हैं पर डेटा नहीं हटता।", + "feat_adaptive": "अनुकूली निर्धारण", + "feat_adaptive_desc": "रखरखाव इतिहास से इष्टतम अंतराल सीखें", + "feat_predictions": "सेंसर पूर्वानुमान", + "feat_predictions_desc": "सेंसर क्षरण से ट्रिगर तिथियों का अनुमान लगाएँ", + "feat_seasonal": "मौसमी समायोजन", + "feat_seasonal_desc": "मौसमी पैटर्न के आधार पर अंतराल समायोजित करें", + "feat_environmental": "पर्यावरणीय सहसंबंध", + "feat_environmental_desc": "तापमान/आर्द्रता के साथ अंतराल का सहसंबंध करें", + "feat_budget": "बजट ट्रैकिंग", + "feat_budget_desc": "मासिक और वार्षिक रखरखाव खर्च ट्रैक करें", + "feat_groups": "कार्य समूह", + "feat_groups_desc": "कार्यों को तार्किक समूहों में व्यवस्थित करें", + "feat_checklists": "चेकलिस्ट", + "feat_checklists_desc": "कार्य पूर्णता के लिए बहु-चरणीय प्रक्रियाएँ", + "settings_general": "सामान्य", + "settings_default_warning": "डिफ़ॉल्ट चेतावनी दिन", + "settings_panel_enabled": "साइडबार पैनल", + "settings_panel_title": "साइडबार पैनल शीर्षक", + "settings_notifications": "सूचनाएँ", + "settings_notify_service": "सूचना सेवा", + "test_notification": "परीक्षण सूचना", + "send_test": "परीक्षण भेजें", + "testing": "भेजा जा रहा है…", + "test_notification_success": "परीक्षण सूचना भेजी गई", + "test_notification_failed": "परीक्षण सूचना विफल", + "settings_notify_due_soon": "जल्द देय होने पर सूचित करें", + "settings_notify_overdue": "अतिदेय होने पर सूचित करें", + "settings_notify_triggered": "ट्रिगर होने पर सूचित करें", + "settings_interval_hours": "दोहराव अंतराल (घंटे, 0 = एक बार)", + "settings_quiet_hours": "शांत घंटे", + "settings_quiet_start": "प्रारंभ", + "settings_quiet_end": "समाप्त", + "settings_max_per_day": "प्रति दिन अधिकतम सूचनाएँ (0 = असीमित)", + "settings_bundling": "सूचनाएँ बंडल करें", + "settings_bundle_threshold": "बंडल सीमा", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "मोबाइल क्रिया बटन", + "settings_action_complete": "'पूर्ण करें' बटन दिखाएँ", + "settings_action_skip": "'छोड़ें' बटन दिखाएँ", + "settings_action_snooze": "'स्नूज़' बटन दिखाएँ", + "settings_weekly_digest": "साप्ताहिक सारांश", + "settings_weekly_digest_hint": "सोमवार सुबह एक सारांश सूचना जब कार्य देय हों।", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "स्नूज़ अवधि (घंटे)", + "settings_budget": "बजट", + "settings_currency": "मुद्रा", + "settings_budget_monthly": "मासिक बजट", + "settings_budget_yearly": "वार्षिक बजट", + "settings_budget_alerts": "बजट अलर्ट", + "settings_budget_threshold": "अलर्ट सीमा (%)", + "settings_import_export": "आयात / निर्यात", + "settings_export_json": "JSON निर्यात करें", + "settings_export_yaml": "YAML निर्यात करें", + "settings_export_csv": "CSV निर्यात करें", + "settings_import_csv": "CSV आयात करें", + "settings_import_placeholder": "JSON या CSV सामग्री यहाँ चिपकाएँ…", + "settings_import_btn": "आयात करें", + "settings_import_success": "{count} वस्तुएँ सफलतापूर्वक आयात की गईं।", + "settings_export_success": "निर्यात डाउनलोड किया गया।", + "settings_saved": "सेटिंग सहेजी गई।", + "settings_include_history": "इतिहास शामिल करें", + "sort_alphabetical": "वर्णानुक्रम", + "sort_due_soonest": "सबसे जल्द देय", + "sort_task_count": "कार्य संख्या", + "sort_area": "क्षेत्र", + "sort_assigned_user": "असाइन किया गया उपयोगकर्ता", + "sort_group": "समूह", + "groupby_none": "कोई समूहन नहीं", + "groupby_area": "क्षेत्र के अनुसार", + "groupby_group": "समूह के अनुसार", + "groupby_user": "उपयोगकर्ता के अनुसार", + "filter_label": "फ़िल्टर", + "user_label": "उपयोगकर्ता", + "sort_label": "क्रमबद्ध करें", + "group_by_label": "इसके अनुसार समूहित करें", + "state_value_help": "HA स्थिति मान का उपयोग करें (आमतौर पर लोअरकेस, उदा. \"on\"/\"off\")। सहेजने पर केस सामान्यीकृत हो जाता है।", + "target_changes_help": "ट्रिगर होने से पहले मिलान वाले संक्रमणों की संख्या (डिफ़ॉल्ट: 1)।", + "qr_print_title": "QR कोड प्रिंट करें", + "qr_print_desc": "अपने उपकरण पर काटकर चिपकाने के लिए QR कोड का प्रिंट-योग्य पृष्ठ बनाएँ।", + "qr_print_load": "वस्तुएँ लोड करें", + "qr_print_filter": "फ़िल्टर", + "qr_print_objects": "वस्तुएँ", + "qr_print_actions": "क्रियाएँ", + "qr_print_url_mode": "लिंक प्रकार", + "qr_print_estimate": "अनुमानित QR कोड", + "qr_print_over_limit": "सीमा 200 है, फ़िल्टर को संकीर्ण करें", + "qr_print_generate": "QR कोड बनाएँ", + "qr_print_generating": "बनाया जा रहा है…", + "qr_print_ready": "QR कोड तैयार", + "qr_print_print_button": "प्रिंट करें", + "qr_print_empty": "बनाने के लिए कुछ नहीं", + "qr_action_skip": "छोड़ें", + "vacation_title": "अवकाश मोड", + "vacation_active": "सक्रिय", + "vacation_ended": "समाप्त", + "vacation_desc": "अवकाश की योजना बनाएँ: इस अवधि और कुछ बफ़र दिनों के दौरान सूचनाएँ रुकी रहती हैं। आप विशिष्ट कार्यों को वापस शामिल कर सकते हैं।", + "vacation_enable": "अवकाश मोड सक्षम करें", + "vacation_start": "प्रारंभ", + "vacation_end": "समाप्त", + "vacation_buffer": "बफ़र (दिन)", + "vacation_exempt_title": "अवकाश के दौरान फिर भी सूचित करें", + "vacation_exempt_desc": "ऐसे कार्य चुनें जिनकी अवकाश के दौरान भी सूचना मिलनी चाहिए (उदा. महत्वपूर्ण पूल रसायन)।", + "vacation_load_tasks": "कार्य लोड करें", + "vacation_preview_btn": "पूर्वावलोकन दिखाएँ", + "vacation_preview_affected": "प्रभावित कार्य", + "vacation_event_due_soon": "जल्द देय हो जाता है", + "vacation_event_overdue": "अतिदेय हो जाता है", + "vacation_event_triggered_est": "सेंसर ट्रिगर संभव", + "vacation_sensor_based": "(सेंसर-आधारित)", + "vacation_action_notify": "फिर भी सूचित करें", + "vacation_action_unsilence": "फिर से चुप करें", + "vacation_marked_complete": "पूर्ण के रूप में चिह्नित", + "vacation_marked_skip": "छोड़ा गया", + "vacation_end_now": "अवकाश अभी समाप्त करें", + "add": "जोड़ें", + "show_stats": "सांख्यिकी + ग्राफ़ दिखाएँ", + "hide_stats": "सांख्यिकी छिपाएँ", + "adaptive_no_data": "अनुकूली विश्लेषण के लिए अभी पर्याप्त पूर्णता इतिहास नहीं। अंतराल अनुशंसाएँ और विश्वसनीयता चार्ट अनलॉक करने के लिए इस कार्य को कुछ और बार पूर्ण करें।", + "suggestion_applied": "सुझाया गया अंतराल लागू किया गया", + "vacation_mode": "अवकाश मोड", + "vacation_status_active": "अभी सक्रिय", + "vacation_status_scheduled": "निर्धारित", + "vacation_status_inactive": "निष्क्रिय", + "vacation_end_now_confirm": "अवकाश तुरंत समाप्त करें?", + "vacation_exempt_count": "छूट प्राप्त", + "vacation_advanced": "उन्नत…", + "vacation_open_panel": "पैनल में खोलें", + "enable": "सक्षम करें", + "saved": "सहेजा गया", + "budget_monthly_set": "मासिक सेट करें", + "budget_yearly_set": "वार्षिक सेट करें", + "budget_advanced": "मुद्रा, अलर्ट…", + "budget_open_panel": "पैनल में खोलें", + "groups_empty": "अभी कोई समूह नहीं।", + "group_new_placeholder": "समूह जोड़ें…", + "group_delete_confirm": "समूह \"{name}\" हटाएँ?", + "groups_manage_tasks": "कार्य असाइनमेंट प्रबंधित करें…", + "groups_open_panel": "पैनल में खोलें", + "unassigned": "असाइन नहीं", + "no_area": "कोई क्षेत्र नहीं", + "has_overdue": "अतिदेय कार्य हैं", + "object": "वस्तु", + "settings_panel_access": "पैनल पहुँच", + "settings_panel_access_desc": "व्यवस्थापकों को हमेशा पूर्ण पहुँच होती है। विशिष्ट गैर-व्यवस्थापकों को बनाना, संपादित करना और हटाना सौंपने के लिए, इसे चालू करें और नीचे उन्हें चुनें — बाकी सभी को केवल पूर्ण करें और छोड़ें दिखता है।", + "settings_operator_write": "चयनित उपयोगकर्ताओं को बनाने, संपादित करने और हटाने की अनुमति दें", + "settings_operator_write_desc": "बंद: केवल व्यवस्थापक सामग्री बदल सकते हैं। चालू: नीचे चयनित उपयोगकर्ताओं को भी पूर्ण पहुँच मिलती है।", + "no_non_admin_users": "कोई गैर-व्यवस्थापक उपयोगकर्ता नहीं मिला। सेटिंग्स → लोग में कुछ जोड़ें।", + "owner_label": "स्वामी", + "feat_completion_actions": "पूर्णता क्रियाएँ", + "feat_completion_actions_desc": "पूर्ण होने पर प्रति-कार्य HA क्रिया + पूर्व-निर्धारित मानों के साथ त्वरित-पूर्ण QR।", + "on_complete_action_title": "पूर्ण होने पर: HA क्रिया ट्रिगर करें (वैकल्पिक)", + "on_complete_action_desc": "कार्य पूर्ण होने पर एक HA सेवा कॉल करता है — उदा. डिवाइस पर काउंटर रीसेट करना।", + "on_complete_action_service": "सेवा", + "on_complete_action_target": "लक्ष्य एंटिटी", + "on_complete_action_target_hint": "ध्यान दें: एंटिटी डोमेन सेवा से मेल खाना चाहिए — उदा. 'button.press' केवल button.* पर काम करता है, 'counter.increment' केवल counter.* पर, 'input_button.press' केवल input_button.* पर आदि। मेल न खाने पर क्रिया चुपचाप विफल हो जाएगी (HA लॉग करता है 'Referenced entities ... missing or not currently available')।", + "on_complete_action_data": "डेटा (JSON, वैकल्पिक)", + "on_complete_action_test": "कॉन्फ़िगरेशन सत्यापित करें", + "on_complete_action_test_success": "✓ कॉन्फ़िगरेशन वैध (क्रिया केवल कार्य पूर्ण होने पर चलेगी)", + "on_complete_action_test_failed": "विफल", + "quick_complete_defaults_title": "त्वरित-पूर्ण डिफ़ॉल्ट (QR स्कैन के लिए, वैकल्पिक)", + "quick_complete_defaults_desc": "त्वरित-पूर्ण QR स्कैन के लिए पूर्व-निर्धारित मान। इनके बिना, QR पूर्ण करें संवाद खोलता है।", + "quick_complete_defaults_notes": "टिप्पणियाँ", + "quick_complete_defaults_cost": "लागत", + "quick_complete_defaults_duration": "अवधि (मिनट)", + "quick_complete_defaults_feedback_none": "कोई प्रतिक्रिया नहीं", + "quick_complete_defaults_feedback_needed": "आवश्यक था", + "quick_complete_defaults_feedback_not_needed": "आवश्यक नहीं", + "quick_complete_success": "त्वरित रूप से पूर्ण के रूप में चिह्नित", + "show_all_objects": "सभी वस्तुएँ दिखाएँ", + "show_all_tasks": "फ़िल्टर हटाएँ — सभी कार्य दिखाएँ", + "filter_to_overdue": "कार्य सूची को केवल अतिदेय तक फ़िल्टर करें", + "filter_to_due_soon": "कार्य सूची को केवल जल्द-देय तक फ़िल्टर करें", + "filter_to_triggered": "कार्य सूची को केवल ट्रिगर हुए तक फ़िल्टर करें", + "open_task": "कार्य खोलें", + "show_details": "इतिहास + सांख्यिकी दिखाएँ", + "hide_details": "विवरण छिपाएँ", + "history_empty": "अभी कोई इतिहास नहीं।", + "history_edit_button": "प्रविष्टि संपादित करें", + "total_cost": "कुल लागत", + "times_performed": "किया गया", + "older_entries": "पुराने", + "open_in_panel": "रखरखाव पैनल में खोलें", + "skip_reason": "छोड़ने का कारण (वैकल्पिक)", + "reset_to_date": "अंतिम बार किया गया को रीसेट करें", + "delete_task_confirm": "इस कार्य और इसके इतिहास को हटाएँ?", + "delete_object_confirm": "इस वस्तु और इसके सभी कार्यों को हटाएँ?", + "loading": "लोड हो रहा है…", + "archive": "संग्रह करें", + "undo": "पूर्ववत करें", + "task_archived": "कार्य संग्रहित", + "object_archived": "ऑब्जेक्ट संग्रहित", + "unarchive": "संग्रह से हटाएँ", + "archived": "संग्रहीत", + "show_archived": "संग्रहीत दिखाएँ", + "hide_archived": "संग्रहीत छिपाएँ", + "confirm_archive_object": "इस ऑब्जेक्ट और इसके कार्यों को संग्रहित करें? इतिहास सुरक्षित रहता है और बाद में पुनर्स्थापित किया जा सकता है।", + "settings_archive": "संग्रह और प्रतिधारण", + "settings_archive_desc": "पूर्ण एक-बार के कार्यों को हटाए बिना अलग रखें। संग्रहीत आइटम छिपे और निष्क्रिय रहते हैं, पर इतिहास और लागत बनाए रखते हैं।", + "settings_archive_oneoff_days": "पूर्ण एक-बार के कार्य स्वतः संग्रहित करें (दिन, 0 = बंद)", + "settings_delete_archived_oneoff_days": "संग्रहीत एक-बार के कार्य स्वतः हटाएँ (दिन, 0 = कभी नहीं)", + "archive_object": "ऑब्जेक्ट संग्रहित करें", + "unarchive_object": "ऑब्जेक्ट संग्रह से हटाएँ", + "documents": "दस्तावेज़", + "documents_empty": "अभी तक कोई दस्तावेज़ नहीं।", + "doc_upload": "फ़ाइल अपलोड करें", + "doc_uploading": "अपलोड हो रहा है…", + "doc_add_link": "लिंक जोड़ें", + "doc_link_url": "URL (https://…)", + "doc_link_title": "शीर्षक (वैकल्पिक)", + "doc_open": "खोलें", + "doc_delete_confirm": "\"{name}\" हटाएँ?", + "doc_too_large": "फ़ाइल बहुत बड़ी है (अधिकतम 25 MB)।", + "doc_upload_failed": "अपलोड विफल रहा।", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "पहले से कहीं और संग्रहीत — साझा, कोई अतिरिक्त स्थान नहीं।", + "doc_dup_in_object": "यह फ़ाइल पहले से इस ऑब्जेक्ट से जुड़ी है।", + "doc_link_invalid": "केवल http/https लिंक की अनुमति है।", + "doc_cat_manual": "मैनुअल", + "doc_cat_warranty": "वारंटी", + "doc_cat_invoice": "चालान", + "doc_cat_spare_parts": "स्पेयर पार्ट्स", + "doc_cat_photo": "फ़ोटो", + "doc_cat_other": "अन्य", + "doc_link_badge": "लिंक", + "doc_storage_title": "दस्तावेज़ संग्रहण", + "doc_storage_saved": "डीडुप्लिकेशन से बचत", + "doc_storage_refresh": "ताज़ा करें", + "doc_download": "डाउनलोड करें", + "doc_close": "बंद करें", + "doc_camera": "फ़ोटो लें", + "doc_drop_hint": "फ़ाइलें यहाँ छोड़ें", + "doc_task_none": "इस कार्य से कोई दस्तावेज़ संबद्ध नहीं है।", + "doc_link_existing": "दस्तावेज़ जोड़ें…", + "doc_attach": "जोड़ें", + "doc_unlink": "अलग करें", + "doc_page": "पृष्ठ", + "chart_range_7d": "7दिन", + "chart_range_30d": "30दिन", + "chart_range_90d": "90दिन", + "chart_range_1y": "1वर्ष", + "chart_since_service": "पिछले रखरखाव के बाद से", + "chart_no_stats": "इस एंटिटी के लिए दीर्घकालिक आँकड़े नहीं हैं — केवल रखरखाव इवेंट मान दिखाए जा रहे हैं", + "auto_complete_on_recovery": "सेंसर ठीक होने पर स्वतः पूर्ण करें", + "auto_complete_on_recovery_help": "जब ट्रिगर स्वयं हल हो जाता है तो एक पूर्णता दर्ज करता है (अंतिम निष्पादन सेट करता है) — जैसे नमक भरा गया, फ़िल्टर बदला गया।", + "doc_search": "दस्तावेज़ खोजें…", + "doc_search_none": "कोई मेल खाता दस्तावेज़ नहीं", + "link_device_optional": "मौजूदा डिवाइस से लिंक करें (वैकल्पिक)", + "parent_object_optional": "मूल ऑब्जेक्ट (वैकल्पिक)", + "parent_none": "(कोई मूल नहीं)", + "paused": "रोका गया", + "pause_object": "रोकें", + "resume_object": "फिर से शुरू करें", + "pause_until_prompt": "इस ऑब्जेक्ट के शेड्यूल फ़्रीज़ करें — फिर से शुरू करने तक कुछ भी देय नहीं होगा और कोई सूचना नहीं आएगी। वैकल्पिक रूप से स्वतः पुनरारंभ की तारीख़ सेट करें।", + "pause_until_label": "फिर से शुरू करें (वैकल्पिक)", + "object_paused": "ऑब्जेक्ट रोका गया", + "object_resumed": "ऑब्जेक्ट फिर से शुरू — शेड्यूल पुनः आरंभ", + "object_paused_badge": "रोका गया", + "paused_until_label": "तक", + "replace_object": "बदलें…", + "replace_object_prompt": "इस ऑब्जेक्ट को सेवानिवृत्त करें और उत्तराधिकारी बनाएं। इतिहास और लागत पुराने पर संग्रहीत रहती है; कार्य और दस्तावेज़ नए में चले जाते हैं, काउंटर नए सिरे से शुरू होते हैं।", + "replace_name_label": "उत्तराधिकारी का नाम", + "object_replaced": "ऑब्जेक्ट बदला गया — उत्तराधिकारी बनाया गया", + "reading_unit_label": "रीडिंग इकाई (जैसे kWh, m³)", + "reading_unit_help": "इस कार्य को पूरा करते समय दर्ज मान के बगल में दिखाई जाती है।", + "reading_value_label": "रीडिंग मान", + "reading_label": "रीडिंग", + "settings_templates_label": "टेम्पलेट गैलरी", + "settings_templates_hint": "जिन टेम्पलेट की कभी ज़रूरत नहीं होगी उन्हें अनचेक करें — वे \"टेम्पलेट से\" चयनकर्ताओं (पैनल और कॉन्फ़िग फ़्लो) से गायब हो जाएंगे। और कुछ नहीं बदलता; कभी भी फिर से सक्षम कर सकते हैं।", + "worksheet": "कार्य पत्रक", + "worksheet_scan_view": "कार्य खोलने के लिए स्कैन करें", + "worksheet_scan_complete": "पूरा करने के लिए स्कैन करें", + "worksheet_manual_excerpt": "मैनुअल अंश", + "worksheet_pages": "पृष्ठ", + "worksheet_printed": "मुद्रित", + "worksheet_never": "कभी नहीं" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/it.json b/custom_components/maintenance_supporter/frontend/locales/it.json new file mode 100644 index 0000000..8397385 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/it.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Manutenzione", + "objects": "Oggetti", + "tasks": "Attività", + "overdue": "Scaduto", + "due_soon": "In scadenza", + "triggered": "Attivato", + "ok": "OK", + "all": "Tutti", + "new_object": "+ Nuovo oggetto", + "templates_from": "Da modello", + "templates_title": "Inizia da un modello", + "templates_task_count": "{n} attività", + "template_created": "Creato da modello", + "onboard_hint": "Aggiungi il primo oggetto per iniziare a tracciare la manutenzione.", + "edit": "Modifica", + "duplicate": "Duplica", + "task_duplicated": "Attività duplicata", + "object_duplicated": "Oggetto duplicato", + "delete": "Elimina", + "add_task": "+ Attività", + "complete": "Completato", + "completed": "Completato", + "skip": "Salta", + "skipped": "Saltato", + "missed": "Missed", + "reset": "Reimposta", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Annulla", + "bulk_select": "Seleziona", + "bulk_select_all": "Seleziona tutto", + "bulk_n_selected": "{n} selezionati", + "bulk_completed": "{n} attività completate", + "bulk_archived": "{n} attività archiviate", + "completing": "Completamento…", + "interval": "Intervallo", + "warning": "Avviso", + "last_performed": "Ultima esecuzione", + "next_due": "Prossima scadenza", + "days_until_due": "Giorni alla scadenza", + "avg_duration": "Ø Durata", + "trigger": "Trigger", + "trigger_type": "Tipo di trigger", + "threshold_above": "Limite superiore", + "threshold_below": "Limite inferiore", + "threshold": "Soglia", + "counter": "Contatore", + "state_change": "Cambio di stato", + "runtime": "Tempo di funzionamento", + "runtime_hours": "Durata obiettivo (ore)", + "target_value": "Valore obiettivo", + "baseline": "Linea di base", + "target_changes": "Modifiche obiettivo", + "for_minutes": "Per (minuti)", + "time_based": "Temporale", + "sensor_based": "Sensore", + "manual": "Manuale", + "one_time": "Una tantum", + "weekdays": "Giorni della settimana", + "nth_weekday": "N-esimo giorno della settimana del mese", + "day_of_month": "Giorno del mese", + "recurrence_on_days": "Ripeti il", + "recurrence_occurrence": "Occorrenza", + "recurrence_weekday": "Giorno della settimana", + "recurrence_day": "Giorno del mese (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1º", + "ord_2": "2º", + "ord_3": "3º", + "ord_4": "4º", + "ord_5": "5º", + "ord_last": "Ultimo", + "day_word": "Giorno", + "interval_value": "Intervallo", + "interval_unit": "Unità", + "unit_days": "Giorni", + "unit_weeks": "Settimane", + "unit_months": "Mesi", + "unit_years": "Anni", + "due_date": "Data di scadenza", + "cleaning": "Pulizia", + "inspection": "Ispezione", + "replacement": "Sostituzione", + "calibration": "Calibrazione", + "service": "Servizio", + "reading": "Lettura", + "custom": "Personalizzato", + "history": "Cronologia", + "cost": "Costo", + "report_button": "Report", + "report_title": "Report di manutenzione", + "report_generated": "Generato", + "report_times_done": "Fatto", + "report_total_cost": "Costo totale", + "report_every": "ogni {n} {unit}", + "report_notes": "Note", + "report_col_type": "Tipo", + "report_col_status": "Stato", + "report_col_schedule": "Pianificazione", + "duration": "Durata", + "both": "Entrambi", + "trigger_val": "Valore trigger", + "complete_title": "Completato: ", + "checklist": "Checklist", + "checklist_steps_optional": "Passaggi della checklist (opzionale)", + "checklist_placeholder": "Pulire il filtro\nSostituire la guarnizione\nTestare la pressione", + "checklist_help": "Un passaggio per riga. Max 100 elementi.", + "err_too_long": "{field}: troppo lungo (max {n} caratteri)", + "err_too_short": "{field}: troppo corto (min {n} caratteri)", + "err_value_too_high": "{field}: troppo grande (max {n})", + "err_value_too_low": "{field}: troppo piccolo (min {n})", + "err_required": "{field}: campo obbligatorio", + "err_wrong_type": "{field}: tipo errato (atteso: {type})", + "err_invalid_choice": "{field}: valore non consentito", + "err_invalid_value": "{field}: valore non valido", + "feat_schedule_time": "Pianificazione oraria", + "feat_schedule_time_desc": "Le attività scadono a un'ora specifica anziché a mezzanotte.", + "schedule_time_optional": "Scadenza all'ora (opzionale, HH:MM)", + "schedule_time_help": "Vuoto = mezzanotte (default). Fuso orario HA.", + "at_time": "alle", + "notes_optional": "Note (opzionale)", + "cost_optional": "Costo (opzionale)", + "duration_minutes": "Durata in minuti (opzionale)", + "days": "giorni", + "day": "giorno", + "today": "Oggi", + "d_overdue": "g in ritardo", + "no_tasks": "Nessuna attività di manutenzione. Crea un oggetto per iniziare.", + "no_tasks_short": "Nessuna attività", + "no_history": "Nessuna voce nella cronologia.", + "show_all": "Mostra tutto", + "cost_duration_chart": "Costi & Durata", + "installed": "Installato", + "confirm_delete_object": "Eliminare questo oggetto e tutte le sue attività?", + "confirm_delete_task": "Eliminare questa attività?", + "min": "Min", + "max": "Max", + "save": "Salva", + "saving": "Salvataggio…", + "edit_task": "Modifica attività", + "new_task": "Nuova attività di manutenzione", + "task_name": "Nome attività", + "maintenance_type": "Tipo di manutenzione", + "priority": "Priorità", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Bassa", + "priority_normal": "Normale", + "priority_high": "Alta", + "schedule_type": "Tipo di pianificazione", + "interval_days": "Intervallo (giorni)", + "warning_days": "Giorni di avviso", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Ultima esecuzione (opzionale)", + "interval_anchor": "Ancoraggio intervallo", + "anchor_completion": "Dalla data di completamento", + "anchor_planned": "Dalla data pianificata (nessuna deriva)", + "edit_object": "Modifica oggetto", + "name": "Nome", + "manufacturer_optional": "Produttore (opzionale)", + "model_optional": "Modello (opzionale)", + "serial_number_optional": "Numero di serie (opzionale)", + "serial_number_label": "N/S", + "documentation_url_label": "Manuale", + "object_notes_label": "Note", + "sort_due_date": "Scadenza", + "sort_object": "Nome oggetto", + "sort_type": "Tipo", + "sort_task_name": "Nome attività", + "all_objects": "Tutti gli oggetti", + "tasks_lower": "attività", + "no_tasks_yet": "Nessuna attività", + "add_first_task": "Aggiungi prima attività", + "trigger_configuration": "Configurazione trigger", + "entity_id": "ID entità", + "comma_separated": "separati da virgola", + "entity_logic": "Logica entità", + "entity_logic_any": "Qualsiasi entità attiva", + "entity_logic_all": "Tutte le entità devono attivare", + "entities": "entità", + "attribute_optional": "Attributo (opzionale, vuoto = stato)", + "use_entity_state": "Usa stato dell'entità (nessun attributo)", + "trigger_above": "Attivare sopra", + "trigger_below": "Attivare sotto", + "for_at_least_minutes": "Per almeno (minuti)", + "safety_interval_days": "Intervallo di sicurezza (giorni, opzionale)", + "safety_interval": "Intervallo di sicurezza (opzionale)", + "delta_mode": "Modalità delta", + "from_state_optional": "Dallo stato (opzionale)", + "to_state_optional": "Allo stato (opzionale)", + "documentation_url_optional": "URL documentazione (opzionale)", + "object_notes_optional": "Note (opzionale)", + "nfc_tag_id_optional": "ID tag NFC (opzionale)", + "nfc_tags_empty_help": "Nessun tag NFC ancora registrato in Home Assistant.", + "nfc_tags_open_settings": "Apri impostazioni tag", + "nfc_tags_refresh": "Aggiorna", + "environmental_entity_optional": "Sensore ambientale (opzionale)", + "environmental_entity_helper": "es. sensor.temperatura_esterna — regola l'intervallo in base alle condizioni ambientali", + "environmental_attribute_optional": "Attributo ambientale (opzionale)", + "nfc_tag_id": "ID tag NFC", + "nfc_linked": "Tag NFC collegato", + "nfc_link_hint": "Clicca per collegare un tag NFC", + "responsible_user": "Utente responsabile", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Nessun utente assegnato)", + "all_users": "Tutti gli utenti", + "my_tasks": "Le mie attività", + "tab_calendar": "Calendario", + "cal_no_events": "Nessuna manutenzione", + "cal_window_7": "7 giorni", + "cal_window_14": "14 giorni", + "cal_window_30": "30 giorni", + "cal_window_365": "1 anno", + "cal_every_n_days": "ogni {n} giorni", + "cal_source_time": "Basato sul tempo", + "cal_source_time_adaptive": "Basato sul tempo (adattivo)", + "cal_source_sensor": "Basato su sensore", + "cal_predicted": "previsto", + "cal_confidence_high": "alta confidenza", + "cal_confidence_medium": "media confidenza", + "cal_confidence_low": "bassa confidenza", + "budget_monthly": "Budget mensile", + "budget_yearly": "Budget annuale", + "groups": "Gruppi", + "new_group": "Nuovo gruppo", + "edit_group": "Modifica gruppo", + "no_groups": "Nessun gruppo", + "delete_group": "Elimina gruppo", + "delete_group_confirm": "Eliminare il gruppo '{name}'?", + "group_select_tasks": "Seleziona attività", + "group_name_required": "Nome richiesto", + "description_optional": "Descrizione (opzionale)", + "selected": "Selezionato", + "loading_chart": "Caricamento dati...", + "hide_outliers": "Nascondi valori anomali (errori sensore)", + "was_maintenance_needed": "Questa manutenzione era necessaria?", + "feedback_needed": "Necessaria", + "feedback_not_needed": "Non necessaria", + "feedback_not_sure": "Non sicuro", + "suggested_interval": "Intervallo suggerito", + "apply_suggestion": "Applica", + "reanalyze": "Rianalizza", + "reanalyze_result": "Nuova analisi", + "reanalyze_insufficient_data": "Dati insufficienti per una raccomandazione", + "data_points": "punti dati", + "dismiss_suggestion": "Ignora", + "confidence_low": "Bassa", + "confidence_medium": "Media", + "confidence_high": "Alta", + "recommended": "consigliato", + "seasonal_awareness": "Consapevolezza stagionale", + "edit_seasonal_overrides": "Modifica fattori stagionali", + "seasonal_overrides_title": "Fattori stagionali (override)", + "seasonal_overrides_hint": "Fattore per mese (0.1–5.0). Vuoto = appreso automaticamente.", + "seasonal_override_invalid": "Valore non valido", + "seasonal_override_range": "Il fattore deve essere tra 0.1 e 5.0", + "clear_all": "Cancella tutto", + "seasonal_chart_title": "Fattori stagionali", + "seasonal_learned": "Appreso", + "seasonal_manual": "Manuale", + "month_jan": "Gen", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "Mag", + "month_jun": "Giu", + "month_jul": "Lug", + "month_aug": "Ago", + "month_sep": "Set", + "month_oct": "Ott", + "month_nov": "Nov", + "month_dec": "Dic", + "sensor_prediction": "Previsione sensore", + "degradation_trend": "Tendenza", + "trend_rising": "In aumento", + "trend_falling": "In calo", + "trend_stable": "Stabile", + "trend_insufficient_data": "Dati insufficienti", + "days_until_threshold": "Giorni alla soglia", + "threshold_exceeded": "Soglia superata", + "environmental_adjustment": "Fattore ambientale", + "sensor_prediction_urgency": "Il sensore prevede la soglia tra ~{days} giorni", + "day_short": "giorno", + "weibull_reliability_curve": "Curva di affidabilità", + "weibull_failure_probability": "Probabilità di guasto", + "weibull_r_squared": "Adattamento R²", + "beta_early_failures": "Guasti precoci", + "beta_random_failures": "Guasti casuali", + "beta_wear_out": "Usura", + "beta_highly_predictable": "Altamente prevedibile", + "confidence_interval": "Intervallo di confidenza", + "confidence_conservative": "Conservativo", + "confidence_aggressive": "Ottimistico", + "current_interval_marker": "Intervallo attuale", + "recommended_marker": "Consigliato", + "characteristic_life": "Vita caratteristica", + "chart_mini_sparkline": "Sparkline di tendenza", + "chart_history": "Cronologia costi e durata", + "chart_seasonal": "Fattori stagionali, 12 mesi", + "chart_weibull": "Curva di affidabilità Weibull", + "chart_sparkline": "Grafico valore trigger sensore", + "days_progress": "Avanzamento giorni", + "qr_code": "Codice QR", + "qr_generating": "Generazione codice QR…", + "qr_error": "Impossibile generare il codice QR.", + "qr_error_no_url": "Nessun URL HA configurato. Impostare un URL esterno o interno in Impostazioni → Sistema → Rete.", + "save_error": "Salvataggio non riuscito. Riprovare.", + "qr_print": "Stampa", + "qr_download": "Scarica SVG", + "qr_action": "Azione alla scansione", + "qr_action_view": "Visualizza info manutenzione", + "qr_action_complete": "Segna manutenzione come completata", + "qr_url_mode": "Tipo di link", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Locale (mDNS)", + "qr_mode_server": "URL server", + "overview": "Panoramica", + "analysis": "Analisi", + "recent_activities": "Attività recenti", + "search_notes": "Cerca nelle note", + "avg_cost": "Ø Costo", + "no_advanced_features": "Nessuna funzione avanzata attivata", + "no_advanced_features_hint": "Attiva “Intervalli Adattivi” o “Modelli Stagionali” nelle impostazioni dell'integrazione per vedere i dati di analisi qui.", + "analysis_not_enough_data": "Non ci sono ancora abbastanza dati per l'analisi.", + "analysis_not_enough_data_hint": "L'analisi Weibull richiede almeno 5 manutenzioni completate; i modelli stagionali diventano visibili dopo 6+ punti dati al mese.", + "analysis_manual_task_hint": "Le attività manuali senza intervallo non generano dati di analisi.", + "completions": "completamenti", + "current": "Attuale", + "shorter": "Più breve", + "longer": "Più lungo", + "normal": "Normale", + "disabled": "Disattivato", + "compound_logic": "Logica composta", + "compound": "Composto (più condizioni)", + "compound_logic_and": "E — tutte le condizioni devono attivarsi", + "compound_logic_or": "O — basta una condizione", + "compound_help": "Combina più condizioni dei sensori in un unico trigger.", + "compound_no_conditions": "Ancora nessuna condizione — aggiungine almeno una.", + "compound_add_condition": "Aggiungi condizione", + "compound_condition": "Condizione", + "compound_remove_condition": "Rimuovi condizione", + "card_title": "Titolo", + "card_show_header": "Mostra intestazione con statistiche", + "card_show_actions": "Mostra pulsanti azione", + "card_compact": "Modalità compatta", + "card_max_items": "Max elementi (0 = tutti)", + "card_filter_status": "Filtra per stato", + "card_filter_status_help": "Vuoto = mostra tutti gli stati.", + "card_filter_objects": "Filtra per oggetti", + "card_filter_objects_help": "Vuoto = mostra tutti gli oggetti.", + "card_filter_entities": "Filtra per entità (entity_ids)", + "card_filter_entities_help": "Seleziona entità sensor / binary_sensor da questa integrazione. Vuoto = tutte.", + "card_loading_objects": "Caricamento oggetti…", + "card_load_error": "Impossibile caricare gli oggetti — verifica la connessione WebSocket.", + "card_no_tasks_title": "Nessuna attività di manutenzione", + "card_no_tasks_cta": "→ Creane una nel pannello Manutenzione", + "no_objects": "Nessun oggetto ancora.", + "action_error": "Azione fallita. Riprova.", + "area_id_optional": "Area (opzionale)", + "installation_date_optional": "Data di installazione (opzionale)", + "warranty_expiry_optional": "Scadenza garanzia (opzionale)", + "warranty": "Garanzia", + "warranty_valid_until": "valida fino al {date}", + "warranty_expires_in": "scade tra {days} giorni", + "warranty_expired": "scaduta", + "cal_past_windows": "Finestre passate", + "cal_forward_windows": "Finestre future", + "history_edit_title": "Modifica voce dello storico", + "history_edit_timestamp": "Data e ora", + "manufacturer": "Produttore", + "model": "Modello", + "area": "Area", + "actions": "Azioni", + "view_mode_label": "Vista", + "view_cards": "Vista schede", + "view_table": "Vista tabella", + "objects_table_columns_label": "Colonne tabella oggetti", + "objects_table_columns_hint": "Scegli quali colonne mostrare nella vista tabella degli oggetti.", + "custom_icon_optional": "Icona (opzionale, es. mdi:wrench)", + "task_enabled": "Attività abilitata", + "skip_reason_prompt": "Saltare questa attività?", + "reason_optional": "Motivo (opzionale)", + "reset_date_prompt": "Segnare l'attività come eseguita?", + "reset_date_optional": "Data ultima esecuzione (opzionale, predefinito: oggi)", + "notes_label": "Note", + "documentation_label": "Documentazione", + "no_nfc_tag": "— Nessun tag —", + "dashboard": "Dashboard", + "tab_today": "Oggi", + "palette_placeholder": "Cerca oggetti e attività…", + "palette_no_results": "Nessun risultato", + "palette_hint": "↑↓ naviga · Invio apri · Esc chiudi", + "today_all_caught_up": "Tutto fatto! Niente in scadenza questa settimana.", + "today_overdue": "Scadute", + "today_due_today": "In scadenza oggi", + "today_this_week": "Questa settimana", + "settings": "Impostazioni", + "settings_features": "Funzioni avanzate", + "settings_features_desc": "Attiva o disattiva le funzioni avanzate. La disattivazione le nasconde dall'interfaccia ma non elimina i dati.", + "feat_adaptive": "Pianificazione adattiva", + "feat_adaptive_desc": "Impara intervalli ottimali dalla cronologia di manutenzione", + "feat_predictions": "Previsioni sensore", + "feat_predictions_desc": "Prevedi date di attivazione dalla degradazione dei sensori", + "feat_seasonal": "Adeguamenti stagionali", + "feat_seasonal_desc": "Adegua gli intervalli in base ai modelli stagionali", + "feat_environmental": "Correlazione ambientale", + "feat_environmental_desc": "Correla gli intervalli con temperatura/umidità", + "feat_budget": "Monitoraggio budget", + "feat_budget_desc": "Monitora le spese di manutenzione mensili e annuali", + "feat_groups": "Gruppi di attività", + "feat_groups_desc": "Organizza le attività in gruppi logici", + "feat_checklists": "Checklist", + "feat_checklists_desc": "Procedure multi-fase per il completamento delle attività", + "settings_general": "Generale", + "settings_default_warning": "Giorni di avviso predefiniti", + "settings_panel_enabled": "Pannello laterale", + "settings_panel_title": "Titolo pannello laterale", + "settings_notifications": "Notifiche", + "settings_notify_service": "Servizio di notifica", + "test_notification": "Notifica di test", + "send_test": "Invia test", + "testing": "Invio in corso…", + "test_notification_success": "Notifica di test inviata", + "test_notification_failed": "Notifica di test non riuscita", + "settings_notify_due_soon": "Notifica quando in scadenza", + "settings_notify_overdue": "Notifica quando scaduto", + "settings_notify_triggered": "Notifica quando attivato", + "settings_interval_hours": "Intervallo di ripetizione (ore, 0 = una volta)", + "settings_quiet_hours": "Ore di silenzio", + "settings_quiet_start": "Inizio", + "settings_quiet_end": "Fine", + "settings_max_per_day": "Max notifiche al giorno (0 = illimitato)", + "settings_bundling": "Raggruppare le notifiche", + "settings_bundle_threshold": "Soglia di raggruppamento", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Pulsanti azione mobili", + "settings_action_complete": "Mostra pulsante 'Completato'", + "settings_action_skip": "Mostra pulsante 'Salta'", + "settings_action_snooze": "Mostra pulsante 'Posticipa'", + "settings_weekly_digest": "Riepilogo settimanale", + "settings_weekly_digest_hint": "Una notifica di riepilogo il lunedì mattina quando ci sono attività.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Durata posticipo (ore)", + "settings_budget": "Budget", + "settings_currency": "Valuta", + "settings_budget_monthly": "Budget mensile", + "settings_budget_yearly": "Budget annuale", + "settings_budget_alerts": "Avvisi budget", + "settings_budget_threshold": "Soglia di avviso (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Esporta JSON", + "settings_export_yaml": "Esporta YAML", + "settings_export_csv": "Esporta CSV", + "settings_import_csv": "Importa CSV", + "settings_import_placeholder": "Incolla il contenuto JSON o CSV qui…", + "settings_import_btn": "Importa", + "settings_import_success": "{count} oggetti importati con successo.", + "settings_export_success": "Export scaricato.", + "settings_saved": "Impostazione salvata.", + "settings_include_history": "Includi cronologia", + "sort_alphabetical": "Alfabetico", + "sort_due_soonest": "Scadenza più vicina", + "sort_task_count": "Numero di attività", + "sort_area": "Area", + "sort_assigned_user": "Utente assegnato", + "sort_group": "Gruppo", + "groupby_none": "Nessun raggruppamento", + "groupby_area": "Per area", + "groupby_group": "Per gruppo", + "groupby_user": "Per utente", + "filter_label": "Filtro", + "user_label": "Utente", + "sort_label": "Ordinamento", + "group_by_label": "Raggruppa per", + "state_value_help": "Usa il valore di stato HA (di solito minuscolo, es. \"on\"/\"off\"). Il case viene normalizzato al salvataggio.", + "target_changes_help": "Numero di transizioni corrispondenti prima che il trigger si attivi (predefinito: 1).", + "qr_print_title": "Stampa codici QR", + "qr_print_desc": "Genera una pagina stampabile di codici QR da ritagliare e applicare sulle apparecchiature.", + "qr_print_load": "Carica oggetti", + "qr_print_filter": "Filtro", + "qr_print_objects": "Oggetti", + "qr_print_actions": "Azioni", + "qr_print_url_mode": "Tipo di link", + "qr_print_estimate": "Codici QR stimati", + "qr_print_over_limit": "limite 200, restringi il filtro", + "qr_print_generate": "Genera codici QR", + "qr_print_generating": "Generazione…", + "qr_print_ready": "Codici QR pronti", + "qr_print_print_button": "Stampa", + "qr_print_empty": "Niente da generare", + "qr_action_skip": "Salta", + "vacation_title": "Modalità vacanza", + "vacation_active": "attiva", + "vacation_ended": "terminata", + "vacation_desc": "Pianifica le tue vacanze: le notifiche vengono messe in pausa durante il periodo più giorni di buffer. Puoi escludere singole attività.", + "vacation_enable": "Attiva modalità vacanza", + "vacation_start": "Inizio", + "vacation_end": "Fine", + "vacation_buffer": "Buffer (giorni)", + "vacation_exempt_title": "Notifica comunque durante le vacanze", + "vacation_exempt_desc": "Scegli attività che devono notificare anche in vacanza (es. chimica della piscina critica).", + "vacation_load_tasks": "Carica attività", + "vacation_preview_btn": "Mostra anteprima", + "vacation_preview_affected": "attività interessate", + "vacation_event_due_soon": "sarà in scadenza", + "vacation_event_overdue": "diventerà scaduta", + "vacation_event_triggered_est": "trigger sensore possibile", + "vacation_sensor_based": "(basato su sensore)", + "vacation_action_notify": "Notifica comunque", + "vacation_action_unsilence": "Silenzia di nuovo", + "vacation_marked_complete": "Segnato come completato", + "vacation_marked_skip": "Saltato", + "vacation_end_now": "Termina vacanza ora", + "unassigned": "Non assegnato", + "no_area": "Nessuna area", + "has_overdue": "Attività scadute", + "object": "Oggetto", + "settings_panel_access": "Accesso al pannello", + "settings_panel_access_desc": "Gli amministratori hanno sempre accesso completo. Per delegare creazione, modifica ed eliminazione a non amministratori specifici, attiva questa opzione e selezionali qui sotto — gli altri vedono solo Completa e Salta.", + "settings_operator_write": "Consenti agli utenti selezionati di creare, modificare ed eliminare", + "settings_operator_write_desc": "Disattivato: solo gli amministratori possono modificare i contenuti. Attivato: anche gli utenti selezionati qui sotto hanno accesso completo.", + "no_non_admin_users": "Nessun utente non amministratore trovato. Aggiungili in Impostazioni → Persone.", + "owner_label": "Proprietario", + "feat_completion_actions": "Azioni di completamento", + "feat_completion_actions_desc": "Azione HA per attività al completamento + QR completamento rapido con valori predefiniti.", + "on_complete_action_title": "Al completamento: attiva azione HA (opzionale)", + "on_complete_action_desc": "Chiama un servizio HA quando l'attività viene completata — es. azzerare un contatore sul dispositivo.", + "on_complete_action_service": "Servizio", + "on_complete_action_target": "Entità target", + "on_complete_action_data": "Dati (JSON, opzionale)", + "on_complete_action_test": "Testa azione", + "on_complete_action_test_success": "Riuscito", + "on_complete_action_test_failed": "Fallito", + "quick_complete_defaults_title": "Valori predefiniti completamento rapido (per scansioni QR, opzionale)", + "quick_complete_defaults_desc": "Valori predefiniti per QR di completamento rapido. Senza, il QR apre la finestra di completamento.", + "quick_complete_defaults_notes": "Note", + "quick_complete_defaults_cost": "Costo", + "quick_complete_defaults_duration": "Durata (minuti)", + "quick_complete_defaults_feedback_none": "Nessun feedback", + "quick_complete_defaults_feedback_needed": "Era necessario", + "quick_complete_defaults_feedback_not_needed": "Non necessario", + "quick_complete_success": "Completato rapidamente", + "trigger_replaced": "Trigger sostituito", + "add": "Aggiungi", + "show_stats": "Mostra statistiche + grafici", + "hide_stats": "Nascondi statistiche", + "adaptive_no_data": "Storico dei completamenti ancora insufficiente per l'analisi adattiva. Completa questa attività ancora qualche volta per sbloccare i suggerimenti sull'intervallo e i grafici di affidabilità.", + "suggestion_applied": "Intervallo suggerito applicato", + "vacation_mode": "Modalità vacanza", + "vacation_status_active": "Attiva ora", + "vacation_status_scheduled": "Pianificata", + "vacation_status_inactive": "Inattiva", + "vacation_end_now_confirm": "Terminare subito la vacanza?", + "vacation_exempt_count": "escluse", + "vacation_advanced": "Avanzate…", + "vacation_open_panel": "Apri nel pannello", + "enable": "Attiva", + "saved": "Salvato", + "budget_monthly_set": "Imposta mensile", + "budget_yearly_set": "Imposta annuale", + "budget_advanced": "Valuta, avvisi…", + "budget_open_panel": "Apri nel pannello", + "groups_empty": "Nessun gruppo ancora.", + "group_new_placeholder": "Aggiungi gruppo…", + "group_delete_confirm": "Eliminare il gruppo \"{name}\"?", + "groups_manage_tasks": "Gestisci assegnazioni attività…", + "groups_open_panel": "Apri nel pannello", + "on_complete_action_target_hint": "Nota: il dominio dell'entità deve corrispondere al servizio — es. 'button.press' funziona solo su button.*, 'counter.increment' solo su counter.*, 'input_button.press' solo su input_button.* ecc. In caso di mancata corrispondenza l'azione fallisce silenziosamente (HA registra 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Mostra tutti gli oggetti", + "show_all_tasks": "Azzera filtro — mostra tutte le attività", + "filter_to_overdue": "Filtra l'elenco solo sulle attività in ritardo", + "filter_to_due_soon": "Filtra l'elenco solo sulle attività in scadenza", + "filter_to_triggered": "Filtra l'elenco solo sulle attività attivate", + "open_task": "Apri attività", + "show_details": "Mostra storico + statistiche", + "hide_details": "Nascondi dettagli", + "history_empty": "Nessuno storico ancora.", + "history_edit_button": "Modifica voce", + "total_cost": "Costo totale", + "times_performed": "Eseguita", + "older_entries": "precedenti", + "open_in_panel": "Apri nel pannello Manutenzione", + "skip_reason": "Motivo del salto (facoltativo)", + "reset_to_date": "Reimposta ultima esecuzione al", + "delete_task_confirm": "Eliminare questa attività e il suo storico?", + "delete_object_confirm": "Eliminare questo oggetto e tutte le sue attività?", + "loading": "Caricamento…", + "archive": "Archivia", + "undo": "Annulla", + "task_archived": "Attività archiviata", + "object_archived": "Oggetto archiviato", + "unarchive": "Annulla archiviazione", + "archived": "Archiviato", + "show_archived": "Mostra archiviati", + "hide_archived": "Nascondi archiviati", + "confirm_archive_object": "Archiviare questo oggetto e le sue attività? La cronologia viene conservata e può essere ripristinata in seguito.", + "settings_archive": "Archiviazione e conservazione", + "settings_archive_desc": "Metti da parte le attività una tantum completate senza eliminarle. Gli elementi archiviati sono nascosti e inattivi, ma conservano cronologia e costi.", + "settings_archive_oneoff_days": "Archivia automaticamente le attività una tantum completate dopo (giorni, 0 = off)", + "settings_delete_archived_oneoff_days": "Elimina automaticamente le attività una tantum archiviate dopo (giorni, 0 = mai)", + "archive_object": "Archivia oggetto", + "unarchive_object": "Annulla archiviazione oggetto", + "documents": "Documenti", + "documents_empty": "Ancora nessun documento.", + "doc_upload": "Carica file", + "doc_uploading": "Caricamento…", + "doc_add_link": "Aggiungi link", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titolo (facoltativo)", + "doc_open": "Apri", + "doc_delete_confirm": "Eliminare «{name}»?", + "doc_too_large": "File troppo grande (max 25 MB).", + "doc_upload_failed": "Caricamento non riuscito.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Già archiviato altrove — condiviso, nessuno spazio aggiuntivo.", + "doc_dup_in_object": "Questo file è già allegato a questo oggetto.", + "doc_link_invalid": "Sono consentiti solo link http/https.", + "doc_cat_manual": "Manuale", + "doc_cat_warranty": "Garanzia", + "doc_cat_invoice": "Fattura", + "doc_cat_spare_parts": "Ricambi", + "doc_cat_photo": "Foto", + "doc_cat_other": "Altro", + "doc_link_badge": "Link", + "doc_storage_title": "Archiviazione documenti", + "doc_storage_saved": "Risparmiato con deduplicazione", + "doc_storage_refresh": "Aggiorna", + "doc_download": "Scarica", + "doc_close": "Chiudi", + "doc_camera": "Scatta foto", + "doc_drop_hint": "Trascina qui i file", + "doc_task_none": "Nessun documento collegato a questa attività.", + "doc_link_existing": "Collega un documento…", + "doc_attach": "Collega", + "doc_unlink": "Scollega", + "doc_page": "Pagina", + "chart_range_7d": "7g", + "chart_range_30d": "30g", + "chart_range_90d": "90g", + "chart_range_1y": "1a", + "chart_since_service": "dall'ultima manutenzione", + "chart_no_stats": "Nessuna statistica a lungo termine per questa entità — vengono mostrati solo i valori degli interventi", + "auto_complete_on_recovery": "Completa automaticamente quando il sensore si ripristina", + "auto_complete_on_recovery_help": "Registra un completamento (imposta l'ultima esecuzione) quando il trigger si risolve da solo — ad es. sale ricaricato, filtro sostituito.", + "doc_search": "Cerca documenti…", + "doc_search_none": "Nessun documento corrispondente", + "link_device_optional": "Collega a dispositivo esistente (opzionale)", + "parent_object_optional": "Oggetto principale (opzionale)", + "parent_none": "(Nessun principale)", + "paused": "In pausa", + "pause_object": "Metti in pausa", + "resume_object": "Riprendi", + "pause_until_prompt": "Congela i calendari di questo oggetto: nulla scade e nulla notifica finché non viene ripreso. Facoltativamente imposta una data di ripresa automatica.", + "pause_until_label": "Riprendi il (facoltativo)", + "object_paused": "Oggetto in pausa", + "object_resumed": "Oggetto ripreso — calendari riavviati", + "object_paused_badge": "In pausa", + "paused_until_label": "fino al", + "replace_object": "Sostituisci…", + "replace_object_prompt": "Ritira questo oggetto e crea un successore. Storico e costi restano archiviati sul vecchio; attività e documenti passano al nuovo, i contatori ripartono da zero.", + "replace_name_label": "Nome del successore", + "object_replaced": "Oggetto sostituito — successore creato", + "reading_unit_label": "Unità di lettura (es. kWh, m³)", + "reading_unit_help": "Mostrata accanto al valore registrato al completamento di questa attività.", + "reading_value_label": "Valore letto", + "reading_label": "Lettura", + "settings_templates_label": "Galleria modelli", + "settings_templates_hint": "Deseleziona i modelli che non ti serviranno mai: spariscono dai selettori \"Da modello\" (pannello e config flow). Nient'altro cambia; riattivabili in qualsiasi momento.", + "worksheet": "Scheda di lavoro", + "worksheet_scan_view": "Scansiona per aprire l'attività", + "worksheet_scan_complete": "Scansiona per completare", + "worksheet_manual_excerpt": "Estratto del manuale", + "worksheet_pages": "pagine", + "worksheet_printed": "Stampato", + "worksheet_never": "Mai" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/ja.json b/custom_components/maintenance_supporter/frontend/locales/ja.json new file mode 100644 index 0000000..6c87564 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/ja.json @@ -0,0 +1,696 @@ +{ + "maintenance": "メンテナンス", + "objects": "対象", + "tasks": "タスク", + "overdue": "期限超過", + "due_soon": "期限間近", + "triggered": "トリガー発生", + "trigger_replaced": "トリガー解除済み", + "ok": "OK", + "all": "すべて", + "new_object": "+ 新規対象", + "templates_from": "テンプレートから", + "templates_title": "テンプレートから始める", + "templates_task_count": "{n} 件のタスク", + "template_created": "テンプレートから作成しました", + "onboard_hint": "最初のオブジェクトを追加してメンテナンスの記録を始めましょう。", + "edit": "編集", + "duplicate": "複製", + "task_duplicated": "タスクを複製しました", + "object_duplicated": "オブジェクトを複製しました", + "delete": "削除", + "add_task": "+ タスク追加", + "complete": "完了", + "completed": "完了済み", + "skip": "スキップ", + "skipped": "スキップ済み", + "missed": "Missed", + "reset": "リセット", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "キャンセル", + "bulk_select": "選択", + "bulk_select_all": "すべて選択", + "bulk_n_selected": "{n} 件選択", + "bulk_completed": "{n} 件のタスクを完了", + "bulk_archived": "{n} 件のタスクをアーカイブ", + "completing": "完了処理中…", + "interval": "間隔", + "warning": "警告", + "last_performed": "最終実施", + "next_due": "次回期限", + "days_until_due": "期限までの日数", + "avg_duration": "平均所要時間", + "trigger": "トリガー", + "trigger_type": "トリガー種別", + "threshold_above": "上限", + "threshold_below": "下限", + "threshold": "しきい値", + "counter": "カウンター", + "state_change": "状態変化", + "runtime": "稼働時間", + "runtime_hours": "目標稼働時間(時間)", + "target_value": "目標値", + "baseline": "基準値", + "target_changes": "変化回数", + "for_minutes": "継続(分)", + "time_based": "時間ベース", + "sensor_based": "センサーベース", + "manual": "手動", + "one_time": "1回のみ", + "weekdays": "曜日", + "nth_weekday": "毎月第N曜日", + "day_of_month": "毎月の日", + "recurrence_on_days": "繰り返す曜日", + "recurrence_occurrence": "出現週", + "recurrence_weekday": "曜日", + "recurrence_day": "日(1〜31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "第1", + "ord_2": "第2", + "ord_3": "第3", + "ord_4": "第4", + "ord_5": "第5", + "ord_last": "最終", + "day_word": "日", + "interval_value": "間隔", + "interval_unit": "単位", + "unit_days": "日", + "unit_weeks": "週", + "unit_months": "か月", + "unit_years": "年", + "due_date": "期限日", + "cleaning": "清掃", + "inspection": "点検", + "replacement": "交換", + "calibration": "校正", + "service": "保守", + "reading": "読み取り", + "custom": "カスタム", + "history": "履歴", + "cost": "費用", + "report_button": "レポート", + "report_title": "メンテナンスレポート", + "report_generated": "作成日", + "report_times_done": "実施回数", + "report_total_cost": "合計費用", + "report_every": "{n} {unit} ごと", + "report_notes": "メモ", + "report_col_type": "種類", + "report_col_status": "状態", + "report_col_schedule": "スケジュール", + "duration": "所要時間", + "both": "両方", + "trigger_val": "トリガー値", + "complete_title": "完了: ", + "checklist": "チェックリスト", + "checklist_steps_optional": "チェックリストの手順(任意)", + "checklist_placeholder": "フィルター清掃\nシール交換\n圧力テスト", + "checklist_help": "1行に1手順。最大100項目。", + "err_too_long": "{field}: 長すぎます(最大{n}文字)", + "err_too_short": "{field}: 短すぎます(最小{n}文字)", + "err_value_too_high": "{field}: 大きすぎます(最大{n})", + "err_value_too_low": "{field}: 小さすぎます(最小{n})", + "err_required": "{field}: 必須です", + "err_wrong_type": "{field}: 型が違います(期待値: {type})", + "err_invalid_choice": "{field}: 許可されていない値です", + "err_invalid_value": "{field}: 無効な値です", + "feat_schedule_time": "時刻指定スケジュール", + "feat_schedule_time_desc": "タスクが深夜0時ではなく指定した時刻に期限超過となります。", + "schedule_time_optional": "期限の時刻(任意、HH:MM)", + "schedule_time_help": "空欄=深夜0時(既定)。HAのタイムゾーン。", + "at_time": "時刻", + "notes_optional": "メモ(任意)", + "cost_optional": "費用(任意)", + "duration_minutes": "所要時間(分、任意)", + "days": "日", + "day": "日", + "today": "今日", + "d_overdue": "日超過", + "no_tasks": "メンテナンスタスクがまだありません。対象を作成して始めましょう。", + "no_tasks_short": "タスクなし", + "no_history": "履歴がまだありません。", + "show_all": "すべて表示", + "cost_duration_chart": "費用と所要時間", + "installed": "設置済み", + "confirm_delete_object": "この対象とすべてのタスクを削除しますか?", + "confirm_delete_task": "このタスクを削除しますか?", + "min": "最小", + "max": "最大", + "save": "保存", + "saving": "保存中…", + "edit_task": "タスクを編集", + "new_task": "新規メンテナンスタスク", + "task_name": "タスク名", + "maintenance_type": "メンテナンス種別", + "priority": "優先度", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "低", + "priority_normal": "標準", + "priority_high": "高", + "schedule_type": "スケジュール種別", + "interval_days": "間隔(日)", + "warning_days": "警告日数", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "最終実施(任意)", + "interval_anchor": "間隔の起点", + "anchor_completion": "完了日から", + "anchor_planned": "予定日から(ずれなし)", + "edit_object": "対象を編集", + "name": "名前", + "manufacturer_optional": "メーカー(任意)", + "model_optional": "型番(任意)", + "serial_number_optional": "シリアル番号(任意)", + "serial_number_label": "S/N", + "documentation_url_label": "取扱説明書", + "object_notes_label": "メモ", + "sort_due_date": "期限日", + "sort_object": "対象名", + "sort_type": "種別", + "sort_task_name": "タスク名", + "all_objects": "すべての対象", + "tasks_lower": "タスク", + "no_tasks_yet": "タスクがまだありません", + "add_first_task": "最初のタスクを追加", + "trigger_configuration": "トリガー設定", + "entity_id": "エンティティID", + "comma_separated": "カンマ区切り", + "entity_logic": "エンティティ条件", + "entity_logic_any": "いずれかのエンティティでトリガー", + "entity_logic_all": "すべてのエンティティが条件を満たす必要あり", + "entities": "エンティティ", + "attribute_optional": "属性(任意、空欄=状態)", + "use_entity_state": "エンティティの状態を使用(属性なし)", + "trigger_above": "この値を超えたらトリガー", + "trigger_below": "この値を下回ったらトリガー", + "for_at_least_minutes": "最低継続時間(分)", + "safety_interval_days": "安全間隔(日、任意)", + "safety_interval": "安全間隔(任意)", + "delta_mode": "差分モード", + "from_state_optional": "変化前の状態(任意)", + "to_state_optional": "変化後の状態(任意)", + "documentation_url_optional": "ドキュメントURL(任意)", + "object_notes_optional": "メモ(任意)", + "nfc_tag_id_optional": "NFCタグID(任意)", + "nfc_tags_empty_help": "Home AssistantにNFCタグがまだ登録されていません。", + "nfc_tags_open_settings": "タグ設定を開く", + "nfc_tags_refresh": "更新", + "environmental_entity_optional": "環境センサー(任意)", + "environmental_entity_helper": "例: sensor.outdoor_temperature — 環境条件に応じて間隔を調整します", + "environmental_attribute_optional": "環境属性(任意)", + "nfc_tag_id": "NFCタグID", + "nfc_linked": "NFCタグ連携済み", + "nfc_link_hint": "クリックしてNFCタグを連携", + "responsible_user": "担当ユーザー", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(担当者なし)", + "all_users": "すべてのユーザー", + "my_tasks": "自分のタスク", + "tab_calendar": "カレンダー", + "cal_no_events": "メンテナンスなし", + "cal_window_7": "7日間", + "cal_window_14": "14日間", + "cal_window_30": "30日間", + "cal_window_365": "1年間", + "cal_every_n_days": "{n}日ごと", + "cal_source_time": "時間ベース", + "cal_source_time_adaptive": "時間ベース(適応)", + "cal_source_sensor": "センサーベース", + "cal_predicted": "予測", + "cal_confidence_high": "信頼度:高", + "cal_confidence_medium": "信頼度:中", + "cal_confidence_low": "信頼度:低", + "budget_monthly": "月間予算", + "budget_yearly": "年間予算", + "groups": "グループ", + "new_group": "新規グループ", + "edit_group": "グループを編集", + "no_groups": "グループがまだありません", + "delete_group": "グループを削除", + "delete_group_confirm": "グループ「{name}」を削除しますか?", + "group_select_tasks": "タスクを選択", + "group_name_required": "名前は必須です", + "description_optional": "説明(任意)", + "selected": "選択中", + "loading_chart": "グラフデータを読み込み中...", + "hide_outliers": "外れ値を隠す(センサー誤差)", + "was_maintenance_needed": "このメンテナンスは必要でしたか?", + "feedback_needed": "必要だった", + "feedback_not_needed": "不要だった", + "feedback_not_sure": "わからない", + "suggested_interval": "推奨間隔", + "apply_suggestion": "適用", + "reanalyze": "再分析", + "reanalyze_result": "新しい分析結果", + "reanalyze_insufficient_data": "推奨を出すにはデータが不足しています", + "data_points": "データポイント", + "dismiss_suggestion": "閉じる", + "confidence_low": "低", + "confidence_medium": "中", + "confidence_high": "高", + "recommended": "推奨", + "seasonal_awareness": "季節対応", + "edit_seasonal_overrides": "季節係数を編集", + "seasonal_overrides_title": "季節係数(上書き)", + "seasonal_overrides_hint": "月ごとの係数(0.1〜5.0)。空欄=自動学習。", + "seasonal_override_invalid": "無効な値です", + "seasonal_override_range": "係数は0.1〜5.0の範囲で指定してください", + "clear_all": "すべてクリア", + "seasonal_chart_title": "季節係数", + "seasonal_learned": "学習値", + "seasonal_manual": "手動", + "month_jan": "1月", + "month_feb": "2月", + "month_mar": "3月", + "month_apr": "4月", + "month_may": "5月", + "month_jun": "6月", + "month_jul": "7月", + "month_aug": "8月", + "month_sep": "9月", + "month_oct": "10月", + "month_nov": "11月", + "month_dec": "12月", + "sensor_prediction": "センサー予測", + "degradation_trend": "傾向", + "trend_rising": "上昇", + "trend_falling": "下降", + "trend_stable": "安定", + "trend_insufficient_data": "データ不足", + "days_until_threshold": "しきい値までの日数", + "threshold_exceeded": "しきい値超過", + "environmental_adjustment": "環境係数", + "sensor_prediction_urgency": "センサー予測:約{days}日でしきい値到達", + "day_short": "日", + "weibull_reliability_curve": "信頼性曲線", + "weibull_failure_probability": "故障確率", + "weibull_r_squared": "適合度 R²", + "beta_early_failures": "初期故障", + "beta_random_failures": "偶発故障", + "beta_wear_out": "摩耗故障", + "beta_highly_predictable": "高い予測可能性", + "confidence_interval": "信頼区間", + "confidence_conservative": "保守的", + "confidence_aggressive": "楽観的", + "current_interval_marker": "現在の間隔", + "recommended_marker": "推奨", + "characteristic_life": "特性寿命", + "chart_mini_sparkline": "傾向スパークライン", + "chart_history": "費用と所要時間の履歴", + "chart_seasonal": "季節係数(12か月)", + "chart_weibull": "ワイブル信頼性曲線", + "chart_sparkline": "センサートリガー値のグラフ", + "days_progress": "経過日数", + "qr_code": "QRコード", + "qr_generating": "QRコードを生成中…", + "qr_error": "QRコードの生成に失敗しました。", + "qr_error_no_url": "HAのURLが設定されていません。設定 → システム → ネットワークで外部または内部URLを設定してください。", + "save_error": "保存に失敗しました。もう一度お試しください。", + "qr_print": "印刷", + "qr_download": "SVGをダウンロード", + "qr_action": "スキャン時の動作", + "qr_action_view": "メンテナンス情報を表示", + "qr_action_complete": "メンテナンスを完了にする", + "qr_url_mode": "リンク種別", + "qr_mode_companion": "コンパニオンアプリ", + "qr_mode_local": "ローカル(mDNS)", + "qr_mode_server": "サーバーURL", + "overview": "概要", + "analysis": "分析", + "recent_activities": "最近のアクティビティ", + "search_notes": "メモを検索", + "avg_cost": "平均費用", + "no_advanced_features": "高度な機能が有効になっていません", + "no_advanced_features_hint": "統合設定で「適応間隔」または「季節パターン」を有効にすると、ここに分析データが表示されます。", + "analysis_not_enough_data": "分析に必要なデータがまだ不足しています。", + "analysis_not_enough_data_hint": "ワイブル分析には完了済みメンテナンスが最低5件必要です。季節パターンは月あたり6件以上のデータポイントで表示されるようになります。", + "analysis_manual_task_hint": "間隔のない手動タスクは分析データを生成しません。", + "completions": "完了回数", + "current": "現在", + "shorter": "短く", + "longer": "長く", + "normal": "通常", + "disabled": "無効", + "compound_logic": "複合条件", + "compound": "複合(複数条件)", + "compound_logic_and": "AND — すべての条件が必要", + "compound_logic_or": "OR — いずれかの条件で発火", + "compound_help": "複数のセンサー条件を1つのトリガーにまとめます。", + "compound_no_conditions": "条件がありません — 少なくとも1つ追加してください。", + "compound_add_condition": "条件を追加", + "compound_condition": "条件", + "compound_remove_condition": "条件を削除", + "card_title": "タイトル", + "card_show_header": "統計付きヘッダーを表示", + "card_show_actions": "操作ボタンを表示", + "card_compact": "コンパクトモード", + "card_max_items": "最大表示件数(0=すべて)", + "card_filter_status": "状態で絞り込み", + "card_filter_status_help": "空欄=すべての状態を表示。", + "card_filter_objects": "対象で絞り込み", + "card_filter_objects_help": "空欄=すべての対象を表示。", + "card_filter_entities": "エンティティで絞り込み(entity_ids)", + "card_filter_entities_help": "この統合のsensor/binary_sensorエンティティを選択。空欄=すべて。", + "card_loading_objects": "対象を読み込み中…", + "card_load_error": "対象を読み込めませんでした — WebSocket接続を確認してください。", + "card_no_tasks_title": "メンテナンスタスクがまだありません", + "card_no_tasks_cta": "→ メンテナンスパネルで作成しましょう", + "no_objects": "対象がまだありません。", + "action_error": "操作に失敗しました。もう一度お試しください。", + "area_id_optional": "エリア(任意)", + "installation_date_optional": "設置日(任意)", + "warranty_expiry_optional": "保証期限(任意)", + "warranty": "保証", + "warranty_valid_until": "{date}まで有効", + "warranty_expires_in": "あと{days}日で期限切れ", + "warranty_expired": "期限切れ", + "cal_past_windows": "過去の期間", + "cal_forward_windows": "今後の期間", + "history_edit_title": "履歴項目を編集", + "history_edit_timestamp": "日時", + "manufacturer": "メーカー", + "model": "型番", + "area": "エリア", + "actions": "操作", + "view_mode_label": "表示", + "view_cards": "カード表示", + "view_table": "テーブル表示", + "objects_table_columns_label": "対象テーブルの列", + "objects_table_columns_hint": "対象のテーブル表示に表示する列を選択します。", + "custom_icon_optional": "アイコン(任意、例: mdi:wrench)", + "task_enabled": "タスク有効", + "skip_reason_prompt": "このタスクをスキップしますか?", + "reason_optional": "理由(任意)", + "reset_date_prompt": "タスクを実施済みにしますか?", + "reset_date_optional": "最終実施日(任意、既定は今日)", + "notes_label": "メモ", + "documentation_label": "ドキュメント", + "no_nfc_tag": "— タグなし —", + "dashboard": "ダッシュボード", + "tab_today": "今日", + "palette_placeholder": "オブジェクトとタスクを検索…", + "palette_no_results": "一致なし", + "palette_hint": "↑↓ 移動 · Enter 開く · Esc 閉じる", + "today_all_caught_up": "すべて完了!今週の予定はありません。", + "today_overdue": "期限切れ", + "today_due_today": "今日期限", + "today_this_week": "今週", + "settings": "設定", + "settings_features": "高度な機能", + "settings_features_desc": "高度な機能の有効/無効を切り替えます。無効にするとUIから非表示になりますが、データは削除されません。", + "feat_adaptive": "適応スケジューリング", + "feat_adaptive_desc": "メンテナンス履歴から最適な間隔を学習します", + "feat_predictions": "センサー予測", + "feat_predictions_desc": "センサーの劣化からトリガー日を予測します", + "feat_seasonal": "季節調整", + "feat_seasonal_desc": "季節パターンに基づいて間隔を調整します", + "feat_environmental": "環境相関", + "feat_environmental_desc": "温度・湿度と間隔を相関させます", + "feat_budget": "予算管理", + "feat_budget_desc": "月間・年間のメンテナンス費用を管理します", + "feat_groups": "タスクグループ", + "feat_groups_desc": "タスクを論理的なグループに整理します", + "feat_checklists": "チェックリスト", + "feat_checklists_desc": "タスク完了のための複数手順の手続き", + "settings_general": "一般", + "settings_default_warning": "既定の警告日数", + "settings_panel_enabled": "サイドバーパネル", + "settings_panel_title": "サイドバーパネルのタイトル", + "settings_notifications": "通知", + "settings_notify_service": "通知サービス", + "test_notification": "テスト通知", + "send_test": "テスト送信", + "testing": "送信中…", + "test_notification_success": "テスト通知を送信しました", + "test_notification_failed": "テスト通知に失敗しました", + "settings_notify_due_soon": "期限間近で通知", + "settings_notify_overdue": "期限超過で通知", + "settings_notify_triggered": "トリガー発生で通知", + "settings_interval_hours": "繰り返し間隔(時間、0=1回のみ)", + "settings_quiet_hours": "通知停止時間", + "settings_quiet_start": "開始", + "settings_quiet_end": "終了", + "settings_max_per_day": "1日あたりの最大通知数(0=無制限)", + "settings_bundling": "通知をまとめる", + "settings_bundle_threshold": "まとめるしきい値", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "モバイル操作ボタン", + "settings_action_complete": "「完了」ボタンを表示", + "settings_action_skip": "「スキップ」ボタンを表示", + "settings_action_snooze": "「スヌーズ」ボタンを表示", + "settings_weekly_digest": "週間まとめ", + "settings_weekly_digest_hint": "タスクがある月曜の朝に1件のまとめ通知。", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "スヌーズ時間(時間)", + "settings_budget": "予算", + "settings_currency": "通貨", + "settings_budget_monthly": "月間予算", + "settings_budget_yearly": "年間予算", + "settings_budget_alerts": "予算アラート", + "settings_budget_threshold": "アラートしきい値(%)", + "settings_import_export": "インポート/エクスポート", + "settings_export_json": "JSONをエクスポート", + "settings_export_yaml": "YAMLをエクスポート", + "settings_export_csv": "CSVをエクスポート", + "settings_import_csv": "CSVをインポート", + "settings_import_placeholder": "JSONまたはCSVの内容をここに貼り付け…", + "settings_import_btn": "インポート", + "settings_import_success": "{count}件の対象を正常にインポートしました。", + "settings_export_success": "エクスポートをダウンロードしました。", + "settings_saved": "設定を保存しました。", + "settings_include_history": "履歴を含める", + "sort_alphabetical": "アルファベット順", + "sort_due_soonest": "期限が近い順", + "sort_task_count": "タスク数", + "sort_area": "エリア", + "sort_assigned_user": "担当ユーザー", + "sort_group": "グループ", + "groupby_none": "グループ化なし", + "groupby_area": "エリア別", + "groupby_group": "グループ別", + "groupby_user": "ユーザー別", + "filter_label": "絞り込み", + "user_label": "ユーザー", + "sort_label": "並べ替え", + "group_by_label": "グループ化", + "state_value_help": "HAの状態値を使用してください(通常は小文字、例: \"on\"/\"off\")。保存時に大文字小文字は正規化されます。", + "target_changes_help": "トリガーが発生するまでに一致する遷移の回数(既定: 1)。", + "qr_print_title": "QRコードを印刷", + "qr_print_desc": "切り取って機器に貼れるQRコードの印刷ページを生成します。", + "qr_print_load": "対象を読み込む", + "qr_print_filter": "絞り込み", + "qr_print_objects": "対象", + "qr_print_actions": "操作", + "qr_print_url_mode": "リンク種別", + "qr_print_estimate": "QRコードの推定数", + "qr_print_over_limit": "上限は200件です。絞り込んでください", + "qr_print_generate": "QRコードを生成", + "qr_print_generating": "生成中…", + "qr_print_ready": "QRコードの準備完了", + "qr_print_print_button": "印刷", + "qr_print_empty": "生成する対象がありません", + "qr_action_skip": "スキップ", + "vacation_title": "休暇モード", + "vacation_active": "有効", + "vacation_ended": "終了済み", + "vacation_desc": "休暇を計画します。期間中とその前後のバッファ日数の間、通知が一時停止されます。特定のタスクは通知対象に戻せます。", + "vacation_enable": "休暇モードを有効にする", + "vacation_start": "開始", + "vacation_end": "終了", + "vacation_buffer": "バッファ(日)", + "vacation_exempt_title": "休暇中でも通知する", + "vacation_exempt_desc": "休暇中でも通知すべきタスクを選択します(例: 重要なプールの水質管理)。", + "vacation_load_tasks": "タスクを読み込む", + "vacation_preview_btn": "プレビューを表示", + "vacation_preview_affected": "件のタスクが影響を受けます", + "vacation_event_due_soon": "期限間近になります", + "vacation_event_overdue": "期限超過になります", + "vacation_event_triggered_est": "センサートリガーの可能性あり", + "vacation_sensor_based": "(センサーベース)", + "vacation_action_notify": "通知する", + "vacation_action_unsilence": "再び停止する", + "vacation_marked_complete": "完了にしました", + "vacation_marked_skip": "スキップしました", + "vacation_end_now": "今すぐ休暇を終了", + "add": "追加", + "show_stats": "統計とグラフを表示", + "hide_stats": "統計を非表示", + "adaptive_no_data": "適応分析に必要な完了履歴がまだ不足しています。このタスクをあと数回完了すると、間隔の推奨と信頼性グラフが利用できるようになります。", + "suggestion_applied": "推奨間隔を適用しました", + "vacation_mode": "休暇モード", + "vacation_status_active": "現在有効", + "vacation_status_scheduled": "予定済み", + "vacation_status_inactive": "無効", + "vacation_end_now_confirm": "今すぐ休暇を終了しますか?", + "vacation_exempt_count": "件 除外", + "vacation_advanced": "詳細…", + "vacation_open_panel": "パネルで開く", + "enable": "有効にする", + "saved": "保存しました", + "budget_monthly_set": "月間を設定", + "budget_yearly_set": "年間を設定", + "budget_advanced": "通貨、アラート…", + "budget_open_panel": "パネルで開く", + "groups_empty": "グループがまだありません。", + "group_new_placeholder": "グループを追加…", + "group_delete_confirm": "グループ「{name}」を削除しますか?", + "groups_manage_tasks": "タスクの割り当てを管理…", + "groups_open_panel": "パネルで開く", + "unassigned": "未割り当て", + "no_area": "エリアなし", + "has_overdue": "期限超過のタスクあり", + "object": "対象", + "settings_panel_access": "パネルアクセス", + "settings_panel_access_desc": "管理者は常にフルアクセスできます。作成・編集・削除を特定の非管理者に委任するには、これをオンにして下記で選択してください。それ以外のユーザーには完了とスキップのみが表示されます。", + "settings_operator_write": "選択したユーザーに作成・編集・削除を許可", + "settings_operator_write_desc": "オフ:管理者のみがコンテンツを変更できます。オン:下記で選択したユーザーもフルアクセスできます。", + "no_non_admin_users": "非管理者ユーザーが見つかりません。設定 → ユーザーで追加してください。", + "owner_label": "オーナー", + "feat_completion_actions": "完了時アクション", + "feat_completion_actions_desc": "タスクごとの完了時HAアクション+事前設定値付きのクイック完了QR。", + "on_complete_action_title": "完了時: HAアクションを実行(任意)", + "on_complete_action_desc": "タスク完了時にHAサービスを呼び出します — 例: デバイスのカウンターをリセット。", + "on_complete_action_service": "サービス", + "on_complete_action_target": "対象エンティティ", + "on_complete_action_target_hint": "注意: エンティティのドメインはサービスと一致する必要があります — 例: 'button.press'はbutton.*のみ、'counter.increment'はcounter.*のみ、'input_button.press'はinput_button.*のみで動作します。不一致の場合、アクションは何も表示せずに失敗します(HAのログに「Referenced entities ... missing or not currently available」と記録されます)。", + "on_complete_action_data": "データ(JSON、任意)", + "on_complete_action_test": "設定を検証", + "on_complete_action_test_success": "✓ 設定は有効です(アクションはタスク完了時のみ実行されます)", + "on_complete_action_test_failed": "失敗しました", + "quick_complete_defaults_title": "クイック完了の既定値(QRスキャン用、任意)", + "quick_complete_defaults_desc": "クイック完了QRスキャンの事前設定値。設定しない場合、QRは完了ダイアログを開きます。", + "quick_complete_defaults_notes": "メモ", + "quick_complete_defaults_cost": "費用", + "quick_complete_defaults_duration": "所要時間(分)", + "quick_complete_defaults_feedback_none": "フィードバックなし", + "quick_complete_defaults_feedback_needed": "必要だった", + "quick_complete_defaults_feedback_not_needed": "不要だった", + "quick_complete_success": "クイック完了にしました", + "show_all_objects": "すべての対象を表示", + "show_all_tasks": "絞り込みを解除 — すべてのタスクを表示", + "filter_to_overdue": "タスク一覧を期限超過のみに絞り込む", + "filter_to_due_soon": "タスク一覧を期限間近のみに絞り込む", + "filter_to_triggered": "タスク一覧をトリガー発生のみに絞り込む", + "open_task": "タスクを開く", + "show_details": "履歴と統計を表示", + "hide_details": "詳細を非表示", + "history_empty": "履歴がまだありません。", + "history_edit_button": "項目を編集", + "total_cost": "費用合計", + "times_performed": "実施回数", + "older_entries": "以前", + "open_in_panel": "メンテナンスパネルで開く", + "skip_reason": "スキップ理由(任意)", + "reset_to_date": "最終実施日を次の日付にリセット", + "delete_task_confirm": "このタスクと履歴を削除しますか?", + "delete_object_confirm": "この対象とすべてのタスクを削除しますか?", + "loading": "読み込み中…", + "archive": "アーカイブ", + "undo": "元に戻す", + "task_archived": "タスクをアーカイブしました", + "object_archived": "オブジェクトをアーカイブしました", + "unarchive": "アーカイブ解除", + "archived": "アーカイブ済み", + "show_archived": "アーカイブを表示", + "hide_archived": "アーカイブを非表示", + "confirm_archive_object": "このオブジェクトとそのタスクをアーカイブしますか?履歴は保持され、後で元に戻せます。", + "settings_archive": "アーカイブと保持", + "settings_archive_desc": "完了した単発タスクを削除せずに片付けます。アーカイブされた項目は非表示で動作しませんが、履歴とコストは保持されます。", + "settings_archive_oneoff_days": "完了した単発タスクを自動アーカイブする日数(0 = 無効)", + "settings_delete_archived_oneoff_days": "アーカイブ済み単発タスクを自動削除する日数(0 = しない)", + "archive_object": "オブジェクトをアーカイブ", + "unarchive_object": "オブジェクトのアーカイブを解除", + "documents": "ドキュメント", + "documents_empty": "まだドキュメントがありません。", + "doc_upload": "ファイルをアップロード", + "doc_uploading": "アップロード中…", + "doc_add_link": "リンクを追加", + "doc_link_url": "URL (https://…)", + "doc_link_title": "タイトル(任意)", + "doc_open": "開く", + "doc_delete_confirm": "「{name}」を削除しますか?", + "doc_too_large": "ファイルが大きすぎます(最大 25 MB)。", + "doc_upload_failed": "アップロードに失敗しました。", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "既に別の場所に保存済み — 共有され、追加容量はかかりません。", + "doc_dup_in_object": "このファイルは既にこのオブジェクトに添付されています。", + "doc_link_invalid": "http/https のリンクのみ許可されています。", + "doc_cat_manual": "取扱説明書", + "doc_cat_warranty": "保証書", + "doc_cat_invoice": "請求書", + "doc_cat_spare_parts": "交換部品", + "doc_cat_photo": "写真", + "doc_cat_other": "その他", + "doc_link_badge": "リンク", + "doc_storage_title": "ドキュメントストレージ", + "doc_storage_saved": "重複排除による節約", + "doc_storage_refresh": "更新", + "doc_download": "ダウンロード", + "doc_close": "閉じる", + "doc_camera": "写真を撮る", + "doc_drop_hint": "ここにファイルをドロップ", + "doc_task_none": "このタスクに紐付いたドキュメントはありません。", + "doc_link_existing": "ドキュメントを紐付け…", + "doc_attach": "紐付け", + "doc_unlink": "紐付け解除", + "doc_page": "ページ", + "chart_range_7d": "7日", + "chart_range_30d": "30日", + "chart_range_90d": "90日", + "chart_range_1y": "1年", + "chart_since_service": "前回のメンテナンス以降", + "chart_no_stats": "このエンティティには長期統計がありません — メンテナンス記録の値のみ表示しています", + "auto_complete_on_recovery": "センサー回復時に自動完了", + "auto_complete_on_recovery_help": "トリガーが自然に解消されたとき(塩の補充、フィルター交換など)に完了を記録し、最終実施日を更新します。", + "doc_search": "ドキュメントを検索…", + "doc_search_none": "一致するドキュメントはありません", + "link_device_optional": "既存のデバイスにリンク(任意)", + "parent_object_optional": "親オブジェクト(任意)", + "parent_none": "(親なし)", + "paused": "一時停止中", + "pause_object": "一時停止", + "resume_object": "再開", + "pause_until_prompt": "このオブジェクトのスケジュールを凍結します。再開するまで期限も通知も発生しません。必要に応じて自動再開日を設定できます。", + "pause_until_label": "再開日(任意)", + "object_paused": "オブジェクトを一時停止しました", + "object_resumed": "オブジェクトを再開しました — スケジュールを再スタート", + "object_paused_badge": "一時停止中", + "paused_until_label": "まで", + "replace_object": "置き換え…", + "replace_object_prompt": "このオブジェクトを退役させ、後継を作成します。履歴とコストは旧オブジェクトにアーカイブされたまま残り、タスクと文書は新しいオブジェクトへ引き継がれ、カウンターはリセットされます。", + "replace_name_label": "後継の名前", + "object_replaced": "オブジェクトを置き換えました — 後継を作成", + "reading_unit_label": "検針単位(例: kWh、m³)", + "reading_unit_help": "このタスクの完了時に記録値の横に表示されます。", + "reading_value_label": "検針値", + "reading_label": "検針", + "settings_templates_label": "テンプレートギャラリー", + "settings_templates_hint": "不要なテンプレートのチェックを外すと、「テンプレートから」の選択肢(パネルと設定フロー)から消えます。それ以外は何も変わらず、いつでも再有効化できます。", + "worksheet": "作業シート", + "worksheet_scan_view": "スキャンしてタスクを開く", + "worksheet_scan_complete": "スキャンして完了", + "worksheet_manual_excerpt": "マニュアル抜粋", + "worksheet_pages": "ページ", + "worksheet_printed": "印刷日", + "worksheet_never": "未実施" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/nb.json b/custom_components/maintenance_supporter/frontend/locales/nb.json new file mode 100644 index 0000000..dacefde --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/nb.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Vedlikehold", + "objects": "Objekter", + "tasks": "Oppgaver", + "overdue": "Forfalt", + "due_soon": "Forfaller snart", + "triggered": "Utløst", + "trigger_replaced": "Utløser erstattet", + "ok": "OK", + "all": "Alle", + "new_object": "+ Nytt objekt", + "templates_from": "Fra mal", + "templates_title": "Start fra en mal", + "templates_task_count": "{n} oppgaver", + "template_created": "Opprettet fra mal", + "onboard_hint": "Legg til ditt første objekt for å spore vedlikehold.", + "edit": "Rediger", + "duplicate": "Dupliser", + "task_duplicated": "Oppgave duplisert", + "object_duplicated": "Objekt duplisert", + "delete": "Slett", + "add_task": "+ Legg til oppgave", + "complete": "Fullfør", + "completed": "Fullført", + "skip": "Hopp over", + "skipped": "Hoppet over", + "missed": "Missed", + "reset": "Tilbakestill", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Avbryt", + "bulk_select": "Velg", + "bulk_select_all": "Velg alle", + "bulk_n_selected": "{n} valgt", + "bulk_completed": "{n} oppgaver fullført", + "bulk_archived": "{n} oppgaver arkivert", + "completing": "Fullfører…", + "interval": "Intervall", + "warning": "Advarsel", + "last_performed": "Sist utført", + "next_due": "Neste forfall", + "days_until_due": "Dager til forfall", + "avg_duration": "Gj.snittlig varighet", + "trigger": "Utløser", + "trigger_type": "Utløsertype", + "threshold_above": "Øvre grense", + "threshold_below": "Nedre grense", + "threshold": "Terskel", + "counter": "Teller", + "state_change": "Tilstandsendring", + "runtime": "Driftstid", + "runtime_hours": "Måldriftstid (timer)", + "target_value": "Målverdi", + "baseline": "Utgangspunkt", + "target_changes": "Antall endringer", + "for_minutes": "I (minutter)", + "time_based": "Tidsbasert", + "sensor_based": "Sensorbasert", + "manual": "Manuell", + "one_time": "Engangs", + "weekdays": "Ukedager", + "nth_weekday": "N-te ukedag i måneden", + "day_of_month": "Dag i måneden", + "recurrence_on_days": "Gjenta på", + "recurrence_occurrence": "Forekomst", + "recurrence_weekday": "Ukedag", + "recurrence_day": "Dag i måneden (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Siste", + "day_word": "Dag", + "interval_value": "Intervall", + "interval_unit": "Enhet", + "unit_days": "Dager", + "unit_weeks": "Uker", + "unit_months": "Måneder", + "unit_years": "År", + "due_date": "Forfallsdato", + "cleaning": "Rengjøring", + "inspection": "Inspeksjon", + "replacement": "Utskifting", + "calibration": "Kalibrering", + "service": "Service", + "reading": "Avlesning", + "custom": "Egendefinert", + "history": "Historikk", + "cost": "Kostnad", + "report_button": "Rapport", + "report_title": "Vedlikeholdsrapport", + "report_generated": "Generert", + "report_times_done": "Utført", + "report_total_cost": "Totalkostnad", + "report_every": "hver {n}. {unit}", + "report_notes": "Notater", + "report_col_type": "Type", + "report_col_status": "Status", + "report_col_schedule": "Tidsplan", + "duration": "Varighet", + "both": "Begge", + "trigger_val": "Utløserverdi", + "complete_title": "Fullfør: ", + "checklist": "Sjekkliste", + "checklist_steps_optional": "Sjekklistetrinn (valgfritt)", + "checklist_placeholder": "Rengjør filter\nBytt pakning\nTest trykk", + "checklist_help": "Ett trinn per linje. Maks 100 elementer.", + "err_too_long": "{field}: for lang (maks {n} tegn)", + "err_too_short": "{field}: for kort (min {n} tegn)", + "err_value_too_high": "{field}: for stor (maks {n})", + "err_value_too_low": "{field}: for liten (min {n})", + "err_required": "{field}: påkrevd", + "err_wrong_type": "{field}: feil type (forventet: {type})", + "err_invalid_choice": "{field}: ikke en tillatt verdi", + "err_invalid_value": "{field}: ugyldig verdi", + "feat_schedule_time": "Planlegging med klokkeslett", + "feat_schedule_time_desc": "Oppgaver forfaller på et bestemt klokkeslett i stedet for ved midnatt.", + "schedule_time_optional": "Forfaller kl. (valgfritt, TT:MM)", + "schedule_time_help": "Tom = midnatt (standard). HA-tidssone.", + "at_time": "kl.", + "notes_optional": "Notater (valgfritt)", + "cost_optional": "Kostnad (valgfritt)", + "duration_minutes": "Varighet i minutter (valgfritt)", + "days": "dager", + "day": "dag", + "today": "I dag", + "d_overdue": "d forfalt", + "no_tasks": "Ingen vedlikeholdsoppgaver ennå. Opprett et objekt for å komme i gang.", + "no_tasks_short": "Ingen oppgaver", + "no_history": "Ingen historikkoppføringer ennå.", + "show_all": "Vis alle", + "cost_duration_chart": "Kostnad og varighet", + "installed": "Installert", + "confirm_delete_object": "Slette dette objektet og alle dets oppgaver?", + "confirm_delete_task": "Slette denne oppgaven?", + "min": "Min", + "max": "Maks", + "save": "Lagre", + "saving": "Lagrer…", + "edit_task": "Rediger oppgave", + "new_task": "Ny vedlikeholdsoppgave", + "task_name": "Oppgavenavn", + "maintenance_type": "Vedlikeholdstype", + "priority": "Prioritet", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Lav", + "priority_normal": "Normal", + "priority_high": "Høy", + "schedule_type": "Planleggingstype", + "interval_days": "Intervall (dager)", + "warning_days": "Varslingsdager", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Sist utført (valgfritt)", + "interval_anchor": "Intervallforankring", + "anchor_completion": "Fra fullføringsdato", + "anchor_planned": "Fra planlagt dato (ingen avdrift)", + "edit_object": "Rediger objekt", + "name": "Navn", + "manufacturer_optional": "Produsent (valgfritt)", + "model_optional": "Modell (valgfritt)", + "serial_number_optional": "Serienummer (valgfritt)", + "serial_number_label": "S/N", + "documentation_url_label": "Bruksanvisning", + "object_notes_label": "Notater", + "sort_due_date": "Forfallsdato", + "sort_object": "Objektnavn", + "sort_type": "Type", + "sort_task_name": "Oppgavenavn", + "all_objects": "Alle objekter", + "tasks_lower": "oppgaver", + "no_tasks_yet": "Ingen oppgaver ennå", + "add_first_task": "Legg til første oppgave", + "trigger_configuration": "Utløserkonfigurasjon", + "entity_id": "Entitets-ID", + "comma_separated": "kommaseparert", + "entity_logic": "Entitetslogikk", + "entity_logic_any": "Hvilken som helst entitet utløser", + "entity_logic_all": "Alle entiteter må utløse", + "entities": "entiteter", + "attribute_optional": "Attributt (valgfritt, tomt = tilstand)", + "use_entity_state": "Bruk entitetstilstand (ingen attributt)", + "trigger_above": "Utløs over", + "trigger_below": "Utløs under", + "for_at_least_minutes": "I minst (minutter)", + "safety_interval_days": "Sikkerhetsintervall (dager, valgfritt)", + "safety_interval": "Sikkerhetsintervall (valgfritt)", + "delta_mode": "Deltamodus", + "from_state_optional": "Fra tilstand (valgfritt)", + "to_state_optional": "Til tilstand (valgfritt)", + "documentation_url_optional": "Dokumentasjons-URL (valgfritt)", + "object_notes_optional": "Notater (valgfritt)", + "nfc_tag_id_optional": "NFC-brikke-ID (valgfritt)", + "nfc_tags_empty_help": "Ingen NFC-brikker registrert i Home Assistant ennå.", + "nfc_tags_open_settings": "Åpne brikkeinnstillinger", + "nfc_tags_refresh": "Oppdater", + "environmental_entity_optional": "Miljøsensor (valgfritt)", + "environmental_entity_helper": "f.eks. sensor.outdoor_temperature — justerer intervallet basert på miljøforhold", + "environmental_attribute_optional": "Miljøattributt (valgfritt)", + "nfc_tag_id": "NFC-brikke-ID", + "nfc_linked": "NFC-brikke koblet", + "nfc_link_hint": "Klikk for å koble NFC-brikke", + "responsible_user": "Ansvarlig bruker", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ingen bruker tildelt)", + "all_users": "Alle brukere", + "my_tasks": "Mine oppgaver", + "tab_calendar": "Kalender", + "cal_no_events": "Ingen vedlikehold", + "cal_window_7": "7 dager", + "cal_window_14": "14 dager", + "cal_window_30": "30 dager", + "cal_window_365": "1 år", + "cal_every_n_days": "hver {n}. dag", + "cal_source_time": "Tidsbasert", + "cal_source_time_adaptive": "Tidsbasert (adaptiv)", + "cal_source_sensor": "Sensorbasert", + "cal_predicted": "forutsagt", + "cal_confidence_high": "høy sikkerhet", + "cal_confidence_medium": "middels sikkerhet", + "cal_confidence_low": "lav sikkerhet", + "budget_monthly": "Månedsbudsjett", + "budget_yearly": "Årsbudsjett", + "groups": "Grupper", + "new_group": "Ny gruppe", + "edit_group": "Rediger gruppe", + "no_groups": "Ingen grupper ennå", + "delete_group": "Slett gruppe", + "delete_group_confirm": "Slette gruppen '{name}'?", + "group_select_tasks": "Velg oppgaver", + "group_name_required": "Navn er påkrevd", + "description_optional": "Beskrivelse (valgfritt)", + "selected": "Valgt", + "loading_chart": "Laster diagramdata...", + "hide_outliers": "Skjul avvik (sensorfeil)", + "was_maintenance_needed": "Var dette vedlikeholdet nødvendig?", + "feedback_needed": "Nødvendig", + "feedback_not_needed": "Ikke nødvendig", + "feedback_not_sure": "Usikker", + "suggested_interval": "Foreslått intervall", + "apply_suggestion": "Bruk", + "reanalyze": "Analyser på nytt", + "reanalyze_result": "Ny analyse", + "reanalyze_insufficient_data": "Ikke nok data til å gi en anbefaling", + "data_points": "datapunkter", + "dismiss_suggestion": "Avvis", + "confidence_low": "Lav", + "confidence_medium": "Middels", + "confidence_high": "Høy", + "recommended": "anbefalt", + "seasonal_awareness": "Sesongbevissthet", + "edit_seasonal_overrides": "Rediger sesongfaktorer", + "seasonal_overrides_title": "Sesongfaktorer (overstyring)", + "seasonal_overrides_hint": "Faktor per måned (0,1–5,0). Tom = læres automatisk.", + "seasonal_override_invalid": "Ugyldig verdi", + "seasonal_override_range": "Faktor må være mellom 0,1 og 5,0", + "clear_all": "Fjern alle", + "seasonal_chart_title": "Sesongfaktorer", + "seasonal_learned": "Lært", + "seasonal_manual": "Manuell", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "Mai", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Des", + "sensor_prediction": "Sensorprediksjon", + "degradation_trend": "Trend", + "trend_rising": "Stigende", + "trend_falling": "Synkende", + "trend_stable": "Stabil", + "trend_insufficient_data": "Utilstrekkelige data", + "days_until_threshold": "Dager til terskel", + "threshold_exceeded": "Terskel overskredet", + "environmental_adjustment": "Miljøfaktor", + "sensor_prediction_urgency": "Sensoren forutsier terskel om ~{days} dager", + "day_short": "dag", + "weibull_reliability_curve": "Pålitelighetskurve", + "weibull_failure_probability": "Feilsannsynlighet", + "weibull_r_squared": "Tilpasning R²", + "beta_early_failures": "Tidlige feil", + "beta_random_failures": "Tilfeldige feil", + "beta_wear_out": "Slitasje", + "beta_highly_predictable": "Svært forutsigbar", + "confidence_interval": "Konfidensintervall", + "confidence_conservative": "Konservativ", + "confidence_aggressive": "Optimistisk", + "current_interval_marker": "Nåværende intervall", + "recommended_marker": "Anbefalt", + "characteristic_life": "Karakteristisk levetid", + "chart_mini_sparkline": "Trend-minigraf", + "chart_history": "Kostnads- og varighetshistorikk", + "chart_seasonal": "Sesongfaktorer, 12 måneder", + "chart_weibull": "Weibull-pålitelighetskurve", + "chart_sparkline": "Diagram over sensorutløserverdi", + "days_progress": "Dagsfremdrift", + "qr_code": "QR-kode", + "qr_generating": "Genererer QR-kode…", + "qr_error": "Kunne ikke generere QR-kode.", + "qr_error_no_url": "Ingen HA-URL konfigurert. Angi en ekstern eller intern URL under Innstillinger → System → Nettverk.", + "save_error": "Kunne ikke lagre. Prøv igjen.", + "qr_print": "Skriv ut", + "qr_download": "Last ned SVG", + "qr_action": "Handling ved skanning", + "qr_action_view": "Vis vedlikeholdsinfo", + "qr_action_complete": "Merk vedlikehold som fullført", + "qr_url_mode": "Lenketype", + "qr_mode_companion": "Companion-app", + "qr_mode_local": "Lokal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Oversikt", + "analysis": "Analyse", + "recent_activities": "Nylige aktiviteter", + "search_notes": "Søk i notater", + "avg_cost": "Gj.snittlig kostnad", + "no_advanced_features": "Ingen avanserte funksjoner aktivert", + "no_advanced_features_hint": "Aktiver «Adaptive intervaller» eller «Sesongmønstre» i integrasjonsinnstillingene for å se analysedata her.", + "analysis_not_enough_data": "Ikke nok data for analyse ennå.", + "analysis_not_enough_data_hint": "Weibull-analyse krever minst 5 fullførte vedlikehold; sesongmønstre blir synlige etter 6+ datapunkter per måned.", + "analysis_manual_task_hint": "Manuelle oppgaver uten intervall genererer ikke analysedata.", + "completions": "fullføringer", + "current": "Nåværende", + "shorter": "Kortere", + "longer": "Lengre", + "normal": "Normal", + "disabled": "Deaktivert", + "compound_logic": "Sammensatt logikk", + "compound": "Sammensatt (flere betingelser)", + "compound_logic_and": "OG — alle betingelser må utløses", + "compound_logic_or": "ELLER — én betingelse er nok", + "compound_help": "Kombiner flere sensorbetingelser til én utløser.", + "compound_no_conditions": "Ingen betingelser ennå — legg til minst én.", + "compound_add_condition": "Legg til betingelse", + "compound_condition": "Betingelse", + "compound_remove_condition": "Fjern betingelse", + "card_title": "Tittel", + "card_show_header": "Vis topptekst med statistikk", + "card_show_actions": "Vis handlingsknapper", + "card_compact": "Kompakt modus", + "card_max_items": "Maks elementer (0 = alle)", + "card_filter_status": "Filtrer etter status", + "card_filter_status_help": "Tom = vis alle statuser.", + "card_filter_objects": "Filtrer etter objekter", + "card_filter_objects_help": "Tom = vis alle objekter.", + "card_filter_entities": "Filtrer etter entiteter (entity_ids)", + "card_filter_entities_help": "Velg sensor-/binary_sensor-entiteter fra denne integrasjonen. Tom = alle.", + "card_loading_objects": "Laster objekter…", + "card_load_error": "Kunne ikke laste objekter — kontroller WebSocket-tilkoblingen.", + "card_no_tasks_title": "Ingen vedlikeholdsoppgaver ennå", + "card_no_tasks_cta": "→ Opprett en i vedlikeholdspanelet", + "no_objects": "Ingen objekter ennå.", + "action_error": "Handlingen mislyktes. Prøv igjen.", + "area_id_optional": "Område (valgfritt)", + "installation_date_optional": "Installasjonsdato (valgfritt)", + "warranty_expiry_optional": "Garanti utløper (valgfritt)", + "warranty": "Garanti", + "warranty_valid_until": "gyldig til {date}", + "warranty_expires_in": "utløper om {days} dager", + "warranty_expired": "utløpt", + "cal_past_windows": "Tidligere vinduer", + "cal_forward_windows": "Fremtidige vinduer", + "history_edit_title": "Rediger historikkoppføring", + "history_edit_timestamp": "Tidsstempel", + "manufacturer": "Produsent", + "model": "Modell", + "area": "Område", + "actions": "Handlinger", + "view_mode_label": "Visning", + "view_cards": "Kortvisning", + "view_table": "Tabellvisning", + "objects_table_columns_label": "Kolonner i objekttabell", + "objects_table_columns_hint": "Velg hvilke kolonner som vises i tabellvisningen for objekter.", + "custom_icon_optional": "Ikon (valgfritt, f.eks. mdi:wrench)", + "task_enabled": "Oppgave aktivert", + "skip_reason_prompt": "Hoppe over denne oppgaven?", + "reason_optional": "Årsak (valgfritt)", + "reset_date_prompt": "Merke oppgaven som utført?", + "reset_date_optional": "Sist utført-dato (valgfritt, standard er i dag)", + "notes_label": "Notater", + "documentation_label": "Dokumentasjon", + "no_nfc_tag": "— Ingen brikke —", + "dashboard": "Dashbord", + "tab_today": "I dag", + "palette_placeholder": "Søk objekter og oppgaver…", + "palette_no_results": "Ingen treff", + "palette_hint": "↑↓ naviger · Enter åpne · Esc lukk", + "today_all_caught_up": "Alt er ajour! Ingenting forfaller denne uken.", + "today_overdue": "Forfalt", + "today_due_today": "Forfaller i dag", + "today_this_week": "Denne uken", + "settings": "Innstillinger", + "settings_features": "Avanserte funksjoner", + "settings_features_desc": "Aktiver eller deaktiver avanserte funksjoner. Deaktivering skjuler dem fra grensesnittet, men sletter ikke data.", + "feat_adaptive": "Adaptiv planlegging", + "feat_adaptive_desc": "Lær optimale intervaller fra vedlikeholdshistorikken", + "feat_predictions": "Sensorprediksjoner", + "feat_predictions_desc": "Forutsi utløserdatoer fra sensorforringelse", + "feat_seasonal": "Sesongjusteringer", + "feat_seasonal_desc": "Juster intervaller basert på sesongmønstre", + "feat_environmental": "Miljøkorrelasjon", + "feat_environmental_desc": "Korreler intervaller med temperatur/fuktighet", + "feat_budget": "Budsjettsporing", + "feat_budget_desc": "Spor månedlige og årlige vedlikeholdsutgifter", + "feat_groups": "Oppgavegrupper", + "feat_groups_desc": "Organiser oppgaver i logiske grupper", + "feat_checklists": "Sjekklister", + "feat_checklists_desc": "Flertrinnsprosedyrer for fullføring av oppgaver", + "settings_general": "Generelt", + "settings_default_warning": "Standard varslingsdager", + "settings_panel_enabled": "Sidepanel", + "settings_panel_title": "Tittel på sidepanel", + "settings_notifications": "Varsler", + "settings_notify_service": "Varslingstjeneste", + "test_notification": "Testvarsel", + "send_test": "Send test", + "testing": "Sender…", + "test_notification_success": "Testvarsel sendt", + "test_notification_failed": "Testvarsel mislyktes", + "settings_notify_due_soon": "Varsle når noe forfaller snart", + "settings_notify_overdue": "Varsle når noe er forfalt", + "settings_notify_triggered": "Varsle når noe utløses", + "settings_interval_hours": "Gjentaksintervall (timer, 0 = én gang)", + "settings_quiet_hours": "Stilletimer", + "settings_quiet_start": "Start", + "settings_quiet_end": "Slutt", + "settings_max_per_day": "Maks varsler per dag (0 = ubegrenset)", + "settings_bundling": "Samle varsler", + "settings_bundle_threshold": "Samlingsterskel", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Handlingsknapper på mobil", + "settings_action_complete": "Vis «Fullfør»-knapp", + "settings_action_skip": "Vis «Hopp over»-knapp", + "settings_action_snooze": "Vis «Slumre»-knapp", + "settings_weekly_digest": "Ukentlig oversikt", + "settings_weekly_digest_hint": "Ett sammendragsvarsel mandag morgen når oppgaver forfaller.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Slumrevarighet (timer)", + "settings_budget": "Budsjett", + "settings_currency": "Valuta", + "settings_budget_monthly": "Månedsbudsjett", + "settings_budget_yearly": "Årsbudsjett", + "settings_budget_alerts": "Budsjettvarsler", + "settings_budget_threshold": "Varslingsterskel (%)", + "settings_import_export": "Import / eksport", + "settings_export_json": "Eksporter JSON", + "settings_export_yaml": "Eksporter YAML", + "settings_export_csv": "Eksporter CSV", + "settings_import_csv": "Importer CSV", + "settings_import_placeholder": "Lim inn JSON- eller CSV-innhold her…", + "settings_import_btn": "Importer", + "settings_import_success": "{count} objekter importert.", + "settings_export_success": "Eksport lastet ned.", + "settings_saved": "Innstilling lagret.", + "settings_include_history": "Inkluder historikk", + "sort_alphabetical": "Alfabetisk", + "sort_due_soonest": "Forfaller først", + "sort_task_count": "Antall oppgaver", + "sort_area": "Område", + "sort_assigned_user": "Tildelt bruker", + "sort_group": "Gruppe", + "groupby_none": "Ingen gruppering", + "groupby_area": "Etter område", + "groupby_group": "Etter gruppe", + "groupby_user": "Etter bruker", + "filter_label": "Filter", + "user_label": "Bruker", + "sort_label": "Sorter", + "group_by_label": "Grupper etter", + "state_value_help": "Bruk HA-tilstandsverdien (vanligvis små bokstaver, f.eks. \"on\"/\"off\"). Store/små bokstaver normaliseres ved lagring.", + "target_changes_help": "Antall samsvarende overganger før utløseren aktiveres (standard: 1).", + "qr_print_title": "Skriv ut QR-koder", + "qr_print_desc": "Generer en utskrivbar side med QR-koder som kan klippes ut og festes på utstyret ditt.", + "qr_print_load": "Last objekter", + "qr_print_filter": "Filter", + "qr_print_objects": "Objekter", + "qr_print_actions": "Handlinger", + "qr_print_url_mode": "Lenketype", + "qr_print_estimate": "Estimerte QR-koder", + "qr_print_over_limit": "grensen er 200, snevre inn filteret", + "qr_print_generate": "Generer QR-koder", + "qr_print_generating": "Genererer…", + "qr_print_ready": "QR-koder klare", + "qr_print_print_button": "Skriv ut", + "qr_print_empty": "Ingenting å generere", + "qr_action_skip": "Hopp over", + "vacation_title": "Feriemodus", + "vacation_active": "aktiv", + "vacation_ended": "avsluttet", + "vacation_desc": "Planlegg en ferie: varsler settes på pause i perioden pluss en buffer på noen dager. Du kan inkludere bestemte oppgaver igjen.", + "vacation_enable": "Aktiver feriemodus", + "vacation_start": "Start", + "vacation_end": "Slutt", + "vacation_buffer": "Buffer (dager)", + "vacation_exempt_title": "Varsle likevel under ferie", + "vacation_exempt_desc": "Velg oppgaver som fortsatt skal varsle under ferie (f.eks. kritisk bassengkjemi).", + "vacation_load_tasks": "Last oppgaver", + "vacation_preview_btn": "Vis forhåndsvisning", + "vacation_preview_affected": "oppgaver berørt", + "vacation_event_due_soon": "forfaller snart", + "vacation_event_overdue": "blir forfalt", + "vacation_event_triggered_est": "sensorutløsning mulig", + "vacation_sensor_based": "(sensorbasert)", + "vacation_action_notify": "Varsle likevel", + "vacation_action_unsilence": "Demp igjen", + "vacation_marked_complete": "Merket som fullført", + "vacation_marked_skip": "Hoppet over", + "vacation_end_now": "Avslutt ferie nå", + "add": "Legg til", + "show_stats": "Vis statistikk + grafer", + "hide_stats": "Skjul statistikk", + "adaptive_no_data": "Ikke nok fullføringshistorikk ennå for adaptiv analyse. Fullfør denne oppgaven noen flere ganger for å låse opp intervallanbefalinger og pålitelighetsdiagrammer.", + "suggestion_applied": "Foreslått intervall tatt i bruk", + "vacation_mode": "Feriemodus", + "vacation_status_active": "Aktiv nå", + "vacation_status_scheduled": "Planlagt", + "vacation_status_inactive": "Inaktiv", + "vacation_end_now_confirm": "Avslutte ferien umiddelbart?", + "vacation_exempt_count": "unntatt", + "vacation_advanced": "Avansert…", + "vacation_open_panel": "Åpne i panel", + "enable": "Aktiver", + "saved": "Lagret", + "budget_monthly_set": "Angi månedlig", + "budget_yearly_set": "Angi årlig", + "budget_advanced": "Valuta, varsler…", + "budget_open_panel": "Åpne i panel", + "groups_empty": "Ingen grupper ennå.", + "group_new_placeholder": "Legg til gruppe…", + "group_delete_confirm": "Slette gruppen \"{name}\"?", + "groups_manage_tasks": "Administrer oppgavetildelinger…", + "groups_open_panel": "Åpne i panel", + "unassigned": "Ikke tildelt", + "no_area": "Intet område", + "has_overdue": "Har forfalte oppgaver", + "object": "Objekt", + "settings_panel_access": "Paneltilgang", + "settings_panel_access_desc": "Administratorer har alltid full tilgang. For å delegere opprettelse, redigering og sletting til bestemte ikke-administratorer, slå dette på og velg dem nedenfor — alle andre ser kun Fullfør og Hopp over.", + "settings_operator_write": "Tillat valgte brukere å opprette, redigere og slette", + "settings_operator_write_desc": "Av: bare administratorer kan endre innhold. På: de valgte brukerne nedenfor får også full tilgang.", + "no_non_admin_users": "Ingen ikke-administratorbrukere funnet. Legg til noen under Innstillinger → Personer.", + "owner_label": "Eier", + "feat_completion_actions": "Fullføringshandlinger", + "feat_completion_actions_desc": "HA-handling per oppgave ved fullføring + hurtigfullføring med QR og forhåndsinnstilte verdier.", + "on_complete_action_title": "Ved fullføring: utløs HA-handling (valgfritt)", + "on_complete_action_desc": "Kaller en HA-tjeneste når oppgaven fullføres — f.eks. tilbakestille en teller på enheten.", + "on_complete_action_service": "Tjeneste", + "on_complete_action_target": "Målentitet", + "on_complete_action_target_hint": "Merk: entitetsdomenet må samsvare med tjenesten — f.eks. fungerer 'button.press' kun på button.*, 'counter.increment' kun på counter.*, 'input_button.press' kun på input_button.* osv. Ved manglende samsvar mislykkes handlingen i det stille (HA logger 'Referenced entities ... missing or not currently available').", + "on_complete_action_data": "Data (JSON, valgfritt)", + "on_complete_action_test": "Valider konfigurasjon", + "on_complete_action_test_success": "✓ Konfigurasjon gyldig (handlingen utløses kun ved fullføring av oppgaven)", + "on_complete_action_test_failed": "Mislyktes", + "quick_complete_defaults_title": "Standardverdier for hurtigfullføring (for QR-skanninger, valgfritt)", + "quick_complete_defaults_desc": "Forhåndsinnstilte verdier for hurtigfullføring via QR-skanning. Uten disse åpner QR-koden fullføringsdialogen.", + "quick_complete_defaults_notes": "Notater", + "quick_complete_defaults_cost": "Kostnad", + "quick_complete_defaults_duration": "Varighet (minutter)", + "quick_complete_defaults_feedback_none": "Ingen tilbakemelding", + "quick_complete_defaults_feedback_needed": "Var nødvendig", + "quick_complete_defaults_feedback_not_needed": "Ikke nødvendig", + "quick_complete_success": "Raskt merket som fullført", + "show_all_objects": "Vis alle objekter", + "show_all_tasks": "Fjern filter — vis alle oppgaver", + "filter_to_overdue": "Filtrer oppgavelisten til kun forfalte", + "filter_to_due_soon": "Filtrer oppgavelisten til kun forfaller snart", + "filter_to_triggered": "Filtrer oppgavelisten til kun utløste", + "open_task": "Åpne oppgave", + "show_details": "Vis historikk + statistikk", + "hide_details": "Skjul detaljer", + "history_empty": "Ingen historikk ennå.", + "history_edit_button": "Rediger oppføring", + "total_cost": "Total kostnad", + "times_performed": "Utført", + "older_entries": "eldre", + "open_in_panel": "Åpne i vedlikeholdspanelet", + "skip_reason": "Årsak for å hoppe over (valgfritt)", + "reset_to_date": "Tilbakestill sist utført til", + "delete_task_confirm": "Slette denne oppgaven og dens historikk?", + "delete_object_confirm": "Slette dette objektet og alle dets oppgaver?", + "loading": "Laster…", + "archive": "Arkivér", + "undo": "Angre", + "task_archived": "Oppgave arkivert", + "object_archived": "Objekt arkivert", + "unarchive": "Gjenopprett", + "archived": "Arkivert", + "show_archived": "Vis arkiverte", + "hide_archived": "Skjul arkiverte", + "confirm_archive_object": "Arkivere dette objektet og oppgavene? Historikken beholdes og kan gjenopprettes senere.", + "settings_archive": "Arkiv og oppbevaring", + "settings_archive_desc": "Legg bort fullførte engangsoppgaver uten å slette dem. Arkiverte elementer er skjult og inaktive, men beholder historikk og kostnad.", + "settings_archive_oneoff_days": "Arkivér fullførte engangsoppgaver automatisk etter (dager, 0 = av)", + "settings_delete_archived_oneoff_days": "Slett arkiverte engangsoppgaver automatisk etter (dager, 0 = aldri)", + "archive_object": "Arkivér objekt", + "unarchive_object": "Gjenopprett objekt", + "documents": "Dokumenter", + "documents_empty": "Ingen dokumenter ennå.", + "doc_upload": "Last opp fil", + "doc_uploading": "Laster opp…", + "doc_add_link": "Legg til lenke", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Tittel (valgfritt)", + "doc_open": "Åpne", + "doc_delete_confirm": "Slette \"{name}\"?", + "doc_too_large": "Filen er for stor (maks. 25 MB).", + "doc_upload_failed": "Opplasting mislyktes.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Allerede lagret et annet sted — delt, ingen ekstra plass.", + "doc_dup_in_object": "Denne filen er allerede knyttet til dette objektet.", + "doc_link_invalid": "Kun http/https-lenker er tillatt.", + "doc_cat_manual": "Bruksanvisning", + "doc_cat_warranty": "Garanti", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Reservedeler", + "doc_cat_photo": "Foto", + "doc_cat_other": "Annet", + "doc_link_badge": "Lenke", + "doc_storage_title": "Dokumentlagring", + "doc_storage_saved": "Spart ved deduplisering", + "doc_storage_refresh": "Oppdater", + "doc_download": "Last ned", + "doc_close": "Lukk", + "doc_camera": "Ta bilde", + "doc_drop_hint": "Slipp filer her", + "doc_task_none": "Ingen dokumenter knyttet til denne oppgaven.", + "doc_link_existing": "Knytt et dokument…", + "doc_attach": "Knytt", + "doc_unlink": "Fjern kobling", + "doc_page": "Side", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1å", + "chart_since_service": "siden siste vedlikehold", + "chart_no_stats": "Ingen langtidsstatistikk for denne enheten — viser bare verdier fra vedlikeholdsoppføringer", + "auto_complete_on_recovery": "Fullfør automatisk når sensoren normaliseres", + "auto_complete_on_recovery_help": "Registrerer en fullføring (setter sist utført) når utløseren løser seg selv — f.eks. salt etterfylt, filter byttet.", + "doc_search": "Søk i dokumenter…", + "doc_search_none": "Ingen samsvarende dokumenter", + "link_device_optional": "Koble til eksisterende enhet (valgfritt)", + "parent_object_optional": "Overordnet objekt (valgfritt)", + "parent_none": "(Ingen overordnet)", + "paused": "Satt på pause", + "pause_object": "Sett på pause", + "resume_object": "Gjenoppta", + "pause_until_prompt": "Frys objektets planer — ingenting forfaller og ingenting varsler før det gjenopptas. Angi eventuelt en dato for automatisk gjenopptakelse.", + "pause_until_label": "Gjenoppta den (valgfritt)", + "object_paused": "Objekt satt på pause", + "object_resumed": "Objekt gjenopptatt — planer startet på nytt", + "object_paused_badge": "På pause", + "paused_until_label": "til", + "replace_object": "Erstatt…", + "replace_object_prompt": "Pensjoner dette objektet og opprett en etterfølger. Historikk og kostnader forblir arkivert på det gamle; oppgaver og dokumenter følger med til det nye, tellere starter på nytt.", + "replace_name_label": "Etterfølgerens navn", + "object_replaced": "Objekt erstattet — etterfølger opprettet", + "reading_unit_label": "Avlesningsenhet (f.eks. kWh, m³)", + "reading_unit_help": "Vises ved siden av den registrerte verdien når oppgaven fullføres.", + "reading_value_label": "Avlest verdi", + "reading_label": "Avlesning", + "settings_templates_label": "Malgalleri", + "settings_templates_hint": "Fjern haken for maler du aldri trenger — de forsvinner fra \"Fra mal\"-velgerne (panel og config flow). Ellers endres ingenting; kan aktiveres igjen når som helst.", + "worksheet": "Arbeidsark", + "worksheet_scan_view": "Skann for å åpne oppgaven", + "worksheet_scan_complete": "Skann for å fullføre", + "worksheet_manual_excerpt": "Utdrag fra manualen", + "worksheet_pages": "sider", + "worksheet_printed": "Skrevet ut", + "worksheet_never": "Aldri" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/nl.json b/custom_components/maintenance_supporter/frontend/locales/nl.json new file mode 100644 index 0000000..ed43fbd --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/nl.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Onderhoud", + "objects": "Objecten", + "tasks": "Taken", + "overdue": "Achterstallig", + "due_soon": "Binnenkort", + "triggered": "Geactiveerd", + "ok": "OK", + "all": "Alle", + "new_object": "+ Nieuw object", + "templates_from": "Uit sjabloon", + "templates_title": "Begin met een sjabloon", + "templates_task_count": "{n} taken", + "template_created": "Aangemaakt uit sjabloon", + "onboard_hint": "Voeg je eerste object toe om onderhoud te volgen.", + "edit": "Bewerken", + "duplicate": "Dupliceren", + "task_duplicated": "Taak gedupliceerd", + "object_duplicated": "Object gedupliceerd", + "delete": "Verwijderen", + "add_task": "+ Taak", + "complete": "Voltooid", + "completed": "Voltooid", + "skip": "Overslaan", + "skipped": "Overgeslagen", + "missed": "Missed", + "reset": "Resetten", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Annuleren", + "bulk_select": "Selecteren", + "bulk_select_all": "Alles selecteren", + "bulk_n_selected": "{n} geselecteerd", + "bulk_completed": "{n} taken voltooid", + "bulk_archived": "{n} taken gearchiveerd", + "completing": "Wordt voltooid…", + "interval": "Interval", + "warning": "Waarschuwing", + "last_performed": "Laatst uitgevoerd", + "next_due": "Volgende keer", + "days_until_due": "Dagen tot vervaldatum", + "avg_duration": "Ø Duur", + "trigger": "Trigger", + "trigger_type": "Triggertype", + "threshold_above": "Bovengrens", + "threshold_below": "Ondergrens", + "threshold": "Drempelwaarde", + "counter": "Teller", + "state_change": "Statuswijziging", + "runtime": "Looptijd", + "runtime_hours": "Doellooptijd (uren)", + "target_value": "Doelwaarde", + "baseline": "Basislijn", + "target_changes": "Doelwijzigingen", + "for_minutes": "Voor (minuten)", + "time_based": "Tijdgebaseerd", + "sensor_based": "Sensorgebaseerd", + "manual": "Handmatig", + "one_time": "Eenmalig", + "weekdays": "Weekdagen", + "nth_weekday": "Zoveelste weekdag van de maand", + "day_of_month": "Dag van de maand", + "recurrence_on_days": "Herhalen op", + "recurrence_occurrence": "Voorkomen", + "recurrence_weekday": "Weekdag", + "recurrence_day": "Dag van de maand (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1e", + "ord_2": "2e", + "ord_3": "3e", + "ord_4": "4e", + "ord_5": "5e", + "ord_last": "Laatste", + "day_word": "Dag", + "interval_value": "Interval", + "interval_unit": "Eenheid", + "unit_days": "Dagen", + "unit_weeks": "Weken", + "unit_months": "Maanden", + "unit_years": "Jaren", + "due_date": "Vervaldatum", + "cleaning": "Reiniging", + "inspection": "Inspectie", + "replacement": "Vervanging", + "calibration": "Kalibratie", + "service": "Service", + "reading": "Aflezing", + "custom": "Aangepast", + "history": "Geschiedenis", + "cost": "Kosten", + "report_button": "Rapport", + "report_title": "Onderhoudsrapport", + "report_generated": "Gegenereerd", + "report_times_done": "Gedaan", + "report_total_cost": "Totale kosten", + "report_every": "elke {n} {unit}", + "report_notes": "Notities", + "report_col_type": "Type", + "report_col_status": "Status", + "report_col_schedule": "Schema", + "duration": "Duur", + "both": "Beide", + "trigger_val": "Triggerwaarde", + "complete_title": "Voltooid: ", + "checklist": "Checklist", + "checklist_steps_optional": "Checklist-stappen (optioneel)", + "checklist_placeholder": "Filter schoonmaken\nPakking vervangen\nDruk testen", + "checklist_help": "Eén stap per regel. Max. 100 items.", + "err_too_long": "{field}: te lang (max. {n} tekens)", + "err_too_short": "{field}: te kort (min. {n} tekens)", + "err_value_too_high": "{field}: te groot (max. {n})", + "err_value_too_low": "{field}: te klein (min. {n})", + "err_required": "{field}: verplicht", + "err_wrong_type": "{field}: verkeerd type (verwacht: {type})", + "err_invalid_choice": "{field}: niet-toegestane waarde", + "err_invalid_value": "{field}: ongeldige waarde", + "feat_schedule_time": "Tijd-van-dag-planning", + "feat_schedule_time_desc": "Taken vervallen op een specifieke tijd in plaats van middernacht.", + "schedule_time_optional": "Vervaldagstijd (optioneel, HH:MM)", + "schedule_time_help": "Leeg = middernacht (standaard). HA-tijdzone.", + "at_time": "om", + "notes_optional": "Notities (optioneel)", + "cost_optional": "Kosten (optioneel)", + "duration_minutes": "Duur in minuten (optioneel)", + "days": "dagen", + "day": "dag", + "today": "Vandaag", + "d_overdue": "d achterstallig", + "no_tasks": "Geen onderhoudstaken. Maak een object aan om te beginnen.", + "no_tasks_short": "Geen taken", + "no_history": "Nog geen geschiedenisitems.", + "show_all": "Alles tonen", + "cost_duration_chart": "Kosten & Duur", + "installed": "Geïnstalleerd", + "confirm_delete_object": "Dit object en alle bijbehorende taken verwijderen?", + "confirm_delete_task": "Deze taak verwijderen?", + "min": "Min", + "max": "Max", + "save": "Opslaan", + "saving": "Opslaan…", + "edit_task": "Taak bewerken", + "new_task": "Nieuwe onderhoudstaak", + "task_name": "Taaknaam", + "maintenance_type": "Onderhoudstype", + "priority": "Prioriteit", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Laag", + "priority_normal": "Normaal", + "priority_high": "Hoog", + "schedule_type": "Planningstype", + "interval_days": "Interval (dagen)", + "warning_days": "Waarschuwingsdagen", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Laatst uitgevoerd (optioneel)", + "interval_anchor": "Interval-anker", + "anchor_completion": "Vanaf voltooiing", + "anchor_planned": "Vanaf geplande datum (geen drift)", + "edit_object": "Object bewerken", + "name": "Naam", + "manufacturer_optional": "Fabrikant (optioneel)", + "model_optional": "Model (optioneel)", + "serial_number_optional": "Serienummer (optioneel)", + "serial_number_label": "S/N", + "documentation_url_label": "Handleiding", + "object_notes_label": "Notities", + "sort_due_date": "Vervaldatum", + "sort_object": "Objectnaam", + "sort_type": "Type", + "sort_task_name": "Taaknaam", + "all_objects": "Alle objecten", + "tasks_lower": "taken", + "no_tasks_yet": "Nog geen taken", + "add_first_task": "Eerste taak toevoegen", + "trigger_configuration": "Triggerconfiguratie", + "entity_id": "Entiteits-ID", + "comma_separated": "kommagescheiden", + "entity_logic": "Entiteitslogica", + "entity_logic_any": "Elke entiteit triggert", + "entity_logic_all": "Alle entiteiten moeten triggeren", + "entities": "entiteiten", + "attribute_optional": "Attribuut (optioneel, leeg = status)", + "use_entity_state": "Entiteitsstatus gebruiken (geen attribuut)", + "trigger_above": "Activeren als boven", + "trigger_below": "Activeren als onder", + "for_at_least_minutes": "Voor minstens (minuten)", + "safety_interval_days": "Veiligheidsinterval (dagen, optioneel)", + "safety_interval": "Veiligheidsinterval (optioneel)", + "delta_mode": "Deltamodus", + "from_state_optional": "Van status (optioneel)", + "to_state_optional": "Naar status (optioneel)", + "documentation_url_optional": "Documentatie-URL (optioneel)", + "object_notes_optional": "Notities (optioneel)", + "nfc_tag_id_optional": "NFC-tag-ID (optioneel)", + "nfc_tags_empty_help": "Nog geen NFC-tags geregistreerd in Home Assistant.", + "nfc_tags_open_settings": "Tag-instellingen openen", + "nfc_tags_refresh": "Vernieuwen", + "environmental_entity_optional": "Omgevingssensor (optioneel)", + "environmental_entity_helper": "bv. sensor.buitentemperatuur — past het interval aan op basis van omgevingswaarden", + "environmental_attribute_optional": "Omgevingsattribuut (optioneel)", + "nfc_tag_id": "NFC-tag-ID", + "nfc_linked": "NFC-tag gekoppeld", + "nfc_link_hint": "Klik om NFC-tag te koppelen", + "responsible_user": "Verantwoordelijke gebruiker", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Geen gebruiker toegewezen)", + "all_users": "Alle gebruikers", + "my_tasks": "Mijn taken", + "tab_calendar": "Kalender", + "cal_no_events": "Geen onderhoud", + "cal_window_7": "7 dagen", + "cal_window_14": "14 dagen", + "cal_window_30": "30 dagen", + "cal_window_365": "1 jaar", + "cal_every_n_days": "elke {n} dagen", + "cal_source_time": "Tijd-gebaseerd", + "cal_source_time_adaptive": "Tijd-gebaseerd (adaptief)", + "cal_source_sensor": "Sensor-gebaseerd", + "cal_predicted": "voorspeld", + "cal_confidence_high": "hoge zekerheid", + "cal_confidence_medium": "gemiddelde zekerheid", + "cal_confidence_low": "lage zekerheid", + "budget_monthly": "Maandbudget", + "budget_yearly": "Jaarbudget", + "groups": "Groepen", + "new_group": "Nieuwe groep", + "edit_group": "Groep bewerken", + "no_groups": "Nog geen groepen", + "delete_group": "Groep verwijderen", + "delete_group_confirm": "Groep '{name}' verwijderen?", + "group_select_tasks": "Taken selecteren", + "group_name_required": "Naam vereist", + "description_optional": "Beschrijving (optioneel)", + "selected": "Geselecteerd", + "loading_chart": "Grafiekgegevens laden...", + "hide_outliers": "Uitschieters verbergen (sensorfouten)", + "was_maintenance_needed": "Was dit onderhoud nodig?", + "feedback_needed": "Nodig", + "feedback_not_needed": "Niet nodig", + "feedback_not_sure": "Niet zeker", + "suggested_interval": "Voorgesteld interval", + "apply_suggestion": "Toepassen", + "reanalyze": "Opnieuw analyseren", + "reanalyze_result": "Nieuwe analyse", + "reanalyze_insufficient_data": "Onvoldoende gegevens voor een aanbeveling", + "data_points": "datapunten", + "dismiss_suggestion": "Negeren", + "confidence_low": "Laag", + "confidence_medium": "Gemiddeld", + "confidence_high": "Hoog", + "recommended": "aanbevolen", + "seasonal_awareness": "Seizoensbewustzijn", + "edit_seasonal_overrides": "Seizoensfactoren bewerken", + "seasonal_overrides_title": "Seizoensfactoren (override)", + "seasonal_overrides_hint": "Factor per maand (0.1–5.0). Leeg = automatisch geleerd.", + "seasonal_override_invalid": "Ongeldige waarde", + "seasonal_override_range": "Factor moet tussen 0.1 en 5.0 liggen", + "clear_all": "Alles wissen", + "seasonal_chart_title": "Seizoensfactoren", + "seasonal_learned": "Geleerd", + "seasonal_manual": "Handmatig", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mrt", + "month_apr": "Apr", + "month_may": "Mei", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Dec", + "sensor_prediction": "Sensorvoorspelling", + "degradation_trend": "Trend", + "trend_rising": "Stijgend", + "trend_falling": "Dalend", + "trend_stable": "Stabiel", + "trend_insufficient_data": "Onvoldoende gegevens", + "days_until_threshold": "Dagen tot drempelwaarde", + "threshold_exceeded": "Drempelwaarde overschreden", + "environmental_adjustment": "Omgevingsfactor", + "sensor_prediction_urgency": "Sensor voorspelt drempelwaarde in ~{days} dagen", + "day_short": "dag", + "weibull_reliability_curve": "Betrouwbaarheidscurve", + "weibull_failure_probability": "Faalkans", + "weibull_r_squared": "Fit R²", + "beta_early_failures": "Vroege uitval", + "beta_random_failures": "Willekeurige uitval", + "beta_wear_out": "Slijtage", + "beta_highly_predictable": "Zeer voorspelbaar", + "confidence_interval": "Betrouwbaarheidsinterval", + "confidence_conservative": "Conservatief", + "confidence_aggressive": "Optimistisch", + "current_interval_marker": "Huidig interval", + "recommended_marker": "Aanbevolen", + "characteristic_life": "Karakteristieke levensduur", + "chart_mini_sparkline": "Trend-sparkline", + "chart_history": "Kosten- en duurgeschiedenis", + "chart_seasonal": "Seizoensfactoren, 12 maanden", + "chart_weibull": "Weibull-betrouwbaarheidscurve", + "chart_sparkline": "Sensor-triggerwaardegrafiek", + "days_progress": "Dagenvoortgang", + "qr_code": "QR-code", + "qr_generating": "QR-code genereren…", + "qr_error": "QR-code kon niet worden gegenereerd.", + "qr_error_no_url": "Geen HA-URL geconfigureerd. Stel een externe of interne URL in via Instellingen → Systeem → Netwerk.", + "save_error": "Opslaan mislukt. Probeer het opnieuw.", + "qr_print": "Afdrukken", + "qr_download": "SVG downloaden", + "qr_action": "Actie bij scannen", + "qr_action_view": "Onderhoudsinfo bekijken", + "qr_action_complete": "Onderhoud als voltooid markeren", + "qr_url_mode": "Linktype", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokaal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Overzicht", + "analysis": "Analyse", + "recent_activities": "Recente activiteiten", + "search_notes": "Notities doorzoeken", + "avg_cost": "Ø Kosten", + "no_advanced_features": "Geen geavanceerde functies ingeschakeld", + "no_advanced_features_hint": "Schakel „Adaptieve Intervallen” of „Seizoenpatronen” in via de integratie-instellingen om hier analysegegevens te zien.", + "analysis_not_enough_data": "Nog niet genoeg gegevens voor analyse.", + "analysis_not_enough_data_hint": "Weibull-analyse vereist minstens 5 voltooide onderhoudsbeurten; seizoenspatronen worden zichtbaar na 6+ datapunten per maand.", + "analysis_manual_task_hint": "Handmatige taken zonder interval genereren geen analysegegevens.", + "completions": "voltooiingen", + "current": "Huidig", + "shorter": "Korter", + "longer": "Langer", + "normal": "Normaal", + "disabled": "Uitgeschakeld", + "compound_logic": "Samengestelde logica", + "compound": "Samengesteld (meerdere voorwaarden)", + "compound_logic_and": "EN — alle voorwaarden moeten activeren", + "compound_logic_or": "OF — één voorwaarde is genoeg", + "compound_help": "Combineer meerdere sensorvoorwaarden tot één trigger.", + "compound_no_conditions": "Nog geen voorwaarden — voeg er ten minste één toe.", + "compound_add_condition": "Voorwaarde toevoegen", + "compound_condition": "Voorwaarde", + "compound_remove_condition": "Voorwaarde verwijderen", + "card_title": "Titel", + "card_show_header": "Koptekst met statistieken tonen", + "card_show_actions": "Actieknoppen tonen", + "card_compact": "Compacte modus", + "card_max_items": "Max items (0 = alle)", + "card_filter_status": "Filteren op status", + "card_filter_status_help": "Leeg = alle statussen tonen.", + "card_filter_objects": "Filteren op objecten", + "card_filter_objects_help": "Leeg = alle objecten tonen.", + "card_filter_entities": "Filteren op entiteiten (entity_ids)", + "card_filter_entities_help": "Kies sensor/binary_sensor entiteiten van deze integratie. Leeg = alle.", + "card_loading_objects": "Objecten laden…", + "card_load_error": "Objecten konden niet worden geladen — controleer de WebSocket-verbinding.", + "card_no_tasks_title": "Nog geen onderhoudstaken", + "card_no_tasks_cta": "→ Maak er een aan in het Maintenance-paneel", + "no_objects": "Nog geen objecten.", + "action_error": "Actie mislukt. Probeer het opnieuw.", + "area_id_optional": "Gebied (optioneel)", + "installation_date_optional": "Installatiedatum (optioneel)", + "warranty_expiry_optional": "Garantie verloopt (optioneel)", + "warranty": "Garantie", + "warranty_valid_until": "geldig tot {date}", + "warranty_expires_in": "verloopt over {days} dagen", + "warranty_expired": "verlopen", + "cal_past_windows": "Eerdere vensters", + "cal_forward_windows": "Toekomstige vensters", + "history_edit_title": "Geschiedenisitem bewerken", + "history_edit_timestamp": "Tijdstempel", + "manufacturer": "Fabrikant", + "model": "Model", + "area": "Gebied", + "actions": "Acties", + "view_mode_label": "Weergave", + "view_cards": "Kaartweergave", + "view_table": "Tabelweergave", + "objects_table_columns_label": "Kolommen objecttabel", + "objects_table_columns_hint": "Kies welke kolommen in de tabelweergave van objecten verschijnen.", + "custom_icon_optional": "Icoon (optioneel, bijv. mdi:wrench)", + "task_enabled": "Taak ingeschakeld", + "skip_reason_prompt": "Deze taak overslaan?", + "reason_optional": "Reden (optioneel)", + "reset_date_prompt": "Taak markeren als uitgevoerd?", + "reset_date_optional": "Laatste uitvoeringsdatum (optioneel, standaard vandaag)", + "notes_label": "Notities", + "documentation_label": "Documentatie", + "no_nfc_tag": "— Geen tag —", + "dashboard": "Dashboard", + "tab_today": "Vandaag", + "palette_placeholder": "Objecten en taken zoeken…", + "palette_no_results": "Geen resultaten", + "palette_hint": "↑↓ navigeren · Enter openen · Esc sluiten", + "today_all_caught_up": "Helemaal bij! Niets te doen deze week.", + "today_overdue": "Achterstallig", + "today_due_today": "Vandaag te doen", + "today_this_week": "Deze week", + "settings": "Instellingen", + "settings_features": "Geavanceerde functies", + "settings_features_desc": "Schakel geavanceerde functies in of uit. Uitschakelen verbergt ze in de interface maar verwijdert geen gegevens.", + "feat_adaptive": "Adaptieve planning", + "feat_adaptive_desc": "Leer optimale intervallen uit onderhoudsgeschiedenis", + "feat_predictions": "Sensorvoorspellingen", + "feat_predictions_desc": "Voorspel triggerdatums op basis van sensordegradatie", + "feat_seasonal": "Seizoensaanpassingen", + "feat_seasonal_desc": "Pas intervallen aan op seizoenspatronen", + "feat_environmental": "Omgevingscorrelatie", + "feat_environmental_desc": "Correleer intervallen met temperatuur/vochtigheid", + "feat_budget": "Budgetbeheer", + "feat_budget_desc": "Volg maandelijkse en jaarlijkse onderhoudsuitgaven", + "feat_groups": "Taakgroepen", + "feat_groups_desc": "Organiseer taken in logische groepen", + "feat_checklists": "Checklists", + "feat_checklists_desc": "Meerstaps procedures voor taakvoltooiing", + "settings_general": "Algemeen", + "settings_default_warning": "Standaard waarschuwingsdagen", + "settings_panel_enabled": "Zijbalkpaneel", + "settings_panel_title": "Titel zijbalkpaneel", + "settings_notifications": "Meldingen", + "settings_notify_service": "Meldingsservice", + "test_notification": "Testmelding", + "send_test": "Test versturen", + "testing": "Verzenden…", + "test_notification_success": "Testmelding verzonden", + "test_notification_failed": "Testmelding mislukt", + "settings_notify_due_soon": "Melding bij bijna verlopen", + "settings_notify_overdue": "Melding bij achterstallig", + "settings_notify_triggered": "Melding bij geactiveerd", + "settings_interval_hours": "Herhalingsinterval (uren, 0 = eenmalig)", + "settings_quiet_hours": "Stille uren", + "settings_quiet_start": "Start", + "settings_quiet_end": "Einde", + "settings_max_per_day": "Max meldingen per dag (0 = onbeperkt)", + "settings_bundling": "Meldingen bundelen", + "settings_bundle_threshold": "Bundeldrempel", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobiele actieknoppen", + "settings_action_complete": "Knop 'Voltooid' tonen", + "settings_action_skip": "Knop 'Overslaan' tonen", + "settings_action_snooze": "Knop 'Snooze' tonen", + "settings_weekly_digest": "Wekelijks overzicht", + "settings_weekly_digest_hint": "Eén samenvattingsmelding op maandagochtend als er taken zijn.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Snoozeduur (uren)", + "settings_budget": "Budget", + "settings_currency": "Valuta", + "settings_budget_monthly": "Maandbudget", + "settings_budget_yearly": "Jaarbudget", + "settings_budget_alerts": "Budgetwaarschuwingen", + "settings_budget_threshold": "Waarschuwingsdrempel (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "JSON exporteren", + "settings_export_yaml": "YAML exporteren", + "settings_export_csv": "CSV exporteren", + "settings_import_csv": "CSV importeren", + "settings_import_placeholder": "Plak JSON- of CSV-inhoud hier…", + "settings_import_btn": "Importeren", + "settings_import_success": "{count} objecten succesvol geïmporteerd.", + "settings_export_success": "Export gedownload.", + "settings_saved": "Instelling opgeslagen.", + "settings_include_history": "Geschiedenis meenemen", + "sort_alphabetical": "Alfabetisch", + "sort_due_soonest": "Eerst vervallend", + "sort_task_count": "Aantal taken", + "sort_area": "Gebied", + "sort_assigned_user": "Toegewezen gebruiker", + "sort_group": "Groep", + "groupby_none": "Geen groepering", + "groupby_area": "Per gebied", + "groupby_group": "Per groep", + "groupby_user": "Per gebruiker", + "filter_label": "Filter", + "user_label": "Gebruiker", + "sort_label": "Sorteren", + "group_by_label": "Groeperen op", + "state_value_help": "Gebruik de HA-statuswaarde (meestal in kleine letters, bv. \"on\"/\"off\"). Hoofdletters worden bij opslaan genormaliseerd.", + "target_changes_help": "Aantal overeenkomende overgangen voordat de trigger wordt geactiveerd (standaard: 1).", + "qr_print_title": "QR-codes afdrukken", + "qr_print_desc": "Genereer een afdrukpagina met QR-codes om uit te knippen en op je apparaten te plakken.", + "qr_print_load": "Objecten laden", + "qr_print_filter": "Filter", + "qr_print_objects": "Objecten", + "qr_print_actions": "Acties", + "qr_print_url_mode": "Linktype", + "qr_print_estimate": "Geschatte QR-codes", + "qr_print_over_limit": "max is 200, beperk de filter", + "qr_print_generate": "QR-codes genereren", + "qr_print_generating": "Genereren…", + "qr_print_ready": "QR-codes klaar", + "qr_print_print_button": "Afdrukken", + "qr_print_empty": "Niets te genereren", + "qr_action_skip": "Overslaan", + "vacation_title": "Vakantiemodus", + "vacation_active": "actief", + "vacation_ended": "beëindigd", + "vacation_desc": "Plan je vakantie: meldingen worden gepauzeerd tijdens de periode plus een buffer van dagen. Je kunt per taak uitzonderingen instellen.", + "vacation_enable": "Vakantiemodus inschakelen", + "vacation_start": "Begin", + "vacation_end": "Einde", + "vacation_buffer": "Buffer (dagen)", + "vacation_exempt_title": "Toch melden tijdens vakantie", + "vacation_exempt_desc": "Kies taken die ook tijdens vakantie meldingen moeten geven (bv. kritische zwembadchemie).", + "vacation_load_tasks": "Taken laden", + "vacation_preview_btn": "Voorvertoning", + "vacation_preview_affected": "taken betrokken", + "vacation_event_due_soon": "wordt binnenkort verschuldigd", + "vacation_event_overdue": "wordt achterstallig", + "vacation_event_triggered_est": "sensortrigger mogelijk", + "vacation_sensor_based": "(sensor-gebaseerd)", + "vacation_action_notify": "Toch melden", + "vacation_action_unsilence": "Weer dempen", + "vacation_marked_complete": "Als voltooid gemarkeerd", + "vacation_marked_skip": "Overgeslagen", + "vacation_end_now": "Vakantie nu beëindigen", + "unassigned": "Niet toegewezen", + "no_area": "Geen gebied", + "has_overdue": "Heeft achterstallige taken", + "object": "Object", + "settings_panel_access": "Paneel-toegang", + "settings_panel_access_desc": "Admins hebben altijd volledige toegang. Om aanmaken, bewerken en verwijderen aan specifieke niet-admins te delegeren, schakel dit in en selecteer hen hieronder — alle anderen zien alleen Voltooien en Overslaan.", + "settings_operator_write": "Geselecteerde gebruikers laten aanmaken, bewerken en verwijderen", + "settings_operator_write_desc": "Uit: alleen admins kunnen inhoud wijzigen. Aan: de geselecteerde gebruikers hieronder krijgen ook volledige toegang.", + "no_non_admin_users": "Geen niet-admin gebruikers gevonden. Voeg ze toe in Instellingen → Personen.", + "owner_label": "Eigenaar", + "feat_completion_actions": "Voltooiings-acties", + "feat_completion_actions_desc": "Per taak HA-actie bij voltooien + snel-voltooien-QR met vooraf ingestelde waarden.", + "on_complete_action_title": "Bij voltooien: HA-actie uitvoeren (optioneel)", + "on_complete_action_desc": "Roept een HA-service aan wanneer de taak is voltooid — bv. een teller op het apparaat resetten.", + "on_complete_action_service": "Service", + "on_complete_action_target": "Doel-entiteit", + "on_complete_action_data": "Data (JSON, optioneel)", + "on_complete_action_test": "Actie testen", + "on_complete_action_test_success": "Geslaagd", + "on_complete_action_test_failed": "Mislukt", + "quick_complete_defaults_title": "Snel-voltooien-standaardwaarden (voor QR-scans, optioneel)", + "quick_complete_defaults_desc": "Vooraf ingestelde waarden voor snel-voltooien-QR. Zonder deze opent de QR de voltooi-dialoog.", + "quick_complete_defaults_notes": "Notities", + "quick_complete_defaults_cost": "Kosten", + "quick_complete_defaults_duration": "Duur (minuten)", + "quick_complete_defaults_feedback_none": "Geen feedback", + "quick_complete_defaults_feedback_needed": "Was nodig", + "quick_complete_defaults_feedback_not_needed": "Niet nodig", + "quick_complete_success": "Snel als voltooid gemarkeerd", + "trigger_replaced": "Trigger vervangen", + "add": "Toevoegen", + "show_stats": "Statistieken + grafieken tonen", + "hide_stats": "Statistieken verbergen", + "adaptive_no_data": "Nog niet genoeg voltooiingsgeschiedenis voor adaptieve analyse. Voer deze taak nog een paar keer uit om intervaladviezen en betrouwbaarheidsgrafieken te ontgrendelen.", + "suggestion_applied": "Voorgesteld interval toegepast", + "vacation_mode": "Vakantiemodus", + "vacation_status_active": "Nu actief", + "vacation_status_scheduled": "Gepland", + "vacation_status_inactive": "Inactief", + "vacation_end_now_confirm": "Vakantie onmiddellijk beëindigen?", + "vacation_exempt_count": "uitgezonderd", + "vacation_advanced": "Geavanceerd…", + "vacation_open_panel": "Openen in paneel", + "enable": "Inschakelen", + "saved": "Opgeslagen", + "budget_monthly_set": "Maandelijks instellen", + "budget_yearly_set": "Jaarlijks instellen", + "budget_advanced": "Valuta, waarschuwingen…", + "budget_open_panel": "Openen in paneel", + "groups_empty": "Nog geen groepen.", + "group_new_placeholder": "Groep toevoegen…", + "group_delete_confirm": "Groep \"{name}\" verwijderen?", + "groups_manage_tasks": "Taaktoewijzingen beheren…", + "groups_open_panel": "Openen in paneel", + "on_complete_action_target_hint": "Let op: het domein van de entiteit moet bij de service passen — bijv. 'button.press' werkt alleen op button.*, 'counter.increment' alleen op counter.*, 'input_button.press' alleen op input_button.* enz. Bij een mismatch mislukt de actie stil (HA logt 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Alle objecten tonen", + "show_all_tasks": "Filter wissen — alle taken tonen", + "filter_to_overdue": "Takenlijst filteren op alleen achterstallig", + "filter_to_due_soon": "Takenlijst filteren op alleen binnenkort verschuldigd", + "filter_to_triggered": "Takenlijst filteren op alleen geactiveerd", + "open_task": "Taak openen", + "show_details": "Geschiedenis + statistieken tonen", + "hide_details": "Details verbergen", + "history_empty": "Nog geen geschiedenis.", + "history_edit_button": "Item bewerken", + "total_cost": "Totale kosten", + "times_performed": "Uitgevoerd", + "older_entries": "ouder", + "open_in_panel": "Openen in Onderhoudspaneel", + "skip_reason": "Reden voor overslaan (optioneel)", + "reset_to_date": "Laatst uitgevoerd terugzetten op", + "delete_task_confirm": "Deze taak en de geschiedenis verwijderen?", + "delete_object_confirm": "Dit object en alle taken verwijderen?", + "loading": "Laden…", + "archive": "Archiveren", + "undo": "Ongedaan maken", + "task_archived": "Taak gearchiveerd", + "object_archived": "Object gearchiveerd", + "unarchive": "Dearchiveren", + "archived": "Gearchiveerd", + "show_archived": "Gearchiveerde tonen", + "hide_archived": "Gearchiveerde verbergen", + "confirm_archive_object": "Dit object en zijn taken archiveren? De geschiedenis blijft behouden en kan later worden hersteld.", + "settings_archive": "Archief & bewaring", + "settings_archive_desc": "Voltooide eenmalige taken archiveren in plaats van verwijderen. Gearchiveerde items zijn verborgen en inactief, maar behouden geschiedenis en kosten.", + "settings_archive_oneoff_days": "Voltooide eenmalige taken automatisch archiveren na (dagen, 0 = uit)", + "settings_delete_archived_oneoff_days": "Gearchiveerde eenmalige taken automatisch verwijderen na (dagen, 0 = nooit)", + "archive_object": "Object archiveren", + "unarchive_object": "Object dearchiveren", + "documents": "Documenten", + "documents_empty": "Nog geen documenten.", + "doc_upload": "Bestand uploaden", + "doc_uploading": "Uploaden…", + "doc_add_link": "Link toevoegen", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titel (optioneel)", + "doc_open": "Openen", + "doc_delete_confirm": "\"{name}\" verwijderen?", + "doc_too_large": "Bestand is te groot (max. 25 MB).", + "doc_upload_failed": "Uploaden mislukt.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Al elders opgeslagen — gedeeld, geen extra ruimte.", + "doc_dup_in_object": "Dit bestand is al aan dit object gekoppeld.", + "doc_link_invalid": "Alleen http/https-links zijn toegestaan.", + "doc_cat_manual": "Handleiding", + "doc_cat_warranty": "Garantie", + "doc_cat_invoice": "Factuur", + "doc_cat_spare_parts": "Onderdelen", + "doc_cat_photo": "Foto", + "doc_cat_other": "Overig", + "doc_link_badge": "Link", + "doc_storage_title": "Documentopslag", + "doc_storage_saved": "Bespaard door deduplicatie", + "doc_storage_refresh": "Vernieuwen", + "doc_download": "Downloaden", + "doc_close": "Sluiten", + "doc_camera": "Foto maken", + "doc_drop_hint": "Sleep bestanden hierheen", + "doc_task_none": "Geen documenten aan deze taak gekoppeld.", + "doc_link_existing": "Document koppelen…", + "doc_attach": "Koppelen", + "doc_unlink": "Ontkoppelen", + "doc_page": "Pagina", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1j", + "chart_since_service": "sinds laatste onderhoud", + "chart_no_stats": "Geen langetermijnstatistieken voor deze entiteit — alleen waarden uit onderhoudsregels worden getoond", + "auto_complete_on_recovery": "Automatisch voltooien wanneer de sensor herstelt", + "auto_complete_on_recovery_help": "Registreert een voltooiing (stelt laatst uitgevoerd in) wanneer de trigger zichzelf oplost — bijv. zout bijgevuld, filter vervangen.", + "doc_search": "Documenten zoeken…", + "doc_search_none": "Geen overeenkomende documenten", + "link_device_optional": "Koppelen aan bestaand apparaat (optioneel)", + "parent_object_optional": "Bovenliggend object (optioneel)", + "parent_none": "(Geen bovenliggend object)", + "paused": "Gepauzeerd", + "pause_object": "Pauzeren", + "resume_object": "Hervatten", + "pause_until_prompt": "Bevries de schema's van dit object — niets wordt verwacht en niets meldt tot het wordt hervat. Stel optioneel een datum voor automatisch hervatten in.", + "pause_until_label": "Hervatten op (optioneel)", + "object_paused": "Object gepauzeerd", + "object_resumed": "Object hervat — schema's herstart", + "object_paused_badge": "Gepauzeerd", + "paused_until_label": "tot", + "replace_object": "Vervangen…", + "replace_object_prompt": "Dit object uitfaseren en een opvolger aanmaken. Geschiedenis en kosten blijven gearchiveerd bij het oude; taken en documenten gaan mee naar het nieuwe, tellers beginnen opnieuw.", + "replace_name_label": "Naam van de opvolger", + "object_replaced": "Object vervangen — opvolger aangemaakt", + "reading_unit_label": "Meeteenheid (bijv. kWh, m³)", + "reading_unit_help": "Wordt naast de vastgelegde waarde getoond bij het afronden van deze taak.", + "reading_value_label": "Meterstand", + "reading_label": "Meting", + "settings_templates_label": "Sjabloongalerij", + "settings_templates_hint": "Vink sjablonen uit die je nooit nodig hebt — ze verdwijnen uit de \"Vanuit sjabloon\"-keuzes (paneel en config flow). Verder verandert er niets; altijd weer in te schakelen.", + "worksheet": "Werkblad", + "worksheet_scan_view": "Scan om de taak te openen", + "worksheet_scan_complete": "Scan om af te ronden", + "worksheet_manual_excerpt": "Handleidingfragment", + "worksheet_pages": "pagina's", + "worksheet_printed": "Afgedrukt", + "worksheet_never": "Nooit" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/pl.json b/custom_components/maintenance_supporter/frontend/locales/pl.json new file mode 100644 index 0000000..e95f4e5 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/pl.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Konserwacja", + "objects": "Obiekty", + "tasks": "Zadania", + "overdue": "Zaległe", + "due_soon": "Wkrótce", + "triggered": "Wyzwolone", + "ok": "OK", + "all": "Wszystkie", + "new_object": "+ Nowy obiekt", + "templates_from": "Z szablonu", + "templates_title": "Zacznij od szablonu", + "templates_task_count": "{n} zadań", + "template_created": "Utworzono z szablonu", + "onboard_hint": "Dodaj pierwszy obiekt, aby śledzić konserwację.", + "edit": "Edytuj", + "duplicate": "Duplikuj", + "task_duplicated": "Zadanie zduplikowane", + "object_duplicated": "Obiekt zduplikowany", + "delete": "Usuń", + "add_task": "+ Dodaj zadanie", + "complete": "Wykonaj", + "completed": "Wykonano", + "skip": "Pomiń", + "skipped": "Pominięte", + "missed": "Missed", + "reset": "Resetuj", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Anuluj", + "bulk_select": "Zaznacz", + "bulk_select_all": "Zaznacz wszystko", + "bulk_n_selected": "Zaznaczono {n}", + "bulk_completed": "Ukończono {n} zadań", + "bulk_archived": "Zarchiwizowano {n} zadań", + "completing": "Wykonywanie…", + "interval": "Interwał", + "warning": "Ostrzeżenie", + "last_performed": "Ostatnio wykonane", + "next_due": "Następny termin", + "days_until_due": "Dni do terminu", + "avg_duration": "Śr. czas trwania", + "trigger": "Wyzwalacz", + "trigger_type": "Typ wyzwalacza", + "threshold_above": "Górny limit", + "threshold_below": "Dolny limit", + "threshold": "Próg", + "counter": "Licznik", + "state_change": "Zmiana stanu", + "runtime": "Czas pracy", + "runtime_hours": "Docelowy czas pracy (godziny)", + "target_value": "Wartość docelowa", + "baseline": "Wartość bazowa", + "target_changes": "Docelowa liczba zmian", + "for_minutes": "Przez (minuty)", + "time_based": "Czasowy", + "sensor_based": "Oparty na czujniku", + "manual": "Ręczny", + "one_time": "Jednorazowo", + "weekdays": "Dni tygodnia", + "nth_weekday": "N-ty dzień tygodnia w miesiącu", + "day_of_month": "Dzień miesiąca", + "recurrence_on_days": "Powtarzaj w", + "recurrence_occurrence": "Wystąpienie", + "recurrence_weekday": "Dzień tygodnia", + "recurrence_day": "Dzień miesiąca (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.", + "ord_2": "2.", + "ord_3": "3.", + "ord_4": "4.", + "ord_5": "5.", + "ord_last": "Ostatni", + "day_word": "Dzień", + "interval_value": "Interwał", + "interval_unit": "Jednostka", + "unit_days": "Dni", + "unit_weeks": "Tygodnie", + "unit_months": "Miesiące", + "unit_years": "Lata", + "due_date": "Termin", + "cleaning": "Czyszczenie", + "inspection": "Inspekcja", + "replacement": "Wymiana", + "calibration": "Kalibracja", + "service": "Serwis", + "reading": "Odczyt", + "custom": "Niestandardowy", + "history": "Historia", + "cost": "Koszt", + "report_button": "Raport", + "report_title": "Raport konserwacji", + "report_generated": "Wygenerowano", + "report_times_done": "Wykonano", + "report_total_cost": "Koszt całkowity", + "report_every": "co {n} {unit}", + "report_notes": "Notatki", + "report_col_type": "Typ", + "report_col_status": "Status", + "report_col_schedule": "Harmonogram", + "duration": "Czas trwania", + "both": "Oba", + "trigger_val": "Wartość wyzwalacza", + "complete_title": "Wykonaj: ", + "checklist": "Lista kontrolna", + "checklist_steps_optional": "Kroki listy kontrolnej (opcjonalne)", + "checklist_placeholder": "Wyczyść filtr\nWymień uszczelkę\nSprawdź ciśnienie", + "checklist_help": "Jeden krok na linię. Maks. 100 elementów.", + "err_too_long": "{field}: za długie (maks. {n} znaków)", + "err_too_short": "{field}: za krótkie (min. {n} znaków)", + "err_value_too_high": "{field}: za duże (maks. {n})", + "err_value_too_low": "{field}: za małe (min. {n})", + "err_required": "{field}: wymagane", + "err_wrong_type": "{field}: zły typ (oczekiwano: {type})", + "err_invalid_choice": "{field}: niedozwolona wartość", + "err_invalid_value": "{field}: nieprawidłowa wartość", + "feat_schedule_time": "Harmonogram według pory dnia", + "feat_schedule_time_desc": "Zadania stają się zaległe o określonej porze dnia zamiast o północy.", + "schedule_time_optional": "Termin o godzinie (opcjonalne, HH:MM)", + "schedule_time_help": "Puste = północ (domyślnie). Strefa czasowa HA.", + "at_time": "o", + "notes_optional": "Notatki (opcjonalne)", + "cost_optional": "Koszt (opcjonalne)", + "duration_minutes": "Czas trwania w minutach (opcjonalne)", + "days": "dni", + "day": "dzień", + "today": "Dzisiaj", + "d_overdue": "d zaległe", + "no_tasks": "Jeszcze brak zadań konserwacyjnych. Utwórz obiekt, aby zacząć.", + "no_tasks_short": "Brak zadań", + "no_history": "Jeszcze brak wpisów historii.", + "show_all": "Pokaż wszystko", + "cost_duration_chart": "Koszt i czas trwania", + "installed": "Zainstalowane", + "confirm_delete_object": "Usunąć ten obiekt i wszystkie jego zadania?", + "confirm_delete_task": "Usunąć to zadanie?", + "min": "Min", + "max": "Maks", + "save": "Zapisz", + "saving": "Zapisywanie…", + "edit_task": "Edytuj zadanie", + "new_task": "Nowe zadanie konserwacyjne", + "task_name": "Nazwa zadania", + "maintenance_type": "Typ konserwacji", + "priority": "Priorytet", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Niski", + "priority_normal": "Normalny", + "priority_high": "Wysoki", + "schedule_type": "Typ harmonogramu", + "interval_days": "Interwał (dni)", + "warning_days": "Dni ostrzeżenia", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Ostatnio wykonane (opcjonalne)", + "interval_anchor": "Punkt zaczepienia interwału", + "anchor_completion": "Od daty wykonania", + "anchor_planned": "Od daty planowanej (bez przesunięć)", + "edit_object": "Edytuj obiekt", + "name": "Nazwa", + "manufacturer_optional": "Producent (opcjonalne)", + "model_optional": "Model (opcjonalne)", + "serial_number_optional": "Numer seryjny (opcjonalne)", + "serial_number_label": "S/N", + "documentation_url_label": "Instrukcja", + "object_notes_label": "Notatki", + "sort_due_date": "Termin", + "sort_object": "Nazwa obiektu", + "sort_type": "Typ", + "sort_task_name": "Nazwa zadania", + "all_objects": "Wszystkie obiekty", + "tasks_lower": "zadań", + "no_tasks_yet": "Jeszcze brak zadań", + "add_first_task": "Dodaj pierwsze zadanie", + "trigger_configuration": "Konfiguracja wyzwalacza", + "entity_id": "ID encji", + "comma_separated": "oddzielone przecinkami", + "entity_logic": "Logika encji", + "entity_logic_any": "Wyzwala dowolna encja", + "entity_logic_all": "Wszystkie encje muszą wyzwolić", + "entities": "encje", + "attribute_optional": "Atrybut (opcjonalny, puste = stan)", + "use_entity_state": "Użyj stanu encji (bez atrybutu)", + "trigger_above": "Wyzwól powyżej", + "trigger_below": "Wyzwól poniżej", + "for_at_least_minutes": "Przez co najmniej (minuty)", + "safety_interval_days": "Interwał bezpieczeństwa (dni, opcjonalny)", + "safety_interval": "Interwał bezpieczeństwa (opcjonalny)", + "delta_mode": "Tryb delta", + "from_state_optional": "Ze stanu (opcjonalne)", + "to_state_optional": "Do stanu (opcjonalne)", + "documentation_url_optional": "URL dokumentacji (opcjonalne)", + "object_notes_optional": "Notatki (opcjonalne)", + "nfc_tag_id_optional": "ID tagu NFC (opcjonalne)", + "nfc_tags_empty_help": "W Home Assistant nie zarejestrowano jeszcze tagów NFC.", + "nfc_tags_open_settings": "Otwórz ustawienia tagów", + "nfc_tags_refresh": "Odśwież", + "environmental_entity_optional": "Czujnik środowiskowy (opcjonalne)", + "environmental_entity_helper": "np. sensor.outdoor_temperature — dostosowuje interwał na podstawie warunków środowiskowych", + "environmental_attribute_optional": "Atrybut środowiskowy (opcjonalne)", + "nfc_tag_id": "ID tagu NFC", + "nfc_linked": "Tag NFC powiązany", + "nfc_link_hint": "Kliknij, aby powiązać tag NFC", + "responsible_user": "Odpowiedzialny użytkownik", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Brak przypisanego użytkownika)", + "all_users": "Wszyscy użytkownicy", + "my_tasks": "Moje zadania", + "tab_calendar": "Kalendarz", + "cal_no_events": "Brak konserwacji", + "cal_window_7": "7 dni", + "cal_window_14": "14 dni", + "cal_window_30": "30 dni", + "cal_window_365": "1 rok", + "cal_every_n_days": "co {n} dni", + "cal_source_time": "Czas", + "cal_source_time_adaptive": "Czas (adaptacyjny)", + "cal_source_sensor": "Czujnik", + "cal_predicted": "przewidywane", + "cal_confidence_high": "wysoka pewność", + "cal_confidence_medium": "średnia pewność", + "cal_confidence_low": "niska pewność", + "budget_monthly": "Budżet miesięczny", + "budget_yearly": "Budżet roczny", + "groups": "Grupy", + "new_group": "Nowa grupa", + "edit_group": "Edytuj grupę", + "no_groups": "Jeszcze brak grup", + "delete_group": "Usuń grupę", + "delete_group_confirm": "Usunąć grupę '{name}'?", + "group_select_tasks": "Wybierz zadania", + "group_name_required": "Nazwa jest wymagana", + "description_optional": "Opis (opcjonalny)", + "selected": "Wybrane", + "loading_chart": "Ładowanie danych wykresu...", + "hide_outliers": "Ukryj wartości odstające (błędy czujnika)", + "was_maintenance_needed": "Czy ta konserwacja była potrzebna?", + "feedback_needed": "Potrzebna", + "feedback_not_needed": "Niepotrzebna", + "feedback_not_sure": "Nie jestem pewien", + "suggested_interval": "Sugerowany interwał", + "apply_suggestion": "Zastosuj", + "reanalyze": "Analizuj ponownie", + "reanalyze_result": "Nowa analiza", + "reanalyze_insufficient_data": "Za mało danych do wygenerowania rekomendacji", + "data_points": "punkty danych", + "dismiss_suggestion": "Odrzuć", + "confidence_low": "Niska", + "confidence_medium": "Średnia", + "confidence_high": "Wysoka", + "recommended": "rekomendowane", + "seasonal_awareness": "Świadomość sezonowa", + "edit_seasonal_overrides": "Edytuj czynniki sezonowe", + "seasonal_overrides_title": "Czynniki sezonowe (nadpisanie)", + "seasonal_overrides_hint": "Czynnik na miesiąc (0.1–5.0). Puste = uczone automatycznie.", + "seasonal_override_invalid": "Nieprawidłowa wartość", + "seasonal_override_range": "Czynnik musi być między 0.1 a 5.0", + "clear_all": "Wyczyść wszystko", + "seasonal_chart_title": "Czynniki sezonowe", + "seasonal_learned": "Wyuczone", + "seasonal_manual": "Ręczne", + "month_jan": "Sty", + "month_feb": "Lut", + "month_mar": "Mar", + "month_apr": "Kwi", + "month_may": "Maj", + "month_jun": "Cze", + "month_jul": "Lip", + "month_aug": "Sie", + "month_sep": "Wrz", + "month_oct": "Paź", + "month_nov": "Lis", + "month_dec": "Gru", + "sensor_prediction": "Predykcja czujnika", + "degradation_trend": "Trend", + "trend_rising": "Rosnący", + "trend_falling": "Malejący", + "trend_stable": "Stabilny", + "trend_insufficient_data": "Niewystarczające dane", + "days_until_threshold": "Dni do progu", + "threshold_exceeded": "Próg przekroczony", + "environmental_adjustment": "Czynnik środowiskowy", + "sensor_prediction_urgency": "Czujnik przewiduje próg za ~{days} dni", + "day_short": "d", + "weibull_reliability_curve": "Krzywa niezawodności", + "weibull_failure_probability": "Prawdopodobieństwo awarii", + "weibull_r_squared": "Dopasowanie R²", + "beta_early_failures": "Wczesne awarie", + "beta_random_failures": "Losowe awarie", + "beta_wear_out": "Zużycie", + "beta_highly_predictable": "Wysoce przewidywalne", + "confidence_interval": "Przedział ufności", + "confidence_conservative": "Konserwatywny", + "confidence_aggressive": "Optymistyczny", + "current_interval_marker": "Bieżący interwał", + "recommended_marker": "Rekomendowany", + "characteristic_life": "Charakterystyczna żywotność", + "chart_mini_sparkline": "Wykres trendu", + "chart_history": "Historia kosztów i czasu trwania", + "chart_seasonal": "Czynniki sezonowe, 12 miesięcy", + "chart_weibull": "Krzywa niezawodności Weibulla", + "chart_sparkline": "Wykres wartości wyzwalacza czujnika", + "days_progress": "Postęp dni", + "qr_code": "Kod QR", + "qr_generating": "Generowanie kodu QR…", + "qr_error": "Nie udało się wygenerować kodu QR.", + "qr_error_no_url": "Brak skonfigurowanego URL HA. Ustaw zewnętrzny lub wewnętrzny URL w Ustawienia → System → Sieć.", + "save_error": "Nie udało się zapisać. Spróbuj ponownie.", + "qr_print": "Drukuj", + "qr_download": "Pobierz SVG", + "qr_action": "Akcja przy skanowaniu", + "qr_action_view": "Wyświetl informacje o konserwacji", + "qr_action_complete": "Oznacz konserwację jako wykonaną", + "qr_url_mode": "Typ linku", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokalny (mDNS)", + "qr_mode_server": "URL serwera", + "overview": "Przegląd", + "analysis": "Analiza", + "recent_activities": "Ostatnie aktywności", + "search_notes": "Szukaj w notatkach", + "avg_cost": "Śr. koszt", + "no_advanced_features": "Brak włączonych funkcji zaawansowanych", + "no_advanced_features_hint": "Włącz „Adaptacyjne interwały” lub „Wzorce sezonowe” w ustawieniach integracji, aby zobaczyć tutaj dane analityczne.", + "analysis_not_enough_data": "Jeszcze za mało danych do analizy.", + "analysis_not_enough_data_hint": "Analiza Weibulla wymaga co najmniej 5 wykonanych konserwacji; wzorce sezonowe stają się widoczne po 6+ punktach danych na miesiąc.", + "analysis_manual_task_hint": "Zadania ręczne bez interwału nie generują danych analitycznych.", + "completions": "wykonania", + "current": "Bieżące", + "shorter": "Krótsze", + "longer": "Dłuższe", + "normal": "Normalne", + "disabled": "Wyłączone", + "compound_logic": "Logika złożona", + "compound": "Złożony (wiele warunków)", + "compound_logic_and": "I — wszystkie warunki muszą zadziałać", + "compound_logic_or": "LUB — wystarczy dowolny warunek", + "compound_help": "Połącz kilka warunków czujników w jeden wyzwalacz.", + "compound_no_conditions": "Brak warunków — dodaj co najmniej jeden.", + "compound_add_condition": "Dodaj warunek", + "compound_condition": "Warunek", + "compound_remove_condition": "Usuń warunek", + "card_title": "Tytuł", + "card_show_header": "Pokaż nagłówek ze statystykami", + "card_show_actions": "Pokaż przyciski akcji", + "card_compact": "Tryb kompaktowy", + "card_max_items": "Maks. elementów (0 = wszystkie)", + "card_filter_status": "Filtruj wg statusu", + "card_filter_status_help": "Puste = pokaż wszystkie statusy.", + "card_filter_objects": "Filtruj wg obiektów", + "card_filter_objects_help": "Puste = pokaż wszystkie obiekty.", + "card_filter_entities": "Filtruj wg encji (entity_ids)", + "card_filter_entities_help": "Wybierz encje sensor / binary_sensor z tej integracji. Puste = wszystkie.", + "card_loading_objects": "Ładowanie obiektów…", + "card_load_error": "Nie udało się załadować obiektów — sprawdź połączenie WebSocket.", + "card_no_tasks_title": "Brak zadań konserwacyjnych", + "card_no_tasks_cta": "→ Utwórz w panelu Maintenance", + "no_objects": "Brak obiektów.", + "action_error": "Akcja nie powiodła się. Spróbuj ponownie.", + "area_id_optional": "Obszar (opcjonalny)", + "installation_date_optional": "Data instalacji (opcjonalna)", + "warranty_expiry_optional": "Wygaśnięcie gwarancji (opcjonalnie)", + "warranty": "Gwarancja", + "warranty_valid_until": "ważna do {date}", + "warranty_expires_in": "wygasa za {days} dni", + "warranty_expired": "wygasła", + "cal_past_windows": "Minione okna", + "cal_forward_windows": "Przyszłe okna", + "history_edit_title": "Edytuj wpis historii", + "history_edit_timestamp": "Znacznik czasu", + "manufacturer": "Producent", + "model": "Model", + "area": "Obszar", + "actions": "Akcje", + "view_mode_label": "Widok", + "view_cards": "Widok kart", + "view_table": "Widok tabeli", + "objects_table_columns_label": "Kolumny tabeli obiektów", + "objects_table_columns_hint": "Wybierz, które kolumny mają być widoczne w widoku tabeli obiektów.", + "custom_icon_optional": "Ikona (opcjonalna, np. mdi:wrench)", + "task_enabled": "Zadanie włączone", + "skip_reason_prompt": "Pominąć to zadanie?", + "reason_optional": "Powód (opcjonalny)", + "reset_date_prompt": "Oznaczyć zadanie jako wykonane?", + "reset_date_optional": "Data ostatniego wykonania (opcjonalna, domyślnie dzisiaj)", + "notes_label": "Notatki", + "documentation_label": "Dokumentacja", + "no_nfc_tag": "— Brak tagu —", + "dashboard": "Pulpit", + "tab_today": "Dziś", + "palette_placeholder": "Szukaj obiektów i zadań…", + "palette_no_results": "Brak wyników", + "palette_hint": "↑↓ nawigacja · Enter otwórz · Esc zamknij", + "today_all_caught_up": "Wszystko zrobione! Nic w tym tygodniu.", + "today_overdue": "Zaległe", + "today_due_today": "Na dziś", + "today_this_week": "W tym tygodniu", + "settings": "Ustawienia", + "settings_features": "Funkcje zaawansowane", + "settings_features_desc": "Włącz lub wyłącz funkcje zaawansowane. Wyłączenie ukrywa je z UI, ale nie usuwa danych.", + "feat_adaptive": "Harmonogram adaptacyjny", + "feat_adaptive_desc": "Ucz się optymalnych interwałów z historii konserwacji", + "feat_predictions": "Predykcje czujników", + "feat_predictions_desc": "Przewiduj daty wyzwolenia z degradacji czujnika", + "feat_seasonal": "Korekty sezonowe", + "feat_seasonal_desc": "Dostosuj interwały do wzorców sezonowych", + "feat_environmental": "Korelacja środowiskowa", + "feat_environmental_desc": "Koreluj interwały z temperaturą/wilgotnością", + "feat_budget": "Śledzenie budżetu", + "feat_budget_desc": "Śledź miesięczne i roczne wydatki na konserwację", + "feat_groups": "Grupy zadań", + "feat_groups_desc": "Organizuj zadania w grupy logiczne", + "feat_checklists": "Listy kontrolne", + "feat_checklists_desc": "Wieloetapowe procedury wykonania zadania", + "settings_general": "Ogólne", + "settings_default_warning": "Domyślne dni ostrzeżenia", + "settings_panel_enabled": "Panel boczny", + "settings_panel_title": "Tytuł panelu bocznego", + "settings_notifications": "Powiadomienia", + "settings_notify_service": "Usługa powiadomień", + "test_notification": "Powiadomienie testowe", + "send_test": "Wyślij test", + "testing": "Wysyłanie…", + "test_notification_success": "Powiadomienie testowe wysłane", + "test_notification_failed": "Powiadomienie testowe nie powiodło się", + "settings_notify_due_soon": "Powiadom gdy wkrótce", + "settings_notify_overdue": "Powiadom gdy zaległe", + "settings_notify_triggered": "Powiadom gdy wyzwolone", + "settings_interval_hours": "Interwał powtarzania (godziny, 0 = raz)", + "settings_quiet_hours": "Godziny ciszy", + "settings_quiet_start": "Początek", + "settings_quiet_end": "Koniec", + "settings_max_per_day": "Maks. powiadomień dziennie (0 = bez limitu)", + "settings_bundling": "Grupowanie powiadomień", + "settings_bundle_threshold": "Próg grupowania", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobilne przyciski akcji", + "settings_action_complete": "Pokaż przycisk 'Wykonaj'", + "settings_action_skip": "Pokaż przycisk 'Pomiń'", + "settings_action_snooze": "Pokaż przycisk 'Drzemka'", + "settings_weekly_digest": "Cotygodniowe podsumowanie", + "settings_weekly_digest_hint": "Jedno powiadomienie podsumowujące w poniedziałek rano, gdy są zadania.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Czas drzemki (godziny)", + "settings_budget": "Budżet", + "settings_currency": "Waluta", + "settings_budget_monthly": "Budżet miesięczny", + "settings_budget_yearly": "Budżet roczny", + "settings_budget_alerts": "Alerty budżetowe", + "settings_budget_threshold": "Próg alertu (%)", + "settings_import_export": "Import / Eksport", + "settings_export_json": "Eksportuj JSON", + "settings_export_yaml": "Eksportuj YAML", + "settings_export_csv": "Eksportuj CSV", + "settings_import_csv": "Importuj CSV", + "settings_import_placeholder": "Wklej tutaj zawartość JSON lub CSV…", + "settings_import_btn": "Importuj", + "settings_import_success": "{count} obiektów zaimportowanych pomyślnie.", + "settings_export_success": "Eksport pobrany.", + "settings_saved": "Ustawienie zapisane.", + "settings_include_history": "Dołącz historię", + "sort_alphabetical": "Alfabetycznie", + "sort_due_soonest": "Najbliższy termin", + "sort_task_count": "Liczba zadań", + "sort_area": "Obszar", + "sort_assigned_user": "Przypisany użytkownik", + "sort_group": "Grupa", + "groupby_none": "Bez grupowania", + "groupby_area": "Wg obszaru", + "groupby_group": "Wg grupy", + "groupby_user": "Wg użytkownika", + "filter_label": "Filtr", + "user_label": "Użytkownik", + "sort_label": "Sortowanie", + "group_by_label": "Grupuj wg", + "state_value_help": "Użyj wartości stanu HA (zwykle małymi literami, np. \"on\"/\"off\"). Wielkość liter jest normalizowana przy zapisie.", + "target_changes_help": "Liczba pasujących przejść przed wyzwoleniem (domyślnie: 1).", + "qr_print_title": "Drukuj kody QR", + "qr_print_desc": "Wygeneruj stronę do druku z kodami QR do wycięcia i naklejenia na sprzęcie.", + "qr_print_load": "Załaduj obiekty", + "qr_print_filter": "Filtr", + "qr_print_objects": "Obiekty", + "qr_print_actions": "Akcje", + "qr_print_url_mode": "Typ linku", + "qr_print_estimate": "Szacowane kody QR", + "qr_print_over_limit": "limit 200, zawęź filtr", + "qr_print_generate": "Generuj kody QR", + "qr_print_generating": "Generowanie…", + "qr_print_ready": "Kody QR gotowe", + "qr_print_print_button": "Drukuj", + "qr_print_empty": "Nic do wygenerowania", + "qr_action_skip": "Pomiń", + "vacation_title": "Tryb urlopowy", + "vacation_active": "aktywny", + "vacation_ended": "zakończony", + "vacation_desc": "Zaplanuj urlop: powiadomienia są wstrzymane podczas okresu plus dni bufora. Możesz dodać wyjątki dla wybranych zadań.", + "vacation_enable": "Włącz tryb urlopowy", + "vacation_start": "Początek", + "vacation_end": "Koniec", + "vacation_buffer": "Bufor (dni)", + "vacation_exempt_title": "Powiadamiaj mimo urlopu", + "vacation_exempt_desc": "Wybierz zadania, które mają powiadamiać także w czasie urlopu (np. krytyczna chemia basenu).", + "vacation_load_tasks": "Wczytaj zadania", + "vacation_preview_btn": "Pokaż podgląd", + "vacation_preview_affected": "zadań dotyczy", + "vacation_event_due_soon": "wkrótce będzie wymagane", + "vacation_event_overdue": "stanie się zaległe", + "vacation_event_triggered_est": "możliwe wyzwolenie czujnika", + "vacation_sensor_based": "(czujnikowe)", + "vacation_action_notify": "Powiadamiaj mimo to", + "vacation_action_unsilence": "Ponownie wycisz", + "vacation_marked_complete": "Oznaczono jako wykonane", + "vacation_marked_skip": "Pominięto", + "vacation_end_now": "Zakończ urlop teraz", + "unassigned": "Nieprzypisane", + "no_area": "Brak obszaru", + "has_overdue": "Ma zaległe zadania", + "object": "Obiekt", + "settings_panel_access": "Dostęp do panelu", + "settings_panel_access_desc": "Administratorzy zawsze mają pełny dostęp. Aby delegować tworzenie, edytowanie i usuwanie wybranym użytkownikom bez uprawnień administratora, włącz tę opcję i zaznacz ich poniżej — pozostali widzą tylko Wykonaj i Pomiń.", + "settings_operator_write": "Zezwól wybranym użytkownikom na tworzenie, edycję i usuwanie", + "settings_operator_write_desc": "Wyłączone: tylko administratorzy mogą zmieniać treść. Włączone: wybrani użytkownicy poniżej również mają pełny dostęp.", + "no_non_admin_users": "Nie znaleziono użytkowników nie-admin. Dodaj ich w Ustawienia → Osoby.", + "owner_label": "Właściciel", + "feat_completion_actions": "Akcje po zakończeniu", + "feat_completion_actions_desc": "Akcja HA per zadanie po zakończeniu + QR szybkiego zakończenia z predefiniowanymi wartościami.", + "on_complete_action_title": "Po zakończeniu: wywołaj akcję HA (opcjonalnie)", + "on_complete_action_desc": "Wywołuje usługę HA po zakończeniu zadania — np. zresetuj licznik urządzenia.", + "on_complete_action_service": "Usługa", + "on_complete_action_target": "Encja docelowa", + "on_complete_action_data": "Dane (JSON, opcjonalnie)", + "on_complete_action_test": "Testuj akcję", + "on_complete_action_test_success": "Sukces", + "on_complete_action_test_failed": "Błąd", + "quick_complete_defaults_title": "Wartości domyślne szybkiego zakończenia (dla skanów QR, opcjonalnie)", + "quick_complete_defaults_desc": "Predefiniowane wartości dla QR szybkiego zakończenia. Bez nich QR otwiera okno dialogowe.", + "quick_complete_defaults_notes": "Notatki", + "quick_complete_defaults_cost": "Koszt", + "quick_complete_defaults_duration": "Czas trwania (minuty)", + "quick_complete_defaults_feedback_none": "Brak opinii", + "quick_complete_defaults_feedback_needed": "Było potrzebne", + "quick_complete_defaults_feedback_not_needed": "Nie było potrzebne", + "quick_complete_success": "Szybko oznaczono jako wykonane", + "trigger_replaced": "Wyzwalacz zastąpiony", + "add": "Dodaj", + "show_stats": "Pokaż statystyki + wykresy", + "hide_stats": "Ukryj statystyki", + "adaptive_no_data": "Za mało historii ukończeń do analizy adaptacyjnej. Wykonaj to zadanie jeszcze kilka razy, aby odblokować propozycje interwału i wykresy niezawodności.", + "suggestion_applied": "Zastosowano sugerowany interwał", + "vacation_mode": "Tryb urlopowy", + "vacation_status_active": "Aktywny teraz", + "vacation_status_scheduled": "Zaplanowany", + "vacation_status_inactive": "Nieaktywny", + "vacation_end_now_confirm": "Zakończyć urlop natychmiast?", + "vacation_exempt_count": "wykluczone", + "vacation_advanced": "Zaawansowane…", + "vacation_open_panel": "Otwórz w panelu", + "enable": "Włącz", + "saved": "Zapisano", + "budget_monthly_set": "Ustaw miesięczny", + "budget_yearly_set": "Ustaw roczny", + "budget_advanced": "Waluta, alerty…", + "budget_open_panel": "Otwórz w panelu", + "groups_empty": "Brak grup.", + "group_new_placeholder": "Dodaj grupę…", + "group_delete_confirm": "Usunąć grupę „{name}”?", + "groups_manage_tasks": "Zarządzaj przypisaniami zadań…", + "groups_open_panel": "Otwórz w panelu", + "on_complete_action_target_hint": "Uwaga: domena encji musi pasować do usługi — np. 'button.press' działa tylko na button.*, 'counter.increment' tylko na counter.*, 'input_button.press' tylko na input_button.* itd. W przypadku niezgodności akcja po cichu się nie wykona (HA zapisze w dzienniku 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Pokaż wszystkie obiekty", + "show_all_tasks": "Wyczyść filtr — pokaż wszystkie zadania", + "filter_to_overdue": "Filtruj listę tylko do zaległych", + "filter_to_due_soon": "Filtruj listę tylko do zbliżających się terminów", + "filter_to_triggered": "Filtruj listę tylko do wyzwolonych", + "open_task": "Otwórz zadanie", + "show_details": "Pokaż historię + statystyki", + "hide_details": "Ukryj szczegóły", + "history_empty": "Brak historii.", + "history_edit_button": "Edytuj wpis", + "total_cost": "Koszt całkowity", + "times_performed": "Wykonano", + "older_entries": "starsze", + "open_in_panel": "Otwórz w panelu Konserwacja", + "skip_reason": "Powód pominięcia (opcjonalnie)", + "reset_to_date": "Zresetuj ostatnie wykonanie na", + "delete_task_confirm": "Usunąć to zadanie i jego historię?", + "delete_object_confirm": "Usunąć ten obiekt i wszystkie jego zadania?", + "loading": "Ładowanie…", + "archive": "Archiwizuj", + "undo": "Cofnij", + "task_archived": "Zadanie zarchiwizowane", + "object_archived": "Obiekt zarchiwizowany", + "unarchive": "Przywróć z archiwum", + "archived": "Zarchiwizowane", + "show_archived": "Pokaż zarchiwizowane", + "hide_archived": "Ukryj zarchiwizowane", + "confirm_archive_object": "Zarchiwizować ten obiekt i jego zadania? Historia zostanie zachowana i można ją później przywrócić.", + "settings_archive": "Archiwum i przechowywanie", + "settings_archive_desc": "Odkładaj ukończone zadania jednorazowe bez usuwania. Zarchiwizowane elementy są ukryte i nieaktywne, ale zachowują historię i koszty.", + "settings_archive_oneoff_days": "Automatycznie archiwizuj ukończone zadania jednorazowe po (dni, 0 = wył.)", + "settings_delete_archived_oneoff_days": "Automatycznie usuwaj zarchiwizowane zadania jednorazowe po (dni, 0 = nigdy)", + "archive_object": "Archiwizuj obiekt", + "unarchive_object": "Przywróć obiekt z archiwum", + "documents": "Dokumenty", + "documents_empty": "Brak dokumentów.", + "doc_upload": "Prześlij plik", + "doc_uploading": "Przesyłanie…", + "doc_add_link": "Dodaj link", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Tytuł (opcjonalnie)", + "doc_open": "Otwórz", + "doc_delete_confirm": "Usunąć „{name}”?", + "doc_too_large": "Plik jest za duży (maks. 25 MB).", + "doc_upload_failed": "Przesyłanie nie powiodło się.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Już zapisany gdzie indziej — współdzielony, bez dodatkowego miejsca.", + "doc_dup_in_object": "Ten plik jest już dołączony do tego obiektu.", + "doc_link_invalid": "Dozwolone są tylko linki http/https.", + "doc_cat_manual": "Instrukcja", + "doc_cat_warranty": "Gwarancja", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Części zamienne", + "doc_cat_photo": "Zdjęcie", + "doc_cat_other": "Inne", + "doc_link_badge": "Link", + "doc_storage_title": "Magazyn dokumentów", + "doc_storage_saved": "Zaoszczędzone przez deduplikację", + "doc_storage_refresh": "Odśwież", + "doc_download": "Pobierz", + "doc_close": "Zamknij", + "doc_camera": "Zrób zdjęcie", + "doc_drop_hint": "Upuść pliki tutaj", + "doc_task_none": "Brak dokumentów powiązanych z tym zadaniem.", + "doc_link_existing": "Powiąż dokument…", + "doc_attach": "Powiąż", + "doc_unlink": "Odłącz", + "doc_page": "Strona", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1r", + "chart_since_service": "od ostatniej konserwacji", + "chart_no_stats": "Brak statystyk długoterminowych dla tej encji — wyświetlane są tylko wartości z wpisów konserwacji", + "auto_complete_on_recovery": "Zakończ automatycznie, gdy czujnik wróci do normy", + "auto_complete_on_recovery_help": "Rejestruje wykonanie (ustawia ostatnie wykonanie), gdy wyzwalacz sam się rozwiąże — np. uzupełniono sól, wymieniono filtr.", + "doc_search": "Szukaj dokumentów…", + "doc_search_none": "Brak pasujących dokumentów", + "link_device_optional": "Połącz z istniejącym urządzeniem (opcjonalnie)", + "parent_object_optional": "Obiekt nadrzędny (opcjonalnie)", + "parent_none": "(Brak nadrzędnego)", + "paused": "Wstrzymane", + "pause_object": "Wstrzymaj", + "resume_object": "Wznów", + "pause_until_prompt": "Zamroź harmonogramy tego obiektu — nic nie staje się wymagane i nic nie powiadamia do czasu wznowienia. Opcjonalnie ustaw datę automatycznego wznowienia.", + "pause_until_label": "Wznów dnia (opcjonalnie)", + "object_paused": "Obiekt wstrzymany", + "object_resumed": "Obiekt wznowiony — harmonogramy uruchomione od nowa", + "object_paused_badge": "Wstrzymane", + "paused_until_label": "do", + "replace_object": "Zastąp…", + "replace_object_prompt": "Wycofaj ten obiekt i utwórz następcę. Historia i koszty pozostają zarchiwizowane przy starym; zadania i dokumenty przechodzą do nowego, liczniki startują od zera.", + "replace_name_label": "Nazwa następcy", + "object_replaced": "Obiekt zastąpiony — utworzono następcę", + "reading_unit_label": "Jednostka odczytu (np. kWh, m³)", + "reading_unit_help": "Wyświetlana obok zarejestrowanej wartości przy ukończeniu tego zadania.", + "reading_value_label": "Wartość odczytu", + "reading_label": "Odczyt", + "settings_templates_label": "Galeria szablonów", + "settings_templates_hint": "Odznacz szablony, których nigdy nie użyjesz — znikną z wyboru „Z szablonu\" (panel i config flow). Nic więcej się nie zmienia; można je włączyć ponownie w każdej chwili.", + "worksheet": "Karta pracy", + "worksheet_scan_view": "Zeskanuj, aby otworzyć zadanie", + "worksheet_scan_complete": "Zeskanuj, aby ukończyć", + "worksheet_manual_excerpt": "Fragment instrukcji", + "worksheet_pages": "strony", + "worksheet_printed": "Wydrukowano", + "worksheet_never": "Nigdy" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/pt.json b/custom_components/maintenance_supporter/frontend/locales/pt.json new file mode 100644 index 0000000..233fdcc --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/pt.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Manutenção", + "objects": "Objetos", + "tasks": "Tarefas", + "overdue": "Atrasada", + "due_soon": "Próxima", + "triggered": "Acionada", + "ok": "OK", + "all": "Todos", + "new_object": "+ Novo objeto", + "templates_from": "De modelo", + "templates_title": "Começar a partir de um modelo", + "templates_task_count": "{n} tarefas", + "template_created": "Criado a partir de modelo", + "onboard_hint": "Adicione o seu primeiro objeto para acompanhar a manutenção.", + "edit": "Editar", + "duplicate": "Duplicar", + "task_duplicated": "Tarefa duplicada", + "object_duplicated": "Objeto duplicado", + "delete": "Eliminar", + "add_task": "+ Tarefa", + "complete": "Concluída", + "completed": "Concluída", + "skip": "Saltar", + "skipped": "Saltada", + "missed": "Missed", + "reset": "Repor", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Cancelar", + "bulk_select": "Selecionar", + "bulk_select_all": "Selecionar tudo", + "bulk_n_selected": "{n} selecionados", + "bulk_completed": "{n} tarefas concluídas", + "bulk_archived": "{n} tarefas arquivadas", + "completing": "A concluir…", + "interval": "Intervalo", + "warning": "Aviso", + "last_performed": "Última execução", + "next_due": "Próximo vencimento", + "days_until_due": "Dias até vencimento", + "avg_duration": "Ø Duração", + "trigger": "Acionador", + "trigger_type": "Tipo de acionador", + "threshold_above": "Limite superior", + "threshold_below": "Limite inferior", + "threshold": "Limiar", + "counter": "Contador", + "state_change": "Mudança de estado", + "runtime": "Tempo de funcionamento", + "runtime_hours": "Duração alvo (horas)", + "target_value": "Valor alvo", + "baseline": "Linha de base", + "target_changes": "Alterações alvo", + "for_minutes": "Durante (minutos)", + "time_based": "Temporal", + "sensor_based": "Sensor", + "manual": "Manual", + "one_time": "Única vez", + "weekdays": "Dias da semana", + "nth_weekday": "N-ésimo dia da semana do mês", + "day_of_month": "Dia do mês", + "recurrence_on_days": "Repetir em", + "recurrence_occurrence": "Ocorrência", + "recurrence_weekday": "Dia da semana", + "recurrence_day": "Dia do mês (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1.º", + "ord_2": "2.º", + "ord_3": "3.º", + "ord_4": "4.º", + "ord_5": "5.º", + "ord_last": "Último", + "day_word": "Dia", + "interval_value": "Intervalo", + "interval_unit": "Unidade", + "unit_days": "Dias", + "unit_weeks": "Semanas", + "unit_months": "Meses", + "unit_years": "Anos", + "due_date": "Data de vencimento", + "cleaning": "Limpeza", + "inspection": "Inspeção", + "replacement": "Substituição", + "calibration": "Calibração", + "service": "Serviço", + "reading": "Leitura", + "custom": "Personalizado", + "history": "Histórico", + "cost": "Custo", + "report_button": "Relatório", + "report_title": "Relatório de manutenção", + "report_generated": "Gerado", + "report_times_done": "Feito", + "report_total_cost": "Custo total", + "report_every": "a cada {n} {unit}", + "report_notes": "Notas", + "report_col_type": "Tipo", + "report_col_status": "Estado", + "report_col_schedule": "Agenda", + "duration": "Duração", + "both": "Ambos", + "trigger_val": "Valor do acionador", + "complete_title": "Concluída: ", + "checklist": "Lista de verificação", + "checklist_steps_optional": "Passos da lista de verificação (opcional)", + "checklist_placeholder": "Limpar filtro\nSubstituir vedação\nTestar pressão", + "checklist_help": "Um passo por linha. Máx. 100 itens.", + "err_too_long": "{field}: demasiado longo (máx. {n} caracteres)", + "err_too_short": "{field}: demasiado curto (mín. {n} caracteres)", + "err_value_too_high": "{field}: demasiado grande (máx. {n})", + "err_value_too_low": "{field}: demasiado pequeno (mín. {n})", + "err_required": "{field}: campo obrigatório", + "err_wrong_type": "{field}: tipo incorreto (esperado: {type})", + "err_invalid_choice": "{field}: valor não permitido", + "err_invalid_value": "{field}: valor inválido", + "feat_schedule_time": "Agendamento por hora", + "feat_schedule_time_desc": "Tarefas vencem em um horário específico em vez de meia-noite.", + "schedule_time_optional": "Vence às (opcional, HH:MM)", + "schedule_time_help": "Vazio = meia-noite (padrão). Fuso horário HA.", + "at_time": "às", + "notes_optional": "Notas (opcional)", + "cost_optional": "Custo (opcional)", + "duration_minutes": "Duração em minutos (opcional)", + "days": "dias", + "day": "dia", + "today": "Hoje", + "d_overdue": "d em atraso", + "no_tasks": "Sem tarefas de manutenção. Crie um objeto para começar.", + "no_tasks_short": "Sem tarefas", + "no_history": "Sem entradas no histórico.", + "show_all": "Mostrar tudo", + "cost_duration_chart": "Custos & Duração", + "installed": "Instalado", + "confirm_delete_object": "Eliminar este objeto e todas as suas tarefas?", + "confirm_delete_task": "Eliminar esta tarefa?", + "min": "Mín", + "max": "Máx", + "save": "Guardar", + "saving": "A guardar…", + "edit_task": "Editar tarefa", + "new_task": "Nova tarefa de manutenção", + "task_name": "Nome da tarefa", + "maintenance_type": "Tipo de manutenção", + "priority": "Prioridade", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Baixa", + "priority_normal": "Normal", + "priority_high": "Alta", + "schedule_type": "Tipo de agendamento", + "interval_days": "Intervalo (dias)", + "warning_days": "Dias de aviso", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Última execução (opcional)", + "interval_anchor": "Âncora do intervalo", + "anchor_completion": "A partir da data de conclusão", + "anchor_planned": "A partir da data planeada (sem desvio)", + "edit_object": "Editar objeto", + "name": "Nome", + "manufacturer_optional": "Fabricante (opcional)", + "model_optional": "Modelo (opcional)", + "serial_number_optional": "Número de série (opcional)", + "serial_number_label": "N/S", + "documentation_url_label": "Manual", + "object_notes_label": "Notas", + "sort_due_date": "Vencimento", + "sort_object": "Nome do objeto", + "sort_type": "Tipo", + "sort_task_name": "Nome da tarefa", + "all_objects": "Todos os objetos", + "tasks_lower": "tarefas", + "no_tasks_yet": "Ainda sem tarefas", + "add_first_task": "Adicionar primeira tarefa", + "trigger_configuration": "Configuração do acionador", + "entity_id": "ID da entidade", + "comma_separated": "separados por vírgulas", + "entity_logic": "Lógica da entidade", + "entity_logic_any": "Qualquer entidade aciona", + "entity_logic_all": "Todas as entidades devem acionar", + "entities": "entidades", + "attribute_optional": "Atributo (opcional, vazio = estado)", + "use_entity_state": "Usar estado da entidade (sem atributo)", + "trigger_above": "Acionar acima de", + "trigger_below": "Acionar abaixo de", + "for_at_least_minutes": "Durante pelo menos (minutos)", + "safety_interval_days": "Intervalo de segurança (dias, opcional)", + "safety_interval": "Intervalo de segurança (opcional)", + "delta_mode": "Modo delta", + "from_state_optional": "Do estado (opcional)", + "to_state_optional": "Para o estado (opcional)", + "documentation_url_optional": "URL de documentação (opcional)", + "object_notes_optional": "Notas (opcional)", + "nfc_tag_id_optional": "ID da etiqueta NFC (opcional)", + "nfc_tags_empty_help": "Ainda nenhuma tag NFC registada no Home Assistant.", + "nfc_tags_open_settings": "Abrir configurações de tags", + "nfc_tags_refresh": "Atualizar", + "environmental_entity_optional": "Sensor ambiental (opcional)", + "environmental_entity_helper": "ex. sensor.temperatura_exterior — ajusta o intervalo segundo as condições ambientais", + "environmental_attribute_optional": "Atributo ambiental (opcional)", + "nfc_tag_id": "ID da etiqueta NFC", + "nfc_linked": "Etiqueta NFC associada", + "nfc_link_hint": "Clique para associar etiqueta NFC", + "responsible_user": "Utilizador responsável", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Nenhum utilizador atribuído)", + "all_users": "Todos os utilizadores", + "my_tasks": "As minhas tarefas", + "tab_calendar": "Calendário", + "cal_no_events": "Sem manutenção", + "cal_window_7": "7 dias", + "cal_window_14": "14 dias", + "cal_window_30": "30 dias", + "cal_window_365": "1 ano", + "cal_every_n_days": "a cada {n} dias", + "cal_source_time": "Baseado em tempo", + "cal_source_time_adaptive": "Baseado em tempo (adaptativo)", + "cal_source_sensor": "Baseado em sensor", + "cal_predicted": "previsto", + "cal_confidence_high": "alta confiança", + "cal_confidence_medium": "confiança média", + "cal_confidence_low": "baixa confiança", + "budget_monthly": "Orçamento mensal", + "budget_yearly": "Orçamento anual", + "groups": "Grupos", + "new_group": "Novo grupo", + "edit_group": "Editar grupo", + "no_groups": "Ainda sem grupos", + "delete_group": "Eliminar grupo", + "delete_group_confirm": "Eliminar o grupo '{name}'?", + "group_select_tasks": "Selecionar tarefas", + "group_name_required": "Nome obrigatório", + "description_optional": "Descrição (opcional)", + "selected": "Selecionado", + "loading_chart": "A carregar dados...", + "hide_outliers": "Ocultar valores atípicos (erros do sensor)", + "was_maintenance_needed": "Esta manutenção era necessária?", + "feedback_needed": "Necessária", + "feedback_not_needed": "Não necessária", + "feedback_not_sure": "Não tenho a certeza", + "suggested_interval": "Intervalo sugerido", + "apply_suggestion": "Aplicar", + "reanalyze": "Reanalisar", + "reanalyze_result": "Nova análise", + "reanalyze_insufficient_data": "Dados insuficientes para uma recomendação", + "data_points": "pontos de dados", + "dismiss_suggestion": "Descartar", + "confidence_low": "Baixa", + "confidence_medium": "Média", + "confidence_high": "Alta", + "recommended": "recomendado", + "seasonal_awareness": "Consciência sazonal", + "edit_seasonal_overrides": "Editar fatores sazonais", + "seasonal_overrides_title": "Fatores sazonais (override)", + "seasonal_overrides_hint": "Fator por mês (0.1–5.0). Vazio = aprendido automaticamente.", + "seasonal_override_invalid": "Valor inválido", + "seasonal_override_range": "O fator deve estar entre 0.1 e 5.0", + "clear_all": "Limpar tudo", + "seasonal_chart_title": "Fatores sazonais", + "seasonal_learned": "Aprendido", + "seasonal_manual": "Manual", + "month_jan": "Jan", + "month_feb": "Fev", + "month_mar": "Mar", + "month_apr": "Abr", + "month_may": "Mai", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Ago", + "month_sep": "Set", + "month_oct": "Out", + "month_nov": "Nov", + "month_dec": "Dez", + "sensor_prediction": "Previsão do sensor", + "degradation_trend": "Tendência", + "trend_rising": "A subir", + "trend_falling": "A descer", + "trend_stable": "Estável", + "trend_insufficient_data": "Dados insuficientes", + "days_until_threshold": "Dias até ao limiar", + "threshold_exceeded": "Limiar ultrapassado", + "environmental_adjustment": "Fator ambiental", + "sensor_prediction_urgency": "O sensor prevê o limiar em ~{days} dias", + "day_short": "dia", + "weibull_reliability_curve": "Curva de fiabilidade", + "weibull_failure_probability": "Probabilidade de falha", + "weibull_r_squared": "Ajuste R²", + "beta_early_failures": "Falhas precoces", + "beta_random_failures": "Falhas aleatórias", + "beta_wear_out": "Desgaste", + "beta_highly_predictable": "Altamente previsível", + "confidence_interval": "Intervalo de confiança", + "confidence_conservative": "Conservador", + "confidence_aggressive": "Otimista", + "current_interval_marker": "Intervalo atual", + "recommended_marker": "Recomendado", + "characteristic_life": "Vida característica", + "chart_mini_sparkline": "Sparkline de tendência", + "chart_history": "Histórico de custos e duração", + "chart_seasonal": "Fatores sazonais, 12 meses", + "chart_weibull": "Curva de fiabilidade Weibull", + "chart_sparkline": "Gráfico de valor do acionador", + "days_progress": "Progresso em dias", + "qr_code": "Código QR", + "qr_generating": "A gerar código QR…", + "qr_error": "Não foi possível gerar o código QR.", + "qr_error_no_url": "Nenhum URL do HA configurado. Defina um URL externo ou interno em Definições → Sistema → Rede.", + "save_error": "Erro ao guardar. Tente novamente.", + "qr_print": "Imprimir", + "qr_download": "Transferir SVG", + "qr_action": "Ação ao digitalizar", + "qr_action_view": "Ver informações de manutenção", + "qr_action_complete": "Marcar manutenção como concluída", + "qr_url_mode": "Tipo de ligação", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Local (mDNS)", + "qr_mode_server": "URL do servidor", + "overview": "Visão geral", + "analysis": "Análise", + "recent_activities": "Atividades recentes", + "search_notes": "Pesquisar notas", + "avg_cost": "Ø Custo", + "no_advanced_features": "Sem funções avançadas ativadas", + "no_advanced_features_hint": "Ative “Intervalos Adaptativos” ou “Padrões Sazonais” nas definições da integração para ver dados de análise aqui.", + "analysis_not_enough_data": "Ainda não há dados suficientes para a análise.", + "analysis_not_enough_data_hint": "A análise Weibull requer pelo menos 5 manutenções concluídas; os padrões sazonais tornam-se visíveis após 6+ pontos de dados por mês.", + "analysis_manual_task_hint": "Tarefas manuais sem intervalo não geram dados de análise.", + "completions": "conclusões", + "current": "Atual", + "shorter": "Mais curto", + "longer": "Mais longo", + "normal": "Normal", + "disabled": "Desativado", + "compound_logic": "Lógica composta", + "compound": "Composto (várias condições)", + "compound_logic_and": "E — todas as condições devem disparar", + "compound_logic_or": "OU — basta uma condição", + "compound_help": "Combine várias condições de sensores num só disparador.", + "compound_no_conditions": "Ainda sem condições — adicione pelo menos uma.", + "compound_add_condition": "Adicionar condição", + "compound_condition": "Condição", + "compound_remove_condition": "Remover condição", + "card_title": "Título", + "card_show_header": "Mostrar cabeçalho com estatísticas", + "card_show_actions": "Mostrar botões de ação", + "card_compact": "Modo compacto", + "card_max_items": "Máx. itens (0 = todos)", + "card_filter_status": "Filtrar por estado", + "card_filter_status_help": "Vazio = mostrar todos os estados.", + "card_filter_objects": "Filtrar por objetos", + "card_filter_objects_help": "Vazio = mostrar todos os objetos.", + "card_filter_entities": "Filtrar por entidades (entity_ids)", + "card_filter_entities_help": "Selecione entidades sensor / binary_sensor desta integração. Vazio = todas.", + "card_loading_objects": "A carregar objetos…", + "card_load_error": "Não foi possível carregar os objetos — verifique a ligação WebSocket.", + "card_no_tasks_title": "Ainda sem tarefas de manutenção", + "card_no_tasks_cta": "→ Crie uma no painel Manutenção", + "no_objects": "Ainda sem objetos.", + "action_error": "Ação falhada. Tente novamente.", + "area_id_optional": "Área (opcional)", + "installation_date_optional": "Data de instalação (opcional)", + "warranty_expiry_optional": "Validade da garantia (opcional)", + "warranty": "Garantia", + "warranty_valid_until": "válida até {date}", + "warranty_expires_in": "expira em {days} dias", + "warranty_expired": "expirada", + "cal_past_windows": "Janelas passadas", + "cal_forward_windows": "Janelas futuras", + "history_edit_title": "Editar entrada do histórico", + "history_edit_timestamp": "Carimbo de data/hora", + "manufacturer": "Fabricante", + "model": "Modelo", + "area": "Área", + "actions": "Ações", + "view_mode_label": "Vista", + "view_cards": "Vista de cartões", + "view_table": "Vista de tabela", + "objects_table_columns_label": "Colunas da tabela de objetos", + "objects_table_columns_hint": "Escolha que colunas aparecem na vista de tabela de objetos.", + "custom_icon_optional": "Ícone (opcional, ex. mdi:wrench)", + "task_enabled": "Tarefa ativada", + "skip_reason_prompt": "Saltar esta tarefa?", + "reason_optional": "Motivo (opcional)", + "reset_date_prompt": "Marcar tarefa como executada?", + "reset_date_optional": "Data da última execução (opcional, padrão: hoje)", + "notes_label": "Notas", + "documentation_label": "Documentação", + "no_nfc_tag": "— Sem etiqueta —", + "dashboard": "Painel", + "tab_today": "Hoje", + "palette_placeholder": "Pesquisar objetos e tarefas…", + "palette_no_results": "Sem resultados", + "palette_hint": "↑↓ navegar · Enter abrir · Esc fechar", + "today_all_caught_up": "Tudo em dia! Nada para esta semana.", + "today_overdue": "Atrasadas", + "today_due_today": "Vence hoje", + "today_this_week": "Esta semana", + "settings": "Definições", + "settings_features": "Funções avançadas", + "settings_features_desc": "Ative ou desative funções avançadas. Desativar oculta-as da interface mas não elimina dados.", + "feat_adaptive": "Agendamento adaptativo", + "feat_adaptive_desc": "Aprender intervalos ideais a partir do histórico de manutenção", + "feat_predictions": "Previsões do sensor", + "feat_predictions_desc": "Prever datas de acionamento pela degradação do sensor", + "feat_seasonal": "Ajustes sazonais", + "feat_seasonal_desc": "Ajustar intervalos com base em padrões sazonais", + "feat_environmental": "Correlação ambiental", + "feat_environmental_desc": "Correlacionar intervalos com temperatura/humidade", + "feat_budget": "Controlo de orçamento", + "feat_budget_desc": "Acompanhar despesas de manutenção mensais e anuais", + "feat_groups": "Grupos de tarefas", + "feat_groups_desc": "Organizar tarefas em grupos lógicos", + "feat_checklists": "Listas de verificação", + "feat_checklists_desc": "Procedimentos com vários passos para conclusão de tarefas", + "settings_general": "Geral", + "settings_default_warning": "Dias de aviso predefinidos", + "settings_panel_enabled": "Painel lateral", + "settings_panel_title": "Título do painel lateral", + "settings_notifications": "Notificações", + "settings_notify_service": "Serviço de notificação", + "test_notification": "Notificação de teste", + "send_test": "Enviar teste", + "testing": "A enviar…", + "test_notification_success": "Notificação de teste enviada", + "test_notification_failed": "Falha na notificação de teste", + "settings_notify_due_soon": "Notificar quando próxima", + "settings_notify_overdue": "Notificar quando atrasada", + "settings_notify_triggered": "Notificar quando acionada", + "settings_interval_hours": "Intervalo de repetição (horas, 0 = uma vez)", + "settings_quiet_hours": "Horas de silêncio", + "settings_quiet_start": "Início", + "settings_quiet_end": "Fim", + "settings_max_per_day": "Máx. notificações por dia (0 = ilimitado)", + "settings_bundling": "Agrupar notificações", + "settings_bundle_threshold": "Limiar de agrupamento", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Botões de ação móveis", + "settings_action_complete": "Mostrar botão 'Concluída'", + "settings_action_skip": "Mostrar botão 'Saltar'", + "settings_action_snooze": "Mostrar botão 'Adiar'", + "settings_weekly_digest": "Resumo semanal", + "settings_weekly_digest_hint": "Uma notificação de resumo na segunda de manhã quando há tarefas.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Duração do adiamento (horas)", + "settings_budget": "Orçamento", + "settings_currency": "Moeda", + "settings_budget_monthly": "Orçamento mensal", + "settings_budget_yearly": "Orçamento anual", + "settings_budget_alerts": "Alertas de orçamento", + "settings_budget_threshold": "Limiar de alerta (%)", + "settings_import_export": "Importar / Exportar", + "settings_export_json": "Exportar JSON", + "settings_export_yaml": "Exportar YAML", + "settings_export_csv": "Exportar CSV", + "settings_import_csv": "Importar CSV", + "settings_import_placeholder": "Cole o conteúdo JSON ou CSV aqui…", + "settings_import_btn": "Importar", + "settings_import_success": "{count} objetos importados com sucesso.", + "settings_export_success": "Exportação transferida.", + "settings_saved": "Definição guardada.", + "settings_include_history": "Incluir histórico", + "sort_alphabetical": "Alfabético", + "sort_due_soonest": "Vencimento mais próximo", + "sort_task_count": "Quantidade de tarefas", + "sort_area": "Área", + "sort_assigned_user": "Usuário atribuído", + "sort_group": "Grupo", + "filter_label": "Filtro", + "user_label": "Utilizador", + "sort_label": "Ordenar", + "group_by_label": "Agrupar por", + "state_value_help": "Use o valor de estado HA (normalmente em minúsculas, p. ex. \"on\"/\"off\"). As maiúsculas são normalizadas ao guardar.", + "target_changes_help": "Número de transições correspondentes antes do trigger disparar (predefinido: 1).", + "qr_print_title": "Imprimir códigos QR", + "qr_print_desc": "Gera uma página imprimível de códigos QR para recortar e colar nos equipamentos.", + "qr_print_load": "Carregar objetos", + "qr_print_filter": "Filtro", + "qr_print_objects": "Objetos", + "qr_print_actions": "Ações", + "qr_print_url_mode": "Tipo de link", + "qr_print_estimate": "Códigos QR estimados", + "qr_print_over_limit": "máximo 200, restrinja o filtro", + "qr_print_generate": "Gerar códigos QR", + "qr_print_generating": "A gerar…", + "qr_print_ready": "Códigos QR prontos", + "qr_print_print_button": "Imprimir", + "qr_print_empty": "Nada a gerar", + "qr_action_skip": "Saltar", + "vacation_title": "Modo de férias", + "vacation_active": "ativo", + "vacation_ended": "terminado", + "vacation_desc": "Planeia as tuas férias: as notificações são pausadas durante o período mais dias de margem. Podes manter exceções por tarefa.", + "vacation_enable": "Ativar modo de férias", + "vacation_start": "Início", + "vacation_end": "Fim", + "vacation_buffer": "Margem (dias)", + "vacation_exempt_title": "Notificar mesmo em férias", + "vacation_exempt_desc": "Escolhe tarefas que devem notificar mesmo em férias (p. ex. química crítica de piscina).", + "vacation_load_tasks": "Carregar tarefas", + "vacation_preview_btn": "Mostrar pré-visualização", + "vacation_preview_affected": "tarefas afetadas", + "vacation_event_due_soon": "ficará próxima do prazo", + "vacation_event_overdue": "ficará em atraso", + "vacation_event_triggered_est": "possível disparo do sensor", + "vacation_sensor_based": "(baseado em sensor)", + "vacation_action_notify": "Notificar mesmo assim", + "vacation_action_unsilence": "Silenciar novamente", + "vacation_marked_complete": "Marcado como concluído", + "vacation_marked_skip": "Saltado", + "vacation_end_now": "Terminar férias agora", + "groupby_none": "Sem agrupamento", + "groupby_area": "Por área", + "groupby_group": "Por grupo", + "groupby_user": "Por usuário", + "unassigned": "Não atribuído", + "no_area": "Sem área", + "has_overdue": "Tarefas em atraso", + "object": "Objeto", + "settings_panel_access": "Acesso ao painel", + "settings_panel_access_desc": "Administradores sempre têm acesso completo. Para delegar criar, editar e excluir a não administradores específicos, ative isto e selecione-os abaixo — os demais só veem Concluir e Ignorar.", + "settings_operator_write": "Permitir que os usuários selecionados criem, editem e excluam", + "settings_operator_write_desc": "Desativado: apenas administradores podem alterar o conteúdo. Ativado: os usuários selecionados abaixo também têm acesso completo.", + "no_non_admin_users": "Nenhum usuário não administrador encontrado. Adicione em Configurações → Pessoas.", + "owner_label": "Proprietário", + "feat_completion_actions": "Ações de conclusão", + "feat_completion_actions_desc": "Ação HA por tarefa ao concluir + QR de conclusão rápida com valores predefinidos.", + "on_complete_action_title": "Na conclusão: acionar ação HA (opcional)", + "on_complete_action_desc": "Chama um serviço HA quando a tarefa é concluída — p. ex. reiniciar um contador no dispositivo.", + "on_complete_action_service": "Serviço", + "on_complete_action_target": "Entidade alvo", + "on_complete_action_data": "Dados (JSON, opcional)", + "on_complete_action_test": "Testar ação", + "on_complete_action_test_success": "Sucesso", + "on_complete_action_test_failed": "Falhou", + "quick_complete_defaults_title": "Valores predefinidos de conclusão rápida (para scans QR, opcional)", + "quick_complete_defaults_desc": "Valores predefinidos para QR de conclusão rápida. Sem eles, o QR abre o diálogo.", + "quick_complete_defaults_notes": "Notas", + "quick_complete_defaults_cost": "Custo", + "quick_complete_defaults_duration": "Duração (minutos)", + "quick_complete_defaults_feedback_none": "Sem feedback", + "quick_complete_defaults_feedback_needed": "Era necessário", + "quick_complete_defaults_feedback_not_needed": "Não era necessário", + "quick_complete_success": "Concluído rapidamente", + "trigger_replaced": "Acionador substituído", + "add": "Adicionar", + "show_stats": "Mostrar estatísticas + gráficos", + "hide_stats": "Ocultar estatísticas", + "adaptive_no_data": "Ainda não há histórico de conclusões suficiente para a análise adaptativa. Conclua esta tarefa mais algumas vezes para desbloquear as recomendações de intervalo e os gráficos de fiabilidade.", + "suggestion_applied": "Intervalo sugerido aplicado", + "vacation_mode": "Modo de férias", + "vacation_status_active": "Ativo agora", + "vacation_status_scheduled": "Agendado", + "vacation_status_inactive": "Inativo", + "vacation_end_now_confirm": "Terminar as férias imediatamente?", + "vacation_exempt_count": "isentas", + "vacation_advanced": "Avançado…", + "vacation_open_panel": "Abrir no painel", + "enable": "Ativar", + "saved": "Guardado", + "budget_monthly_set": "Definir mensal", + "budget_yearly_set": "Definir anual", + "budget_advanced": "Moeda, alertas…", + "budget_open_panel": "Abrir no painel", + "groups_empty": "Ainda sem grupos.", + "group_new_placeholder": "Adicionar grupo…", + "group_delete_confirm": "Eliminar o grupo \"{name}\"?", + "groups_manage_tasks": "Gerir atribuições de tarefas…", + "groups_open_panel": "Abrir no painel", + "on_complete_action_target_hint": "Nota: o domínio da entidade tem de corresponder ao serviço — p. ex. 'button.press' só funciona em button.*, 'counter.increment' só em counter.*, 'input_button.press' só em input_button.* etc. Em caso de incompatibilidade, a ação falha silenciosamente (o HA regista 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Mostrar todos os objetos", + "show_all_tasks": "Limpar filtro — mostrar todas as tarefas", + "filter_to_overdue": "Filtrar a lista apenas para em atraso", + "filter_to_due_soon": "Filtrar a lista apenas para a vencer em breve", + "filter_to_triggered": "Filtrar a lista apenas para acionadas", + "open_task": "Abrir tarefa", + "show_details": "Mostrar histórico + estatísticas", + "hide_details": "Ocultar detalhes", + "history_empty": "Ainda sem histórico.", + "history_edit_button": "Editar entrada", + "total_cost": "Custo total", + "times_performed": "Realizada", + "older_entries": "mais antigas", + "open_in_panel": "Abrir no painel Manutenção", + "skip_reason": "Motivo para saltar (opcional)", + "reset_to_date": "Repor última execução para", + "delete_task_confirm": "Eliminar esta tarefa e o seu histórico?", + "delete_object_confirm": "Eliminar este objeto e todas as suas tarefas?", + "loading": "A carregar…", + "archive": "Arquivar", + "undo": "Desfazer", + "task_archived": "Tarefa arquivada", + "object_archived": "Objeto arquivado", + "unarchive": "Desarquivar", + "archived": "Arquivado", + "show_archived": "Mostrar arquivados", + "hide_archived": "Ocultar arquivados", + "confirm_archive_object": "Arquivar este objeto e as suas tarefas? O histórico é mantido e pode ser restaurado mais tarde.", + "settings_archive": "Arquivo e retenção", + "settings_archive_desc": "Retire tarefas únicas concluídas sem as eliminar. Os itens arquivados ficam ocultos e inativos, mas mantêm histórico e custos.", + "settings_archive_oneoff_days": "Arquivar automaticamente tarefas únicas concluídas após (dias, 0 = desligado)", + "settings_delete_archived_oneoff_days": "Eliminar automaticamente tarefas únicas arquivadas após (dias, 0 = nunca)", + "archive_object": "Arquivar objeto", + "unarchive_object": "Desarquivar objeto", + "documents": "Documentos", + "documents_empty": "Ainda sem documentos.", + "doc_upload": "Carregar ficheiro", + "doc_uploading": "A carregar…", + "doc_add_link": "Adicionar ligação", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Título (opcional)", + "doc_open": "Abrir", + "doc_delete_confirm": "Eliminar «{name}»?", + "doc_too_large": "Ficheiro demasiado grande (máx. 25 MB).", + "doc_upload_failed": "Falha no carregamento.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Já armazenado noutro local — partilhado, sem espaço extra.", + "doc_dup_in_object": "Este ficheiro já está anexado a este objeto.", + "doc_link_invalid": "Apenas ligações http/https são permitidas.", + "doc_cat_manual": "Manual", + "doc_cat_warranty": "Garantia", + "doc_cat_invoice": "Fatura", + "doc_cat_spare_parts": "Peças", + "doc_cat_photo": "Foto", + "doc_cat_other": "Outro", + "doc_link_badge": "Ligação", + "doc_storage_title": "Armazenamento de documentos", + "doc_storage_saved": "Poupado por deduplicação", + "doc_storage_refresh": "Atualizar", + "doc_download": "Transferir", + "doc_close": "Fechar", + "doc_camera": "Tirar foto", + "doc_drop_hint": "Solte os ficheiros aqui", + "doc_task_none": "Nenhum documento associado a esta tarefa.", + "doc_link_existing": "Associar um documento…", + "doc_attach": "Associar", + "doc_unlink": "Desassociar", + "doc_page": "Página", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1a", + "chart_since_service": "desde a última manutenção", + "chart_no_stats": "Sem estatísticas de longo prazo para esta entidade — mostrando apenas valores de eventos de manutenção", + "auto_complete_on_recovery": "Concluir automaticamente quando o sensor recuperar", + "auto_complete_on_recovery_help": "Regista uma conclusão (define a última realização) quando o gatilho se resolve sozinho — p. ex., sal reabastecido, filtro substituído.", + "doc_search": "Pesquisar documentos…", + "doc_search_none": "Nenhum documento correspondente", + "link_device_optional": "Associar a dispositivo existente (opcional)", + "parent_object_optional": "Objeto principal (opcional)", + "parent_none": "(Sem objeto principal)", + "paused": "Pausado", + "pause_object": "Pausar", + "resume_object": "Retomar", + "pause_until_prompt": "Congela os agendamentos deste objeto — nada vence nem notifica até ser retomado. Opcionalmente, defina uma data de retoma automática.", + "pause_until_label": "Retomar em (opcional)", + "object_paused": "Objeto pausado", + "object_resumed": "Objeto retomado — agendamentos reiniciados", + "object_paused_badge": "Pausado", + "paused_until_label": "até", + "replace_object": "Substituir…", + "replace_object_prompt": "Retire este objeto e crie um sucessor. O histórico e os custos ficam arquivados no antigo; as tarefas e os documentos passam para o novo, os contadores começam do zero.", + "replace_name_label": "Nome do sucessor", + "object_replaced": "Objeto substituído — sucessor criado", + "reading_unit_label": "Unidade de leitura (p. ex. kWh, m³)", + "reading_unit_help": "Mostrada ao lado do valor registado ao concluir esta tarefa.", + "reading_value_label": "Valor de leitura", + "reading_label": "Leitura", + "settings_templates_label": "Galeria de modelos", + "settings_templates_hint": "Desmarque modelos de que nunca precisará — desaparecem dos seletores \"A partir de modelo\" (painel e config flow). Nada mais muda; reativáveis a qualquer momento.", + "worksheet": "Folha de trabalho", + "worksheet_scan_view": "Digitalize para abrir a tarefa", + "worksheet_scan_complete": "Digitalize para concluir", + "worksheet_manual_excerpt": "Excerto do manual", + "worksheet_pages": "páginas", + "worksheet_printed": "Impresso", + "worksheet_never": "Nunca" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/ru.json b/custom_components/maintenance_supporter/frontend/locales/ru.json new file mode 100644 index 0000000..cabacb7 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/ru.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Обслуживание", + "objects": "Объекты", + "tasks": "Задачи", + "overdue": "Просрочено", + "due_soon": "Скоро", + "triggered": "Сработало", + "ok": "OK", + "all": "Все", + "new_object": "+ Новый объект", + "templates_from": "Из шаблона", + "templates_title": "Начать с шаблона", + "templates_task_count": "{n} задач", + "template_created": "Создано из шаблона", + "onboard_hint": "Добавьте первый объект, чтобы отслеживать обслуживание.", + "edit": "Изменить", + "duplicate": "Дублировать", + "task_duplicated": "Задача продублирована", + "object_duplicated": "Объект продублирован", + "delete": "Удалить", + "add_task": "+ Добавить задачу", + "complete": "Выполнить", + "completed": "Выполнено", + "skip": "Пропустить", + "skipped": "Пропущено", + "missed": "Missed", + "reset": "Сбросить", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Отмена", + "bulk_select": "Выбрать", + "bulk_select_all": "Выбрать все", + "bulk_n_selected": "Выбрано: {n}", + "bulk_completed": "Выполнено задач: {n}", + "bulk_archived": "Архивировано задач: {n}", + "completing": "Выполнение…", + "interval": "Интервал", + "warning": "Предупреждение", + "last_performed": "Последнее выполнение", + "next_due": "Следующий срок", + "days_until_due": "Дней до срока", + "avg_duration": "Ср. длительность", + "trigger": "Триггер", + "trigger_type": "Тип триггера", + "threshold_above": "Верхний предел", + "threshold_below": "Нижний предел", + "threshold": "Порог", + "counter": "Счётчик", + "state_change": "Изменение состояния", + "runtime": "Время работы", + "runtime_hours": "Целевое время работы (часы)", + "target_value": "Целевое значение", + "baseline": "Базовое значение", + "target_changes": "Целевые изменения", + "for_minutes": "На (минут)", + "time_based": "По времени", + "sensor_based": "По датчику", + "manual": "Вручную", + "one_time": "Однократно", + "weekdays": "Дни недели", + "nth_weekday": "N-й день недели месяца", + "day_of_month": "День месяца", + "recurrence_on_days": "Повторять в", + "recurrence_occurrence": "Вхождение", + "recurrence_weekday": "День недели", + "recurrence_day": "День месяца (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1-й", + "ord_2": "2-й", + "ord_3": "3-й", + "ord_4": "4-й", + "ord_5": "5-й", + "ord_last": "Последний", + "day_word": "День", + "interval_value": "Интервал", + "interval_unit": "Единица", + "unit_days": "Дни", + "unit_weeks": "Недели", + "unit_months": "Месяцы", + "unit_years": "Годы", + "due_date": "Дата выполнения", + "cleaning": "Чистка", + "inspection": "Осмотр", + "replacement": "Замена", + "calibration": "Калибровка", + "service": "Сервис", + "reading": "Показания", + "custom": "Своё", + "history": "История", + "cost": "Стоимость", + "report_button": "Отчёт", + "report_title": "Отчёт об обслуживании", + "report_generated": "Создан", + "report_times_done": "Выполнено", + "report_total_cost": "Итого", + "report_every": "каждые {n} {unit}", + "report_notes": "Заметки", + "report_col_type": "Тип", + "report_col_status": "Статус", + "report_col_schedule": "Расписание", + "duration": "Длительность", + "both": "Оба", + "trigger_val": "Значение триггера", + "complete_title": "Выполнить: ", + "checklist": "Контрольный список", + "checklist_steps_optional": "Шаги контрольного списка (необязательно)", + "checklist_placeholder": "Очистить фильтр\nЗаменить уплотнитель\nПроверить давление", + "checklist_help": "Один шаг на строку. Макс. 100 элементов.", + "err_too_long": "{field}: слишком длинно (макс. {n} символов)", + "err_too_short": "{field}: слишком коротко (мин. {n} символов)", + "err_value_too_high": "{field}: слишком велико (макс. {n})", + "err_value_too_low": "{field}: слишком мало (мин. {n})", + "err_required": "{field}: обязательное поле", + "err_wrong_type": "{field}: неверный тип (ожидался: {type})", + "err_invalid_choice": "{field}: недопустимое значение", + "err_invalid_value": "{field}: неверное значение", + "feat_schedule_time": "Планирование по времени дня", + "feat_schedule_time_desc": "Задачи становятся просроченными в определённое время дня, а не в полночь.", + "schedule_time_optional": "Срок (необязательно, HH:MM)", + "schedule_time_help": "Пусто = полночь (по умолчанию). Часовой пояс HA.", + "at_time": "в", + "notes_optional": "Примечания (опционально)", + "cost_optional": "Стоимость (опционально)", + "duration_minutes": "Длительность в минутах (опционально)", + "days": "дней", + "day": "день", + "today": "Сегодня", + "d_overdue": "дн. просрочено", + "no_tasks": "Пока нет задач по обслуживанию. Создайте объект, чтобы начать.", + "no_tasks_short": "Нет задач", + "no_history": "Пока нет записей в истории.", + "show_all": "Показать все", + "cost_duration_chart": "Стоимость и длительность", + "installed": "Установлен", + "confirm_delete_object": "Удалить этот объект и все его задачи?", + "confirm_delete_task": "Удалить эту задачу?", + "min": "Мин", + "max": "Макс", + "save": "Сохранить", + "saving": "Сохранение…", + "edit_task": "Изменить задачу", + "new_task": "Новая задача обслуживания", + "task_name": "Название задачи", + "maintenance_type": "Тип обслуживания", + "priority": "Приоритет", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Низкий", + "priority_normal": "Обычный", + "priority_high": "Высокий", + "schedule_type": "Тип расписания", + "interval_days": "Интервал (дни)", + "warning_days": "Дни предупреждения", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "interval_anchor": "Якорь интервала", + "anchor_completion": "От даты выполнения", + "anchor_planned": "От плановой даты (без смещения)", + "edit_object": "Изменить объект", + "name": "Имя", + "manufacturer_optional": "Производитель (опционально)", + "model_optional": "Модель (опционально)", + "serial_number_optional": "Серийный номер (опционально)", + "serial_number_label": "С/Н", + "documentation_url_label": "Руководство", + "object_notes_label": "Заметки", + "sort_due_date": "Срок", + "sort_object": "Имя объекта", + "sort_type": "Тип", + "sort_task_name": "Имя задачи", + "all_objects": "Все объекты", + "tasks_lower": "задач", + "no_tasks_yet": "Пока нет задач", + "add_first_task": "Добавить первую задачу", + "last_performed_optional": "Последнее выполнение (опционально)", + "trigger_configuration": "Настройка триггера", + "entity_id": "ID сущности", + "comma_separated": "через запятую", + "entity_logic": "Логика сущностей", + "entity_logic_any": "Любая сущность срабатывает", + "entity_logic_all": "Все сущности должны сработать", + "entities": "сущности", + "attribute_optional": "Атрибут (опционально, пусто = состояние)", + "use_entity_state": "Использовать состояние сущности (без атрибута)", + "trigger_above": "Срабатывать выше", + "trigger_below": "Срабатывать ниже", + "for_at_least_minutes": "Не менее (минут)", + "safety_interval_days": "Интервал безопасности (дни, опционально)", + "safety_interval": "Интервал безопасности (опционально)", + "delta_mode": "Режим дельты", + "from_state_optional": "Из состояния (опционально)", + "to_state_optional": "В состояние (опционально)", + "documentation_url_optional": "URL документации (опционально)", + "object_notes_optional": "Заметки (опционально)", + "nfc_tag_id_optional": "ID NFC-метки (опционально)", + "nfc_tags_empty_help": "В Home Assistant ещё не зарегистрировано NFC-тегов.", + "nfc_tags_open_settings": "Открыть настройки тегов", + "nfc_tags_refresh": "Обновить", + "environmental_entity_optional": "Датчик окружающей среды (опционально)", + "environmental_entity_helper": "напр. sensor.outdoor_temperature — корректирует интервал в зависимости от условий", + "environmental_attribute_optional": "Атрибут среды (опционально)", + "nfc_tag_id": "ID NFC-метки", + "nfc_linked": "NFC-метка привязана", + "nfc_link_hint": "Нажмите, чтобы привязать NFC-метку", + "responsible_user": "Ответственный пользователь", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Не назначен)", + "all_users": "Все пользователи", + "my_tasks": "Мои задачи", + "tab_calendar": "Календарь", + "cal_no_events": "Без обслуживания", + "cal_window_7": "7 дней", + "cal_window_14": "14 дней", + "cal_window_30": "30 дней", + "cal_window_365": "1 год", + "cal_every_n_days": "каждые {n} дней", + "cal_source_time": "По времени", + "cal_source_time_adaptive": "По времени (адаптивно)", + "cal_source_sensor": "По датчику", + "cal_predicted": "прогноз", + "cal_confidence_high": "высокая надёжность", + "cal_confidence_medium": "средняя надёжность", + "cal_confidence_low": "низкая надёжность", + "budget_monthly": "Месячный бюджет", + "budget_yearly": "Годовой бюджет", + "groups": "Группы", + "new_group": "Новая группа", + "edit_group": "Редактировать группу", + "no_groups": "Групп пока нет", + "delete_group": "Удалить группу", + "delete_group_confirm": "Удалить группу '{name}'?", + "group_select_tasks": "Выбрать задачи", + "group_name_required": "Требуется имя", + "description_optional": "Описание (опционально)", + "selected": "Выбрано", + "loading_chart": "Загрузка данных графика...", + "hide_outliers": "Скрыть выбросы (сбои датчика)", + "was_maintenance_needed": "Требовалось ли это обслуживание?", + "feedback_needed": "Требовалось", + "feedback_not_needed": "Не требовалось", + "feedback_not_sure": "Не уверен", + "suggested_interval": "Рекомендуемый интервал", + "apply_suggestion": "Применить", + "reanalyze": "Повторный анализ", + "reanalyze_result": "Новый анализ", + "reanalyze_insufficient_data": "Недостаточно данных для рекомендации", + "data_points": "точек данных", + "dismiss_suggestion": "Отклонить", + "confidence_low": "Низкая", + "confidence_medium": "Средняя", + "confidence_high": "Высокая", + "recommended": "рекомендуется", + "seasonal_awareness": "Сезонная адаптация", + "edit_seasonal_overrides": "Редактировать сезонные коэффициенты", + "seasonal_overrides_title": "Сезонные коэффициенты (переопределение)", + "seasonal_overrides_hint": "Коэффициент на месяц (0.1–5.0). Пусто = автоматически.", + "seasonal_override_invalid": "Недопустимое значение", + "seasonal_override_range": "Коэффициент должен быть от 0.1 до 5.0", + "clear_all": "Очистить все", + "seasonal_chart_title": "Сезонные факторы", + "seasonal_learned": "Изученные", + "seasonal_manual": "Ручные", + "month_jan": "Янв", + "month_feb": "Фев", + "month_mar": "Мар", + "month_apr": "Апр", + "month_may": "Май", + "month_jun": "Июн", + "month_jul": "Июл", + "month_aug": "Авг", + "month_sep": "Сен", + "month_oct": "Окт", + "month_nov": "Ноя", + "month_dec": "Дек", + "sensor_prediction": "Предсказание по датчику", + "degradation_trend": "Тренд", + "trend_rising": "Растущий", + "trend_falling": "Падающий", + "trend_stable": "Стабильный", + "trend_insufficient_data": "Недостаточно данных", + "days_until_threshold": "Дней до порога", + "threshold_exceeded": "Порог превышен", + "environmental_adjustment": "Фактор среды", + "sensor_prediction_urgency": "Датчик предсказывает порог через ~{days} дней", + "day_short": "дн", + "weibull_reliability_curve": "Кривая надёжности", + "weibull_failure_probability": "Вероятность отказа", + "weibull_r_squared": "Качество аппроксимации R²", + "beta_early_failures": "Ранние отказы", + "beta_random_failures": "Случайные отказы", + "beta_wear_out": "Износ", + "beta_highly_predictable": "Высокая предсказуемость", + "confidence_interval": "Доверительный интервал", + "confidence_conservative": "Консервативный", + "confidence_aggressive": "Оптимистичный", + "current_interval_marker": "Текущий интервал", + "recommended_marker": "Рекомендуемый", + "characteristic_life": "Характеристический срок службы", + "chart_mini_sparkline": "Мини-график тренда", + "chart_history": "История стоимости и длительности", + "chart_seasonal": "Сезонные факторы, 12 месяцев", + "chart_weibull": "Кривая надёжности Вейбулла", + "chart_sparkline": "График значений триггера датчика", + "days_progress": "Прогресс по дням", + "qr_code": "QR-код", + "qr_generating": "Генерация QR-кода…", + "qr_error": "Не удалось сгенерировать QR-код.", + "qr_error_no_url": "URL HA не настроен. Установите внешний или внутренний URL в Настройках → Система → Сеть.", + "save_error": "Не удалось сохранить. Попробуйте ещё раз.", + "qr_print": "Печать", + "qr_download": "Скачать SVG", + "qr_action": "Действие при сканировании", + "qr_action_view": "Просмотр", + "qr_action_complete": "Отметить обслуживание как выполненное", + "qr_url_mode": "Тип ссылки", + "qr_mode_companion": "Приложение-компаньон", + "qr_mode_local": "Локальный (mDNS)", + "qr_mode_server": "URL сервера", + "overview": "Обзор", + "analysis": "Анализ", + "recent_activities": "Недавние действия", + "search_notes": "Поиск по заметкам", + "avg_cost": "Ср. стоимость", + "no_advanced_features": "Расширенные функции не включены", + "no_advanced_features_hint": "Включите «Адаптивные интервалы» или «Сезонные паттерны» в настройках интеграции, чтобы увидеть здесь аналитику.", + "analysis_not_enough_data": "Недостаточно данных для анализа.", + "analysis_not_enough_data_hint": "Для анализа Вейбулла требуется минимум 5 выполненных обслуживаний; сезонные паттерны становятся видны после 6+ точек данных в месяц.", + "analysis_manual_task_hint": "Ручные задачи без интервала не генерируют аналитику.", + "completions": "выполнений", + "current": "Текущий", + "shorter": "Короче", + "longer": "Длиннее", + "normal": "Нормальный", + "disabled": "Отключено", + "compound_logic": "Составная логика", + "compound": "Составной (несколько условий)", + "compound_logic_and": "И — должны сработать все условия", + "compound_logic_or": "ИЛИ — достаточно любого условия", + "compound_help": "Объедините несколько условий датчиков в один триггер.", + "compound_no_conditions": "Условий пока нет — добавьте хотя бы одно.", + "compound_add_condition": "Добавить условие", + "compound_condition": "Условие", + "compound_remove_condition": "Удалить условие", + "card_title": "Заголовок", + "card_show_header": "Показывать заголовок со статистикой", + "card_show_actions": "Показывать кнопки действий", + "card_compact": "Компактный режим", + "card_max_items": "Макс. элементов (0 = все)", + "card_filter_status": "Фильтровать по статусу", + "card_filter_status_help": "Пусто = показать все статусы.", + "card_filter_objects": "Фильтровать по объектам", + "card_filter_objects_help": "Пусто = показать все объекты.", + "card_filter_entities": "Фильтровать по сущностям (entity_ids)", + "card_filter_entities_help": "Выберите сущности sensor / binary_sensor из этой интеграции. Пусто = все.", + "card_loading_objects": "Загрузка объектов…", + "card_load_error": "Не удалось загрузить объекты — проверьте WebSocket-соединение.", + "card_no_tasks_title": "Пока нет задач обслуживания", + "card_no_tasks_cta": "→ Создайте в панели Maintenance", + "no_objects": "Пока нет объектов.", + "action_error": "Не удалось выполнить действие. Попробуйте ещё раз.", + "area_id_optional": "Зона (опционально)", + "installation_date_optional": "Дата установки (опционально)", + "warranty_expiry_optional": "Окончание гарантии (опционально)", + "warranty": "Гарантия", + "warranty_valid_until": "действует до {date}", + "warranty_expires_in": "истекает через {days} дн.", + "warranty_expired": "истекла", + "cal_past_windows": "Прошлые окна", + "cal_forward_windows": "Будущие окна", + "history_edit_title": "Редактировать запись истории", + "history_edit_timestamp": "Метка времени", + "manufacturer": "Производитель", + "model": "Модель", + "area": "Зона", + "actions": "Действия", + "view_mode_label": "Вид", + "view_cards": "Карточки", + "view_table": "Таблица", + "objects_table_columns_label": "Столбцы таблицы объектов", + "objects_table_columns_hint": "Выберите, какие столбцы показывать в табличном виде объектов.", + "custom_icon_optional": "Иконка (опционально, например mdi:wrench)", + "task_enabled": "Задача включена", + "skip_reason_prompt": "Пропустить эту задачу?", + "reason_optional": "Причина (опционально)", + "reset_date_prompt": "Отметить задачу как выполненную?", + "reset_date_optional": "Дата последнего выполнения (опционально, по умолчанию: сегодня)", + "notes_label": "Примечания", + "documentation_label": "Документация", + "no_nfc_tag": "— Нет метки —", + "dashboard": "Панель", + "tab_today": "Сегодня", + "palette_placeholder": "Поиск объектов и задач…", + "palette_no_results": "Нет совпадений", + "palette_hint": "↑↓ выбор · Enter открыть · Esc закрыть", + "today_all_caught_up": "Всё сделано! На этой неделе ничего нет.", + "today_overdue": "Просрочено", + "today_due_today": "Сегодня", + "today_this_week": "На этой неделе", + "settings": "Настройки", + "settings_features": "Расширенные функции", + "settings_features_desc": "Включите или отключите расширенные функции. Отключение скрывает их из интерфейса, но не удаляет данные.", + "feat_adaptive": "Адаптивное планирование", + "feat_adaptive_desc": "Изучать оптимальные интервалы из истории обслуживания", + "feat_predictions": "Предсказания по датчикам", + "feat_predictions_desc": "Предсказывать даты срабатывания по деградации датчика", + "feat_seasonal": "Сезонные корректировки", + "feat_seasonal_desc": "Корректировать интервалы на основе сезонных паттернов", + "feat_environmental": "Экологическая корреляция", + "feat_environmental_desc": "Связывать интервалы с температурой/влажностью", + "feat_budget": "Отслеживание бюджета", + "feat_budget_desc": "Отслеживать месячные и годовые расходы на обслуживание", + "feat_groups": "Группы задач", + "feat_groups_desc": "Организовывать задачи в логические группы", + "feat_checklists": "Контрольные списки", + "feat_checklists_desc": "Многошаговые процедуры для выполнения задачи", + "settings_general": "Основные", + "settings_default_warning": "Дни предупреждения по умолчанию", + "settings_panel_enabled": "Боковая панель", + "settings_panel_title": "Заголовок панели", + "settings_notifications": "Уведомления", + "settings_notify_service": "Сервис уведомлений", + "test_notification": "Тестовое уведомление", + "send_test": "Отправить тест", + "testing": "Отправка…", + "test_notification_success": "Тестовое уведомление отправлено", + "test_notification_failed": "Не удалось отправить тестовое уведомление", + "settings_notify_due_soon": "Уведомлять, когда срок скоро истекает", + "settings_notify_overdue": "Уведомлять при просрочке", + "settings_notify_triggered": "Уведомлять при срабатывании", + "settings_interval_hours": "Интервал повторения (часы, 0 = один раз)", + "settings_quiet_hours": "Часы тишины", + "settings_quiet_start": "Начало", + "settings_quiet_end": "Конец", + "settings_max_per_day": "Макс. уведомлений в день (0 = без ограничений)", + "settings_bundling": "Группировать уведомления", + "settings_bundle_threshold": "Порог группировки", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Кнопки действий в мобильном приложении", + "settings_action_complete": "Показывать кнопку «Выполнить»", + "settings_action_skip": "Показывать кнопку «Пропустить»", + "settings_action_snooze": "Показывать кнопку «Отложить»", + "settings_weekly_digest": "Еженедельная сводка", + "settings_weekly_digest_hint": "Одно сводное уведомление в понедельник утром, когда есть задачи.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Длительность откладывания (часы)", + "settings_budget": "Бюджет", + "settings_currency": "Валюта", + "settings_budget_monthly": "Месячный бюджет", + "settings_budget_yearly": "Годовой бюджет", + "settings_budget_alerts": "Оповещения о бюджете", + "settings_budget_threshold": "Порог оповещения (%)", + "settings_import_export": "Импорт / Экспорт", + "settings_export_json": "Экспорт JSON", + "settings_export_yaml": "Экспорт YAML", + "settings_export_csv": "Экспорт CSV", + "settings_import_csv": "Импорт CSV", + "settings_import_placeholder": "Вставьте содержимое JSON или CSV здесь…", + "settings_import_btn": "Импортировать", + "settings_import_success": "Импортировано объектов: {count}.", + "settings_export_success": "Экспорт загружен.", + "settings_saved": "Настройка сохранена.", + "settings_include_history": "Включать историю", + "sort_alphabetical": "По алфавиту", + "sort_due_soonest": "Ближайший срок", + "sort_task_count": "Количество задач", + "sort_area": "Область", + "sort_assigned_user": "Назначенный пользователь", + "sort_group": "Группа", + "groupby_none": "Без группировки", + "groupby_area": "По области", + "groupby_group": "По группе", + "groupby_user": "По пользователю", + "filter_label": "Фильтр", + "user_label": "Пользователь", + "sort_label": "Сортировка", + "group_by_label": "Группировать по", + "state_value_help": "Используйте значение состояния HA (обычно в нижнем регистре, напр. \"on\"/\"off\"). Регистр нормализуется при сохранении.", + "target_changes_help": "Количество совпадающих переходов, после которых срабатывает триггер (по умолчанию: 1).", + "qr_print_title": "Печать QR-кодов", + "qr_print_desc": "Создай страницу для печати с QR-кодами для вырезания и наклеивания на оборудование.", + "qr_print_load": "Загрузить объекты", + "qr_print_filter": "Фильтр", + "qr_print_objects": "Объекты", + "qr_print_actions": "Действия", + "qr_print_url_mode": "Тип ссылки", + "qr_print_estimate": "Оценка QR-кодов", + "qr_print_over_limit": "лимит 200, сузь фильтр", + "qr_print_generate": "Создать QR-коды", + "qr_print_generating": "Создание…", + "qr_print_ready": "QR-коды готовы", + "qr_print_print_button": "Печать", + "qr_print_empty": "Нечего создавать", + "qr_action_skip": "Пропустить", + "vacation_title": "Режим отпуска", + "vacation_active": "активен", + "vacation_ended": "завершён", + "vacation_desc": "Запланируй отпуск: уведомления приостанавливаются на период плюс несколько буферных дней. Можно задать исключения по задачам.", + "vacation_enable": "Включить режим отпуска", + "vacation_start": "Начало", + "vacation_end": "Конец", + "vacation_buffer": "Буфер (дней)", + "vacation_exempt_title": "Уведомлять несмотря на отпуск", + "vacation_exempt_desc": "Выбери задачи, по которым нужно уведомлять и в отпуске (например, критичная химия бассейна).", + "vacation_load_tasks": "Загрузить задачи", + "vacation_preview_btn": "Показать предпросмотр", + "vacation_preview_affected": "задач затронуто", + "vacation_event_due_soon": "скоро наступит срок", + "vacation_event_overdue": "станет просроченной", + "vacation_event_triggered_est": "возможно срабатывание сенсора", + "vacation_sensor_based": "(сенсорная)", + "vacation_action_notify": "Всё равно уведомлять", + "vacation_action_unsilence": "Снова заглушить", + "vacation_marked_complete": "Отмечено как выполнено", + "vacation_marked_skip": "Пропущено", + "vacation_end_now": "Завершить отпуск сейчас", + "unassigned": "Не назначено", + "no_area": "Без области", + "has_overdue": "Просроченные задачи", + "object": "Объект", + "settings_panel_access": "Доступ к панели", + "settings_panel_access_desc": "Администраторы всегда имеют полный доступ. Чтобы делегировать создание, редактирование и удаление определённым не-админ пользователям, включите это и выберите их ниже — остальные видят только Выполнить и Пропустить.", + "settings_operator_write": "Разрешить выбранным пользователям создавать, редактировать и удалять", + "settings_operator_write_desc": "Выключено: только администраторы могут изменять содержимое. Включено: выбранные пользователи ниже также получают полный доступ.", + "no_non_admin_users": "Не найдено не-админ пользователей. Добавьте их в Настройках → Люди.", + "owner_label": "Владелец", + "feat_completion_actions": "Действия при завершении", + "feat_completion_actions_desc": "HA-действие по задаче при завершении + QR быстрого завершения с предустановленными значениями.", + "on_complete_action_title": "При завершении: запустить HA-действие (опционально)", + "on_complete_action_desc": "Вызывает HA-сервис при завершении задачи — напр., сбросить счётчик на устройстве.", + "on_complete_action_service": "Сервис", + "on_complete_action_target": "Целевая сущность", + "on_complete_action_data": "Данные (JSON, опционально)", + "on_complete_action_test": "Тестировать действие", + "on_complete_action_test_success": "Успешно", + "on_complete_action_test_failed": "Ошибка", + "quick_complete_defaults_title": "Значения по умолчанию для быстрого завершения (для QR-сканов, опционально)", + "quick_complete_defaults_desc": "Предустановленные значения для QR быстрого завершения. Без них QR открывает диалог.", + "quick_complete_defaults_notes": "Заметки", + "quick_complete_defaults_cost": "Стоимость", + "quick_complete_defaults_duration": "Длительность (минут)", + "quick_complete_defaults_feedback_none": "Без отзыва", + "quick_complete_defaults_feedback_needed": "Было необходимо", + "quick_complete_defaults_feedback_not_needed": "Не было необходимо", + "quick_complete_success": "Быстро отмечено как выполнено", + "trigger_replaced": "Триггер заменён", + "add": "Добавить", + "show_stats": "Показать статистику + графики", + "hide_stats": "Скрыть статистику", + "adaptive_no_data": "Пока недостаточно истории выполнений для адаптивного анализа. Выполните эту задачу ещё несколько раз, чтобы разблокировать рекомендации по интервалу и графики надёжности.", + "suggestion_applied": "Предлагаемый интервал применён", + "vacation_mode": "Режим отпуска", + "vacation_status_active": "Активен сейчас", + "vacation_status_scheduled": "Запланирован", + "vacation_status_inactive": "Неактивен", + "vacation_end_now_confirm": "Завершить отпуск немедленно?", + "vacation_exempt_count": "исключено", + "vacation_advanced": "Дополнительно…", + "vacation_open_panel": "Открыть на панели", + "enable": "Включить", + "saved": "Сохранено", + "budget_monthly_set": "Задать месячный", + "budget_yearly_set": "Задать годовой", + "budget_advanced": "Валюта, оповещения…", + "budget_open_panel": "Открыть на панели", + "groups_empty": "Пока нет групп.", + "group_new_placeholder": "Добавить группу…", + "group_delete_confirm": "Удалить группу «{name}»?", + "groups_manage_tasks": "Управление назначениями задач…", + "groups_open_panel": "Открыть на панели", + "on_complete_action_target_hint": "Примечание: домен сущности должен соответствовать сервису — напр. 'button.press' работает только с button.*, 'counter.increment' только с counter.*, 'input_button.press' только с input_button.* и т. д. При несоответствии действие молча не выполнится (HA запишет в журнал 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Показать все объекты", + "show_all_tasks": "Сбросить фильтр — показать все задачи", + "filter_to_overdue": "Фильтровать список только по просроченным", + "filter_to_due_soon": "Фильтровать список только по скоро наступающим", + "filter_to_triggered": "Фильтровать список только по сработавшим", + "open_task": "Открыть задачу", + "show_details": "Показать историю + статистику", + "hide_details": "Скрыть детали", + "history_empty": "Пока нет истории.", + "history_edit_button": "Изменить запись", + "total_cost": "Общая стоимость", + "times_performed": "Выполнено", + "older_entries": "ранее", + "open_in_panel": "Открыть на панели обслуживания", + "skip_reason": "Причина пропуска (необязательно)", + "reset_to_date": "Сбросить последнее выполнение на", + "delete_task_confirm": "Удалить эту задачу и её историю?", + "delete_object_confirm": "Удалить этот объект и все его задачи?", + "loading": "Загрузка…", + "archive": "Архивировать", + "undo": "Отменить", + "task_archived": "Задача архивирована", + "object_archived": "Объект архивирован", + "unarchive": "Разархивировать", + "archived": "В архиве", + "show_archived": "Показать архив", + "hide_archived": "Скрыть архив", + "confirm_archive_object": "Архивировать этот объект и его задачи? История сохранится, позже можно восстановить.", + "settings_archive": "Архив и хранение", + "settings_archive_desc": "Убирайте выполненные разовые задачи в архив, не удаляя их. Архивные элементы скрыты и неактивны, но сохраняют историю и расходы.", + "settings_archive_oneoff_days": "Автоархивация выполненных разовых задач через (дней, 0 = выкл)", + "settings_delete_archived_oneoff_days": "Автоудаление архивных разовых задач через (дней, 0 = никогда)", + "archive_object": "Архивировать объект", + "unarchive_object": "Разархивировать объект", + "documents": "Документы", + "documents_empty": "Пока нет документов.", + "doc_upload": "Загрузить файл", + "doc_uploading": "Загрузка…", + "doc_add_link": "Добавить ссылку", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Название (необязательно)", + "doc_open": "Открыть", + "doc_delete_confirm": "Удалить «{name}»?", + "doc_too_large": "Файл слишком большой (макс. 25 МБ).", + "doc_upload_failed": "Не удалось загрузить.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Уже сохранён в другом месте — общий, без доп. места.", + "doc_dup_in_object": "Этот файл уже прикреплён к этому объекту.", + "doc_link_invalid": "Разрешены только ссылки http/https.", + "doc_cat_manual": "Инструкция", + "doc_cat_warranty": "Гарантия", + "doc_cat_invoice": "Счёт", + "doc_cat_spare_parts": "Запчасти", + "doc_cat_photo": "Фото", + "doc_cat_other": "Другое", + "doc_link_badge": "Ссылка", + "doc_storage_title": "Хранилище документов", + "doc_storage_saved": "Сэкономлено дедупликацией", + "doc_storage_refresh": "Обновить", + "doc_download": "Скачать", + "doc_close": "Закрыть", + "doc_camera": "Сделать фото", + "doc_drop_hint": "Перетащите файлы сюда", + "doc_task_none": "Нет документов, связанных с этой задачей.", + "doc_link_existing": "Связать документ…", + "doc_attach": "Связать", + "doc_unlink": "Отвязать", + "doc_page": "Страница", + "chart_range_7d": "7д", + "chart_range_30d": "30д", + "chart_range_90d": "90д", + "chart_range_1y": "1г", + "chart_since_service": "с последнего обслуживания", + "chart_no_stats": "Нет долгосрочной статистики для этой сущности — показаны только значения из записей обслуживания", + "auto_complete_on_recovery": "Автозавершение при восстановлении датчика", + "auto_complete_on_recovery_help": "Записывает выполнение (обновляет дату последнего обслуживания), когда триггер устраняется сам — например, соль досыпана, фильтр заменён.", + "doc_search": "Поиск документов…", + "doc_search_none": "Нет подходящих документов", + "link_device_optional": "Привязать к существующему устройству (необязательно)", + "parent_object_optional": "Родительский объект (необязательно)", + "parent_none": "(Без родителя)", + "paused": "Приостановлено", + "pause_object": "Приостановить", + "resume_object": "Возобновить", + "pause_until_prompt": "Заморозить расписания этого объекта — ничего не наступает и не уведомляет до возобновления. При желании укажите дату автоматического возобновления.", + "pause_until_label": "Возобновить (необязательно)", + "object_paused": "Объект приостановлен", + "object_resumed": "Объект возобновлён — расписания перезапущены", + "object_paused_badge": "Приостановлено", + "paused_until_label": "до", + "replace_object": "Заменить…", + "replace_object_prompt": "Списать этот объект и создать преемника. История и затраты остаются в архиве старого; задачи и документы переходят к новому, счётчики начинаются заново.", + "replace_name_label": "Имя преемника", + "object_replaced": "Объект заменён — преемник создан", + "reading_unit_label": "Единица показания (напр. кВт·ч, м³)", + "reading_unit_help": "Отображается рядом с записанным значением при выполнении этой задачи.", + "reading_value_label": "Показание", + "reading_label": "Показание", + "settings_templates_label": "Галерея шаблонов", + "settings_templates_hint": "Снимите отметку с шаблонов, которые вам не нужны — они исчезнут из выбора «Из шаблона» (панель и config flow). Больше ничего не меняется; можно включить обратно в любой момент.", + "worksheet": "Рабочий лист", + "worksheet_scan_view": "Сканируйте, чтобы открыть задачу", + "worksheet_scan_complete": "Сканируйте для выполнения", + "worksheet_manual_excerpt": "Выдержка из руководства", + "worksheet_pages": "страницы", + "worksheet_printed": "Напечатано", + "worksheet_never": "Никогда" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/sv.json b/custom_components/maintenance_supporter/frontend/locales/sv.json new file mode 100644 index 0000000..6e88345 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/sv.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Underhåll", + "objects": "Objekt", + "tasks": "Uppgifter", + "overdue": "Försenad", + "due_soon": "Snart", + "triggered": "Utlöst", + "ok": "OK", + "all": "Alla", + "new_object": "+ Nytt objekt", + "templates_from": "Från mall", + "templates_title": "Börja från en mall", + "templates_task_count": "{n} uppgifter", + "template_created": "Skapad från mall", + "onboard_hint": "Lägg till ditt första objekt för att spåra underhåll.", + "edit": "Redigera", + "duplicate": "Duplicera", + "task_duplicated": "Uppgift duplicerad", + "object_duplicated": "Objekt duplicerat", + "delete": "Ta bort", + "add_task": "+ Lägg till uppgift", + "complete": "Slutför", + "completed": "Slutförd", + "skip": "Hoppa över", + "skipped": "Hoppade över", + "missed": "Missed", + "reset": "Återställ", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Avbryt", + "bulk_select": "Välj", + "bulk_select_all": "Välj alla", + "bulk_n_selected": "{n} valda", + "bulk_completed": "{n} uppgifter slutförda", + "bulk_archived": "{n} uppgifter arkiverade", + "completing": "Slutför…", + "interval": "Intervall", + "warning": "Varning", + "last_performed": "Senast utförd", + "next_due": "Nästa förfallodatum", + "days_until_due": "Dagar till förfallodatum", + "avg_duration": "Snittlig varaktighet", + "trigger": "Utlösare", + "trigger_type": "Utlösartyp", + "threshold_above": "Övre gräns", + "threshold_below": "Undre gräns", + "threshold": "Tröskel", + "counter": "Räknare", + "state_change": "Tillståndsändring", + "runtime": "Körtid", + "runtime_hours": "Måltid (timmar)", + "target_value": "Målvärde", + "baseline": "Baslinje", + "target_changes": "Antal målförändringar", + "for_minutes": "Under (minuter)", + "time_based": "Tidsbaserad", + "sensor_based": "Sensorbaserad", + "manual": "Manuell", + "one_time": "Engångs", + "weekdays": "Veckodagar", + "nth_weekday": "N:te veckodag i månaden", + "day_of_month": "Dag i månaden", + "recurrence_on_days": "Upprepa på", + "recurrence_occurrence": "Förekomst", + "recurrence_weekday": "Veckodag", + "recurrence_day": "Dag i månaden (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1:a", + "ord_2": "2:a", + "ord_3": "3:e", + "ord_4": "4:e", + "ord_5": "5:e", + "ord_last": "Sista", + "day_word": "Dag", + "interval_value": "Intervall", + "interval_unit": "Enhet", + "unit_days": "Dagar", + "unit_weeks": "Veckor", + "unit_months": "Månader", + "unit_years": "År", + "due_date": "Förfallodatum", + "cleaning": "Rengöring", + "inspection": "Inspektion", + "replacement": "Byte", + "calibration": "Kalibrering", + "service": "Service", + "reading": "Avläsning", + "custom": "Anpassad", + "history": "Historik", + "cost": "Kostnad", + "report_button": "Rapport", + "report_title": "Underhållsrapport", + "report_generated": "Genererad", + "report_times_done": "Utfört", + "report_total_cost": "Total kostnad", + "report_every": "var {n}:e {unit}", + "report_notes": "Anteckningar", + "report_col_type": "Typ", + "report_col_status": "Status", + "report_col_schedule": "Schema", + "duration": "Varaktighet", + "both": "Båda", + "trigger_val": "Utlösarvärde", + "complete_title": "Slutför: ", + "checklist": "Checklista", + "checklist_steps_optional": "Checkliststeg (valfritt)", + "checklist_placeholder": "Rengör filter\nByt tätning\nTesta tryck", + "checklist_help": "Ett steg per rad. Max 100 objekt.", + "err_too_long": "{field}: för lång (max {n} tecken)", + "err_too_short": "{field}: för kort (min {n} tecken)", + "err_value_too_high": "{field}: för stor (max {n})", + "err_value_too_low": "{field}: för liten (min {n})", + "err_required": "{field}: krävs", + "err_wrong_type": "{field}: fel typ (förväntad: {type})", + "err_invalid_choice": "{field}: ej tillåtet värde", + "err_invalid_value": "{field}: ogiltigt värde", + "feat_schedule_time": "Schemaläggning per tid på dygnet", + "feat_schedule_time_desc": "Uppgifter blir försenade vid en specifik tid på dygnet istället för midnatt.", + "schedule_time_optional": "Förfaller kl. (valfritt, HH:MM)", + "schedule_time_help": "Tomt = midnatt (standard). HA-tidszon.", + "at_time": "kl.", + "notes_optional": "Anteckningar (valfritt)", + "cost_optional": "Kostnad (valfritt)", + "duration_minutes": "Varaktighet i minuter (valfritt)", + "days": "dagar", + "day": "dag", + "today": "Idag", + "d_overdue": "d försenad", + "no_tasks": "Inga underhållsuppgifter ännu. Skapa ett objekt för att komma igång.", + "no_tasks_short": "Inga uppgifter", + "no_history": "Inga historikposter ännu.", + "show_all": "Visa alla", + "cost_duration_chart": "Kostnad och varaktighet", + "installed": "Installerad", + "confirm_delete_object": "Ta bort detta objekt och alla dess uppgifter?", + "confirm_delete_task": "Ta bort denna uppgift?", + "min": "Min", + "max": "Max", + "save": "Spara", + "saving": "Sparar…", + "edit_task": "Redigera uppgift", + "new_task": "Ny underhållsuppgift", + "task_name": "Uppgiftsnamn", + "maintenance_type": "Underhållstyp", + "priority": "Prioritet", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Låg", + "priority_normal": "Normal", + "priority_high": "Hög", + "schedule_type": "Schematyp", + "interval_days": "Intervall (dagar)", + "warning_days": "Varningsdagar", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "Senast utförd (valfritt)", + "interval_anchor": "Intervallankare", + "anchor_completion": "Från slutförandedatum", + "anchor_planned": "Från planerat datum (ingen drift)", + "edit_object": "Redigera objekt", + "name": "Namn", + "manufacturer_optional": "Tillverkare (valfritt)", + "model_optional": "Modell (valfritt)", + "serial_number_optional": "Serienummer (valfritt)", + "serial_number_label": "S/N", + "documentation_url_label": "Manual", + "object_notes_label": "Anteckningar", + "sort_due_date": "Förfallodatum", + "sort_object": "Objektnamn", + "sort_type": "Typ", + "sort_task_name": "Uppgiftsnamn", + "all_objects": "Alla objekt", + "tasks_lower": "uppgifter", + "no_tasks_yet": "Inga uppgifter ännu", + "add_first_task": "Lägg till första uppgift", + "trigger_configuration": "Utlösarkonfiguration", + "entity_id": "Entitets-ID", + "comma_separated": "kommaseparerad", + "entity_logic": "Entitetslogik", + "entity_logic_any": "Vilken entitet som helst utlöser", + "entity_logic_all": "Alla entiteter måste utlösa", + "entities": "entiteter", + "attribute_optional": "Attribut (valfritt, tomt = tillstånd)", + "use_entity_state": "Använd entitetstillstånd (inget attribut)", + "trigger_above": "Utlös över", + "trigger_below": "Utlös under", + "for_at_least_minutes": "Under minst (minuter)", + "safety_interval_days": "Säkerhetsintervall (dagar, valfritt)", + "safety_interval": "Säkerhetsintervall (valfritt)", + "delta_mode": "Delta-läge", + "from_state_optional": "Från tillstånd (valfritt)", + "to_state_optional": "Till tillstånd (valfritt)", + "documentation_url_optional": "Dokumentations-URL (valfritt)", + "object_notes_optional": "Anteckningar (valfritt)", + "nfc_tag_id_optional": "NFC-tagg-ID (valfritt)", + "nfc_tags_empty_help": "Inga NFC-taggar registrerade i Home Assistant än.", + "nfc_tags_open_settings": "Öppna tag-inställningar", + "nfc_tags_refresh": "Uppdatera", + "environmental_entity_optional": "Miljösensor (valfritt)", + "environmental_entity_helper": "t.ex. sensor.outdoor_temperature — justerar intervallet baserat på miljöförhållanden", + "environmental_attribute_optional": "Miljöattribut (valfritt)", + "nfc_tag_id": "NFC-tagg-ID", + "nfc_linked": "NFC-tagg länkad", + "nfc_link_hint": "Klicka för att länka NFC-tagg", + "responsible_user": "Ansvarig användare", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Ingen användare tilldelad)", + "all_users": "Alla användare", + "my_tasks": "Mina uppgifter", + "tab_calendar": "Kalender", + "cal_no_events": "Inget underhåll", + "cal_window_7": "7 dagar", + "cal_window_14": "14 dagar", + "cal_window_30": "30 dagar", + "cal_window_365": "1 år", + "cal_every_n_days": "var {n} dag", + "cal_source_time": "Tidsbaserad", + "cal_source_time_adaptive": "Tidsbaserad (adaptiv)", + "cal_source_sensor": "Sensorbaserad", + "cal_predicted": "förutsagt", + "cal_confidence_high": "hög säkerhet", + "cal_confidence_medium": "medelhög säkerhet", + "cal_confidence_low": "låg säkerhet", + "budget_monthly": "Månatlig budget", + "budget_yearly": "Årlig budget", + "groups": "Grupper", + "new_group": "Ny grupp", + "edit_group": "Redigera grupp", + "no_groups": "Inga grupper ännu", + "delete_group": "Ta bort grupp", + "delete_group_confirm": "Ta bort grupp '{name}'?", + "group_select_tasks": "Välj uppgifter", + "group_name_required": "Namn krävs", + "description_optional": "Beskrivning (valfritt)", + "selected": "Valda", + "loading_chart": "Laddar diagramdata...", + "hide_outliers": "Dölj avvikare (sensorfel)", + "was_maintenance_needed": "Behövdes detta underhåll?", + "feedback_needed": "Behövdes", + "feedback_not_needed": "Behövdes inte", + "feedback_not_sure": "Osäker", + "suggested_interval": "Föreslaget intervall", + "apply_suggestion": "Tillämpa", + "reanalyze": "Analysera igen", + "reanalyze_result": "Ny analys", + "reanalyze_insufficient_data": "Otillräckligt med data för rekommendation", + "data_points": "datapunkter", + "dismiss_suggestion": "Avvisa", + "confidence_low": "Låg", + "confidence_medium": "Medel", + "confidence_high": "Hög", + "recommended": "rekommenderad", + "seasonal_awareness": "Säsongsmedvetenhet", + "edit_seasonal_overrides": "Redigera säsongsfaktorer", + "seasonal_overrides_title": "Säsongsfaktorer (åsidosätt)", + "seasonal_overrides_hint": "Faktor per månad (0.1–5.0). Tomt = lärt automatiskt.", + "seasonal_override_invalid": "Ogiltigt värde", + "seasonal_override_range": "Faktor måste vara mellan 0.1 och 5.0", + "clear_all": "Rensa alla", + "seasonal_chart_title": "Säsongsfaktorer", + "seasonal_learned": "Lärt", + "seasonal_manual": "Manuell", + "month_jan": "Jan", + "month_feb": "Feb", + "month_mar": "Mar", + "month_apr": "Apr", + "month_may": "Maj", + "month_jun": "Jun", + "month_jul": "Jul", + "month_aug": "Aug", + "month_sep": "Sep", + "month_oct": "Okt", + "month_nov": "Nov", + "month_dec": "Dec", + "sensor_prediction": "Sensorprediktion", + "degradation_trend": "Trend", + "trend_rising": "Stigande", + "trend_falling": "Fallande", + "trend_stable": "Stabil", + "trend_insufficient_data": "Otillräcklig data", + "days_until_threshold": "Dagar till tröskel", + "threshold_exceeded": "Tröskel överskriden", + "environmental_adjustment": "Miljöfaktor", + "sensor_prediction_urgency": "Sensor förutsäger tröskel om ~{days} dagar", + "day_short": "d", + "weibull_reliability_curve": "Tillförlitlighetskurva", + "weibull_failure_probability": "Felsannolikhet", + "weibull_r_squared": "Anpassning R²", + "beta_early_failures": "Tidiga fel", + "beta_random_failures": "Slumpmässiga fel", + "beta_wear_out": "Slitage", + "beta_highly_predictable": "Mycket förutsägbar", + "confidence_interval": "Konfidensintervall", + "confidence_conservative": "Konservativ", + "confidence_aggressive": "Optimistisk", + "current_interval_marker": "Aktuellt intervall", + "recommended_marker": "Rekommenderat", + "characteristic_life": "Karakteristisk livslängd", + "chart_mini_sparkline": "Trenddiagram", + "chart_history": "Kostnads- och varaktighetshistorik", + "chart_seasonal": "Säsongsfaktorer, 12 månader", + "chart_weibull": "Weibull tillförlitlighetskurva", + "chart_sparkline": "Sensorutlösarvärdesdiagram", + "days_progress": "Dagsförlopp", + "qr_code": "QR-kod", + "qr_generating": "Genererar QR-kod…", + "qr_error": "Kunde inte generera QR-kod.", + "qr_error_no_url": "Ingen HA-URL konfigurerad. Ange en extern eller intern URL i Inställningar → System → Nätverk.", + "save_error": "Kunde inte spara. Försök igen.", + "qr_print": "Skriv ut", + "qr_download": "Ladda ner SVG", + "qr_action": "Åtgärd vid skanning", + "qr_action_view": "Visa underhållsinformation", + "qr_action_complete": "Markera underhåll som slutfört", + "qr_url_mode": "Länktyp", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Lokal (mDNS)", + "qr_mode_server": "Server-URL", + "overview": "Översikt", + "analysis": "Analys", + "recent_activities": "Senaste aktiviteter", + "search_notes": "Sök i anteckningar", + "avg_cost": "Snittlig kostnad", + "no_advanced_features": "Inga avancerade funktioner aktiverade", + "no_advanced_features_hint": "Aktivera „Adaptiva intervall” eller „Säsongsmönster” i integrationsinställningar för att se analysdata här.", + "analysis_not_enough_data": "Inte tillräckligt med data för analys ännu.", + "analysis_not_enough_data_hint": "Weibull-analys kräver minst 5 slutförda underhåll; säsongsmönster blir synliga efter 6+ datapunkter per månad.", + "analysis_manual_task_hint": "Manuella uppgifter utan intervall genererar inte analysdata.", + "completions": "slutföranden", + "current": "Aktuell", + "shorter": "Kortare", + "longer": "Längre", + "normal": "Normal", + "disabled": "Inaktiverad", + "compound_logic": "Sammansatt logik", + "compound": "Sammansatt (flera villkor)", + "compound_logic_and": "OCH — alla villkor måste utlösas", + "compound_logic_or": "ELLER — ett villkor räcker", + "compound_help": "Kombinera flera sensorvillkor till en utlösare.", + "compound_no_conditions": "Inga villkor än — lägg till minst ett.", + "compound_add_condition": "Lägg till villkor", + "compound_condition": "Villkor", + "compound_remove_condition": "Ta bort villkor", + "card_title": "Titel", + "card_show_header": "Visa rubrik med statistik", + "card_show_actions": "Visa åtgärdsknappar", + "card_compact": "Kompakt läge", + "card_max_items": "Max objekt (0 = alla)", + "card_filter_status": "Filtrera efter status", + "card_filter_status_help": "Tomt = visa alla statusar.", + "card_filter_objects": "Filtrera efter objekt", + "card_filter_objects_help": "Tomt = visa alla objekt.", + "card_filter_entities": "Filtrera efter entiteter (entity_ids)", + "card_filter_entities_help": "Välj sensor- / binary_sensor-entiteter från denna integration. Tomt = alla.", + "card_loading_objects": "Laddar objekt…", + "card_load_error": "Kunde inte ladda objekt — kontrollera WebSocket-anslutningen.", + "card_no_tasks_title": "Inga underhållsuppgifter än", + "card_no_tasks_cta": "→ Skapa en i Maintenance-panelen", + "no_objects": "Inga objekt än.", + "action_error": "Åtgärden misslyckades. Försök igen.", + "area_id_optional": "Område (valfritt)", + "installation_date_optional": "Installationsdatum (valfritt)", + "warranty_expiry_optional": "Garantins utgång (valfritt)", + "warranty": "Garanti", + "warranty_valid_until": "giltig till {date}", + "warranty_expires_in": "går ut om {days} dagar", + "warranty_expired": "utgången", + "cal_past_windows": "Tidigare fönster", + "cal_forward_windows": "Kommande fönster", + "history_edit_title": "Redigera historikpost", + "history_edit_timestamp": "Tidsstämpel", + "manufacturer": "Tillverkare", + "model": "Modell", + "area": "Område", + "actions": "Åtgärder", + "view_mode_label": "Vy", + "view_cards": "Kortvy", + "view_table": "Tabellvy", + "objects_table_columns_label": "Kolumner i objekttabell", + "objects_table_columns_hint": "Välj vilka kolumner som visas i objekttabellvyn.", + "custom_icon_optional": "Ikon (valfritt, t.ex. mdi:wrench)", + "task_enabled": "Uppgift aktiverad", + "skip_reason_prompt": "Hoppa över denna uppgift?", + "reason_optional": "Anledning (valfritt)", + "reset_date_prompt": "Markera uppgift som utförd?", + "reset_date_optional": "Datum för senaste utförande (valfritt, standard idag)", + "notes_label": "Anteckningar", + "documentation_label": "Dokumentation", + "no_nfc_tag": "— Ingen tagg —", + "dashboard": "Översikt", + "tab_today": "Idag", + "palette_placeholder": "Sök objekt och uppgifter…", + "palette_no_results": "Inga träffar", + "palette_hint": "↑↓ navigera · Enter öppna · Esc stäng", + "today_all_caught_up": "Allt klart! Inget den här veckan.", + "today_overdue": "Försenade", + "today_due_today": "Förfaller idag", + "today_this_week": "Den här veckan", + "settings": "Inställningar", + "settings_features": "Avancerade funktioner", + "settings_features_desc": "Aktivera eller inaktivera avancerade funktioner. Inaktivering döljer dem från UI men tar inte bort data.", + "feat_adaptive": "Adaptiv schemaläggning", + "feat_adaptive_desc": "Lär dig optimala intervall från underhållshistorik", + "feat_predictions": "Sensorpredictions", + "feat_predictions_desc": "Förutsäg utlösningsdatum från sensordegradering", + "feat_seasonal": "Säsongsjusteringar", + "feat_seasonal_desc": "Justera intervall baserat på säsongsmönster", + "feat_environmental": "Miljökorrelation", + "feat_environmental_desc": "Korrelera intervall med temperatur/luftfuktighet", + "feat_budget": "Budgetuppföljning", + "feat_budget_desc": "Spåra månatliga och årliga underhållsutgifter", + "feat_groups": "Uppgiftsgrupper", + "feat_groups_desc": "Organisera uppgifter i logiska grupper", + "feat_checklists": "Checklistor", + "feat_checklists_desc": "Flerstegs procedurer för uppgiftens slutförande", + "settings_general": "Allmänt", + "settings_default_warning": "Standard varningsdagar", + "settings_panel_enabled": "Sidopanel", + "settings_panel_title": "Sidopanelens titel", + "settings_notifications": "Notifikationer", + "settings_notify_service": "Notifikationstjänst", + "test_notification": "Testnotifikation", + "send_test": "Skicka test", + "testing": "Skickar…", + "test_notification_success": "Testnotifikation skickad", + "test_notification_failed": "Testnotifikation misslyckades", + "settings_notify_due_soon": "Notifiera när snart förfallande", + "settings_notify_overdue": "Notifiera när försenad", + "settings_notify_triggered": "Notifiera när utlöst", + "settings_interval_hours": "Upprepningsintervall (timmar, 0 = en gång)", + "settings_quiet_hours": "Tysta timmar", + "settings_quiet_start": "Start", + "settings_quiet_end": "Slut", + "settings_max_per_day": "Max notifikationer per dag (0 = obegränsat)", + "settings_bundling": "Bunta notifikationer", + "settings_bundle_threshold": "Buntningströskel", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Mobila åtgärdsknappar", + "settings_action_complete": "Visa 'Slutför'-knapp", + "settings_action_skip": "Visa 'Hoppa över'-knapp", + "settings_action_snooze": "Visa 'Snooza'-knapp", + "settings_weekly_digest": "Veckosammanfattning", + "settings_weekly_digest_hint": "En sammanfattningsavisering måndag morgon när uppgifter förfaller.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Snooza-tid (timmar)", + "settings_budget": "Budget", + "settings_currency": "Valuta", + "settings_budget_monthly": "Månatlig budget", + "settings_budget_yearly": "Årlig budget", + "settings_budget_alerts": "Budgetvarningar", + "settings_budget_threshold": "Varningströskel (%)", + "settings_import_export": "Import / Export", + "settings_export_json": "Exportera JSON", + "settings_export_yaml": "Exportera YAML", + "settings_export_csv": "Exportera CSV", + "settings_import_csv": "Importera CSV", + "settings_import_placeholder": "Klistra in JSON- eller CSV-innehåll här…", + "settings_import_btn": "Importera", + "settings_import_success": "{count} objekt importerade.", + "settings_export_success": "Export nedladdad.", + "settings_saved": "Inställning sparad.", + "settings_include_history": "Inkludera historik", + "sort_alphabetical": "Alfabetisk", + "sort_due_soonest": "Närmaste förfallodatum", + "sort_task_count": "Antal uppgifter", + "sort_area": "Område", + "sort_assigned_user": "Tilldelad användare", + "sort_group": "Grupp", + "groupby_none": "Ingen gruppering", + "groupby_area": "Per område", + "groupby_group": "Per grupp", + "groupby_user": "Per användare", + "filter_label": "Filter", + "user_label": "Användare", + "sort_label": "Sortering", + "group_by_label": "Gruppera efter", + "state_value_help": "Använd HA-tillståndsvärdet (vanligtvis med små bokstäver, t.ex. \"on\"/\"off\"). Versaler normaliseras vid sparande.", + "target_changes_help": "Antal matchande övergångar innan utlösaren aktiveras (standard: 1).", + "qr_print_title": "Skriv ut QR-koder", + "qr_print_desc": "Skapa en utskriftssida med QR-koder att klippa ut och sätta på din utrustning.", + "qr_print_load": "Ladda objekt", + "qr_print_filter": "Filter", + "qr_print_objects": "Objekt", + "qr_print_actions": "Åtgärder", + "qr_print_url_mode": "Länktyp", + "qr_print_estimate": "Uppskattade QR-koder", + "qr_print_over_limit": "gräns 200, begränsa filtret", + "qr_print_generate": "Skapa QR-koder", + "qr_print_generating": "Skapar…", + "qr_print_ready": "QR-koder klara", + "qr_print_print_button": "Skriv ut", + "qr_print_empty": "Inget att skapa", + "qr_action_skip": "Hoppa över", + "vacation_title": "Semesterläge", + "vacation_active": "aktivt", + "vacation_ended": "avslutat", + "vacation_desc": "Planera din semester: aviseringar pausas under perioden plus några buffert-dagar. Du kan göra undantag per uppgift.", + "vacation_enable": "Aktivera semesterläge", + "vacation_start": "Start", + "vacation_end": "Slut", + "vacation_buffer": "Buffert (dagar)", + "vacation_exempt_title": "Avisera ändå under semester", + "vacation_exempt_desc": "Välj uppgifter som ska avisera även under semestern (t.ex. kritisk poolkemi).", + "vacation_load_tasks": "Ladda uppgifter", + "vacation_preview_btn": "Visa förhandsgranskning", + "vacation_preview_affected": "uppgifter berörda", + "vacation_event_due_soon": "blir snart förfallen", + "vacation_event_overdue": "blir försenad", + "vacation_event_triggered_est": "möjlig sensorutlösning", + "vacation_sensor_based": "(sensorbaserad)", + "vacation_action_notify": "Avisera ändå", + "vacation_action_unsilence": "Tysta igen", + "vacation_marked_complete": "Markerad som klar", + "vacation_marked_skip": "Hoppade över", + "vacation_end_now": "Avsluta semester nu", + "unassigned": "Otilldelad", + "no_area": "Inget område", + "has_overdue": "Har försenade uppgifter", + "object": "Objekt", + "settings_panel_access": "Paneltillgång", + "settings_panel_access_desc": "Administratörer har alltid full åtkomst. För att delegera skapa, redigera och ta bort till specifika icke-admin-användare, slå på detta och välj dem nedan — alla andra ser endast Slutför och Hoppa över.", + "settings_operator_write": "Tillåt valda användare att skapa, redigera och ta bort", + "settings_operator_write_desc": "Av: endast administratörer kan ändra innehåll. På: de valda användarna nedan får också full åtkomst.", + "no_non_admin_users": "Inga icke-admin-användare hittades. Lägg till några i Inställningar → Personer.", + "owner_label": "Ägare", + "feat_completion_actions": "Slutförande-åtgärder", + "feat_completion_actions_desc": "HA-åtgärd per uppgift vid slutförande + snabb-slutför-QR med förinställda värden.", + "on_complete_action_title": "Vid slutförande: utlös HA-åtgärd (valfritt)", + "on_complete_action_desc": "Anropar en HA-tjänst när uppgiften slutförs — t.ex. återställ en räknare på enheten.", + "on_complete_action_service": "Tjänst", + "on_complete_action_target": "Målentitet", + "on_complete_action_data": "Data (JSON, valfritt)", + "on_complete_action_test": "Testa åtgärd", + "on_complete_action_test_success": "Lyckades", + "on_complete_action_test_failed": "Misslyckades", + "quick_complete_defaults_title": "Snabb-slutför standardvärden (för QR-skanningar, valfritt)", + "quick_complete_defaults_desc": "Förinställda värden för snabb-slutför-QR. Utan dessa öppnar QR slutför-dialogen.", + "quick_complete_defaults_notes": "Anteckningar", + "quick_complete_defaults_cost": "Kostnad", + "quick_complete_defaults_duration": "Varaktighet (minuter)", + "quick_complete_defaults_feedback_none": "Ingen feedback", + "quick_complete_defaults_feedback_needed": "Var nödvändigt", + "quick_complete_defaults_feedback_not_needed": "Inte nödvändigt", + "quick_complete_success": "Snabbt markerad som klar", + "trigger_replaced": "Utlösare ersatt", + "add": "Lägg till", + "show_stats": "Visa statistik + grafer", + "hide_stats": "Dölj statistik", + "adaptive_no_data": "Ännu inte tillräckligt med slutförandehistorik för adaptiv analys. Slutför denna uppgift några gånger till för att låsa upp intervallrekommendationer och tillförlitlighetsdiagram.", + "suggestion_applied": "Föreslaget intervall tillämpat", + "vacation_mode": "Semesterläge", + "vacation_status_active": "Aktivt nu", + "vacation_status_scheduled": "Schemalagt", + "vacation_status_inactive": "Inaktivt", + "vacation_end_now_confirm": "Avsluta semestern omedelbart?", + "vacation_exempt_count": "undantagna", + "vacation_advanced": "Avancerat…", + "vacation_open_panel": "Öppna i panelen", + "enable": "Aktivera", + "saved": "Sparat", + "budget_monthly_set": "Ange månad", + "budget_yearly_set": "Ange år", + "budget_advanced": "Valuta, varningar…", + "budget_open_panel": "Öppna i panelen", + "groups_empty": "Inga grupper än.", + "group_new_placeholder": "Lägg till grupp…", + "group_delete_confirm": "Ta bort gruppen ”{name}”?", + "groups_manage_tasks": "Hantera uppgiftstilldelningar…", + "groups_open_panel": "Öppna i panelen", + "on_complete_action_target_hint": "Obs: entitetens domän måste matcha tjänsten — t.ex. 'button.press' fungerar bara på button.*, 'counter.increment' bara på counter.*, 'input_button.press' bara på input_button.* osv. Vid en avvikelse misslyckas åtgärden tyst (HA loggar 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Visa alla objekt", + "show_all_tasks": "Rensa filter — visa alla uppgifter", + "filter_to_overdue": "Filtrera listan till endast försenade", + "filter_to_due_soon": "Filtrera listan till endast snart förfallna", + "filter_to_triggered": "Filtrera listan till endast utlösta", + "open_task": "Öppna uppgift", + "show_details": "Visa historik + statistik", + "hide_details": "Dölj detaljer", + "history_empty": "Ingen historik än.", + "history_edit_button": "Redigera post", + "total_cost": "Total kostnad", + "times_performed": "Utförd", + "older_entries": "äldre", + "open_in_panel": "Öppna i Underhållspanelen", + "skip_reason": "Anledning till överhoppning (valfritt)", + "reset_to_date": "Återställ senast utförd till", + "delete_task_confirm": "Ta bort denna uppgift och dess historik?", + "delete_object_confirm": "Ta bort detta objekt och alla dess uppgifter?", + "loading": "Laddar…", + "archive": "Arkivera", + "undo": "Ångra", + "task_archived": "Uppgift arkiverad", + "object_archived": "Objekt arkiverat", + "unarchive": "Återställ", + "archived": "Arkiverad", + "show_archived": "Visa arkiverade", + "hide_archived": "Dölj arkiverade", + "confirm_archive_object": "Arkivera detta objekt och dess uppgifter? Historiken behålls och kan återställas senare.", + "settings_archive": "Arkiv och lagring", + "settings_archive_desc": "Lägg undan slutförda engångsuppgifter utan att radera dem. Arkiverade objekt döljs och är inaktiva men behåller historik och kostnad.", + "settings_archive_oneoff_days": "Arkivera slutförda engångsuppgifter automatiskt efter (dagar, 0 = av)", + "settings_delete_archived_oneoff_days": "Radera arkiverade engångsuppgifter automatiskt efter (dagar, 0 = aldrig)", + "archive_object": "Arkivera objekt", + "unarchive_object": "Återställ objekt", + "documents": "Dokument", + "documents_empty": "Inga dokument ännu.", + "doc_upload": "Ladda upp fil", + "doc_uploading": "Laddar upp…", + "doc_add_link": "Lägg till länk", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Titel (valfritt)", + "doc_open": "Öppna", + "doc_delete_confirm": "Ta bort \"{name}\"?", + "doc_too_large": "Filen är för stor (max 25 MB).", + "doc_upload_failed": "Uppladdningen misslyckades.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Redan lagrad någon annanstans — delad, inget extra utrymme.", + "doc_dup_in_object": "Den här filen är redan kopplad till detta objekt.", + "doc_link_invalid": "Endast http/https-länkar tillåts.", + "doc_cat_manual": "Manual", + "doc_cat_warranty": "Garanti", + "doc_cat_invoice": "Faktura", + "doc_cat_spare_parts": "Reservdelar", + "doc_cat_photo": "Foto", + "doc_cat_other": "Övrigt", + "doc_link_badge": "Länk", + "doc_storage_title": "Dokumentlagring", + "doc_storage_saved": "Sparat genom deduplicering", + "doc_storage_refresh": "Uppdatera", + "doc_download": "Ladda ner", + "doc_close": "Stäng", + "doc_camera": "Ta foto", + "doc_drop_hint": "Släpp filer här", + "doc_task_none": "Inga dokument kopplade till denna uppgift.", + "doc_link_existing": "Koppla ett dokument…", + "doc_attach": "Koppla", + "doc_unlink": "Koppla bort", + "doc_page": "Sida", + "chart_range_7d": "7d", + "chart_range_30d": "30d", + "chart_range_90d": "90d", + "chart_range_1y": "1å", + "chart_since_service": "sedan senaste underhåll", + "chart_no_stats": "Ingen långtidsstatistik för denna entitet — visar endast värden från underhållsposter", + "auto_complete_on_recovery": "Slutför automatiskt när sensorn återhämtar sig", + "auto_complete_on_recovery_help": "Registrerar ett slutförande (sätter senast utförd) när utlösaren löser sig själv — t.ex. salt påfyllt, filter bytt.", + "doc_search": "Sök dokument…", + "doc_search_none": "Inga matchande dokument", + "link_device_optional": "Länka till befintlig enhet (valfritt)", + "parent_object_optional": "Överordnat objekt (valfritt)", + "parent_none": "(Inget överordnat)", + "paused": "Pausad", + "pause_object": "Pausa", + "resume_object": "Återuppta", + "pause_until_prompt": "Frys objektets scheman — inget förfaller och inget aviserar förrän det återupptas. Ange valfritt ett datum för automatisk återupptagning.", + "pause_until_label": "Återuppta den (valfritt)", + "object_paused": "Objekt pausat", + "object_resumed": "Objekt återupptaget — scheman omstartade", + "object_paused_badge": "Pausad", + "paused_until_label": "till", + "replace_object": "Ersätt…", + "replace_object_prompt": "Pensionera detta objekt och skapa en efterträdare. Historik och kostnader arkiveras på det gamla; uppgifter och dokument följer med till det nya, räknare börjar om.", + "replace_name_label": "Efterträdarens namn", + "object_replaced": "Objekt ersatt — efterträdare skapad", + "reading_unit_label": "Avläsningsenhet (t.ex. kWh, m³)", + "reading_unit_help": "Visas bredvid det registrerade värdet när uppgiften slutförs.", + "reading_value_label": "Avläst värde", + "reading_label": "Avläsning", + "settings_templates_label": "Mallgalleri", + "settings_templates_hint": "Avmarkera mallar du aldrig behöver — de försvinner från \"Från mall\"-väljarna (panel och config flow). Inget annat ändras; kan återaktiveras när som helst.", + "worksheet": "Arbetsblad", + "worksheet_scan_view": "Skanna för att öppna uppgiften", + "worksheet_scan_complete": "Skanna för att slutföra", + "worksheet_manual_excerpt": "Utdrag ur manualen", + "worksheet_pages": "sidor", + "worksheet_printed": "Utskriven", + "worksheet_never": "Aldrig" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/uk.json b/custom_components/maintenance_supporter/frontend/locales/uk.json new file mode 100644 index 0000000..ff18efc --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/uk.json @@ -0,0 +1,696 @@ +{ + "maintenance": "Обслуговування", + "objects": "Об'єкти", + "tasks": "Завдання", + "overdue": "Прострочено", + "due_soon": "Незабаром", + "triggered": "Спрацювало", + "ok": "Норма", + "all": "Всі", + "new_object": "+ Новий об'єкт", + "templates_from": "Із шаблону", + "templates_title": "Почати із шаблону", + "templates_task_count": "{n} завдань", + "template_created": "Створено із шаблону", + "onboard_hint": "Додайте перший об'єкт, щоб відстежувати обслуговування.", + "edit": "Редагувати", + "duplicate": "Дублювати", + "task_duplicated": "Завдання продубльовано", + "object_duplicated": "Об'єкт продубльовано", + "delete": "Видалити", + "add_task": "+ Додати завдання", + "complete": "Виконати", + "completed": "Виконано", + "skip": "Пропустити", + "skipped": "Пропущено", + "missed": "Missed", + "reset": "Скинути", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "Скасувати", + "bulk_select": "Вибрати", + "bulk_select_all": "Вибрати все", + "bulk_n_selected": "Вибрано: {n}", + "bulk_completed": "Виконано завдань: {n}", + "bulk_archived": "Архівовано завдань: {n}", + "completing": "Виконується…", + "interval": "Інтервал", + "warning": "Попередження", + "last_performed": "Останнє виконання", + "next_due": "Наступний термін", + "days_until_due": "Днів до терміну", + "avg_duration": "Сер. тривалість", + "trigger": "Тригер", + "trigger_type": "Тип тригера", + "threshold_above": "Верхня межа", + "threshold_below": "Нижня межа", + "threshold": "Поріг", + "counter": "Лічильник", + "state_change": "Зміна стану", + "runtime": "Напрацювання", + "runtime_hours": "Цільове напрацювання (години)", + "target_value": "Цільове значення", + "baseline": "Базове значення", + "target_changes": "Цільова кількість змін", + "for_minutes": "Протягом (хвилин)", + "time_based": "За часом", + "sensor_based": "За сенсором", + "manual": "Вручну", + "one_time": "Одноразово", + "weekdays": "Дні тижня", + "nth_weekday": "N-й день тижня в місяці", + "day_of_month": "День місяця", + "recurrence_on_days": "Повторювати у", + "recurrence_occurrence": "Входження", + "recurrence_weekday": "День тижня", + "recurrence_day": "День місяця (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "1-й", + "ord_2": "2-й", + "ord_3": "3-й", + "ord_4": "4-й", + "ord_5": "5-й", + "ord_last": "Останній", + "day_word": "День", + "interval_value": "Інтервал", + "interval_unit": "Одиниця", + "unit_days": "Дні", + "unit_weeks": "Тижні", + "unit_months": "Місяці", + "unit_years": "Роки", + "due_date": "Дата виконання", + "cleaning": "Очищення", + "inspection": "Огляд", + "replacement": "Заміна", + "calibration": "Калібрування", + "service": "Сервіс", + "reading": "Показання", + "custom": "Власний", + "history": "Історія", + "cost": "Вартість", + "report_button": "Звіт", + "report_title": "Звіт про обслуговування", + "report_generated": "Створено", + "report_times_done": "Виконано", + "report_total_cost": "Загальна вартість", + "report_every": "кожні {n} {unit}", + "report_notes": "Нотатки", + "report_col_type": "Тип", + "report_col_status": "Статус", + "report_col_schedule": "Розклад", + "duration": "Тривалість", + "both": "Обидва", + "trigger_val": "Значення тригера", + "complete_title": "Виконати: ", + "checklist": "Чекліст", + "checklist_steps_optional": "Кроки чекліста (необов'язково)", + "checklist_placeholder": "Очистити фільтр\nЗамінити ущільнювач\nПеревірити тиск", + "checklist_help": "Один крок на рядок. Макс. 100 елементів.", + "err_too_long": "{field}: задовге (макс. {n} символів)", + "err_too_short": "{field}: закоротке (мін. {n} символів)", + "err_value_too_high": "{field}: завелике (макс. {n})", + "err_value_too_low": "{field}: замале (мін. {n})", + "err_required": "{field}: обов'язкове поле", + "err_wrong_type": "{field}: невірний тип (очікувалось: {type})", + "err_invalid_choice": "{field}: недопустиме значення", + "err_invalid_value": "{field}: невірне значення", + "feat_schedule_time": "Планування за часом доби", + "feat_schedule_time_desc": "Задачі стають простроченими у певний час доби, а не опівночі.", + "schedule_time_optional": "Час прострочення (необов'язково, HH:MM)", + "schedule_time_help": "Порожньо = опівночі (за замовчуванням). Часовий пояс HA.", + "at_time": "о", + "notes_optional": "Примітки (необов'язково)", + "cost_optional": "Вартість (необов'язково)", + "duration_minutes": "Тривалість у хвилинах (необов'язково)", + "days": "днів", + "day": "день", + "today": "Сьогодні", + "d_overdue": "д прострочено", + "no_tasks": "Завдань обслуговування ще немає. Створіть об'єкт, щоб почати.", + "no_tasks_short": "Немає завдань", + "no_history": "Записів в історії ще немає.", + "show_all": "Показати всі", + "cost_duration_chart": "Вартість і тривалість", + "installed": "Встановлено", + "confirm_delete_object": "Видалити цей об'єкт і всі його завдання?", + "confirm_delete_task": "Видалити це завдання?", + "min": "Мін", + "max": "Макс", + "save": "Зберегти", + "saving": "Збереження…", + "edit_task": "Редагувати завдання", + "new_task": "Нове завдання обслуговування", + "task_name": "Назва завдання", + "maintenance_type": "Тип обслуговування", + "priority": "Пріоритет", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "Низький", + "priority_normal": "Звичайний", + "priority_high": "Високий", + "schedule_type": "Тип розкладу", + "interval_days": "Інтервал (дні)", + "warning_days": "Днів попередження", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "interval_anchor": "Прив'язка інтервалу", + "anchor_completion": "Від дати виконання", + "anchor_planned": "Від запланованої дати (без зміщення)", + "edit_object": "Редагувати об'єкт", + "name": "Назва", + "manufacturer_optional": "Виробник (необов'язково)", + "model_optional": "Модель (необов'язково)", + "serial_number_optional": "Серійний номер (необов'язково)", + "serial_number_label": "С/Н", + "documentation_url_label": "Посібник", + "object_notes_label": "Примітки", + "last_performed_optional": "Останнé виконання (необов'язково)", + "sort_due_date": "Дата терміну", + "sort_object": "Назва об'єкта", + "sort_type": "Тип", + "sort_task_name": "Назва завдання", + "all_objects": "Всі об'єкти", + "tasks_lower": "завдань", + "no_tasks_yet": "Завдань ще немає", + "add_first_task": "Додати перше завдання", + "trigger_configuration": "Налаштування тригера", + "entity_id": "ID об'єкта", + "comma_separated": "через кому", + "entity_logic": "Логіка об'єктів", + "entity_logic_any": "Будь-який об'єкт спрацьовує", + "entity_logic_all": "Всі об'єкти мають спрацювати", + "entities": "об'єктів", + "attribute_optional": "Атрибут (необов'язково, порожньо = стан)", + "use_entity_state": "Використовувати стан об'єкта (без атрибута)", + "trigger_above": "Спрацювати, коли вище", + "trigger_below": "Спрацювати, коли нижче", + "for_at_least_minutes": "Протягом не менше (хвилин)", + "safety_interval_days": "Страховий інтервал (дні, необов'язково)", + "safety_interval": "Страховий інтервал (необов'язково)", + "delta_mode": "Режим дельти", + "from_state_optional": "З стану (необов'язково)", + "to_state_optional": "До стану (необов'язково)", + "documentation_url_optional": "URL документації (необов'язково)", + "object_notes_optional": "Примітки (необов'язково)", + "nfc_tag_id_optional": "ID NFC-тега (необов'язково)", + "nfc_tags_empty_help": "У Home Assistant ще не зареєстровано NFC-теги.", + "nfc_tags_open_settings": "Відкрити налаштування тегів", + "nfc_tags_refresh": "Оновити", + "environmental_entity_optional": "Датчик навколишнього середовища (необов'язково)", + "environmental_entity_helper": "напр. sensor.outdoor_temperature — коригує інтервал відповідно до умов навколишнього середовища", + "environmental_attribute_optional": "Атрибут середовища (необов'язково)", + "nfc_tag_id": "ID NFC-тега", + "nfc_linked": "NFC-тег прив'язано", + "nfc_link_hint": "Натисніть, щоб прив'язати NFC-тег", + "responsible_user": "Відповідальний користувач", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(Користувача не призначено)", + "all_users": "Всі користувачі", + "my_tasks": "Мої завдання", + "tab_calendar": "Календар", + "cal_no_events": "Без обслуговування", + "cal_window_7": "7 днів", + "cal_window_14": "14 днів", + "cal_window_30": "30 днів", + "cal_window_365": "1 рік", + "cal_every_n_days": "кожні {n} днів", + "cal_source_time": "За часом", + "cal_source_time_adaptive": "За часом (адаптивно)", + "cal_source_sensor": "За датчиком", + "cal_predicted": "прогноз", + "cal_confidence_high": "висока надійність", + "cal_confidence_medium": "середня надійність", + "cal_confidence_low": "низька надійність", + "budget_monthly": "Щомісячний бюджет", + "budget_yearly": "Щорічний бюджет", + "groups": "Групи", + "new_group": "Нова група", + "edit_group": "Редагувати групу", + "no_groups": "Груп ще немає", + "delete_group": "Видалити групу", + "delete_group_confirm": "Видалити групу '{name}'?", + "group_select_tasks": "Обрати завдання", + "group_name_required": "Потрібна назва", + "description_optional": "Опис (необов'язково)", + "selected": "Обрано", + "loading_chart": "Завантаження даних графіка...", + "hide_outliers": "Приховати викиди (збої датчика)", + "was_maintenance_needed": "Чи було потрібне це обслуговування?", + "feedback_needed": "Потрібне", + "feedback_not_needed": "Не потрібне", + "feedback_not_sure": "Не впевнений", + "suggested_interval": "Рекомендований інтервал", + "apply_suggestion": "Застосувати", + "reanalyze": "Повторно проаналізувати", + "reanalyze_result": "Новий аналіз", + "reanalyze_insufficient_data": "Недостатньо даних для рекомендації", + "data_points": "точок даних", + "dismiss_suggestion": "Відхилити", + "confidence_low": "Низька", + "confidence_medium": "Середня", + "confidence_high": "Висока", + "recommended": "рекомендовано", + "seasonal_awareness": "Сезонна корекція", + "edit_seasonal_overrides": "Редагувати сезонні коефіцієнти", + "seasonal_overrides_title": "Сезонні коефіцієнти (перевизначення)", + "seasonal_overrides_hint": "Коефіцієнт на місяць (0.1–5.0). Порожньо = автоматично.", + "seasonal_override_invalid": "Недійсне значення", + "seasonal_override_range": "Коефіцієнт має бути між 0.1 та 5.0", + "clear_all": "Очистити все", + "seasonal_chart_title": "Сезонні коефіцієнти", + "seasonal_learned": "Навчена", + "seasonal_manual": "Ручна", + "month_jan": "Січ", + "month_feb": "Лют", + "month_mar": "Бер", + "month_apr": "Кві", + "month_may": "Тра", + "month_jun": "Чер", + "month_jul": "Лип", + "month_aug": "Сер", + "month_sep": "Вер", + "month_oct": "Жов", + "month_nov": "Лис", + "month_dec": "Гру", + "sensor_prediction": "Прогноз сенсора", + "degradation_trend": "Тренд", + "trend_rising": "Зростає", + "trend_falling": "Спадає", + "trend_stable": "Стабільний", + "trend_insufficient_data": "Недостатньо даних", + "days_until_threshold": "Днів до порогу", + "threshold_exceeded": "Поріг перевищено", + "environmental_adjustment": "Екологічний коефіцієнт", + "sensor_prediction_urgency": "Сенсор прогнозує досягнення порогу через ~{days} днів", + "day_short": "день", + "weibull_reliability_curve": "Крива надійності", + "weibull_failure_probability": "Ймовірність відмови", + "weibull_r_squared": "Точність R²", + "beta_early_failures": "Ранні відмови", + "beta_random_failures": "Випадкові відмови", + "beta_wear_out": "Знос", + "beta_highly_predictable": "Дуже передбачуваний", + "confidence_interval": "Довірчий інтервал", + "confidence_conservative": "Консервативний", + "confidence_aggressive": "Оптимістичний", + "current_interval_marker": "Поточний інтервал", + "recommended_marker": "Рекомендовано", + "characteristic_life": "Характеристичний ресурс", + "chart_mini_sparkline": "Мінімальний графік тренду", + "chart_history": "Історія вартості та тривалості", + "chart_seasonal": "Сезонні коефіцієнти, 12 місяців", + "chart_weibull": "Крива надійності Вейбулла", + "chart_sparkline": "Графік значень тригера сенсора", + "days_progress": "Прогрес днів", + "qr_code": "QR-код", + "qr_generating": "Генерація QR-коду…", + "qr_error": "Не вдалося згенерувати QR-код.", + "qr_error_no_url": "URL Home Assistant не налаштовано. Задайте зовнішню або внутрішню URL-адресу в Налаштування → Система → Мережа.", + "save_error": "Не вдалося зберегти. Спробуйте ще раз.", + "qr_print": "Друкувати", + "qr_download": "Завантажити SVG", + "qr_action": "Дія при скануванні", + "qr_action_view": "Переглянути", + "qr_action_complete": "Позначити обслуговування виконаним", + "qr_url_mode": "Тип посилання", + "qr_mode_companion": "Companion App", + "qr_mode_local": "Локальний (mDNS)", + "qr_mode_server": "URL сервера", + "overview": "Огляд", + "analysis": "Аналіз", + "recent_activities": "Остання активність", + "search_notes": "Пошук у примітках", + "avg_cost": "Сер. вартість", + "no_advanced_features": "Розширені функції не увімкнено", + "no_advanced_features_hint": "Увімкніть «Адаптивні інтервали» або «Сезонні закономірності» в налаштуваннях інтеграції, щоб побачити тут дані аналізу.", + "analysis_not_enough_data": "Недостатньо даних для аналізу.", + "analysis_not_enough_data_hint": "Аналіз Вейбулла потребує щонайменше 5 виконаних обслуговувань; сезонні закономірності стають видимими після 6+ записів на місяць.", + "analysis_manual_task_hint": "Ручні завдання без інтервалу не генерують дані аналізу.", + "completions": "виконань", + "current": "Поточний", + "shorter": "Коротший", + "longer": "Довший", + "normal": "Звичайний", + "disabled": "Вимкнено", + "compound_logic": "Складена логіка", + "compound": "Складений (кілька умов)", + "compound_logic_and": "І — усі умови мають спрацювати", + "compound_logic_or": "АБО — достатньо будь-якої умови", + "compound_help": "Об'єднайте кілька умов датчиків в один тригер.", + "compound_no_conditions": "Ще немає умов — додайте принаймні одну.", + "compound_add_condition": "Додати умову", + "compound_condition": "Умова", + "compound_remove_condition": "Видалити умову", + "card_title": "Заголовок", + "card_show_header": "Показувати заголовок зі статистикою", + "card_show_actions": "Показувати кнопки дій", + "card_compact": "Компактний режим", + "card_max_items": "Макс. елементів (0 = всі)", + "card_filter_status": "Фільтрувати за статусом", + "card_filter_status_help": "Порожньо = показати всі статуси.", + "card_filter_objects": "Фільтрувати за об'єктами", + "card_filter_objects_help": "Порожньо = показати всі об'єкти.", + "card_filter_entities": "Фільтрувати за сутностями (entity_ids)", + "card_filter_entities_help": "Виберіть сутності sensor / binary_sensor з цієї інтеграції. Порожньо = всі.", + "card_loading_objects": "Завантаження об'єктів…", + "card_load_error": "Не вдалося завантажити об'єкти — перевірте WebSocket-з'єднання.", + "card_no_tasks_title": "Поки немає завдань обслуговування", + "card_no_tasks_cta": "→ Створіть на панелі Maintenance", + "no_objects": "Поки немає об'єктів.", + "action_error": "Дія не вдалась. Спробуйте ще раз.", + "area_id_optional": "Зона (необов'язково)", + "installation_date_optional": "Дата встановлення (необов'язково)", + "warranty_expiry_optional": "Закінчення гарантії (необов'язково)", + "warranty": "Гарантія", + "warranty_valid_until": "дійсна до {date}", + "warranty_expires_in": "закінчується через {days} дн.", + "warranty_expired": "закінчилася", + "cal_past_windows": "Минулі вікна", + "cal_forward_windows": "Майбутні вікна", + "history_edit_title": "Редагувати запис історії", + "history_edit_timestamp": "Позначка часу", + "manufacturer": "Виробник", + "model": "Модель", + "area": "Зона", + "actions": "Дії", + "view_mode_label": "Вигляд", + "view_cards": "Картки", + "view_table": "Таблиця", + "objects_table_columns_label": "Стовпці таблиці об'єктів", + "objects_table_columns_hint": "Виберіть, які стовпці показувати в табличному вигляді об'єктів.", + "custom_icon_optional": "Іконка (необов'язково, наприклад mdi:wrench)", + "task_enabled": "Завдання увімкнено", + "skip_reason_prompt": "Пропустити це завдання?", + "reason_optional": "Причина (необов'язково)", + "reset_date_prompt": "Позначити як виконане?", + "reset_date_optional": "Дата останнього виконання (необов'язково, типово: сьогодні)", + "notes_label": "Примітки", + "documentation_label": "Документація", + "no_nfc_tag": "— Без тега —", + "dashboard": "Дашборд", + "tab_today": "Сьогодні", + "palette_placeholder": "Пошук об'єктів і завдань…", + "palette_no_results": "Немає збігів", + "palette_hint": "↑↓ навігація · Enter відкрити · Esc закрити", + "today_all_caught_up": "Усе зроблено! Цього тижня нічого немає.", + "today_overdue": "Прострочені", + "today_due_today": "Сьогодні", + "today_this_week": "Цього тижня", + "settings": "Налаштування", + "settings_features": "Розширені функції", + "settings_features_desc": "Увімкніть або вимкніть розширені функції. Вимкнення приховує їх з інтерфейсу, але не видаляє дані.", + "feat_adaptive": "Адаптивне планування", + "feat_adaptive_desc": "Навчатися оптимальним інтервалам з історії обслуговування", + "feat_predictions": "Прогнози за сенсорами", + "feat_predictions_desc": "Прогнозувати дати спрацювання за деградацією сенсора", + "feat_seasonal": "Сезонні корекції", + "feat_seasonal_desc": "Коригувати інтервали на основі сезонних закономірностей", + "feat_environmental": "Кореляція з довкіллям", + "feat_environmental_desc": "Корелювати інтервали з температурою/вологістю", + "feat_budget": "Відстеження бюджету", + "feat_budget_desc": "Відстежувати щомісячні та щорічні витрати на обслуговування", + "feat_groups": "Групи завдань", + "feat_groups_desc": "Організовувати завдання в логічні групи", + "feat_checklists": "Чеклісти", + "feat_checklists_desc": "Багатокрокові процедури для виконання завдань", + "settings_general": "Загальне", + "settings_default_warning": "Днів попередження за замовчуванням", + "settings_panel_enabled": "Панель у бічному меню", + "settings_panel_title": "Заголовок панелі", + "settings_notifications": "Сповіщення", + "settings_notify_service": "Служба сповіщень", + "test_notification": "Тестове сповіщення", + "send_test": "Надіслати тест", + "testing": "Надсилання…", + "test_notification_success": "Тестове сповіщення надіслано", + "test_notification_failed": "Не вдалося надіслати тестове сповіщення", + "settings_notify_due_soon": "Сповіщати, коли термін наближається", + "settings_notify_overdue": "Сповіщати про прострочення", + "settings_notify_triggered": "Сповіщати про спрацювання", + "settings_interval_hours": "Інтервал повторення (години, 0 = одноразово)", + "settings_quiet_hours": "Тихі години", + "settings_quiet_start": "Початок", + "settings_quiet_end": "Кінець", + "settings_max_per_day": "Макс. сповіщень на день (0 = без обмежень)", + "settings_bundling": "Групувати сповіщення", + "settings_bundle_threshold": "Поріг групування", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "Кнопки дій у мобільних сповіщеннях", + "settings_action_complete": "Показувати кнопку «Виконати»", + "settings_action_skip": "Показувати кнопку «Пропустити»", + "settings_action_snooze": "Показувати кнопку «Відкласти»", + "settings_weekly_digest": "Щотижневий огляд", + "settings_weekly_digest_hint": "Одне зведене сповіщення в понеділок вранці, коли є завдання.", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "Тривалість відкладення (години)", + "settings_budget": "Бюджет", + "settings_currency": "Валюта", + "settings_budget_monthly": "Щомісячний бюджет", + "settings_budget_yearly": "Щорічний бюджет", + "settings_budget_alerts": "Сповіщення про бюджет", + "settings_budget_threshold": "Поріг сповіщення (%)", + "settings_import_export": "Імпорт / Експорт", + "settings_export_json": "Експортувати JSON", + "settings_export_yaml": "Експортувати YAML", + "settings_export_csv": "Експортувати CSV", + "settings_import_csv": "Імпортувати CSV", + "settings_import_placeholder": "Вставте вміст JSON або CSV сюди…", + "settings_import_btn": "Імпортувати", + "settings_import_success": "{count} об'єктів успішно імпортовано.", + "settings_export_success": "Експорт завантажено.", + "settings_saved": "Налаштування збережено.", + "settings_include_history": "Включити історію", + "sort_alphabetical": "За алфавітом", + "sort_due_soonest": "Найближчий термін", + "sort_task_count": "Кількість завдань", + "sort_area": "Зона", + "sort_assigned_user": "Призначений користувач", + "sort_group": "Група", + "groupby_none": "Без групування", + "groupby_area": "За зоною", + "groupby_group": "За групою", + "groupby_user": "За користувачем", + "filter_label": "Фільтр", + "user_label": "Користувач", + "sort_label": "Сортування", + "group_by_label": "Групувати за", + "state_value_help": "Використовуйте значення стану HA (зазвичай у нижньому регістрі, напр. \"on\"/\"off\"). Регістр нормалізується при збереженні.", + "target_changes_help": "Кількість відповідних переходів, після яких тригер спрацює (за замовчуванням: 1).", + "qr_print_title": "Друкувати QR-коди", + "qr_print_desc": "Створи сторінку для друку з QR-кодами, які можна вирізати та наклеїти на обладнання.", + "qr_print_load": "Завантажити об'єкти", + "qr_print_filter": "Фільтр", + "qr_print_objects": "Об'єкти", + "qr_print_actions": "Дії", + "qr_print_url_mode": "Тип посилання", + "qr_print_estimate": "Прогноз QR-кодів", + "qr_print_over_limit": "ліміт 200, звузь фільтр", + "qr_print_generate": "Створити QR-коди", + "qr_print_generating": "Створення…", + "qr_print_ready": "QR-коди готові", + "qr_print_print_button": "Друкувати", + "qr_print_empty": "Нічого створювати", + "qr_action_skip": "Пропустити", + "vacation_title": "Режим відпустки", + "vacation_active": "активний", + "vacation_ended": "завершено", + "vacation_desc": "Сплануй відпустку: сповіщення призупиняються на період плюс кілька буферних днів. Можна налаштувати винятки.", + "vacation_enable": "Увімкнути режим відпустки", + "vacation_start": "Початок", + "vacation_end": "Кінець", + "vacation_buffer": "Буфер (днів)", + "vacation_exempt_title": "Сповіщати попри відпустку", + "vacation_exempt_desc": "Обери завдання, які мають сповіщати навіть під час відпустки (наприклад, критична хімія басейну).", + "vacation_load_tasks": "Завантажити завдання", + "vacation_preview_btn": "Показати попередній перегляд", + "vacation_preview_affected": "завдань зачеплено", + "vacation_event_due_soon": "наближається термін", + "vacation_event_overdue": "стане простроченим", + "vacation_event_triggered_est": "можливе спрацювання сенсора", + "vacation_sensor_based": "(сенсорне)", + "vacation_action_notify": "Сповіщати все одно", + "vacation_action_unsilence": "Знову вимкнути сповіщення", + "vacation_marked_complete": "Позначено як виконане", + "vacation_marked_skip": "Пропущено", + "vacation_end_now": "Завершити відпустку зараз", + "unassigned": "Не призначено", + "no_area": "Без зони", + "has_overdue": "Прострочені завдання", + "object": "Об'єкт", + "settings_panel_access": "Доступ до панелі", + "settings_panel_access_desc": "Адміністратори завжди мають повний доступ. Щоб делегувати створення, редагування та видалення певним не-адмін користувачам, увімкніть це та виберіть їх нижче — інші бачать лише Виконати та Пропустити.", + "settings_operator_write": "Дозволити вибраним користувачам створювати, редагувати та видаляти", + "settings_operator_write_desc": "Вимкнено: лише адміністратори можуть змінювати вміст. Увімкнено: вибрані користувачі нижче також отримують повний доступ.", + "no_non_admin_users": "Не знайдено не-адмін користувачів. Додайте їх у Налаштуваннях → Особи.", + "owner_label": "Власник", + "feat_completion_actions": "Дії при завершенні", + "feat_completion_actions_desc": "Дія HA по завданню при завершенні + QR швидкого завершення з попередньо встановленими значеннями.", + "on_complete_action_title": "При завершенні: викликати HA-дію (опційно)", + "on_complete_action_desc": "Викликає HA-сервіс, коли завдання завершено — напр., скинути лічильник на пристрої.", + "on_complete_action_service": "Сервіс", + "on_complete_action_target": "Цільова сутність", + "on_complete_action_data": "Дані (JSON, опційно)", + "on_complete_action_test": "Тестувати дію", + "on_complete_action_test_success": "Успіх", + "on_complete_action_test_failed": "Помилка", + "quick_complete_defaults_title": "Стандартні значення швидкого завершення (для QR-сканів, опційно)", + "quick_complete_defaults_desc": "Попередньо встановлені значення для QR швидкого завершення. Без них QR відкриває діалог.", + "quick_complete_defaults_notes": "Нотатки", + "quick_complete_defaults_cost": "Вартість", + "quick_complete_defaults_duration": "Тривалість (хвилин)", + "quick_complete_defaults_feedback_none": "Без зворотного зв'язку", + "quick_complete_defaults_feedback_needed": "Було необхідно", + "quick_complete_defaults_feedback_not_needed": "Не було необхідно", + "quick_complete_success": "Швидко позначено виконаним", + "trigger_replaced": "Тригер замінено", + "add": "Додати", + "show_stats": "Показати статистику + графіки", + "hide_stats": "Сховати статистику", + "adaptive_no_data": "Поки що недостатньо історії виконань для адаптивного аналізу. Виконайте це завдання ще кілька разів, щоб розблокувати рекомендації щодо інтервалу та графіки надійності.", + "suggestion_applied": "Запропонований інтервал застосовано", + "vacation_mode": "Режим відпустки", + "vacation_status_active": "Активний зараз", + "vacation_status_scheduled": "Заплановано", + "vacation_status_inactive": "Неактивний", + "vacation_end_now_confirm": "Завершити відпустку негайно?", + "vacation_exempt_count": "виключено", + "vacation_advanced": "Додатково…", + "vacation_open_panel": "Відкрити на панелі", + "enable": "Увімкнути", + "saved": "Збережено", + "budget_monthly_set": "Задати місячний", + "budget_yearly_set": "Задати річний", + "budget_advanced": "Валюта, сповіщення…", + "budget_open_panel": "Відкрити на панелі", + "groups_empty": "Поки немає груп.", + "group_new_placeholder": "Додати групу…", + "group_delete_confirm": "Видалити групу «{name}»?", + "groups_manage_tasks": "Керування призначеннями завдань…", + "groups_open_panel": "Відкрити на панелі", + "on_complete_action_target_hint": "Примітка: домен сутності має відповідати сервісу — напр. 'button.press' працює лише з button.*, 'counter.increment' лише з counter.*, 'input_button.press' лише з input_button.* тощо. У разі невідповідності дія мовчки не виконається (HA запише в журнал 'Referenced entities ... missing or not currently available').", + "show_all_objects": "Показати всі об'єкти", + "show_all_tasks": "Скинути фільтр — показати всі завдання", + "filter_to_overdue": "Фільтрувати список лише за простроченими", + "filter_to_due_soon": "Фільтрувати список лише за тими, що скоро настануть", + "filter_to_triggered": "Фільтрувати список лише за спрацьованими", + "open_task": "Відкрити завдання", + "show_details": "Показати історію + статистику", + "hide_details": "Сховати деталі", + "history_empty": "Поки немає історії.", + "history_edit_button": "Редагувати запис", + "total_cost": "Загальна вартість", + "times_performed": "Виконано", + "older_entries": "раніше", + "open_in_panel": "Відкрити на панелі обслуговування", + "skip_reason": "Причина пропуску (необов'язково)", + "reset_to_date": "Скинути останнє виконання на", + "delete_task_confirm": "Видалити це завдання та його історію?", + "delete_object_confirm": "Видалити цей об'єкт і всі його завдання?", + "loading": "Завантаження…", + "archive": "Архівувати", + "undo": "Скасувати", + "task_archived": "Завдання архівовано", + "object_archived": "Об'єкт архівовано", + "unarchive": "Розархівувати", + "archived": "В архіві", + "show_archived": "Показати архів", + "hide_archived": "Сховати архів", + "confirm_archive_object": "Архівувати цей об'єкт та його завдання? Історія збережеться, згодом можна відновити.", + "settings_archive": "Архів і зберігання", + "settings_archive_desc": "Відкладайте виконані одноразові завдання в архів, не видаляючи їх. Архівні елементи приховані й неактивні, але зберігають історію та витрати.", + "settings_archive_oneoff_days": "Автоархівування виконаних одноразових завдань через (днів, 0 = вимк)", + "settings_delete_archived_oneoff_days": "Автовидалення архівних одноразових завдань через (днів, 0 = ніколи)", + "archive_object": "Архівувати об'єкт", + "unarchive_object": "Розархівувати об'єкт", + "documents": "Документи", + "documents_empty": "Ще немає документів.", + "doc_upload": "Завантажити файл", + "doc_uploading": "Завантаження…", + "doc_add_link": "Додати посилання", + "doc_link_url": "URL (https://…)", + "doc_link_title": "Назва (необов'язково)", + "doc_open": "Відкрити", + "doc_delete_confirm": "Видалити «{name}»?", + "doc_too_large": "Файл завеликий (макс. 25 МБ).", + "doc_upload_failed": "Не вдалося завантажити.", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "Уже збережено в іншому місці — спільний, без додаткового місця.", + "doc_dup_in_object": "Цей файл уже прикріплено до цього об'єкта.", + "doc_link_invalid": "Дозволені лише посилання http/https.", + "doc_cat_manual": "Інструкція", + "doc_cat_warranty": "Гарантія", + "doc_cat_invoice": "Рахунок", + "doc_cat_spare_parts": "Запчастини", + "doc_cat_photo": "Фото", + "doc_cat_other": "Інше", + "doc_link_badge": "Посилання", + "doc_storage_title": "Сховище документів", + "doc_storage_saved": "Заощаджено дедуплікацією", + "doc_storage_refresh": "Оновити", + "doc_download": "Завантажити", + "doc_close": "Закрити", + "doc_camera": "Зробити фото", + "doc_drop_hint": "Перетягніть файли сюди", + "doc_task_none": "Немає документів, пов'язаних із цим завданням.", + "doc_link_existing": "Пов'язати документ…", + "doc_attach": "Пов'язати", + "doc_unlink": "Відв'язати", + "doc_page": "Сторінка", + "chart_range_7d": "7д", + "chart_range_30d": "30д", + "chart_range_90d": "90д", + "chart_range_1y": "1р", + "chart_since_service": "з останнього обслуговування", + "chart_no_stats": "Немає довгострокової статистики для цієї сутності — показано лише значення із записів обслуговування", + "auto_complete_on_recovery": "Автозавершення при відновленні датчика", + "auto_complete_on_recovery_help": "Записує виконання (оновлює дату останнього обслуговування), коли тригер зникає сам — наприклад, сіль досипано, фільтр замінено.", + "doc_search": "Пошук документів…", + "doc_search_none": "Немає відповідних документів", + "link_device_optional": "Прив'язати до наявного пристрою (необов'язково)", + "parent_object_optional": "Батьківський об'єкт (необов'язково)", + "parent_none": "(Без батьківського)", + "paused": "Призупинено", + "pause_object": "Призупинити", + "resume_object": "Відновити", + "pause_until_prompt": "Заморозити розклади цього об'єкта — нічого не настає і не сповіщає до відновлення. За бажанням укажіть дату автоматичного відновлення.", + "pause_until_label": "Відновити (необов'язково)", + "object_paused": "Об'єкт призупинено", + "object_resumed": "Об'єкт відновлено — розклади перезапущено", + "object_paused_badge": "Призупинено", + "paused_until_label": "до", + "replace_object": "Замінити…", + "replace_object_prompt": "Списати цей об'єкт і створити наступника. Історія та витрати лишаються в архіві старого; завдання й документи переходять до нового, лічильники починаються заново.", + "replace_name_label": "Ім'я наступника", + "object_replaced": "Об'єкт замінено — наступника створено", + "reading_unit_label": "Одиниця показання (напр. кВт·год, м³)", + "reading_unit_help": "Показується поруч із записаним значенням під час виконання цієї задачі.", + "reading_value_label": "Показання", + "reading_label": "Показання", + "settings_templates_label": "Галерея шаблонів", + "settings_templates_hint": "Зніміть позначку з шаблонів, які вам не потрібні — вони зникнуть із вибору «З шаблону» (панель і config flow). Більше нічого не змінюється; можна ввімкнути знову будь-коли.", + "worksheet": "Робочий аркуш", + "worksheet_scan_view": "Скануйте, щоб відкрити задачу", + "worksheet_scan_complete": "Скануйте для виконання", + "worksheet_manual_excerpt": "Витяг з посібника", + "worksheet_pages": "сторінки", + "worksheet_printed": "Надруковано", + "worksheet_never": "Ніколи" +} diff --git a/custom_components/maintenance_supporter/frontend/locales/zh.json b/custom_components/maintenance_supporter/frontend/locales/zh.json new file mode 100644 index 0000000..c1cee8a --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/locales/zh.json @@ -0,0 +1,696 @@ +{ + "maintenance": "维护", + "objects": "维护项", + "tasks": "任务", + "overdue": "逾期", + "due_soon": "即将到期", + "triggered": "已触发", + "trigger_replaced": "触发器已更换", + "ok": "正常", + "all": "全部", + "new_object": "+ 新建维护项", + "templates_from": "从模板", + "templates_title": "从模板开始", + "templates_task_count": "{n} 个任务", + "template_created": "已从模板创建", + "onboard_hint": "添加第一个对象以开始跟踪维护。", + "edit": "编辑", + "duplicate": "复制", + "task_duplicated": "任务已复制", + "object_duplicated": "对象已复制", + "delete": "删除", + "add_task": "+ 添加任务", + "complete": "完成", + "completed": "已完成", + "skip": "跳过", + "skipped": "已跳过", + "missed": "Missed", + "reset": "重置", + "snooze": "Snooze", + "snoozed": "Snoozed", + "cancel": "取消", + "bulk_select": "选择", + "bulk_select_all": "全选", + "bulk_n_selected": "已选 {n} 项", + "bulk_completed": "已完成 {n} 个任务", + "bulk_archived": "已归档 {n} 个任务", + "completing": "正在完成…", + "interval": "间隔", + "warning": "预警", + "last_performed": "上次执行", + "next_due": "下次到期", + "days_until_due": "距离到期天数", + "avg_duration": "平均耗时", + "trigger": "触发器", + "trigger_type": "触发类型", + "threshold_above": "超过阈值", + "threshold_below": "低于阈值", + "threshold": "阈值", + "counter": "计数器", + "state_change": "状态变化", + "runtime": "运行时间", + "runtime_hours": "目标运行时长 (小时)", + "target_value": "目标值", + "baseline": "基准线", + "target_changes": "目标变化次数", + "for_minutes": "持续 (分钟)", + "time_based": "基于时间", + "sensor_based": "基于传感器", + "manual": "手动", + "one_time": "一次性", + "weekdays": "工作日", + "nth_weekday": "每月第 N 个工作日", + "day_of_month": "每月某天", + "recurrence_on_days": "重复日期", + "recurrence_occurrence": "发生次数", + "recurrence_weekday": "星期", + "recurrence_day": "每月日期 (1–31)", + "recurrence_last_day": "Last day of the month", + "recurrence_business_day": "Business days only (roll back from weekend)", + "recurrence_offset": "Offset (days, ±)", + "recurrence_offset_help": "Shift the date by ±N days, e.g. -2 = two days before.", + "last_day_month": "Last day of month", + "last_business_day_month": "Last business day", + "ord_1": "第 1", + "ord_2": "第 2", + "ord_3": "第 3", + "ord_4": "第 4", + "ord_5": "第 5", + "ord_last": "最后", + "day_word": "日", + "interval_value": "间隔数值", + "interval_unit": "单位", + "unit_days": "天", + "unit_weeks": "周", + "unit_months": "月", + "unit_years": "年", + "due_date": "到期日期", + "cleaning": "清洁", + "inspection": "检查", + "replacement": "更换", + "calibration": "校准", + "service": "保养", + "reading": "读数", + "custom": "自定义", + "history": "历史记录", + "cost": "成本", + "report_button": "报告", + "report_title": "维护报告", + "report_generated": "生成于", + "report_times_done": "完成次数", + "report_total_cost": "总费用", + "report_every": "每 {n} {unit}", + "report_notes": "备注", + "report_col_type": "类型", + "report_col_status": "状态", + "report_col_schedule": "计划", + "duration": "耗时", + "both": "两者", + "trigger_val": "触发值", + "complete_title": "完成: ", + "checklist": "检查清单", + "checklist_steps_optional": "检查步骤 (可选)", + "checklist_placeholder": "清理过滤器\n更换密封圈\n测试压力", + "checklist_help": "每行一个步骤。最多 100 项。", + "err_too_long": "{field}: 太长 (最多 {n} 个字符)", + "err_too_short": "{field}: 太短 (最少 {n} 个字符)", + "err_value_too_high": "{field}: 太大 (最大 {n})", + "err_value_too_low": "{field}: 太小 (最小 {n})", + "err_required": "{field}: 必填项", + "err_wrong_type": "{field}: 类型错误 (预期: {type})", + "err_invalid_choice": "{field}: 无效选项", + "err_invalid_value": "{field}: 无效数值", + "feat_schedule_time": "具体时间调度", + "feat_schedule_time_desc": "任务将在特定时间点变为逾期,而非默认的午夜。", + "schedule_time_optional": "到期时间 (可选, HH:MM)", + "schedule_time_help": "留空 = 午夜 (默认)。基于 HA 时区。", + "at_time": "于", + "notes_optional": "备注 (可选)", + "cost_optional": "成本 (可选)", + "duration_minutes": "耗时 (分钟, 可选)", + "days": "天", + "day": "天", + "today": "今天", + "d_overdue": "天逾期", + "no_tasks": "尚无维护任务。请创建一个维护项开始使用。", + "no_tasks_short": "无任务", + "no_history": "尚无历史记录。", + "show_all": "全部显示", + "cost_duration_chart": "成本与耗时", + "installed": "已安装", + "confirm_delete_object": "确定删除此维护项及其所有任务吗?", + "confirm_delete_task": "确定删除此任务吗?", + "min": "最小", + "max": "最大", + "save": "保存", + "saving": "正在保存…", + "edit_task": "编辑任务", + "new_task": "新建维护任务", + "task_name": "任务名称", + "maintenance_type": "维护类型", + "priority": "优先级", + "labels": "Labels", + "labels_placeholder": "e.g. safety, seasonal, tenant-visible", + "labels_help": "Comma-separated tags for filtering and reporting.", + "priority_low": "低", + "priority_normal": "普通", + "priority_high": "高", + "schedule_type": "计划类型", + "interval_days": "间隔 (天)", + "warning_days": "预警天数", + "earliest_completion_days": "Earliest completion (days before due)", + "earliest_completion_days_help": "Leave empty to allow completing any time. 0 = only on/after the due date.", + "last_performed_optional": "上次执行时间 (可选)", + "interval_anchor": "计划锚点", + "anchor_completion": "从完成日期起算", + "anchor_planned": "从计划日期起算 (无偏差)", + "edit_object": "编辑维护项", + "name": "名称", + "manufacturer_optional": "制造商 (可选)", + "model_optional": "型号 (可选)", + "serial_number_optional": "序列号 (可选)", + "serial_number_label": "序列号", + "documentation_url_label": "手册", + "object_notes_label": "备注", + "sort_due_date": "到期时间", + "sort_object": "维护项名称", + "sort_type": "类型", + "sort_task_name": "任务名称", + "all_objects": "所有维护项", + "tasks_lower": "任务", + "no_tasks_yet": "尚无任务", + "add_first_task": "添加首个任务", + "trigger_configuration": "触发器配置", + "entity_id": "实体 ID", + "comma_separated": "逗号分隔", + "entity_logic": "实体逻辑", + "entity_logic_any": "任一实体触发", + "entity_logic_all": "所有实体均触发", + "entities": "实体", + "attribute_optional": "属性 (可选, 留空 = 状态)", + "use_entity_state": "使用实体状态 (不使用属性)", + "trigger_above": "高于此值触发", + "trigger_below": "低于此值触发", + "for_at_least_minutes": "持续至少 (分钟)", + "safety_interval_days": "安全间隔 (天, 可选)", + "safety_interval": "安全间隔 (可选)", + "delta_mode": "增量模式", + "from_state_optional": "起始状态 (可选)", + "to_state_optional": "目标状态 (可选)", + "documentation_url_optional": "文档链接 (可选)", + "object_notes_optional": "备注 (可选)", + "nfc_tag_id_optional": "NFC 标签 ID (可选)", + "nfc_tags_empty_help": "Home Assistant 中尚未注册任何 NFC 标签。", + "nfc_tags_open_settings": "打开标签设置", + "nfc_tags_refresh": "刷新", + "environmental_entity_optional": "环境传感器 (可选)", + "environmental_entity_helper": "例如:sensor.outdoor_temperature — 根据环境条件自动调整间隔", + "environmental_attribute_optional": "环境属性 (可选)", + "nfc_tag_id": "NFC 标签 ID", + "nfc_linked": "NFC 标签已链接", + "nfc_link_hint": "点击链接 NFC 标签", + "responsible_user": "负责人", + "shared_with": "Shared with (rotation)", + "shared_with_help": "Pick multiple people to share this task; the responsible person rotates on each completion.", + "rotation_strategy": "Rotation", + "rotation_none": "No rotation", + "rotation_round_robin": "Round-robin", + "rotation_least_completed": "Least completed", + "rotation_random": "Random", + "no_user_assigned": "(未分配)", + "all_users": "所有用户", + "my_tasks": "我的任务", + "tab_calendar": "日历", + "cal_no_events": "无维护事项", + "cal_window_7": "7 天", + "cal_window_14": "14 天", + "cal_window_30": "30 天", + "cal_window_365": "1 年", + "cal_every_n_days": "每 {n} 天", + "cal_source_time": "基于时间", + "cal_source_time_adaptive": "基于时间 (自适应)", + "cal_source_sensor": "基于传感器", + "cal_predicted": "预测", + "cal_confidence_high": "高置信度", + "cal_confidence_medium": "中置信度", + "cal_confidence_low": "低置信度", + "budget_monthly": "月度预算", + "budget_yearly": "年度预算", + "groups": "分组", + "new_group": "新建分组", + "edit_group": "编辑分组", + "no_groups": "尚无分组", + "delete_group": "删除分组", + "delete_group_confirm": "确定删除分组 '{name}' 吗?", + "group_select_tasks": "选择任务", + "group_name_required": "名称必填", + "description_optional": "描述 (可选)", + "selected": "已选择", + "loading_chart": "正在加载图表数据...", + "hide_outliers": "隐藏离群值(传感器故障)", + "was_maintenance_needed": "此次维护是否确实需要?", + "feedback_needed": "需要", + "feedback_not_needed": "不需要", + "feedback_not_sure": "不确定", + "suggested_interval": "建议间隔", + "apply_suggestion": "应用建议", + "reanalyze": "重新分析", + "reanalyze_result": "新分析结果", + "reanalyze_insufficient_data": "数据不足,无法生成建议", + "data_points": "个数据点", + "dismiss_suggestion": "忽略", + "confidence_low": "低", + "confidence_medium": "中", + "confidence_high": "高", + "recommended": "推荐", + "seasonal_awareness": "季节性感知", + "edit_seasonal_overrides": "编辑季节性修正系数", + "seasonal_overrides_title": "季节性修正系数 (覆盖)", + "seasonal_overrides_hint": "每月系数 (0.1–5.0)。留空则自动学习。", + "seasonal_override_invalid": "无效数值", + "seasonal_override_range": "系数必须介于 0.1 到 5.0 之间", + "clear_all": "全部清除", + "seasonal_chart_title": "季节性因素", + "seasonal_learned": "自动学习", + "seasonal_manual": "手动设置", + "month_jan": "一月", + "month_feb": "二月", + "month_mar": "三月", + "month_apr": "四月", + "month_may": "五月", + "month_jun": "六月", + "month_jul": "七月", + "month_aug": "八月", + "month_sep": "九月", + "month_oct": "十月", + "month_nov": "十一月", + "month_dec": "十二月", + "sensor_prediction": "传感器预测", + "degradation_trend": "趋势", + "trend_rising": "上升", + "trend_falling": "下降", + "trend_stable": "稳定", + "trend_insufficient_data": "数据不足", + "days_until_threshold": "距离阈值天数", + "threshold_exceeded": "已超过阈值", + "environmental_adjustment": "环境因子", + "sensor_prediction_urgency": "传感器预测约 {days} 天后达到阈值", + "day_short": "天", + "weibull_reliability_curve": "韦伯可靠性曲线", + "weibull_failure_probability": "故障概率", + "weibull_r_squared": "拟合度 R²", + "beta_early_failures": "早期失效", + "beta_random_failures": "随机失效", + "beta_wear_out": "耗损失效", + "beta_highly_predictable": "高度可预测", + "confidence_interval": "置信区间", + "confidence_conservative": "保守", + "confidence_aggressive": "进取", + "current_interval_marker": "当前间隔", + "recommended_marker": "推荐", + "characteristic_life": "特征寿命", + "chart_mini_sparkline": "趋势走势图", + "chart_history": "成本与时长历史", + "chart_seasonal": "12个月季节性因素图表", + "chart_weibull": "韦伯可靠性曲线图表", + "chart_sparkline": "传感器触发值图表", + "days_progress": "天数进度", + "qr_code": "二维码", + "qr_generating": "正在生成二维码…", + "qr_error": "无法生成二维码。", + "qr_error_no_url": "未配置 HA URL。请前往“设置 → 系统 → 网络”设置外部或内部 URL。", + "save_error": "保存失败。请重试。", + "qr_print": "打印", + "qr_download": "下载 SVG", + "qr_action": "扫码后动作", + "qr_action_view": "查看维护信息", + "qr_action_complete": "标记维护为已完成", + "qr_url_mode": "链接类型", + "qr_mode_companion": "Companion App", + "qr_mode_local": "本地 (mDNS)", + "qr_mode_server": "服务器 URL", + "overview": "概览", + "analysis": "分析", + "recent_activities": "近期活动", + "search_notes": "搜索备注", + "avg_cost": "平均成本", + "no_advanced_features": "未启用高级功能", + "no_advanced_features_hint": "请在集成设置中启用“自适应间隔”或“季节性模式”以在此查看分析数据。", + "analysis_not_enough_data": "暂无足够的分析数据。", + "analysis_not_enough_data_hint": "韦伯分析需要至少 5 次完成记录;季节性模式在每月有 6 个以上数据点后可见。", + "analysis_manual_task_hint": "无间隔的手动任务不会生成分析数据。", + "completions": "次完成记录", + "current": "当前", + "shorter": "较短", + "longer": "较长", + "normal": "正常", + "disabled": "已禁用", + "compound_logic": "组合逻辑", + "compound": "组合(多个条件)", + "compound_logic_and": "AND — 所有条件都需满足", + "compound_logic_or": "OR — 任一条件即可", + "compound_help": "将多个传感器条件组合成一个触发器。", + "compound_no_conditions": "还没有条件 — 至少添加一个。", + "compound_add_condition": "添加条件", + "compound_condition": "条件", + "compound_remove_condition": "删除条件", + "card_title": "标题", + "card_show_header": "显示统计信息页眉", + "card_show_actions": "显示操作按钮", + "card_compact": "紧凑模式", + "card_max_items": "最大显示项 (0 = 全部)", + "card_filter_status": "按状态过滤", + "card_filter_status_help": "留空则显示所有状态。", + "card_filter_objects": "按维护项过滤", + "card_filter_objects_help": "留空则显示所有维护项。", + "card_filter_entities": "按实体过滤 (entity_ids)", + "card_filter_entities_help": "选择该集成的传感器或二进制传感器实体。留空则显示全部。", + "card_loading_objects": "正在加载维护项…", + "card_load_error": "无法加载维护项 — 请检查 WebSocket 连接。", + "card_no_tasks_title": "暂无维护任务", + "card_no_tasks_cta": "→ 请前往维护面板创建", + "no_objects": "暂无维护项。", + "action_error": "操作失败。请重试。", + "area_id_optional": "区域 (可选)", + "installation_date_optional": "安装日期 (可选)", + "warranty_expiry_optional": "保修到期 (可选)", + "warranty": "保修", + "warranty_valid_until": "有效期至 {date}", + "warranty_expires_in": "{days} 天后到期", + "warranty_expired": "已过期", + "cal_past_windows": "过去的窗口", + "cal_forward_windows": "未来的窗口", + "history_edit_title": "编辑历史记录", + "history_edit_timestamp": "时间戳", + "manufacturer": "制造商", + "model": "型号", + "area": "区域", + "actions": "操作", + "view_mode_label": "视图", + "view_cards": "卡片视图", + "view_table": "表格视图", + "objects_table_columns_label": "对象表格列", + "objects_table_columns_hint": "选择对象表格视图中显示的列。", + "custom_icon_optional": "图标 (可选,例如 mdi:wrench)", + "task_enabled": "任务已启用", + "skip_reason_prompt": "跳过此任务?", + "reason_optional": "原因 (可选)", + "reset_date_prompt": "标记任务为已执行?", + "reset_date_optional": "最后执行日期 (可选,默认为今天)", + "notes_label": "备注", + "documentation_label": "文档", + "no_nfc_tag": "— 无标签 —", + "dashboard": "仪表盘", + "tab_today": "今天", + "palette_placeholder": "搜索对象和任务…", + "palette_no_results": "无匹配", + "palette_hint": "↑↓ 导航 · Enter 打开 · Esc 关闭", + "today_all_caught_up": "全部完成!本周没有待办。", + "today_overdue": "逾期", + "today_due_today": "今天到期", + "today_this_week": "本周", + "settings": "设置", + "settings_features": "高级功能", + "settings_features_desc": "启用或禁用高级功能。禁用将从界面隐藏相关功能,但不会删除数据。", + "feat_adaptive": "自适应计划", + "feat_adaptive_desc": "根据维护历史学习最佳间隔", + "feat_predictions": "传感器预测", + "feat_predictions_desc": "根据传感器损耗趋势预测触发日期", + "feat_seasonal": "季节性调整", + "feat_seasonal_desc": "根据季节性模式自动调整间隔", + "feat_environmental": "环境关联", + "feat_environmental_desc": "将维护间隔与温度或湿度关联", + "feat_budget": "预算追踪", + "feat_budget_desc": "追踪月度及年度维护支出", + "feat_groups": "任务组", + "feat_groups_desc": "将任务组织进逻辑组", + "feat_checklists": "检查清单", + "feat_checklists_desc": "为任务完成提供多步骤操作流程", + "settings_general": "常规", + "settings_default_warning": "默认预警天数", + "settings_panel_enabled": "侧边栏面板", + "settings_panel_title": "侧边栏面板标题", + "settings_notifications": "通知", + "settings_notify_service": "通知服务", + "test_notification": "测试通知", + "send_test": "发送测试", + "testing": "正在发送…", + "test_notification_success": "测试通知已发送", + "test_notification_failed": "测试通知发送失败", + "settings_notify_due_soon": "到期前通知提醒", + "settings_notify_overdue": "超期后通知提醒", + "settings_notify_triggered": "触发时通知提醒", + "settings_interval_hours": "重复提醒间隔 (小时,0 = 仅一次)", + "settings_quiet_hours": "静默时段", + "settings_quiet_start": "开始时间", + "settings_quiet_end": "结束时间", + "settings_max_per_day": "每日最高通知次数 (0 = 不限)", + "settings_bundling": "合并通知", + "settings_bundle_threshold": "合并阈值", + "settings_reminder_leads": "Extra reminders (days before due)", + "settings_reminder_leads_hint": "Comma-separated lead times, e.g. 14, 3, 0 — one extra reminder fires on each matching day. Empty = off.", + "settings_actions": "移动端操作按钮", + "settings_action_complete": "显示“完成”按钮", + "settings_action_skip": "显示“跳过”按钮", + "settings_action_snooze": "显示“稍后”按钮", + "settings_weekly_digest": "每周摘要", + "settings_weekly_digest_hint": "当有待办任务时,周一早上发送一条汇总通知。", + "settings_warranty_reminder": "Warranty expiry reminder", + "settings_warranty_reminder_days": "Days before expiry", + "settings_warranty_reminder_hint": "Notify once when an object's warranty is this many days from expiring.", + "settings_snooze_hours": "稍后提醒间隔 (小时)", + "settings_budget": "预算", + "settings_currency": "货币单位", + "settings_budget_monthly": "月度预算", + "settings_budget_yearly": "年度预算", + "settings_budget_alerts": "预算警报", + "settings_budget_threshold": "警报阈值 (%)", + "settings_import_export": "导入 / 导出", + "settings_export_json": "导出 JSON", + "settings_export_yaml": "导出 YAML", + "settings_export_csv": "导出 CSV", + "settings_import_csv": "导入 CSV", + "settings_import_placeholder": "在此粘贴 JSON 或 CSV 内容…", + "settings_import_btn": "导入", + "settings_import_success": "成功导入 {count} 个维护项。", + "settings_export_success": "导出已下载。", + "settings_saved": "设置已保存。", + "settings_include_history": "包含历史记录", + "sort_alphabetical": "字母排序", + "sort_due_soonest": "最近到期", + "sort_task_count": "任务数量", + "sort_area": "区域", + "sort_assigned_user": "负责人", + "sort_group": "任务组", + "groupby_none": "不分组", + "groupby_area": "按区域分组", + "groupby_group": "按分组", + "groupby_user": "按负责人", + "filter_label": "过滤器", + "user_label": "用户", + "sort_label": "排序", + "group_by_label": "分组依据", + "state_value_help": "使用 HA 状态值(通常为小写,例如 \"on\"/\"off\")。保存时大小写会自动规范化。", + "target_changes_help": "触发前需要满足的状态转换次数(默认为 1)。", + "qr_print_title": "打印二维码", + "qr_print_desc": "生成一张包含多个二维码的可打印页面,方便裁剪并贴在设备上。", + "qr_print_load": "加载设备", + "qr_print_filter": "过滤器", + "qr_print_objects": "设备", + "qr_print_actions": "动作", + "qr_print_url_mode": "链接类型", + "qr_print_estimate": "预计二维码数量", + "qr_print_over_limit": "上限为 200,请缩小过滤范围", + "qr_print_generate": "生成二维码", + "qr_print_generating": "正在生成…", + "qr_print_ready": "二维码已就绪", + "qr_print_print_button": "打印", + "qr_print_empty": "无内容可生成", + "qr_action_skip": "跳过", + "vacation_title": "度假模式", + "vacation_active": "已激活", + "vacation_ended": "已结束", + "vacation_desc": "计划假期:在此时段及设定的缓冲天数内将暂停通知。您可以为特定任务设置例外。", + "vacation_enable": "开启度假模式", + "vacation_start": "开始日期", + "vacation_end": "结束日期", + "vacation_buffer": "缓冲(天)", + "vacation_exempt_title": "度假期间仍保持通知的任务", + "vacation_exempt_desc": "选择在假期中仍需接收通知的任务(例如关键的水池维护)。", + "vacation_load_tasks": "加载任务", + "vacation_preview_btn": "显示预览", + "vacation_preview_affected": "受影响的任务", + "vacation_event_due_soon": "即将到期", + "vacation_event_overdue": "将超期", + "vacation_event_triggered_est": "可能触发传感器", + "vacation_sensor_based": "(基于传感器)", + "vacation_action_notify": "依然通知", + "vacation_action_unsilence": "恢复静默", + "vacation_marked_complete": "已标记为完成", + "vacation_marked_skip": "已跳过", + "vacation_end_now": "立即结束度假", + "add": "添加", + "show_stats": "显示统计与图表", + "hide_stats": "隐藏统计", + "adaptive_no_data": "尚无足够的完成记录用于自适应分析。请完成此任务几次,以解锁间隔建议和可靠性图表。", + "suggestion_applied": "已应用建议间隔", + "vacation_mode": "度假模式", + "vacation_status_active": "当前激活", + "vacation_status_scheduled": "已计划", + "vacation_status_inactive": "未激活", + "vacation_end_now_confirm": "确定立即结束度假吗?", + "vacation_exempt_count": "例外项", + "vacation_advanced": "高级选项…", + "vacation_open_panel": "在面板中打开", + "enable": "启用", + "saved": "已保存", + "budget_monthly_set": "设置月度预算", + "budget_yearly_set": "设置年度预算", + "budget_advanced": "货币、预警…", + "budget_open_panel": "在面板中打开", + "groups_empty": "尚无分组。", + "group_new_placeholder": "添加分组…", + "group_delete_confirm": "确定删除分组 \"{name}\" 吗?", + "groups_manage_tasks": "管理任务分配…", + "groups_open_panel": "在面板中打开", + "unassigned": "未分配", + "no_area": "无区域", + "has_overdue": "有超期任务", + "object": "设备", + "settings_panel_access": "面板访问权限", + "settings_panel_access_desc": "管理员始终拥有完整访问权限。要将创建、编辑和删除权限委派给特定的普通用户,请开启此项并在下方选择他们 — 其他用户将仅能看到“完成”和“跳过”。", + "settings_operator_write": "允许所选用户创建、编辑和删除", + "settings_operator_write_desc": "关闭时:仅管理员可修改内容。开启时:下方所选用户也获得完整访问权限。", + "no_non_admin_users": "未找到普通用户。请在“设置 -> 人员”中添加。", + "owner_label": "所有者", + "feat_completion_actions": "完成动作", + "feat_completion_actions_desc": "为每个任务配置完成时的 HA 动作 + 预设值的快速完成二维码。", + "on_complete_action_title": "完成时:触发 HA 动作(可选)", + "on_complete_action_desc": "任务完成时调用 HA 服务 — 例如重置设备上的物理计数器。", + "on_complete_action_service": "服务", + "on_complete_action_target": "目标实体", + "on_complete_action_target_hint": "注意:实体的域必须与服务匹配 — 例如 'button.press' 仅适用于 button.* 实体,'counter.increment' 仅适用于 counter.* 实体等。如果不匹配,动作将静默失败。", + "on_complete_action_data": "数据(JSON, 可选)", + "on_complete_action_test": "验证配置", + "on_complete_action_test_success": "✓ 配置有效(动作将在任务完成时触发)", + "on_complete_action_test_failed": "失败", + "quick_complete_defaults_title": "快速完成默认值(用于二维码扫描, 可选)", + "quick_complete_defaults_desc": "扫描快速完成二维码时使用的预设值。如果不设置,二维码将打开完成对话框。", + "quick_complete_defaults_notes": "备注", + "quick_complete_defaults_cost": "费用", + "quick_complete_defaults_duration": "耗时(分钟)", + "quick_complete_defaults_feedback_none": "无反馈", + "quick_complete_defaults_feedback_needed": "是必要的", + "quick_complete_defaults_feedback_not_needed": "不必要的", + "quick_complete_success": "已快速标记为完成", + "show_all_objects": "显示所有设备", + "show_all_tasks": "清除过滤 — 显示所有任务", + "filter_to_overdue": "仅显示超期任务", + "filter_to_due_soon": "仅显示即将到期任务", + "filter_to_triggered": "仅显示已触发任务", + "open_task": "打开任务", + "show_details": "显示历史与统计", + "hide_details": "隐藏详情", + "history_empty": "尚无历史记录。", + "history_edit_button": "编辑条目", + "total_cost": "总计费用", + "times_performed": "执行次数", + "older_entries": "更早的条目", + "open_in_panel": "在维护面板中打开", + "skip_reason": "跳过原因(可选)", + "reset_to_date": "重置最后执行日期为", + "delete_task_confirm": "确定删除此任务及其历史记录吗?", + "delete_object_confirm": "确定删除此设备及其所有任务吗?", + "loading": "加载中…", + "archive": "归档", + "undo": "撤销", + "task_archived": "任务已归档", + "object_archived": "对象已归档", + "unarchive": "取消归档", + "archived": "已归档", + "show_archived": "显示已归档", + "hide_archived": "隐藏已归档", + "confirm_archive_object": "归档此对象及其任务?历史记录将保留,以后可恢复。", + "settings_archive": "归档与保留", + "settings_archive_desc": "将已完成的一次性任务归档而不删除。已归档项目会被隐藏且不再活动,但保留其历史记录和费用。", + "settings_archive_oneoff_days": "已完成的一次性任务在多少天后自动归档(0 = 关闭)", + "settings_delete_archived_oneoff_days": "已归档的一次性任务在多少天后自动删除(0 = 永不)", + "archive_object": "归档对象", + "unarchive_object": "取消归档对象", + "documents": "文档", + "documents_empty": "暂无文档。", + "doc_upload": "上传文件", + "doc_uploading": "上传中…", + "doc_add_link": "添加链接", + "doc_link_url": "网址 (https://…)", + "doc_link_title": "标题(可选)", + "doc_open": "打开", + "doc_delete_confirm": "删除“{name}”?", + "doc_too_large": "文件过大(最大 25 MB)。", + "doc_upload_failed": "上传失败。", + "completion_photo_optional": "Completion photo (optional)", + "add_photo": "Add photo", + "uploading": "Uploading…", + "remove": "Remove", + "doc_deduped": "已在别处存储——共享,不占用额外空间。", + "doc_dup_in_object": "此文件已附加到该对象。", + "doc_link_invalid": "仅允许 http/https 链接。", + "doc_cat_manual": "手册", + "doc_cat_warranty": "保修", + "doc_cat_invoice": "发票", + "doc_cat_spare_parts": "备件", + "doc_cat_photo": "照片", + "doc_cat_other": "其他", + "doc_link_badge": "链接", + "doc_storage_title": "文档存储", + "doc_storage_saved": "通过去重节省", + "doc_storage_refresh": "刷新", + "doc_download": "下载", + "doc_close": "关闭", + "doc_camera": "拍照", + "doc_drop_hint": "将文件拖放到此处", + "doc_task_none": "没有与此任务关联的文档。", + "doc_link_existing": "关联文档…", + "doc_attach": "关联", + "doc_unlink": "取消关联", + "doc_page": "页码", + "chart_range_7d": "7天", + "chart_range_30d": "30天", + "chart_range_90d": "90天", + "chart_range_1y": "1年", + "chart_since_service": "自上次维护以来", + "chart_no_stats": "此实体没有长期统计数据 — 仅显示维护记录中的数值", + "auto_complete_on_recovery": "传感器恢复时自动完成", + "auto_complete_on_recovery_help": "当触发条件自行解除时(如已加盐、已更换滤芯),记录一次完成并更新上次执行日期。", + "doc_search": "搜索文档…", + "doc_search_none": "没有匹配的文档", + "link_device_optional": "关联到现有设备(可选)", + "parent_object_optional": "父对象(可选)", + "parent_none": "(无父对象)", + "paused": "已暂停", + "pause_object": "暂停", + "resume_object": "恢复", + "pause_until_prompt": "冻结此对象的日程——在恢复之前不会有任何到期或通知。可选设置自动恢复日期。", + "pause_until_label": "恢复日期(可选)", + "object_paused": "对象已暂停", + "object_resumed": "对象已恢复——日程已重新开始", + "object_paused_badge": "已暂停", + "paused_until_label": "至", + "replace_object": "更换…", + "replace_object_prompt": "让此对象退役并创建继任者。历史和费用留在旧对象的归档中;任务和文档转移到新对象,计数器重新开始。", + "replace_name_label": "继任者名称", + "object_replaced": "对象已更换——已创建继任者", + "reading_unit_label": "读数单位(如 kWh、m³)", + "reading_unit_help": "完成此任务时显示在记录值旁边。", + "reading_value_label": "读数值", + "reading_label": "读数", + "settings_templates_label": "模板库", + "settings_templates_hint": "取消勾选你永远不需要的模板——它们会从\"从模板\"选择器(面板和配置流程)中消失。其他一切不变;可随时重新启用。", + "worksheet": "工作单", + "worksheet_scan_view": "扫码打开任务", + "worksheet_scan_complete": "扫码完成", + "worksheet_manual_excerpt": "手册摘录", + "worksheet_pages": "页", + "worksheet_printed": "打印于", + "worksheet_never": "从未" +} diff --git a/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js b/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js new file mode 100644 index 0000000..af2d2bb --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/maintenance-calendar-card.js @@ -0,0 +1,1514 @@ +var Xe=Object.defineProperty;var Ze=Object.getOwnPropertyDescriptor;var v=(n,e,t,o)=>{for(var r=o>1?void 0:o?Ze(e,t):e,a=n.length-1,i;a>=0;a--)(i=n[a])&&(r=(o?i(e,t,r):i(r))||r);return o&&r&&Xe(e,t,r),r};var K=globalThis,J=K.ShadowRoot&&(K.ShadyCSS===void 0||K.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,ie=Symbol(),ve=new WeakMap,U=class{constructor(e,t,o){if(this._$cssResult$=!0,o!==ie)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o,t=this.t;if(J&&e===void 0){let o=t!==void 0&&t.length===1;o&&(e=ve.get(t)),e===void 0&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),o&&ve.set(t,e))}return e}toString(){return this.cssText}},xe=n=>new U(typeof n=="string"?n:n+"",void 0,ie),S=(n,...e)=>{let t=n.length===1?n[0]:e.reduce((o,r,a)=>o+(i=>{if(i._$cssResult$===!0)return i.cssText;if(typeof i=="number")return i;throw Error("Value passed to 'css' function must be a 'css' function result: "+i+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(r)+n[a+1],n[0]);return new U(t,n,ie)},we=(n,e)=>{if(J)n.adoptedStyleSheets=e.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(let t of e){let o=document.createElement("style"),r=K.litNonce;r!==void 0&&o.setAttribute("nonce",r),o.textContent=t.cssText,n.appendChild(o)}},se=J?n=>n:n=>n instanceof CSSStyleSheet?(e=>{let t="";for(let o of e.cssRules)t+=o.cssText;return xe(t)})(n):n;var{is:et,defineProperty:tt,getOwnPropertyDescriptor:ot,getOwnPropertyNames:rt,getOwnPropertySymbols:nt,getPrototypeOf:at}=Object,X=globalThis,ke=X.trustedTypes,it=ke?ke.emptyScript:"",st=X.reactiveElementPolyfillSupport,H=(n,e)=>n,q={toAttribute(n,e){switch(e){case Boolean:n=n?it:null;break;case Object:case Array:n=n==null?n:JSON.stringify(n)}return n},fromAttribute(n,e){let t=n;switch(e){case Boolean:t=n!==null;break;case Number:t=n===null?null:Number(n);break;case Object:case Array:try{t=JSON.parse(n)}catch{t=null}}return t}},Z=(n,e)=>!et(n,e),$e={attribute:!0,type:String,converter:q,reflect:!1,useDefault:!1,hasChanged:Z};Symbol.metadata??=Symbol("metadata"),X.litPropertyMetadata??=new WeakMap;var x=class extends HTMLElement{static addInitializer(e){this._$Ei(),(this.l??=[]).push(e)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(e,t=$e){if(t.state&&(t.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(e)&&((t=Object.create(t)).wrapped=!0),this.elementProperties.set(e,t),!t.noAccessor){let o=Symbol(),r=this.getPropertyDescriptor(e,o,t);r!==void 0&&tt(this.prototype,e,r)}}static getPropertyDescriptor(e,t,o){let{get:r,set:a}=ot(this.prototype,e)??{get(){return this[t]},set(i){this[t]=i}};return{get:r,set(i){let s=r?.call(this);a?.call(this,i),this.requestUpdate(e,s,o)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)??$e}static _$Ei(){if(this.hasOwnProperty(H("elementProperties")))return;let e=at(this);e.finalize(),e.l!==void 0&&(this.l=[...e.l]),this.elementProperties=new Map(e.elementProperties)}static finalize(){if(this.hasOwnProperty(H("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(H("properties"))){let t=this.properties,o=[...rt(t),...nt(t)];for(let r of o)this.createProperty(r,t[r])}let e=this[Symbol.metadata];if(e!==null){let t=litPropertyMetadata.get(e);if(t!==void 0)for(let[o,r]of t)this.elementProperties.set(o,r)}this._$Eh=new Map;for(let[t,o]of this.elementProperties){let r=this._$Eu(t,o);r!==void 0&&this._$Eh.set(r,t)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(e){let t=[];if(Array.isArray(e)){let o=new Set(e.flat(1/0).reverse());for(let r of o)t.unshift(se(r))}else e!==void 0&&t.push(se(e));return t}static _$Eu(e,t){let o=t.attribute;return o===!1?void 0:typeof o=="string"?o:typeof e=="string"?e.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(e=>e(this))}addController(e){(this._$EO??=new Set).add(e),this.renderRoot!==void 0&&this.isConnected&&e.hostConnected?.()}removeController(e){this._$EO?.delete(e)}_$E_(){let e=new Map,t=this.constructor.elementProperties;for(let o of t.keys())this.hasOwnProperty(o)&&(e.set(o,this[o]),delete this[o]);e.size>0&&(this._$Ep=e)}createRenderRoot(){let e=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return we(e,this.constructor.elementStyles),e}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(e=>e.hostConnected?.())}enableUpdating(e){}disconnectedCallback(){this._$EO?.forEach(e=>e.hostDisconnected?.())}attributeChangedCallback(e,t,o){this._$AK(e,o)}_$ET(e,t){let o=this.constructor.elementProperties.get(e),r=this.constructor._$Eu(e,o);if(r!==void 0&&o.reflect===!0){let a=(o.converter?.toAttribute!==void 0?o.converter:q).toAttribute(t,o.type);this._$Em=e,a==null?this.removeAttribute(r):this.setAttribute(r,a),this._$Em=null}}_$AK(e,t){let o=this.constructor,r=o._$Eh.get(e);if(r!==void 0&&this._$Em!==r){let a=o.getPropertyOptions(r),i=typeof a.converter=="function"?{fromAttribute:a.converter}:a.converter?.fromAttribute!==void 0?a.converter:q;this._$Em=r;let s=i.fromAttribute(t,a.type);this[r]=s??this._$Ej?.get(r)??s,this._$Em=null}}requestUpdate(e,t,o,r=!1,a){if(e!==void 0){let i=this.constructor;if(r===!1&&(a=this[e]),o??=i.getPropertyOptions(e),!((o.hasChanged??Z)(a,t)||o.useDefault&&o.reflect&&a===this._$Ej?.get(e)&&!this.hasAttribute(i._$Eu(e,o))))return;this.C(e,t,o)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(e,t,{useDefault:o,reflect:r,wrapped:a},i){o&&!(this._$Ej??=new Map).has(e)&&(this._$Ej.set(e,i??t??this[e]),a!==!0||i!==void 0)||(this._$AL.has(e)||(this.hasUpdated||o||(t=void 0),this._$AL.set(e,t)),r===!0&&this._$Em!==e&&(this._$Eq??=new Set).add(e))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}let e=this.scheduleUpdate();return e!=null&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[r,a]of this._$Ep)this[r]=a;this._$Ep=void 0}let o=this.constructor.elementProperties;if(o.size>0)for(let[r,a]of o){let{wrapped:i}=a,s=this[r];i!==!0||this._$AL.has(r)||s===void 0||this.C(r,void 0,a,s)}}let e=!1,t=this._$AL;try{e=this.shouldUpdate(t),e?(this.willUpdate(t),this._$EO?.forEach(o=>o.hostUpdate?.()),this.update(t)):this._$EM()}catch(o){throw e=!1,this._$EM(),o}e&&this._$AE(t)}willUpdate(e){}_$AE(e){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(e){return!0}update(e){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(e){}firstUpdated(e){}};x.elementStyles=[],x.shadowRootOptions={mode:"open"},x[H("elementProperties")]=new Map,x[H("finalized")]=new Map,st?.({ReactiveElement:x}),(X.reactiveElementVersions??=[]).push("2.1.2");var he=globalThis,Se=n=>n,ee=he.trustedTypes,Ae=ee?ee.createPolicy("lit-html",{createHTML:n=>n}):void 0,Ne="$lit$",A=`lit$${Math.random().toFixed(9).slice(2)}$`,Re="?"+A,lt=`<${Re}>`,j=document,F=()=>j.createComment(""),B=n=>n===null||typeof n!="object"&&typeof n!="function",ge=Array.isArray,ct=n=>ge(n)||typeof n?.[Symbol.iterator]=="function",le=`[ +\f\r]`,I=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Ce=/-->/g,Ee=/>/g,C=RegExp(`>|${le}(?:([^\\s"'>=/]+)(${le}*=${le}*(?:[^ +\f\r"'\`<>=]|("|')|))|$)`,"g"),je=/'/g,De=/"/g,ze=/^(?:script|style|textarea|title)$/i,me=n=>(e,...t)=>({_$litType$:n,strings:e,values:t}),m=me(1),Tt=me(2),Nt=me(3),D=Symbol.for("lit-noChange"),g=Symbol.for("lit-nothing"),Te=new WeakMap,E=j.createTreeWalker(j,129);function Pe(n,e){if(!ge(n)||!n.hasOwnProperty("raw"))throw Error("invalid template strings array");return Ae!==void 0?Ae.createHTML(e):e}var dt=(n,e)=>{let t=n.length-1,o=[],r,a=e===2?"":e===3?"":"",i=I;for(let s=0;s"?(i=r??I,d=-1):u[1]===void 0?d=-2:(d=i.lastIndex-u[2].length,p=u[1],i=u[3]===void 0?C:u[3]==='"'?De:je):i===De||i===je?i=C:i===Ce||i===Ee?i=I:(i=C,r=void 0);let h=i===C&&n[s+1].startsWith("/>")?" ":"";a+=i===I?c+lt:d>=0?(o.push(p),c.slice(0,d)+Ne+c.slice(d)+A+h):c+A+(d===-2?s:h)}return[Pe(n,a+(n[t]||"")+(e===2?"":e===3?"":"")),o]},W=class n{constructor({strings:e,_$litType$:t},o){let r;this.parts=[];let a=0,i=0,s=e.length-1,c=this.parts,[p,u]=dt(e,t);if(this.el=n.createElement(p,o),E.currentNode=this.el.content,t===2||t===3){let d=this.el.content.firstChild;d.replaceWith(...d.childNodes)}for(;(r=E.nextNode())!==null&&c.length0){r.textContent=ee?ee.emptyScript:"";for(let h=0;h<_;h++)r.append(d[h],F()),E.nextNode(),c.push({type:2,index:++a});r.append(d[_],F())}}}else if(r.nodeType===8)if(r.data===Re)c.push({type:2,index:a});else{let d=-1;for(;(d=r.data.indexOf(A,d+1))!==-1;)c.push({type:7,index:a}),d+=A.length-1}a++}}static createElement(e,t){let o=j.createElement("template");return o.innerHTML=e,o}};function z(n,e,t=n,o){if(e===D)return e;let r=o!==void 0?t._$Co?.[o]:t._$Cl,a=B(e)?void 0:e._$litDirective$;return r?.constructor!==a&&(r?._$AO?.(!1),a===void 0?r=void 0:(r=new a(n),r._$AT(n,t,o)),o!==void 0?(t._$Co??=[])[o]=r:t._$Cl=r),r!==void 0&&(e=z(n,r._$AS(n,e.values),r,o)),e}var ce=class{constructor(e,t){this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=t}get parentNode(){return this._$AM.parentNode}get _$AU(){return this._$AM._$AU}u(e){let{el:{content:t},parts:o}=this._$AD,r=(e?.creationScope??j).importNode(t,!0);E.currentNode=r;let a=E.nextNode(),i=0,s=0,c=o[0];for(;c!==void 0;){if(i===c.index){let p;c.type===2?p=new G(a,a.nextSibling,this,e):c.type===1?p=new c.ctor(a,c.name,c.strings,this,e):c.type===6&&(p=new _e(a,this,e)),this._$AV.push(p),c=o[++s]}i!==c?.index&&(a=E.nextNode(),i++)}return E.currentNode=j,r}p(e){let t=0;for(let o of this._$AV)o!==void 0&&(o.strings!==void 0?(o._$AI(e,o,t),t+=o.strings.length-2):o._$AI(e[t])),t++}},G=class n{get _$AU(){return this._$AM?._$AU??this._$Cv}constructor(e,t,o,r){this.type=2,this._$AH=g,this._$AN=void 0,this._$AA=e,this._$AB=t,this._$AM=o,this.options=r,this._$Cv=r?.isConnected??!0}get parentNode(){let e=this._$AA.parentNode,t=this._$AM;return t!==void 0&&e?.nodeType===11&&(e=t.parentNode),e}get startNode(){return this._$AA}get endNode(){return this._$AB}_$AI(e,t=this){e=z(this,e,t),B(e)?e===g||e==null||e===""?(this._$AH!==g&&this._$AR(),this._$AH=g):e!==this._$AH&&e!==D&&this._(e):e._$litType$!==void 0?this.$(e):e.nodeType!==void 0?this.T(e):ct(e)?this.k(e):this._(e)}O(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}T(e){this._$AH!==e&&(this._$AR(),this._$AH=this.O(e))}_(e){this._$AH!==g&&B(this._$AH)?this._$AA.nextSibling.data=e:this.T(j.createTextNode(e)),this._$AH=e}$(e){let{values:t,_$litType$:o}=e,r=typeof o=="number"?this._$AC(e):(o.el===void 0&&(o.el=W.createElement(Pe(o.h,o.h[0]),this.options)),o);if(this._$AH?._$AD===r)this._$AH.p(t);else{let a=new ce(r,this),i=a.u(this.options);a.p(t),this.T(i),this._$AH=a}}_$AC(e){let t=Te.get(e.strings);return t===void 0&&Te.set(e.strings,t=new W(e)),t}k(e){ge(this._$AH)||(this._$AH=[],this._$AR());let t=this._$AH,o,r=0;for(let a of e)r===t.length?t.push(o=new n(this.O(F()),this.O(F()),this,this.options)):o=t[r],o._$AI(a),r++;r2||o[0]!==""||o[1]!==""?(this._$AH=Array(o.length-1).fill(new String),this.strings=o):this._$AH=g}_$AI(e,t=this,o,r){let a=this.strings,i=!1;if(a===void 0)e=z(this,e,t,0),i=!B(e)||e!==this._$AH&&e!==D,i&&(this._$AH=e);else{let s=e,c,p;for(e=a[0],c=0;c{let o=t?.renderBefore??e,r=o._$litPart$;if(r===void 0){let a=t?.renderBefore??null;o._$litPart$=r=new G(e.insertBefore(F(),a),a,void 0,t??{})}return r._$AI(n),r};var fe=globalThis,w=class extends x{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let e=super.createRenderRoot();return this.renderOptions.renderBefore??=e.firstChild,e}update(e){let t=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(e),this._$Do=Oe(t,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return D}};w._$litElement$=!0,w.finalized=!0,fe.litElementHydrateSupport?.({LitElement:w});var ut=fe.litElementPolyfillSupport;ut?.({LitElement:w});(fe.litElementVersions??=[]).push("4.2.2");var _t={attribute:!0,type:String,converter:q,reflect:!1,hasChanged:Z},ht=(n=_t,e,t)=>{let{kind:o,metadata:r}=t,a=globalThis.litPropertyMetadata.get(r);if(a===void 0&&globalThis.litPropertyMetadata.set(r,a=new Map),o==="setter"&&((n=Object.create(n)).wrapped=!0),a.set(t.name,n),o==="accessor"){let{name:i}=t;return{set(s){let c=e.get.call(this);e.set.call(this,s),this.requestUpdate(i,c,n,!0,s)},init(s){return s!==void 0&&this.C(i,void 0,n,s),s}}}if(o==="setter"){let{name:i}=t;return function(s){let c=this[i];e.call(this,s),this.requestUpdate(i,c,n,!0,s)}}throw Error("Unsupported decorator location: "+o)};function V(n){return(e,t)=>typeof t=="object"?ht(n,e,t):((o,r,a)=>{let i=r.hasOwnProperty(a);return r.constructor.createProperty(a,o),i?Object.getOwnPropertyDescriptor(r,a):void 0})(n,e,t)}function k(n){return V({...n,state:!0,attribute:!1})}var gt={days:1,weeks:7,months:30.4368,years:365.25};function Le(n,e){return!n||n<=0?0:n*(gt[e||"days"]??1)}var Me=5;function Y(n){let e=n.getFullYear(),t=String(n.getMonth()+1).padStart(2,"0"),o=String(n.getDate()).padStart(2,"0");return`${e}-${t}-${o}`}function mt(n,e){let t=[];for(let o=0;ot.cost).filter(t=>typeof t=="number");return e.length===0?null:e.reduce((t,o)=>t+o,0)/e.length}function yt(n){let{windowStart:e,windowEnd:t,task:o,entryId:r,objectName:a}=n,i=[],s=(d,_)=>({date:d,entry_id:r,task_id:o.id,task_name:o.name,object_name:a,status:_&&(o.status==="overdue"||o.status==="triggered")?"ok":o.status,days_until_due:_?null:o.days_until_due??null,projected:_,schedule_type:o.schedule_type,interval_days:o.interval_days??null,interval_unit:o.interval_unit??null,responsible_user_id:o.responsible_user_id??null,avg_cost:ft(o.history),adaptive_enabled:!!o.adaptive_config?.enabled,prediction_confidence:o.threshold_prediction_confidence??null}),c=Math.max(1,Math.round(Le(o.interval_days,o.interval_unit)));if(o.status==="overdue"||o.status==="triggered"){if(i.push(s(e,!1)),o.schedule_type==="time_based"&&o.interval_days&&o.interval_days>0){let d=oe(e,c),_=1;for(;d<=t&&_=e&&u<=t)i.push(s(u,!1));else if(u>t)return i;if(o.schedule_type==="time_based"&&o.interval_days&&o.interval_days>0){let d=oe(u,c),_=i.length;for(;d<=t&&_=e&&(i.push(s(d,!0)),_++),d=oe(d,c)}return i}var Ue={overdue:0,triggered:1,due_soon:2,ok:3};function He(n,e,t,o=null){let r=mt(e,t),a=r[0],i=r[r.length-1],s=[];for(let p of n){let u=p.object?.name||"",d=p.entry_id,_=p.tasks||[];for(let h of _){if(o&&h.responsible_user_id!==o||h.enabled===!1)continue;let y=yt({windowStart:a,windowEnd:i,task:h,entryId:d,objectName:u});s.push(...y)}}let c=new Map;for(let p of r)c.set(p,[]);for(let p of s){let u=c.get(p.date);u&&u.push(p)}for(let[,p]of c)p.sort((u,d)=>{let _=Ue[u.status]??99,h=Ue[d.status]??99;if(_!==h)return _-h;if(u.projected!==d.projected)return u.projected?1:-1;let y=u.object_name.localeCompare(d.object_name);return y!==0?y:u.task_name.localeCompare(d.task_name)});return r.map(p=>({date:p,events:c.get(p)??[]}))}var bt={completed:"ok",reset:"ok",skipped:"due_soon",triggered:"triggered",trigger_replaced:"triggered",trigger_removed:"ok"};function vt(n,e){let t=[];for(let o=e-1;o>=0;o--){let r=new Date(n);r.setDate(r.getDate()-o),r.setHours(0,0,0,0),t.push(Y(r))}return t}function qe(n,e,t,o=null){let r=vt(e,t),a=r[0],i=r[r.length-1],s=new Map;for(let p of r)s.set(p,[]);for(let p of n){let u=p.object?.name||"",d=p.entry_id,_=p.tasks||[];for(let h of _){if(o&&h.responsible_user_id!==o)continue;let y=h.history||[];for(let l of y){if(typeof l?.timestamp!="string")continue;let $=l.timestamp.slice(0,10);if($i)continue;let N=s.get($);if(!N)continue;let R=l.type??"completed";N.push({date:$,entry_id:d,task_id:h.id,task_name:h.name,object_name:u,status:bt[R]??"ok",days_until_due:null,projected:!1,schedule_type:h.schedule_type,interval_days:h.interval_days??null,responsible_user_id:h.responsible_user_id??null,avg_cost:typeof l.cost=="number"?l.cost:null,adaptive_enabled:!!h.adaptive_config?.enabled,prediction_confidence:null,history_timestamp:l.timestamp,history_type:R,history_cost:typeof l.cost=="number"?l.cost:null,history_notes:typeof l.notes=="string"?l.notes:null,history_duration:typeof l.duration=="number"?l.duration:null})}}}let c={completed:0,reset:1,skipped:2,triggered:3,trigger_replaced:4};for(let[,p]of s)p.sort((u,d)=>{let _=c[u.history_type??""]??99,h=c[d.history_type??""]??99;if(_!==h)return _-h;let y=u.object_name.localeCompare(d.object_name);return y!==0?y:u.task_name.localeCompare(d.task_name)});return r.map(p=>({date:p,events:s.get(p)??[]}))}var Ie=S` + .cal-controls { + display: flex; + gap: 12px; + align-items: center; + flex-wrap: wrap; + padding: 12px 16px; + border-bottom: 1px solid var(--divider-color); + } + .cal-window-chips { + display: flex; + gap: 4px; + background: var(--card-background-color, var(--ha-card-background, #1c1c1c)); + border-radius: 999px; + padding: 3px; + } + .cal-window-chip { + padding: 6px 14px; + border: none; + background: transparent; + color: var(--secondary-text-color); + font-size: 13px; + font-weight: 500; + cursor: pointer; + border-radius: 999px; + transition: background 0.12s, color 0.12s; + } + .cal-window-chip:hover { color: var(--primary-text-color); } + .cal-window-chip.active { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + } + /* v2.2.0 — past-window chips: visually distinguished from forward chips + so the user grasps the time-direction switch at a glance. Uses a + muted secondary tone instead of the primary blue. v2.3.x: explicit + "−N d" / "+N d" prefixes + dot separator so past vs forward groups + read at a glance instead of being two pill rows that look identical + except for a small arrow. (User feedback: *"das −30 und die + sind + noch schlecht angeordnet"*.) */ + .cal-past-chips { + /* margin-right replaced by explicit separator below */ + } + .cal-past-chip.active { + background: var(--secondary-text-color, #888); + } + .cal-chip-separator { + color: var(--divider-color); + font-size: 8px; + align-self: center; + margin: 0 2px; + line-height: 1; + } + .cal-user-filter { + margin-left: auto; + padding: 6px 10px; + background: var(--card-background-color, var(--ha-card-background, #1c1c1c)); + color: var(--primary-text-color); + border: 1px solid var(--divider-color); + border-radius: 6px; + font-size: 13px; + cursor: pointer; + } + .cal-rolling { padding: 8px 16px 32px; } + .cal-day-row { + display: flex; + gap: 12px; + padding: 12px 0; + border-bottom: 1px solid var(--divider-color); + } + .cal-day-pill { + width: 56px; + height: 56px; + border-radius: 12px; + background: var(--card-background-color, var(--ha-card-background, #1c1c1c)); + border: 1px solid var(--divider-color); + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + flex-shrink: 0; + } + .cal-day-pill.cal-today { + background: var(--primary-color); + border-color: var(--primary-color); + } + .cal-pill-weekday { + font-size: 10px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.4px; + color: var(--secondary-text-color); + } + .cal-pill-day { + font-size: 20px; + font-weight: 700; + color: var(--primary-text-color); + line-height: 1.1; + } + .cal-day-pill.cal-today .cal-pill-weekday, + .cal-day-pill.cal-today .cal-pill-day { + color: var(--text-primary-color, #fff); + } + .cal-day-content { flex: 1; min-width: 0; } + .cal-day-header { + display: flex; + align-items: baseline; + gap: 8px; + margin-bottom: 6px; + } + .cal-day-month { color: var(--secondary-text-color); font-size: 13px; } + .cal-day-today-badge { + color: var(--primary-color); + font-size: 11px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.5px; + } + .cal-empty { + color: var(--secondary-text-color); + font-size: 13px; + font-style: italic; + padding: 4px 0 4px; + } + .cal-event { + display: flex; + align-items: center; + gap: 10px; + padding: 8px 0; + cursor: pointer; + border-radius: 4px; + transition: background 0.12s; + } + .cal-event:hover { background: var(--state-icon-color, rgba(255,255,255,0.04)); } + .cal-event-projected { opacity: 0.55; } + .cal-event-body { flex: 1; min-width: 0; } + .cal-event-title { + font-size: 14px; + color: var(--primary-text-color); + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .cal-event-recur { + display: block; + font-size: 11px; + color: var(--secondary-text-color); + margin-top: 2px; + } + .cal-event-icon { + --mdc-icon-size: 18px; + flex-shrink: 0; + } + .cal-source-time { color: var(--secondary-text-color); } + .cal-source-sensor { color: var(--primary-color); } + .cal-event-prediction { + display: inline-block; + font-size: 11px; + margin-top: 2px; + padding: 1px 6px; + border-radius: 999px; + background: var(--card-background-color, var(--ha-card-background, #1c1c1c)); + border: 1px solid var(--divider-color); + } + .cal-conf-high { color: var(--success-color, #4caf50); border-color: #4caf5044; } + .cal-conf-medium { color: var(--warning-color, #f9a825); border-color: #f9a82544; } + .cal-conf-low { color: var(--error-color, #d32f2f); border-color: #d32f2f44; } + .cal-event-cost { + font-size: 12px; + color: var(--secondary-text-color); + flex-shrink: 0; + } + .cal-status-pill { + flex-shrink: 0; + padding: 2px 8px; + border-radius: 999px; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.4px; + color: #fff; + } + .cal-status-overdue { background: #d32f2f; } + .cal-status-triggered { background: #038fc7; } + .cal-status-due_soon { background: #f9a825; color: #000; } + .cal-status-ok { background: #2e7d32; } + + @media (max-width: 600px) { + .cal-controls { padding: 10px 12px; } + .cal-rolling { padding: 6px 12px 24px; } + .cal-day-pill { width: 48px; height: 48px; } + .cal-pill-day { font-size: 17px; } + .cal-user-filter { margin-left: 0; width: 100%; } + } +`;var Fe={maintenance:"Maintenance",objects:"Objects",tasks:"Tasks",overdue:"Overdue",due_soon:"Due Soon",triggered:"Triggered",trigger_replaced:"Trigger replaced",ok:"OK",all:"All",new_object:"+ New Object",templates_from:"From template",templates_title:"Start from a template",templates_task_count:"{n} tasks",template_created:"Created from template",onboard_hint:"Add your first object to start tracking maintenance.",edit:"Edit",duplicate:"Duplicate",task_duplicated:"Task duplicated",object_duplicated:"Object duplicated",delete:"Delete",add_task:"+ Add Task",complete:"Complete",completed:"Completed",skip:"Skip",skipped:"Skipped",missed:"Missed",reset:"Reset",snooze:"Snooze",snoozed:"Snoozed",cancel:"Cancel",bulk_select:"Select",bulk_select_all:"Select all",bulk_n_selected:"{n} selected",bulk_completed:"{n} tasks completed",bulk_archived:"{n} tasks archived",completing:"Completing\u2026",interval:"Interval",warning:"Warning",last_performed:"Last performed",next_due:"Next due",days_until_due:"Days until due",avg_duration:"Avg duration",trigger:"Trigger",trigger_type:"Trigger type",threshold_above:"Upper limit",threshold_below:"Lower limit",threshold:"Threshold",counter:"Counter",state_change:"State change",runtime:"Runtime",runtime_hours:"Target runtime (hours)",target_value:"Target value",baseline:"Baseline",target_changes:"Target changes",for_minutes:"For (minutes)",time_based:"Time-based",sensor_based:"Sensor-based",manual:"Manual",one_time:"One-time",weekdays:"Weekdays",nth_weekday:"Nth weekday of month",day_of_month:"Day of month",recurrence_on_days:"Repeat on",recurrence_occurrence:"Occurrence",recurrence_weekday:"Weekday",recurrence_day:"Day of month (1\u201331)",recurrence_last_day:"Last day of the month",recurrence_business_day:"Business days only (roll back from weekend)",recurrence_offset:"Offset (days, \xB1)",recurrence_offset_help:"Shift the date by \xB1N days, e.g. -2 = two days before.",last_day_month:"Last day of month",last_business_day_month:"Last business day",ord_1:"1st",ord_2:"2nd",ord_3:"3rd",ord_4:"4th",ord_5:"5th",ord_last:"Last",day_word:"Day",interval_value:"Interval",interval_unit:"Unit",unit_days:"Days",unit_weeks:"Weeks",unit_months:"Months",unit_years:"Years",due_date:"Due date",cleaning:"Cleaning",inspection:"Inspection",replacement:"Replacement",calibration:"Calibration",service:"Service",reading:"Reading",custom:"Custom",history:"History",cost:"Cost",report_button:"Report",report_title:"Maintenance report",report_generated:"Generated",report_times_done:"Done",report_total_cost:"Total cost",report_every:"every {n} {unit}",report_notes:"Notes",report_col_type:"Type",report_col_status:"Status",report_col_schedule:"Schedule",duration:"Duration",both:"Both",trigger_val:"Trigger value",complete_title:"Complete: ",checklist:"Checklist",checklist_steps_optional:"Checklist steps (optional)",checklist_placeholder:`Clean filter +Replace seal +Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"{field}: too long (max {n} characters)",err_too_short:"{field}: too short (min {n} characters)",err_value_too_high:"{field}: too large (max {n})",err_value_too_low:"{field}: too small (min {n})",err_required:"{field}: required",err_wrong_type:"{field}: wrong type (expected: {type})",err_invalid_choice:"{field}: not an allowed value",err_invalid_value:"{field}: invalid value",feat_schedule_time:"Time-of-day scheduling",feat_schedule_time_desc:"Tasks become overdue at a specific time of day instead of midnight.",schedule_time_optional:"Due at time (optional, HH:MM)",schedule_time_help:"Empty = midnight (default). HA timezone.",at_time:"at",notes_optional:"Notes (optional)",cost_optional:"Cost (optional)",duration_minutes:"Duration in minutes (optional)",days:"days",day:"day",today:"Today",d_overdue:"d overdue",no_tasks:"No maintenance tasks yet. Create an object to get started.",no_tasks_short:"No tasks",no_history:"No history entries yet.",show_all:"Show all",cost_duration_chart:"Cost & Duration",installed:"Installed",confirm_delete_object:"Delete this object and all its tasks?",confirm_delete_task:"Delete this task?",min:"Min",max:"Max",save:"Save",saving:"Saving\u2026",edit_task:"Edit Task",new_task:"New Maintenance Task",task_name:"Task name",maintenance_type:"Maintenance type",priority:"Priority",labels:"Labels",labels_placeholder:"e.g. safety, seasonal, tenant-visible",labels_help:"Comma-separated tags for filtering and reporting.",priority_low:"Low",priority_normal:"Normal",priority_high:"High",schedule_type:"Schedule type",interval_days:"Interval (days)",warning_days:"Warning days",earliest_completion_days:"Earliest completion (days before due)",earliest_completion_days_help:"Leave empty to allow completing any time. 0 = only on/after the due date.",last_performed_optional:"Last performed (optional)",interval_anchor:"Interval anchor",anchor_completion:"From completion date",anchor_planned:"From planned date (no drift)",edit_object:"Edit Object",name:"Name",manufacturer_optional:"Manufacturer (optional)",model_optional:"Model (optional)",serial_number_optional:"Serial number (optional)",serial_number_label:"S/N",documentation_url_label:"Manual",object_notes_label:"Notes",sort_due_date:"Due date",sort_object:"Object name",sort_type:"Type",sort_task_name:"Task name",all_objects:"All objects",tasks_lower:"tasks",no_tasks_yet:"No tasks yet",add_first_task:"Add first task",trigger_configuration:"Trigger Configuration",entity_id:"Entity ID",comma_separated:"comma-separated",entity_logic:"Entity logic",entity_logic_any:"Any entity triggers",entity_logic_all:"All entities must trigger",entities:"entities",attribute_optional:"Attribute (optional, blank = state)",use_entity_state:"Use entity state (no attribute)",trigger_above:"Trigger above",trigger_below:"Trigger below",for_at_least_minutes:"For at least (minutes)",safety_interval_days:"Safety interval (days, optional)",safety_interval:"Safety interval (optional)",delta_mode:"Delta mode",from_state_optional:"From state (optional)",to_state_optional:"To state (optional)",documentation_url_optional:"Documentation URL (optional)",object_notes_optional:"Notes (optional)",nfc_tag_id_optional:"NFC Tag ID (optional)",nfc_tags_empty_help:"No NFC tags registered in Home Assistant yet.",nfc_tags_open_settings:"Open Tags settings",nfc_tags_refresh:"Refresh",environmental_entity_optional:"Environmental sensor (optional)",environmental_entity_helper:"e.g. sensor.outdoor_temperature \u2014 adjusts the interval based on environmental conditions",environmental_attribute_optional:"Environmental attribute (optional)",nfc_tag_id:"NFC Tag ID",nfc_linked:"NFC tag linked",nfc_link_hint:"Click to link NFC tag",responsible_user:"Responsible User",shared_with:"Shared with (rotation)",shared_with_help:"Pick multiple people to share this task; the responsible person rotates on each completion.",rotation_strategy:"Rotation",rotation_none:"No rotation",rotation_round_robin:"Round-robin",rotation_least_completed:"Least completed",rotation_random:"Random",no_user_assigned:"(No user assigned)",all_users:"All Users",my_tasks:"My Tasks",tab_calendar:"Calendar",cal_no_events:"No maintenance",cal_window_7:"7 days",cal_window_14:"14 days",cal_window_30:"30 days",cal_window_365:"1 year",cal_every_n_days:"every {n} days",cal_source_time:"Time-based",cal_source_time_adaptive:"Time-based (adaptive)",cal_source_sensor:"Sensor-based",cal_predicted:"predicted",cal_confidence_high:"high confidence",cal_confidence_medium:"medium confidence",cal_confidence_low:"low confidence",budget_monthly:"Monthly budget",budget_yearly:"Yearly budget",groups:"Groups",new_group:"New group",edit_group:"Edit group",no_groups:"No groups yet",delete_group:"Delete group",delete_group_confirm:"Delete group '{name}'?",group_select_tasks:"Select tasks",group_name_required:"Name is required",description_optional:"Description (optional)",selected:"Selected",loading_chart:"Loading chart data...",hide_outliers:"Hide outliers (sensor glitches)",was_maintenance_needed:"Was this maintenance needed?",feedback_needed:"Needed",feedback_not_needed:"Not needed",feedback_not_sure:"Not sure",suggested_interval:"Suggested interval",apply_suggestion:"Apply",reanalyze:"Re-analyze",reanalyze_result:"New analysis",reanalyze_insufficient_data:"Not enough data to produce a recommendation",data_points:"data points",dismiss_suggestion:"Dismiss",confidence_low:"Low",confidence_medium:"Medium",confidence_high:"High",recommended:"recommended",seasonal_awareness:"Seasonal Awareness",edit_seasonal_overrides:"Edit seasonal factors",seasonal_overrides_title:"Seasonal factors (override)",seasonal_overrides_hint:"Factor per month (0.1\u20135.0). Empty = learned automatically.",seasonal_override_invalid:"Invalid value",seasonal_override_range:"Factor must be between 0.1 and 5.0",clear_all:"Clear all",seasonal_chart_title:"Seasonal Factors",seasonal_learned:"Learned",seasonal_manual:"Manual",month_jan:"Jan",month_feb:"Feb",month_mar:"Mar",month_apr:"Apr",month_may:"May",month_jun:"Jun",month_jul:"Jul",month_aug:"Aug",month_sep:"Sep",month_oct:"Oct",month_nov:"Nov",month_dec:"Dec",sensor_prediction:"Sensor Prediction",degradation_trend:"Trend",trend_rising:"Rising",trend_falling:"Falling",trend_stable:"Stable",trend_insufficient_data:"Insufficient data",days_until_threshold:"Days until threshold",threshold_exceeded:"Threshold exceeded",environmental_adjustment:"Environmental factor",sensor_prediction_urgency:"Sensor predicts threshold in ~{days} days",day_short:"day",weibull_reliability_curve:"Reliability Curve",weibull_failure_probability:"Failure Probability",weibull_r_squared:"Fit R\xB2",beta_early_failures:"Early Failures",beta_random_failures:"Random Failures",beta_wear_out:"Wear-out",beta_highly_predictable:"Highly Predictable",confidence_interval:"Confidence Interval",confidence_conservative:"Conservative",confidence_aggressive:"Optimistic",current_interval_marker:"Current interval",recommended_marker:"Recommended",characteristic_life:"Characteristic life",chart_mini_sparkline:"Trend sparkline",chart_history:"Cost and duration history",chart_seasonal:"Seasonal factors, 12 months",chart_weibull:"Weibull reliability curve",chart_sparkline:"Sensor trigger value chart",days_progress:"Days progress",qr_code:"QR Code",qr_generating:"Generating QR code\u2026",qr_error:"Failed to generate QR code.",qr_error_no_url:"No HA URL configured. Please set an external or internal URL in Settings \u2192 System \u2192 Network.",save_error:"Failed to save. Please try again.",qr_print:"Print",qr_download:"Download SVG",qr_action:"Action on scan",qr_action_view:"View maintenance info",qr_action_complete:"Mark maintenance as complete",qr_url_mode:"Link type",qr_mode_companion:"Companion App",qr_mode_local:"Local (mDNS)",qr_mode_server:"Server URL",overview:"Overview",analysis:"Analysis",recent_activities:"Recent Activities",search_notes:"Search notes",avg_cost:"Avg Cost",no_advanced_features:"No advanced features enabled",no_advanced_features_hint:"Enable \u201CAdaptive Intervals\u201D or \u201CSeasonal Patterns\u201D in the integration settings to see analysis data here.",analysis_not_enough_data:"Not enough data for analysis yet.",analysis_not_enough_data_hint:"Weibull analysis requires at least 5 completed maintenances; seasonal patterns become visible after 6+ data points per month.",analysis_manual_task_hint:"Manual tasks without an interval do not generate analysis data.",completions:"completions",current:"Current",shorter:"Shorter",longer:"Longer",normal:"Normal",disabled:"Disabled",compound_logic:"Compound logic",compound:"Compound (multiple conditions)",compound_logic_and:"AND \u2014 all conditions must trigger",compound_logic_or:"OR \u2014 any condition triggers",compound_help:"Combine several sensor conditions into one trigger.",compound_no_conditions:"No conditions yet \u2014 add at least one.",compound_add_condition:"Add condition",compound_condition:"Condition",compound_remove_condition:"Remove condition",card_title:"Title",card_show_header:"Show header with statistics",card_show_actions:"Show action buttons",card_compact:"Compact mode",card_max_items:"Max items (0 = all)",card_filter_status:"Filter by status",card_filter_status_help:"Empty = show all statuses.",card_filter_objects:"Filter by objects",card_filter_objects_help:"Empty = show all objects.",card_filter_entities:"Filter by entities (entity_ids)",card_filter_entities_help:"Pick sensor / binary_sensor entities from this integration. Empty = all.",card_loading_objects:"Loading objects\u2026",card_load_error:"Could not load objects \u2014 check the WebSocket connection.",card_no_tasks_title:"No maintenance tasks yet",card_no_tasks_cta:"\u2192 Create one in the Maintenance panel",no_objects:"No objects yet.",action_error:"Action failed. Please try again.",area_id_optional:"Area (optional)",installation_date_optional:"Installation date (optional)",warranty_expiry_optional:"Warranty expiry (optional)",warranty:"Warranty",warranty_valid_until:"valid until {date}",warranty_expires_in:"expires in {days} days",warranty_expired:"expired",cal_past_windows:"Past windows",cal_forward_windows:"Forward windows",history_edit_title:"Edit history entry",history_edit_timestamp:"Timestamp",manufacturer:"Manufacturer",model:"Model",area:"Area",actions:"Actions",view_mode_label:"View",view_cards:"Card view",view_table:"Table view",objects_table_columns_label:"Objects table columns",objects_table_columns_hint:"Choose which columns appear in the objects table view.",custom_icon_optional:"Icon (optional, e.g. mdi:wrench)",task_enabled:"Task enabled",skip_reason_prompt:"Skip this task?",reason_optional:"Reason (optional)",reset_date_prompt:"Mark task as performed?",reset_date_optional:"Last performed date (optional, defaults to today)",notes_label:"Notes",documentation_label:"Documentation",no_nfc_tag:"\u2014 No tag \u2014",dashboard:"Dashboard",tab_today:"Today",palette_placeholder:"Search objects and tasks\u2026",palette_no_results:"No matches",palette_hint:"\u2191\u2193 to navigate \xB7 Enter to open \xB7 Esc to close",today_all_caught_up:"All caught up! Nothing due this week.",today_overdue:"Overdue",today_due_today:"Due today",today_this_week:"This week",settings:"Settings",settings_features:"Advanced Features",settings_features_desc:"Enable or disable advanced features. Disabling hides them from the UI but does not delete data.",feat_adaptive:"Adaptive Scheduling",feat_adaptive_desc:"Learn optimal intervals from maintenance history",feat_predictions:"Sensor Predictions",feat_predictions_desc:"Predict trigger dates from sensor degradation",feat_seasonal:"Seasonal Adjustments",feat_seasonal_desc:"Adjust intervals based on seasonal patterns",feat_environmental:"Environmental Correlation",feat_environmental_desc:"Correlate intervals with temperature/humidity",feat_budget:"Budget Tracking",feat_budget_desc:"Track monthly and yearly maintenance spending",feat_groups:"Task Groups",feat_groups_desc:"Organize tasks into logical groups",feat_checklists:"Checklists",feat_checklists_desc:"Multi-step procedures for task completion",settings_general:"General",settings_default_warning:"Default warning days",settings_panel_enabled:"Sidebar panel",settings_panel_title:"Sidebar panel title",settings_notifications:"Notifications",settings_notify_service:"Notification service",test_notification:"Test notification",send_test:"Send test",testing:"Sending\u2026",test_notification_success:"Test notification sent",test_notification_failed:"Test notification failed",settings_notify_due_soon:"Notify when due soon",settings_notify_overdue:"Notify when overdue",settings_notify_triggered:"Notify when triggered",settings_interval_hours:"Repeat interval (hours, 0 = once)",settings_quiet_hours:"Quiet hours",settings_quiet_start:"Start",settings_quiet_end:"End",settings_max_per_day:"Max notifications per day (0 = unlimited)",settings_bundling:"Bundle notifications",settings_bundle_threshold:"Bundle threshold",settings_reminder_leads:"Extra reminders (days before due)",settings_reminder_leads_hint:"Comma-separated lead times, e.g. 14, 3, 0 \u2014 one extra reminder fires on each matching day. Empty = off.",settings_actions:"Mobile Action Buttons",settings_action_complete:"Show 'Complete' button",settings_action_skip:"Show 'Skip' button",settings_action_snooze:"Show 'Snooze' button",settings_weekly_digest:"Weekly digest",settings_weekly_digest_hint:"A single summary notification on Monday morning when tasks are due.",settings_warranty_reminder:"Warranty expiry reminder",settings_warranty_reminder_days:"Days before expiry",settings_warranty_reminder_hint:"Notify once when an object's warranty is this many days from expiring.",settings_snooze_hours:"Snooze duration (hours)",settings_budget:"Budget",settings_currency:"Currency",settings_budget_monthly:"Monthly budget",settings_budget_yearly:"Yearly budget",settings_budget_alerts:"Budget alerts",settings_budget_threshold:"Alert threshold (%)",settings_import_export:"Import / Export",settings_export_json:"Export JSON",settings_export_yaml:"Export YAML",settings_export_csv:"Export CSV",settings_import_csv:"Import CSV",settings_import_placeholder:"Paste JSON or CSV content here\u2026",settings_import_btn:"Import",settings_import_success:"{count} objects imported successfully.",settings_export_success:"Export downloaded.",settings_saved:"Setting saved.",settings_include_history:"Include history",sort_alphabetical:"Alphabetical",sort_due_soonest:"Due soonest",sort_task_count:"Task count",sort_area:"Area",sort_assigned_user:"Assigned user",sort_group:"Group",groupby_none:"No grouping",groupby_area:"By area",groupby_group:"By group",groupby_user:"By user",filter_label:"Filter",user_label:"User",sort_label:"Sort",group_by_label:"Group by",state_value_help:'Use the HA state value (usually lowercase, e.g. "on"/"off"). Case is normalised on save.',target_changes_help:"Number of matching transitions before the trigger fires (default: 1).",qr_print_title:"Print QR codes",qr_print_desc:"Generate a printable page of QR codes to cut out and stick on your equipment.",qr_print_load:"Load objects",qr_print_filter:"Filter",qr_print_objects:"Objects",qr_print_actions:"Actions",qr_print_url_mode:"Link type",qr_print_estimate:"Estimated QR codes",qr_print_over_limit:"cap is 200, narrow the filter",qr_print_generate:"Generate QR codes",qr_print_generating:"Generating\u2026",qr_print_ready:"QR codes ready",qr_print_print_button:"Print",qr_print_empty:"Nothing to generate",qr_action_skip:"Skip",vacation_title:"Vacation mode",vacation_active:"active",vacation_ended:"ended",vacation_desc:"Plan a vacation: notifications are paused during the period plus a buffer of days. You can opt specific tasks back in.",vacation_enable:"Enable vacation mode",vacation_start:"Start",vacation_end:"End",vacation_buffer:"Buffer (days)",vacation_exempt_title:"Notify anyway during vacation",vacation_exempt_desc:"Pick tasks that should still notify during vacation (e.g. critical pool chemistry).",vacation_load_tasks:"Load tasks",vacation_preview_btn:"Show preview",vacation_preview_affected:"tasks affected",vacation_event_due_soon:"becomes due soon",vacation_event_overdue:"becomes overdue",vacation_event_triggered_est:"sensor trigger possible",vacation_sensor_based:"(sensor-based)",vacation_action_notify:"Notify anyway",vacation_action_unsilence:"Silence again",vacation_marked_complete:"Marked complete",vacation_marked_skip:"Skipped",vacation_end_now:"End vacation now",add:"Add",show_stats:"Show stats + graphs",hide_stats:"Hide stats",adaptive_no_data:"Not enough completion history yet for adaptive analysis. Complete this task a few more times to unlock interval recommendations and reliability charts.",suggestion_applied:"Suggested interval applied",vacation_mode:"Vacation mode",vacation_status_active:"Active now",vacation_status_scheduled:"Scheduled",vacation_status_inactive:"Inactive",vacation_end_now_confirm:"End vacation immediately?",vacation_exempt_count:"exempt",vacation_advanced:"Advanced\u2026",vacation_open_panel:"Open in panel",enable:"Enable",saved:"Saved",budget_monthly_set:"Set monthly",budget_yearly_set:"Set yearly",budget_advanced:"Currency, alerts\u2026",budget_open_panel:"Open in panel",groups_empty:"No groups yet.",group_new_placeholder:"Add group\u2026",group_delete_confirm:'Delete group "{name}"?',groups_manage_tasks:"Manage task assignments\u2026",groups_open_panel:"Open in panel",unassigned:"Unassigned",no_area:"No area",has_overdue:"Has overdue tasks",object:"Object",settings_panel_access:"Panel access",settings_panel_access_desc:"Admins always have full access. To delegate create, edit and delete to specific non-admins, switch this on and pick them below \u2014 everyone else sees only Complete and Skip.",settings_operator_write:"Allow selected users to create, edit & delete",settings_operator_write_desc:"Off: only admins can change content. On: the selected users below get full access too.",no_non_admin_users:"No non-admin users found. Add some in Settings \u2192 People.",owner_label:"Owner",feat_completion_actions:"Completion actions",feat_completion_actions_desc:"Per-task HA action on complete + quick-complete QR with pre-set values.",on_complete_action_title:"On complete: trigger HA action (optional)",on_complete_action_desc:"Calls an HA service when the task is completed \u2014 e.g. reset a counter on the device.",on_complete_action_service:"Service",on_complete_action_target:"Target entity",on_complete_action_target_hint:"Note: the entity domain must match the service \u2014 e.g. 'button.press' only works on button.*, 'counter.increment' only on counter.*, 'input_button.press' only on input_button.* etc. On a mismatch the action will silently fail (HA logs 'Referenced entities ... missing or not currently available').",on_complete_action_data:"Data (JSON, optional)",on_complete_action_test:"Validate configuration",on_complete_action_test_success:"\u2713 Configuration valid (action will fire only on task completion)",on_complete_action_test_failed:"Failed",quick_complete_defaults_title:"Quick-complete defaults (for QR scans, optional)",quick_complete_defaults_desc:"Pre-set values for quick-complete QR scans. Without these, the QR opens the complete dialog.",quick_complete_defaults_notes:"Notes",quick_complete_defaults_cost:"Cost",quick_complete_defaults_duration:"Duration (minutes)",quick_complete_defaults_feedback_none:"No feedback",quick_complete_defaults_feedback_needed:"Was needed",quick_complete_defaults_feedback_not_needed:"Not needed",quick_complete_success:"Quickly marked complete",show_all_objects:"Show all objects",show_all_tasks:"Clear filter \u2014 show all tasks",filter_to_overdue:"Filter task list to overdue only",filter_to_due_soon:"Filter task list to due-soon only",filter_to_triggered:"Filter task list to triggered only",open_task:"Open task",show_details:"Show history + stats",hide_details:"Hide details",history_empty:"No history yet.",history_edit_button:"Edit entry",total_cost:"Total cost",times_performed:"Performed",older_entries:"older",open_in_panel:"Open in Maintenance panel",skip_reason:"Skip reason (optional)",reset_to_date:"Reset last_performed to",delete_task_confirm:"Delete this task and its history?",delete_object_confirm:"Delete this object and all its tasks?",loading:"Loading\u2026",archive:"Archive",undo:"Undo",task_archived:"Task archived",object_archived:"Object archived",unarchive:"Unarchive",archived:"Archived",show_archived:"Show archived",hide_archived:"Hide archived",confirm_archive_object:"Archive this object and its tasks? They keep their history and can be unarchived later.",settings_archive:"Archive & Retention",settings_archive_desc:"Retire completed one-off tasks without deleting them. Archived items are hidden and inert but keep their history and cost.",settings_archive_oneoff_days:"Auto-archive completed one-off tasks after (days, 0 = off)",settings_delete_archived_oneoff_days:"Auto-delete archived one-off tasks after (days, 0 = never)",archive_object:"Archive object",unarchive_object:"Unarchive object",documents:"Documents",documents_empty:"No documents yet.",doc_upload:"Upload file",doc_uploading:"Uploading\u2026",doc_add_link:"Add link",doc_link_url:"URL (https://\u2026)",doc_link_title:"Title (optional)",doc_open:"Open",doc_delete_confirm:'Delete "{name}"?',doc_too_large:"File is too large (max 25 MB).",doc_upload_failed:"Upload failed.",completion_photo_optional:"Completion photo (optional)",add_photo:"Add photo",uploading:"Uploading\u2026",remove:"Remove",doc_deduped:"Already stored elsewhere \u2014 shared, no extra space used.",doc_dup_in_object:"This file is already attached to this object.",doc_link_invalid:"Only http/https links are allowed.",doc_cat_manual:"Manual",doc_cat_warranty:"Warranty",doc_cat_invoice:"Invoice",doc_cat_spare_parts:"Spare parts",doc_cat_photo:"Photo",doc_cat_other:"Other",doc_link_badge:"Link",doc_storage_title:"Document storage",doc_storage_saved:"Saved via deduplication",doc_storage_refresh:"Refresh",doc_download:"Download",doc_close:"Close",doc_camera:"Take photo",doc_drop_hint:"Drop files here",doc_task_none:"No documents linked to this task.",doc_link_existing:"Link a document\u2026",doc_attach:"Link",doc_unlink:"Unlink",doc_page:"Page",chart_range_7d:"7d",chart_range_30d:"30d",chart_range_90d:"90d",chart_range_1y:"1y",chart_since_service:"since last service",chart_no_stats:"No long-term statistics for this entity \u2014 showing maintenance-event values only",auto_complete_on_recovery:"Auto-complete when the sensor recovers",auto_complete_on_recovery_help:"Records a completion (sets last performed) when the trigger clears itself \u2014 e.g. salt refilled, filter replaced.",doc_search:"Search documents\u2026",doc_search_none:"No matching documents",link_device_optional:"Link to existing device (optional)",parent_object_optional:"Parent object (optional)",parent_none:"(No parent)",paused:"Paused",pause_object:"Pause",resume_object:"Resume",pause_until_prompt:"Freeze this object's schedules \u2014 nothing becomes due and nothing notifies until it is resumed. Optionally set an auto-resume date.",pause_until_label:"Resume on (optional)",object_paused:"Object paused",object_resumed:"Object resumed \u2014 schedules restarted",object_paused_badge:"Paused",paused_until_label:"until",replace_object:"Replace\u2026",replace_object_prompt:"Retire this object and create a successor. History and costs stay archived on the old one; tasks and documents carry over to the new one, counters start fresh.",replace_name_label:"Successor name",object_replaced:"Object replaced \u2014 successor created",reading_unit_label:"Reading unit (e.g. kWh, m\xB3)",reading_unit_help:"Shown next to the recorded value when completing this task.",reading_value_label:"Reading value",reading_label:"Reading",settings_templates_label:"Template gallery",settings_templates_hint:`Untick templates you'll never need \u2014 they disappear from the "From template" pickers (panel and config flow). Nothing else changes; you can re-enable them any time.`,worksheet:"Work sheet",worksheet_scan_view:"Scan to open the task",worksheet_scan_complete:"Scan to complete",worksheet_manual_excerpt:"Manual excerpt",worksheet_pages:"pages",worksheet_printed:"Printed",worksheet_never:"Never"};var Be="\u20AC";var ye="en",We=(()=>{let n=window;return n.__msLocales||(n.__msLocales={store:{},inflight:{}}),n.__msLocales})(),T=We.store;T.en||(T.en=Fe);var wt=new Set(["de","nl","fr","it","es","pt","ru","uk","pl","cs","sv","zh","da","fi","nb","ja","hi"]),kt="/maintenance_supporter_locales",Q=We.inflight;function be(n){return(n||ye).substring(0,2).toLowerCase()}function f(n,e){let t=be(e);return T[t]?.[n]??T.en[n]??n}function Ge(n){let e=be(n);return e===ye||e in T}function Ve(n){let e=be(n);return e===ye||e in T||!wt.has(e)?Promise.resolve():(e in Q||(Q[e]=fetch(`${kt}/${e}.json`).then(t=>t.ok?t.json():null).then(t=>{t?T[e]=t:delete Q[e]}).catch(()=>{delete Q[e]})),Q[e])}var Ye=S` + :host { + --maint-ok-color: var(--success-color, #4caf50); + --maint-due-soon-color: var(--warning-color, #ff9800); + --maint-overdue-color: var(--error-color, #f44336); + --maint-triggered-color: #ff5722; + } + + .status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + color: white; + white-space: nowrap; + /* Fixed minimum so OK / Due Soon / Overdue / Triggered pills are uniform + width in the task table — keeps the object-name column aligned. */ + min-width: 70px; + box-sizing: border-box; + } + /* Shape icon so status is not conveyed by colour alone (accessibility). */ + .status-badge ha-icon { --mdc-icon-size: 14px; margin-left: -1px; } + + .status-badge.ok { background-color: var(--maint-ok-color); } + .status-badge.due_soon { background-color: var(--maint-due-soon-color); } + .status-badge.overdue { background-color: var(--maint-overdue-color); } + .status-badge.triggered { background-color: var(--maint-triggered-color); } + /* Completed one-time task ("done") — muted blue-grey. */ + .status-badge.done { background-color: var(--maint-done-color, #78909c); } + /* v2.10.0: archived (retire-but-retain) — neutral grey, clearly inert. */ + .status-badge.archived { background-color: var(--disabled-color, #9e9e9e); } + /* v2.20 (N3): paused — frozen but present, info blue. */ + .status-badge.paused { background-color: var(--info-color, #2196f3); } + + /* v1.4.7: 5-column grid so all 5 KPIs (Objects/Tasks/Overdue/Due Soon/ + Triggered) always stay in one row. The previous flex-wrap layout was + wrapping the 5th item (Triggered, the widest label) onto its own row + on narrow viewports because the natural width of the items pushed past + the container width. Grid forces equal 1/5 distribution regardless of + label length. */ + .stats-bar { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 16px; + padding: 16px; + } + + .stat-item { + display: flex; + flex-direction: column; + align-items: center; + min-width: 0; + } + .stat-item .stat-label { + /* Allow long labels to ellipsis rather than overflow the grid cell. */ + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .stat-item.clickable { cursor: pointer; border-radius: 8px; padding: 4px 8px; transition: background 0.15s, box-shadow 0.15s; } + .stat-item.clickable:hover { background: var(--secondary-background-color); } + /* v2.1.0 — KPIs that map to a status filter highlight when active so the + user can see at a glance which filter is on, even after scrolling away. */ + .stat-item.clickable.active { + background: var(--secondary-background-color); + box-shadow: inset 0 -3px 0 var(--primary-color); + } + + .objects-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 16px; + padding: 16px 0; + } + .object-card { + padding: 16px; + background: var(--card-background-color); + border-radius: 8px; + cursor: pointer; + border: 1px solid var(--divider-color); + transition: transform 0.15s, box-shadow 0.15s; + /* Large installs (100+ objects): skip rendering off-screen cards. The + intrinsic size keeps the scrollbar stable while they're skipped. */ + content-visibility: auto; + contain-intrinsic-size: auto 120px; + } + .object-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); } + .object-card-header { display: flex; justify-content: space-between; align-items: center; } + .object-card-name { font-weight: 500; font-size: 16px; } + .object-card-count { color: var(--secondary-text-color); font-size: 13px; } + .object-card-meta { color: var(--secondary-text-color); font-size: 13px; margin-top: 4px; } + .object-card-empty { color: var(--warning-color); font-size: 13px; margin-top: 8px; font-style: italic; } + + /* Overdue indicator dot on object cards (#35) */ + .object-card { position: relative; } + .object-card-overdue { border-left: 3px solid var(--error-color); } + .overdue-dot { + position: absolute; + top: 12px; + right: 12px; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--error-color); + box-shadow: 0 0 0 2px var(--card-background-color); + } + + /* Group-by collapsible sections (#35 + #36) */ + .group-section { + margin: 12px 0; + border: 1px solid var(--divider-color); + border-radius: 8px; + background: var(--card-background-color); + } + .group-section[open] { padding-bottom: 8px; } + .group-section-header { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + cursor: pointer; + font-weight: 500; + list-style: none; + user-select: none; + } + .group-section-header::-webkit-details-marker { display: none; } + .group-section-header::before { + content: "▶"; + font-size: 10px; + color: var(--secondary-text-color); + transition: transform 0.15s; + } + .group-section[open] .group-section-header::before { transform: rotate(90deg); } + .group-section-count { + color: var(--secondary-text-color); + font-size: 13px; + font-weight: 400; + } + .group-section .objects-grid, + .group-section .task-table { + padding: 0 12px; + } + + .empty-state-centered { text-align: center; padding: 32px 16px; } + .empty-state-centered ha-button { margin-top: 16px; } + + .stat-value { + font-size: 24px; + font-weight: bold; + color: var(--primary-text-color); + } + + .stat-label { + font-size: 12px; + color: var(--secondary-text-color); + } + + .card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + } + + .card-header h1 { + margin: 0; + font-size: 20px; + font-weight: 500; + } + + .action-buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; + } + + .action-buttons ha-button { + --ha-button-font-size: 13px; + } + + .history-timeline { padding: 0 16px 16px; } + + .history-entry { + display: flex; + gap: 12px; + padding: 8px 0; + border-bottom: 1px solid var(--divider-color); + /* Long histories: skip painting off-screen entries (flex, not subgrid, so + safe — subgrid task rows can't use this without breaking alignment). */ + content-visibility: auto; + contain-intrinsic-size: auto 48px; + } + .history-entry:last-child { border-bottom: none; } + + .history-icon { + flex-shrink: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: white; + } + + .history-icon.completed { background: var(--maint-ok-color); } + .history-icon.skipped { background: var(--secondary-text-color); } + .history-icon.reset { background: var(--info-color, #2196f3); } + .history-icon.triggered { background: var(--maint-triggered-color); } + + .history-content { flex: 1; min-width: 0; } + + /* v2.2.0 — row holds the type label + the small Edit button */ + .history-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + .history-edit-btn { + background: transparent; + color: var(--secondary-text-color); + border: none; + border-radius: 4px; + padding: 4px; + cursor: pointer; + display: inline-flex; + align-items: center; + transition: background 0.15s, color 0.15s; + } + .history-edit-btn:hover { + background: var(--secondary-background-color); + color: var(--primary-color); + } + .history-edit-btn ha-icon { --mdc-icon-size: 16px; } + + .history-date { + font-size: 12px; + color: var(--secondary-text-color); + } + + .history-details { + display: flex; + gap: 12px; + font-size: 13px; + color: var(--secondary-text-color); + margin-top: 4px; + } + + /* History filter chips */ + .history-filters { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; + } + + .filter-chip { + display: inline-flex; + align-items: center; + padding: 4px 12px; + border-radius: 16px; + font-size: 12px; + cursor: pointer; + background: var(--secondary-background-color, #f5f5f5); + color: var(--primary-text-color); + border: 1px solid var(--divider-color); + transition: all 0.2s; + user-select: none; + } + + .filter-chip:hover { background: var(--divider-color); } + + .filter-chip.active { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color); + } + + .filter-chip.clear { + font-style: italic; + opacity: 0.7; + } + + /* Cost/Duration history chart */ + .history-chart { + width: 100%; + height: 200px; + display: block; + } + + .chart-legend { + display: flex; + justify-content: center; + gap: 16px; + margin-top: 4px; + font-size: 11px; + color: var(--secondary-text-color); + } + + .legend-item { + display: inline-flex; + align-items: center; + gap: 4px; + } + + .legend-swatch { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 2px; + } + + .empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 16px; + color: var(--secondary-text-color); + } + + .empty-state ha-svg-icon { + --mdc-icon-size: 48px; + margin-bottom: 16px; + } + + /* Sparkline chart */ + .sparkline-container { position: relative; margin: 8px 0; } + + .sparkline-svg { + width: 100%; + height: 140px; + display: block; + } + + /* Trigger info card */ + .trigger-card { + background: var(--card-background-color, #fff); + border-radius: 12px; + padding: 12px 16px; + margin: 8px 0; + border: 1px solid var(--divider-color); + } + + .trigger-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + } + + .trigger-entity-name { font-weight: 500; font-size: 14px; } + .trigger-entity-id { font-size: 11px; color: var(--secondary-text-color); font-family: monospace; } + + .entity-link { + cursor: pointer; + text-decoration: underline dotted; + text-underline-offset: 2px; + } + .entity-link:hover { + color: var(--primary-color); + text-decoration: underline solid; + } + + .trigger-value-row { + display: flex; + align-items: baseline; + gap: 6px; + margin: 4px 0; + } + + .trigger-current { font-size: 28px; font-weight: 700; color: var(--primary-text-color); } + .trigger-current.active { color: var(--maint-triggered-color); } + .trigger-unit { font-size: 14px; color: var(--secondary-text-color); } + + /* Counter progress ("8,507 / 15,000 km · 57 %" + bar) */ + .counter-progress { margin: 6px 0 4px; } + .counter-progress-nums { + display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; + } + .counter-progress-main { font-size: 26px; font-weight: 700; color: var(--primary-text-color); } + .counter-progress-target { font-size: 15px; font-weight: 500; color: var(--secondary-text-color); } + .counter-progress-pct { font-size: 15px; font-weight: 700; } + .counter-progress-pct.ok { color: var(--success-color, #4caf50); } + .counter-progress-pct.near { color: var(--warning-color, #ff9800); } + .counter-progress-pct.over { color: var(--error-color, #f44336); } + .counter-progress-bar { + height: 8px; border-radius: 4px; margin: 6px 0 4px; overflow: hidden; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.08)); + } + .counter-progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s ease; } + .counter-progress-fill.ok { background: var(--success-color, #4caf50); } + .counter-progress-fill.near { background: var(--warning-color, #ff9800); } + .counter-progress-fill.over { background: var(--error-color, #f44336); } + .counter-progress-caption { font-size: 12px; color: var(--secondary-text-color); } + + /* Note under a chart that fell back to sparse maintenance-event values */ + .chart-note { + display: flex; align-items: center; gap: 6px; margin-top: 2px; + font-size: 12px; color: var(--secondary-text-color); + } + .chart-note ha-icon { --mdc-icon-size: 15px; flex: none; } + + .trigger-limits { + display: flex; + gap: 16px; + font-size: 13px; + color: var(--secondary-text-color); + margin: 6px 0; + flex-wrap: wrap; + } + + .trigger-limit-item { + display: flex; + align-items: center; + gap: 4px; + } + + .trigger-limit-item .dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + .trigger-limit-item .dot.warn { background: var(--error-color, #f44336); } + .trigger-limit-item .dot.range { background: var(--secondary-text-color); } + .trigger-limit-item .dot.ok { background: var(--maint-ok-color); } + + /* Row action buttons */ + .row-actions { + display: flex; + gap: 0; + flex-shrink: 0; + margin-left: auto; + } + + .row-actions mwc-icon-button { + --mdc-icon-button-size: 32px; + --mdc-icon-size: 18px; + } + + .row-actions .btn-complete { color: var(--maint-ok-color); } + .row-actions .btn-skip { color: var(--secondary-text-color); } + + /* Days bar for overview */ + .due-cell { + display: flex; + flex-direction: column; + align-items: flex-end; + min-width: 90px; + gap: 2px; + } + + .due-text { font-size: 13px; } + + .days-bar { + width: 100%; + height: 3px; + background: var(--divider-color); + border-radius: 2px; + overflow: hidden; + } + + .days-bar-fill { + height: 100%; + border-radius: 2px; + transition: width 0.3s; + } + + /* Trigger progress bar (overview rows) */ + .trigger-progress { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 90px; + } + + .trigger-progress-bar { + width: 100%; + height: 6px; + background: var(--divider-color); + border-radius: 3px; + overflow: hidden; + } + + .trigger-progress-fill { + height: 100%; + border-radius: 2px; + transition: width 0.3s; + } + + .trigger-progress-label { + font-size: 12px; + color: var(--secondary-text-color); + text-align: right; + } + + /* Days progress bar (detail view) */ + .days-progress { + margin: 8px 0 16px; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color); + } + + .days-progress-labels { + display: flex; + justify-content: space-between; + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 6px; + } + + .days-progress-bar { + width: 100%; + height: 6px; + background: var(--divider-color); + border-radius: 3px; + overflow: hidden; + } + + .days-progress-fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s; + } + + .days-progress-text { + font-size: 13px; + font-weight: 500; + text-align: center; + margin-top: 6px; + color: var(--primary-text-color); + } + + /* Mini-sparkline in overview rows */ + .mini-sparkline { + width: 60px; + height: 20px; + display: block; + margin-top: 2px; + opacity: 0.7; + } + + /* Overflow indicator for overdue progress bars */ + .days-bar-fill.overflow, + .days-progress-fill.overflow, + .trigger-progress-fill.overflow { + background-image: repeating-linear-gradient( + -45deg, + transparent, + transparent 3px, + rgba(255,255,255,0.2) 3px, + rgba(255,255,255,0.2) 6px + ); + animation: overflow-pulse 2s ease-in-out infinite; + } + + @keyframes overflow-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } + } + + /* Budget bars */ + .budget-bars { + display: flex; + gap: 16px; + padding: 8px 16px; + flex-wrap: wrap; + } + + .budget-item { + flex: 1; + min-width: 200px; + } + + .budget-label { + display: flex; + justify-content: space-between; + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 4px; + } + + .budget-bar { + width: 100%; + height: 6px; + background: var(--divider-color); + border-radius: 3px; + overflow: hidden; + } + + .budget-bar-fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s; + } + + /* Groups section */ + .groups-section { + padding: 8px 16px 16px; + } + + .groups-section h3 { + font-size: 14px; + font-weight: 500; + color: var(--secondary-text-color); + margin: 0 0 8px; + } + + .groups-grid { + display: flex; + gap: 12px; + flex-wrap: wrap; + } + + .group-card { + background: var(--card-background-color, #fff); + border: 1px solid var(--divider-color); + border-radius: 12px; + padding: 12px 16px; + min-width: 180px; + flex: 1; + max-width: 300px; + cursor: default; + } + + .group-card-head { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; + } + + .group-card-name { + font-weight: 500; + font-size: 14px; + margin-bottom: 4px; + } + + .group-card-actions { + display: flex; + gap: 0; + } + .group-card-actions mwc-icon-button { + --mdc-icon-button-size: 28px; + --mdc-icon-size: 16px; + color: var(--secondary-text-color); + } + + .groups-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + margin-bottom: 8px; + } + .groups-header h3 { margin: 0; } + + .seasonal-actions { + display: flex; + justify-content: flex-end; + padding: 4px 0; + } + + .group-card-desc { + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 8px; + } + + .group-card-tasks { + display: flex; + gap: 6px; + flex-wrap: wrap; + } + + .group-task-chip { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + padding: 2px 8px; + border-radius: 10px; + background: var(--secondary-background-color, #f5f5f5); + color: var(--primary-text-color); + } + + /* Adaptive scheduling suggestion badge */ + .suggestion-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 16px; + font-size: 12px; + font-weight: 500; + background: var(--info-color, #2196f3); + color: white; + margin-left: 8px; + } + + .suggestion-actions { + display: flex; + gap: 8px; + margin-top: 8px; + } + + .suggestion-actions ha-button { + --ha-button-font-size: 12px; + } + + .confidence-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + + .confidence-dot.low { background: var(--secondary-text-color); } + .confidence-dot.medium { background: var(--warning-color, #ff9800); } + .confidence-dot.high { background: var(--success-color, #4caf50); } + + /* Feedback toggle buttons in complete dialog */ + .feedback-section { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 0; + border-top: 1px solid var(--divider-color); + } + + .feedback-label { + font-weight: 500; + font-size: 13px; + color: var(--secondary-text-color); + } + + .feedback-buttons { + display: flex; + gap: 8px; + } + + .feedback-btn { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--divider-color); + border-radius: 8px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 13px; + cursor: pointer; + text-align: center; + transition: all 0.2s; + } + + .feedback-btn:hover { + background: var(--secondary-background-color, #f5f5f5); + } + + .feedback-btn.selected { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color); + } + + /* Seasonal chart */ + .seasonal-chart { + padding: 12px 16px; + margin: 8px 0; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color); + } + + .seasonal-chart-title { + font-size: 13px; + font-weight: 500; + color: var(--secondary-text-color); + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 6px; + } + + .seasonal-chart-title .source-tag { + font-size: 10px; + padding: 1px 6px; + border-radius: 8px; + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + font-weight: 400; + } + + .seasonal-chart svg { + width: 100%; + height: 100px; + display: block; + } + + .seasonal-labels { + display: flex; + justify-content: space-between; + padding: 0 2px; + margin-top: 4px; + } + + .seasonal-label { + font-size: 10px; + color: var(--secondary-text-color); + text-align: center; + flex: 1; + } + + .seasonal-label.active-month { + font-weight: 700; + color: var(--primary-color); + } + + .seasonal-factor-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 500; + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + margin-left: 6px; + } + + .seasonal-factor-tag.short { + background: rgba(76, 175, 80, 0.15); + color: var(--success-color, #4caf50); + } + + .seasonal-factor-tag.long { + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + } + + /* --- Sensor Prediction Section (Phase 3) --- */ + + .prediction-section { + margin: 16px 0; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color, #e0e0e0); + } + + .prediction-urgency-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + margin-bottom: 12px; + border-radius: 8px; + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + font-size: 13px; + font-weight: 500; + } + .prediction-urgency-banner ha-svg-icon { + --mdc-icon-size: 18px; + flex-shrink: 0; + } + + .prediction-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--primary-text-color); + margin-bottom: 10px; + } + .prediction-title ha-svg-icon { + --mdc-icon-size: 16px; + color: var(--primary-color); + } + + .prediction-grid { + display: flex; + flex-wrap: wrap; + gap: 12px; + } + + .prediction-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--secondary-text-color); + } + .prediction-item ha-svg-icon { + --mdc-icon-size: 14px; + color: var(--secondary-text-color); + flex-shrink: 0; + } + + .prediction-label { + font-weight: 500; + } + + .prediction-value { + font-weight: 600; + color: var(--primary-text-color); + } + .prediction-value.rising { color: var(--error-color, #f44336); } + .prediction-value.falling { color: var(--info-color, #2196f3); } + .prediction-value.stable { color: var(--success-color, #4caf50); } + .prediction-value.exceeded { color: var(--error-color, #f44336); font-weight: 700; } + .prediction-value.urgent { color: var(--warning-color, #ff9800); font-weight: 700; } + + .prediction-rate { + font-size: 11px; + opacity: 0.7; + font-family: monospace; + } + + .prediction-date { + font-size: 11px; + opacity: 0.7; + } + + .prediction-entity { + font-size: 10px; + opacity: 0.6; + font-family: monospace; + } + + /* --- Weibull Reliability Section (Phase 4) --- */ + + .weibull-section { + margin: 16px 0; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color, #e0e0e0); + } + + .weibull-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--primary-text-color); + margin-bottom: 10px; + } + .weibull-title ha-svg-icon { + --mdc-icon-size: 16px; + color: var(--primary-color); + } + + .weibull-chart svg { + width: 100%; + height: 160px; + display: block; + } + + .weibull-info-row { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-top: 10px; + } + + .weibull-info-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--secondary-text-color); + } + + .weibull-info-value { + font-weight: 600; + color: var(--primary-text-color); + } + + /* Beta interpretation badge */ + .beta-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + white-space: nowrap; + } + .beta-badge ha-svg-icon { + --mdc-icon-size: 14px; + } + + .beta-badge.early_failures { + background: rgba(244, 67, 54, 0.15); + color: var(--error-color, #f44336); + } + .beta-badge.random_failures { + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + } + .beta-badge.wear_out { + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + } + .beta-badge.highly_predictable { + background: rgba(76, 175, 80, 0.15); + color: var(--success-color, #4caf50); + } + + /* Confidence interval range bar */ + .confidence-range { + margin-top: 12px; + } + + .confidence-range-title { + font-size: 12px; + font-weight: 500; + color: var(--secondary-text-color); + margin-bottom: 6px; + } + + .confidence-bar { + position: relative; + width: 100%; + height: 8px; + background: var(--divider-color, #e0e0e0); + border-radius: 4px; + overflow: visible; + } + + .confidence-fill { + position: absolute; + height: 100%; + border-radius: 4px; + background: var(--primary-color, #03a9f4); + opacity: 0.25; + } + + .confidence-marker { + position: absolute; + top: -4px; + width: 3px; + height: 16px; + border-radius: 1px; + transform: translateX(-50%); + } + .confidence-marker.recommended { + background: var(--success-color, #4caf50); + } + .confidence-marker.current { + background: var(--primary-color, #03a9f4); + } + + .confidence-labels { + display: flex; + justify-content: space-between; + margin-top: 4px; + } + + .confidence-text { + font-size: 10px; + color: var(--secondary-text-color); + } + .confidence-text.low { + text-align: left; + } + .confidence-text.high { + text-align: right; + } + + .task-disabled { opacity: 0.5; } + .badge-disabled { + font-size: 10px; + padding: 1px 6px; + border-radius: 8px; + background: var(--disabled-color, #9e9e9e); + color: white; + } + + /* ── Shared responsive styles (panel + card) ── */ + @media (max-width: 600px) { + .row-actions mwc-icon-button { + --mdc-icon-button-size: 44px; + --mdc-icon-size: 22px; + } + + .due-cell { min-width: 70px; } + + .trigger-card { padding: 10px 12px; } + .trigger-current { font-size: 22px; } + + .prediction-grid { flex-direction: column; gap: 8px; } + + .weibull-info-row { flex-direction: column; gap: 8px; } + + .budget-bars { flex-direction: column; } + .budget-item { min-width: 0; } + + .group-card { min-width: 0; max-width: 100%; } + + .filter-chip { padding: 6px 12px; font-size: 13px; } + + .history-details { flex-wrap: wrap; gap: 6px; } + + .sparkline-container { max-width: 100%; overflow: hidden; } + .sparkline-svg { height: 100px; } + + .stats-bar { gap: 8px; padding: 12px; } + .stat-item { min-width: 60px; } + .stat-value { font-size: 20px; } + } +`;var b=class extends w{constructor(){super(...arguments);this._config={type:"custom:maintenance-supporter-calendar-card"};this._objects=[];this._stats=null;this._windowDays=30;this._pastDays=0;this._userFilter="";this._unsub=null;this._dataLoaded=!1;this._lastConnection=null}static getConfigElement(){return document.createElement("maintenance-supporter-calendar-card-editor")}static getStubConfig(){return{type:"custom:maintenance-supporter-calendar-card",window_days:30,show_window_chips:!0,show_user_filter:!0}}setConfig(t){this._config={...t},t.past_days&&[30,90].includes(t.past_days)?this._pastDays=t.past_days:t.window_days&&[7,14,30,365].includes(t.window_days)&&(this._windowDays=t.window_days,this._pastDays=0),typeof t.user_filter=="string"&&(this._userFilter=t.user_filter)}getCardSize(){return 6}get _lang(){return this.hass?.language||"en"}disconnectedCallback(){if(super.disconnectedCallback(),this._unsub){try{this._unsub()}catch{}this._unsub=null}this._dataLoaded=!1,this._lastConnection=null}updated(t){super.updated(t);let o=this.hass?.language;if(o&&!Ge(o)&&Ve(o).then(()=>this.requestUpdate()),t.has("hass")&&this.hass){if(!this._dataLoaded)this._dataLoaded=!0,this._lastConnection=this.hass.connection,this._loadData(),this._subscribe();else if(this.hass.connection!==this._lastConnection){if(this._lastConnection=this.hass.connection,this._unsub){try{this._unsub()}catch{}this._unsub=null}this._subscribe(),this._loadData()}}}async _loadData(){try{let[t,o]=await Promise.all([this.hass.connection.sendMessagePromise({type:"maintenance_supporter/objects"}),this.hass.connection.sendMessagePromise({type:"maintenance_supporter/statistics"})]);this._objects=t.objects,this._stats=o}catch{}}async _subscribe(){try{let t=await this.hass.connection.subscribeMessage(o=>{let r=o;this._objects=r.objects},{type:"maintenance_supporter/subscribe"});if(!this.isConnected){t();return}this._unsub=t}catch{}}_onEventClick(t){if(t.history_timestamp){this.dispatchEvent(new CustomEvent("ll-custom",{detail:{type:"maintenance-supporter:edit-history",entry_id:t.entry_id,task_id:t.task_id,original_timestamp:t.history_timestamp},bubbles:!0,composed:!0}));return}this.dispatchEvent(new CustomEvent("ll-custom",{detail:{type:"maintenance-supporter:open-task",entry_id:t.entry_id,task_id:t.task_id},bubbles:!0,composed:!0}))}render(){if(!this.hass)return g;let t=this._lang,o=this._config.show_window_chips!==!1,r=this._config.show_user_filter!==!1,a=this._config.title,i=null;this._userFilter&&(i=this._userFilter==="current_user"?this.hass?.user?.id??null:this._userFilter);let s=new Date;s.setHours(0,0,0,0);let c=this._pastDays>0,p=c?qe(this._objects,s,this._pastDays,i):He(this._objects,s,this._windowDays,i),u=Y(s),d=this._windowDays===365||c,_=d?p.filter(l=>l.events.length>0):p,h=l=>{let $=`cal-status-${l.status}`,N=l.projected?"cal-event-projected":"",R=l.status==="overdue"&&l.days_until_due!=null?` (${Math.abs(l.days_until_due)}d ${f("overdue",t).toLowerCase()})`:"",L=l.projected&&l.interval_days?m`${l.interval_unit&&l.interval_unit!=="days"?`${l.interval_days} ${f("unit_"+l.interval_unit,t)}`:f("cal_every_n_days",t).replace("{n}",String(l.interval_days))}`:g,M=l.schedule_type==="sensor_based",ne=M?m``:m``,ae=M&&l.prediction_confidence&&l.status!=="triggered"&&!l.projected?m` + ${f("cal_predicted",t)} · ${f(`cal_confidence_${l.prediction_confidence}`,t)} + `:g,Ke=this._stats?.budget?.currency_symbol||Be,Je=l.history_type?f(l.history_type,t):f(l.status,t);return m` +
    this._onEventClick(l)}> + ${ne} + ${Je} +
    +
    ${l.object_name} · ${l.task_name}${R}
    + ${ae} + ${L} +
    + ${l.avg_cost!=null&&l.avg_cost>0?m`${l.avg_cost.toFixed(0)} ${Ke}`:g} +
    + `},y=l=>{let[$,N,R]=l.date.split("-").map(Number),L=new Date($,N-1,R),M=l.date===u,ne=L.toLocaleDateString(t,{weekday:"short"}),ae=L.toLocaleDateString(t,{month:"long"});return m` +
    +
    + ${ne} + ${L.getDate()} +
    +
    +
    + ${ae} + ${M?m`${f("today",t)}`:g} +
    + ${l.events.length===0?m`
    ${f("cal_no_events",t)}
    `:l.events.map(h)} +
    +
    + `};return m` + + ${o||r?m` +
    + ${o?m` +
    + ${[30,90].map(l=>m` + + `)} +
    + +
    + ${[7,14,30,365].map(l=>m` + + `)} +
    + `:g} + ${r?m` + + `:g} +
    + `:g} +
    + ${_.length===0&&d?m`
    ${f("cal_no_events",t)}
    `:_.map(y)} +
    +
    + `}};b.styles=[Ye,Ie,S` + :host { display: block; } + ha-card { padding: 0; overflow: hidden; } + `],v([V({attribute:!1})],b.prototype,"hass",2),v([k()],b.prototype,"_config",2),v([k()],b.prototype,"_objects",2),v([k()],b.prototype,"_stats",2),v([k()],b.prototype,"_windowDays",2),v([k()],b.prototype,"_pastDays",2),v([k()],b.prototype,"_userFilter",2),v([k()],b.prototype,"_unsub",2);var $t=[{value:7,label:"Week (7 days)"},{value:14,label:"Fortnight (14 days)"},{value:30,label:"Month (30 days, default)"},{value:365,label:"Year (365 days, empty days collapsed)"}],O=class extends w{constructor(){super(...arguments);this._config={type:"custom:maintenance-supporter-calendar-card"}}setConfig(t){this._config={...t}}_valueChanged(t,o){let r={...this._config,[t]:o};t==="show_window_chips"&&o===!0&&delete r.show_window_chips,t==="show_user_filter"&&o===!0&&delete r.show_user_filter,t==="title"&&(!o||typeof o=="string"&&o.trim()==="")&&delete r.title,t==="user_filter"&&o===""&&delete r.user_filter,this._config=r,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:r},bubbles:!0,composed:!0}))}render(){let t=this._config.window_days??30,o=this._config.show_window_chips!==!1,r=this._config.show_user_filter!==!1,a=this._config.user_filter??"",i=this._config.title??"";return m` +
    +
    + + this._valueChanged("title",s.target.value)} + /> +
    +
    + + +
    +
    + + this._valueChanged("show_window_chips",s.target.checked)} + /> +
    +
    + Hide the chips when the card is embedded in a strategy view that + already serves as the window selector. +
    +
    + + this._valueChanged("show_user_filter",s.target.checked)} + /> +
    +
    + + +
    +
    + `}};O.styles=S` + :host { display: block; padding: 8px 0; } + .editor { display: flex; flex-direction: column; gap: 12px; } + .row { display: flex; flex-direction: column; gap: 4px; } + .row.toggle { + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 12px; + } + label { font-weight: 500; color: var(--primary-text-color); font-size: 14px; } + input[type="text"], select { + padding: 8px; + font-size: 14px; + background: var(--card-background-color, white); + color: var(--primary-text-color, black); + border: 1px solid var(--divider-color, #e0e0e0); + border-radius: 4px; + } + .hint { + margin-top: -4px; + font-size: 12px; + color: var(--secondary-text-color, #666); + } + `,v([V({attribute:!1})],O.prototype,"hass",2),v([k()],O.prototype,"_config",2);customElements.get("maintenance-supporter-calendar-card")||customElements.define("maintenance-supporter-calendar-card",b);customElements.get("maintenance-supporter-calendar-card-editor")||customElements.define("maintenance-supporter-calendar-card-editor",O);var re=window;re.customCards=re.customCards||[];var Qe="maintenance-supporter-calendar-card",St=re.customCards.some(n=>n.type===Qe);St||re.customCards.push({type:Qe,name:"Maintenance Supporter \u2014 Calendar",description:"Rolling calendar of maintenance tasks with 7/14/30/365 day windows, source icons, and prediction-confidence pills.",preview:!0});export{b as MaintenanceCalendarCard}; +/*! Bundled license information: + +@lit/reactive-element/css-tag.js: + (** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/reactive-element.js: +lit-html/lit-html.js: +lit-element/lit-element.js: +@lit/reactive-element/decorators/custom-element.js: +@lit/reactive-element/decorators/property.js: +@lit/reactive-element/decorators/state.js: +@lit/reactive-element/decorators/event-options.js: +@lit/reactive-element/decorators/base.js: +@lit/reactive-element/decorators/query.js: +@lit/reactive-element/decorators/query-all.js: +@lit/reactive-element/decorators/query-async.js: +@lit/reactive-element/decorators/query-assigned-nodes.js: + (** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +lit-html/is-server.js: + (** + * @license + * Copyright 2022 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) + +@lit/reactive-element/decorators/query-assigned-elements.js: + (** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + *) +*/ diff --git a/custom_components/maintenance_supporter/frontend/maintenance-card.js b/custom_components/maintenance_supporter/frontend/maintenance-card.js new file mode 100644 index 0000000..9ba6723 --- /dev/null +++ b/custom_components/maintenance_supporter/frontend/maintenance-card.js @@ -0,0 +1,3676 @@ +var st=Object.defineProperty;var _i=Object.getOwnPropertyDescriptor;var y=(a,s,e)=>()=>{if(e)throw e[0];try{return a&&(s=a(a=0)),s}catch(t){throw e=[t],t}};var ui=(a,s)=>{for(var e in s)st(a,e,{get:s[e],enumerable:!0})};var l=(a,s,e,t)=>{for(var i=t>1?void 0:t?_i(s,e):s,o=a.length-1,d;o>=0;o--)(d=a[o])&&(i=(t?d(s,e,i):d(i))||i);return t&&i&&st(s,e,i),i};var ye,xe,Pe,rt,ne,at,E,ot,ze,Ne=y(()=>{ye=globalThis,xe=ye.ShadowRoot&&(ye.ShadyCSS===void 0||ye.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Pe=Symbol(),rt=new WeakMap,ne=class{constructor(s,e,t){if(this._$cssResult$=!0,t!==Pe)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=s,this.t=e}get styleSheet(){let s=this.o,e=this.t;if(xe&&s===void 0){let t=e!==void 0&&e.length===1;t&&(s=rt.get(e)),s===void 0&&((this.o=s=new CSSStyleSheet).replaceSync(this.cssText),t&&rt.set(e,s))}return s}toString(){return this.cssText}},at=a=>new ne(typeof a=="string"?a:a+"",void 0,Pe),E=(a,...s)=>{let e=a.length===1?a[0]:s.reduce((t,i,o)=>t+(d=>{if(d._$cssResult$===!0)return d.cssText;if(typeof d=="number")return d;throw Error("Value passed to 'css' function must be a 'css' function result: "+d+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+a[o+1],a[0]);return new ne(e,a,Pe)},ot=(a,s)=>{if(xe)a.adoptedStyleSheets=s.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet);else for(let e of s){let t=document.createElement("style"),i=ye.litNonce;i!==void 0&&t.setAttribute("nonce",i),t.textContent=e.cssText,a.appendChild(t)}},ze=xe?a=>a:a=>a instanceof CSSStyleSheet?(s=>{let e="";for(let t of s.cssRules)e+=t.cssText;return at(e)})(a):a});var gi,mi,fi,vi,bi,yi,$e,nt,xi,$i,le,ce,we,lt,q,de=y(()=>{Ne();Ne();({is:gi,defineProperty:mi,getOwnPropertyDescriptor:fi,getOwnPropertyNames:vi,getOwnPropertySymbols:bi,getPrototypeOf:yi}=Object),$e=globalThis,nt=$e.trustedTypes,xi=nt?nt.emptyScript:"",$i=$e.reactiveElementPolyfillSupport,le=(a,s)=>a,ce={toAttribute(a,s){switch(s){case Boolean:a=a?xi:null;break;case Object:case Array:a=a==null?a:JSON.stringify(a)}return a},fromAttribute(a,s){let e=a;switch(s){case Boolean:e=a!==null;break;case Number:e=a===null?null:Number(a);break;case Object:case Array:try{e=JSON.parse(a)}catch{e=null}}return e}},we=(a,s)=>!gi(a,s),lt={attribute:!0,type:String,converter:ce,reflect:!1,useDefault:!1,hasChanged:we};Symbol.metadata??=Symbol("metadata"),$e.litPropertyMetadata??=new WeakMap;q=class extends HTMLElement{static addInitializer(s){this._$Ei(),(this.l??=[]).push(s)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(s,e=lt){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(s)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(s,e),!e.noAccessor){let t=Symbol(),i=this.getPropertyDescriptor(s,t,e);i!==void 0&&mi(this.prototype,s,i)}}static getPropertyDescriptor(s,e,t){let{get:i,set:o}=fi(this.prototype,s)??{get(){return this[e]},set(d){this[e]=d}};return{get:i,set(d){let p=i?.call(this);o?.call(this,d),this.requestUpdate(s,p,t)},configurable:!0,enumerable:!0}}static getPropertyOptions(s){return this.elementProperties.get(s)??lt}static _$Ei(){if(this.hasOwnProperty(le("elementProperties")))return;let s=yi(this);s.finalize(),s.l!==void 0&&(this.l=[...s.l]),this.elementProperties=new Map(s.elementProperties)}static finalize(){if(this.hasOwnProperty(le("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(le("properties"))){let e=this.properties,t=[...vi(e),...bi(e)];for(let i of t)this.createProperty(i,e[i])}let s=this[Symbol.metadata];if(s!==null){let e=litPropertyMetadata.get(s);if(e!==void 0)for(let[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(let[e,t]of this.elementProperties){let i=this._$Eu(e,t);i!==void 0&&this._$Eh.set(i,e)}this.elementStyles=this.finalizeStyles(this.styles)}static finalizeStyles(s){let e=[];if(Array.isArray(s)){let t=new Set(s.flat(1/0).reverse());for(let i of t)e.unshift(ze(i))}else s!==void 0&&e.push(ze(s));return e}static _$Eu(s,e){let t=e.attribute;return t===!1?void 0:typeof t=="string"?t:typeof s=="string"?s.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(s=>this.enableUpdating=s),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(s=>s(this))}addController(s){(this._$EO??=new Set).add(s),this.renderRoot!==void 0&&this.isConnected&&s.hostConnected?.()}removeController(s){this._$EO?.delete(s)}_$E_(){let s=new Map,e=this.constructor.elementProperties;for(let t of e.keys())this.hasOwnProperty(t)&&(s.set(t,this[t]),delete this[t]);s.size>0&&(this._$Ep=s)}createRenderRoot(){let s=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return ot(s,this.constructor.elementStyles),s}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(s=>s.hostConnected?.())}enableUpdating(s){}disconnectedCallback(){this._$EO?.forEach(s=>s.hostDisconnected?.())}attributeChangedCallback(s,e,t){this._$AK(s,t)}_$ET(s,e){let t=this.constructor.elementProperties.get(s),i=this.constructor._$Eu(s,t);if(i!==void 0&&t.reflect===!0){let o=(t.converter?.toAttribute!==void 0?t.converter:ce).toAttribute(e,t.type);this._$Em=s,o==null?this.removeAttribute(i):this.setAttribute(i,o),this._$Em=null}}_$AK(s,e){let t=this.constructor,i=t._$Eh.get(s);if(i!==void 0&&this._$Em!==i){let o=t.getPropertyOptions(i),d=typeof o.converter=="function"?{fromAttribute:o.converter}:o.converter?.fromAttribute!==void 0?o.converter:ce;this._$Em=i;let p=d.fromAttribute(e,o.type);this[i]=p??this._$Ej?.get(i)??p,this._$Em=null}}requestUpdate(s,e,t,i=!1,o){if(s!==void 0){let d=this.constructor;if(i===!1&&(o=this[s]),t??=d.getPropertyOptions(s),!((t.hasChanged??we)(o,e)||t.useDefault&&t.reflect&&o===this._$Ej?.get(s)&&!this.hasAttribute(d._$Eu(s,t))))return;this.C(s,e,t)}this.isUpdatePending===!1&&(this._$ES=this._$EP())}C(s,e,{useDefault:t,reflect:i,wrapped:o},d){t&&!(this._$Ej??=new Map).has(s)&&(this._$Ej.set(s,d??e??this[s]),o!==!0||d!==void 0)||(this._$AL.has(s)||(this.hasUpdated||t||(e=void 0),this._$AL.set(s,e)),i===!0&&this._$Em!==s&&(this._$Eq??=new Set).add(s))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(e){Promise.reject(e)}let s=this.scheduleUpdate();return s!=null&&await s,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(let[i,o]of this._$Ep)this[i]=o;this._$Ep=void 0}let t=this.constructor.elementProperties;if(t.size>0)for(let[i,o]of t){let{wrapped:d}=o,p=this[i];d!==!0||this._$AL.has(i)||p===void 0||this.C(i,void 0,o,p)}}let s=!1,e=this._$AL;try{s=this.shouldUpdate(e),s?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(t){throw s=!1,this._$EM(),t}s&&this._$AE(e)}willUpdate(s){}_$AE(s){this._$EO?.forEach(e=>e.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(s)),this.updated(s)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(s){return!0}update(s){this._$Eq&&=this._$Eq.forEach(e=>this._$ET(e,this[e])),this._$EM()}updated(s){}firstUpdated(s){}};q.elementStyles=[],q.shadowRootOptions={mode:"open"},q[le("elementProperties")]=new Map,q[le("finalized")]=new Map,$i?.({ReactiveElement:q}),($e.reactiveElementVersions??=[]).push("2.1.2")});function bt(a,s){if(!Ve(a)||!a.hasOwnProperty("raw"))throw Error("invalid template strings array");return dt!==void 0?dt.createHTML(s):s}function te(a,s,e=a,t){if(s===J)return s;let i=t!==void 0?e._$Co?.[t]:e._$Cl,o=_e(s)?void 0:s._$litDirective$;return i?.constructor!==o&&(i?._$AO?.(!1),o===void 0?i=void 0:(i=new o(a),i._$AT(a,e,t)),t!==void 0?(e._$Co??=[])[t]=i:e._$Cl=i),i!==void 0&&(s=te(a,i._$AS(a,s.values),i,t)),s}var We,ct,ke,dt,mt,B,ft,wi,Y,he,_e,Ve,ki,Me,pe,pt,ht,G,_t,ut,vt,Be,n,se,us,J,_,gt,K,Ei,ue,qe,ge,ie,De,Oe,Ue,Fe,Ai,yt,Ee=y(()=>{We=globalThis,ct=a=>a,ke=We.trustedTypes,dt=ke?ke.createPolicy("lit-html",{createHTML:a=>a}):void 0,mt="$lit$",B=`lit$${Math.random().toFixed(9).slice(2)}$`,ft="?"+B,wi=`<${ft}>`,Y=document,he=()=>Y.createComment(""),_e=a=>a===null||typeof a!="object"&&typeof a!="function",Ve=Array.isArray,ki=a=>Ve(a)||typeof a?.[Symbol.iterator]=="function",Me=`[ +\f\r]`,pe=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,pt=/-->/g,ht=/>/g,G=RegExp(`>|${Me}(?:([^\\s"'>=/]+)(${Me}*=${Me}*(?:[^ +\f\r"'\`<>=]|("|')|))|$)`,"g"),_t=/'/g,ut=/"/g,vt=/^(?:script|style|textarea|title)$/i,Be=a=>(s,...e)=>({_$litType$:a,strings:s,values:e}),n=Be(1),se=Be(2),us=Be(3),J=Symbol.for("lit-noChange"),_=Symbol.for("lit-nothing"),gt=new WeakMap,K=Y.createTreeWalker(Y,129);Ei=(a,s)=>{let e=a.length-1,t=[],i,o=s===2?"":s===3?"":"",d=pe;for(let p=0;p"?(d=i??pe,m=-1):b[1]===void 0?m=-2:(m=d.lastIndex-b[2].length,g=b[1],d=b[3]===void 0?G:b[3]==='"'?ut:_t):d===ut||d===_t?d=G:d===pt||d===ht?d=pe:(d=G,i=void 0);let f=d===G&&a[p+1].startsWith("/>")?" ":"";o+=d===pe?c+wi:m>=0?(t.push(g),c.slice(0,m)+mt+c.slice(m)+B+f):c+B+(m===-2?p:f)}return[bt(a,o+(a[e]||"")+(s===2?"":s===3?"":"")),t]},ue=class a{constructor({strings:s,_$litType$:e},t){let i;this.parts=[];let o=0,d=0,p=s.length-1,c=this.parts,[g,b]=Ei(s,e);if(this.el=a.createElement(g,t),K.currentNode=this.el.content,e===2||e===3){let m=this.el.content.firstChild;m.replaceWith(...m.childNodes)}for(;(i=K.nextNode())!==null&&c.length0){i.textContent=ke?ke.emptyScript:"";for(let f=0;f2||t[0]!==""||t[1]!==""?(this._$AH=Array(t.length-1).fill(new String),this.strings=t):this._$AH=_}_$AI(s,e=this,t,i){let o=this.strings,d=!1;if(o===void 0)s=te(this,s,e,0),d=!_e(s)||s!==this._$AH&&s!==J,d&&(this._$AH=s);else{let p=s,c,g;for(s=o[0],c=0;c{let t=e?.renderBefore??s,i=t._$litPart$;if(i===void 0){let o=e?.renderBefore??null;t._$litPart$=i=new ge(s.insertBefore(he(),o),o,void 0,e??{})}return i._$AI(a),i}});var Ge,$,Si,xt=y(()=>{de();de();Ee();Ee();Ge=globalThis,$=class extends q{constructor(){super(...arguments),this.renderOptions={host:this},this._$Do=void 0}createRenderRoot(){let s=super.createRenderRoot();return this.renderOptions.renderBefore??=s.firstChild,s}update(s){let e=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),super.update(s),this._$Do=yt(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return J}};$._$litElement$=!0,$.finalized=!0,Ge.litElementHydrateSupport?.({LitElement:$});Si=Ge.litElementPolyfillSupport;Si?.({LitElement:$});(Ge.litElementVersions??=[]).push("4.2.2")});var $t=y(()=>{});var T=y(()=>{de();Ee();xt();$t()});var wt=y(()=>{});function v(a){return(s,e)=>typeof e=="object"?Ci(a,s,e):((t,i,o)=>{let d=i.hasOwnProperty(o);return i.constructor.createProperty(o,t),d?Object.getOwnPropertyDescriptor(i,o):void 0})(a,s,e)}var Ti,Ci,Ke=y(()=>{de();Ti={attribute:!0,type:String,converter:ce,reflect:!1,hasChanged:we},Ci=(a=Ti,s,e)=>{let{kind:t,metadata:i}=e,o=globalThis.litPropertyMetadata.get(i);if(o===void 0&&globalThis.litPropertyMetadata.set(i,o=new Map),t==="setter"&&((a=Object.create(a)).wrapped=!0),o.set(e.name,a),t==="accessor"){let{name:d}=e;return{set(p){let c=s.get.call(this);s.set.call(this,p),this.requestUpdate(d,c,a,!0,p)},init(p){return p!==void 0&&this.C(d,void 0,a,p),p}}}if(t==="setter"){let{name:d}=e;return function(p){let c=this[d];s.call(this,p),this.requestUpdate(d,c,a,!0,p)}}throw Error("Unsupported decorator location: "+t)}});function h(a){return v({...a,state:!0,attribute:!1})}var kt=y(()=>{Ke();});var Et=y(()=>{});var re=y(()=>{});var At=y(()=>{re();});var St=y(()=>{re();});var Tt=y(()=>{re();});var Ct=y(()=>{re();});var It=y(()=>{re();});var j=y(()=>{wt();Ke();kt();Et();At();St();Tt();Ct();It()});var Ht,Lt=y(()=>{Ht={maintenance:"Maintenance",objects:"Objects",tasks:"Tasks",overdue:"Overdue",due_soon:"Due Soon",triggered:"Triggered",trigger_replaced:"Trigger replaced",ok:"OK",all:"All",new_object:"+ New Object",templates_from:"From template",templates_title:"Start from a template",templates_task_count:"{n} tasks",template_created:"Created from template",onboard_hint:"Add your first object to start tracking maintenance.",edit:"Edit",duplicate:"Duplicate",task_duplicated:"Task duplicated",object_duplicated:"Object duplicated",delete:"Delete",add_task:"+ Add Task",complete:"Complete",completed:"Completed",skip:"Skip",skipped:"Skipped",missed:"Missed",reset:"Reset",snooze:"Snooze",snoozed:"Snoozed",cancel:"Cancel",bulk_select:"Select",bulk_select_all:"Select all",bulk_n_selected:"{n} selected",bulk_completed:"{n} tasks completed",bulk_archived:"{n} tasks archived",completing:"Completing\u2026",interval:"Interval",warning:"Warning",last_performed:"Last performed",next_due:"Next due",days_until_due:"Days until due",avg_duration:"Avg duration",trigger:"Trigger",trigger_type:"Trigger type",threshold_above:"Upper limit",threshold_below:"Lower limit",threshold:"Threshold",counter:"Counter",state_change:"State change",runtime:"Runtime",runtime_hours:"Target runtime (hours)",target_value:"Target value",baseline:"Baseline",target_changes:"Target changes",for_minutes:"For (minutes)",time_based:"Time-based",sensor_based:"Sensor-based",manual:"Manual",one_time:"One-time",weekdays:"Weekdays",nth_weekday:"Nth weekday of month",day_of_month:"Day of month",recurrence_on_days:"Repeat on",recurrence_occurrence:"Occurrence",recurrence_weekday:"Weekday",recurrence_day:"Day of month (1\u201331)",recurrence_last_day:"Last day of the month",recurrence_business_day:"Business days only (roll back from weekend)",recurrence_offset:"Offset (days, \xB1)",recurrence_offset_help:"Shift the date by \xB1N days, e.g. -2 = two days before.",last_day_month:"Last day of month",last_business_day_month:"Last business day",ord_1:"1st",ord_2:"2nd",ord_3:"3rd",ord_4:"4th",ord_5:"5th",ord_last:"Last",day_word:"Day",interval_value:"Interval",interval_unit:"Unit",unit_days:"Days",unit_weeks:"Weeks",unit_months:"Months",unit_years:"Years",due_date:"Due date",cleaning:"Cleaning",inspection:"Inspection",replacement:"Replacement",calibration:"Calibration",service:"Service",reading:"Reading",custom:"Custom",history:"History",cost:"Cost",report_button:"Report",report_title:"Maintenance report",report_generated:"Generated",report_times_done:"Done",report_total_cost:"Total cost",report_every:"every {n} {unit}",report_notes:"Notes",report_col_type:"Type",report_col_status:"Status",report_col_schedule:"Schedule",duration:"Duration",both:"Both",trigger_val:"Trigger value",complete_title:"Complete: ",checklist:"Checklist",checklist_steps_optional:"Checklist steps (optional)",checklist_placeholder:`Clean filter +Replace seal +Test pressure`,checklist_help:"One step per line. Max 100 items.",err_too_long:"{field}: too long (max {n} characters)",err_too_short:"{field}: too short (min {n} characters)",err_value_too_high:"{field}: too large (max {n})",err_value_too_low:"{field}: too small (min {n})",err_required:"{field}: required",err_wrong_type:"{field}: wrong type (expected: {type})",err_invalid_choice:"{field}: not an allowed value",err_invalid_value:"{field}: invalid value",feat_schedule_time:"Time-of-day scheduling",feat_schedule_time_desc:"Tasks become overdue at a specific time of day instead of midnight.",schedule_time_optional:"Due at time (optional, HH:MM)",schedule_time_help:"Empty = midnight (default). HA timezone.",at_time:"at",notes_optional:"Notes (optional)",cost_optional:"Cost (optional)",duration_minutes:"Duration in minutes (optional)",days:"days",day:"day",today:"Today",d_overdue:"d overdue",no_tasks:"No maintenance tasks yet. Create an object to get started.",no_tasks_short:"No tasks",no_history:"No history entries yet.",show_all:"Show all",cost_duration_chart:"Cost & Duration",installed:"Installed",confirm_delete_object:"Delete this object and all its tasks?",confirm_delete_task:"Delete this task?",min:"Min",max:"Max",save:"Save",saving:"Saving\u2026",edit_task:"Edit Task",new_task:"New Maintenance Task",task_name:"Task name",maintenance_type:"Maintenance type",priority:"Priority",labels:"Labels",labels_placeholder:"e.g. safety, seasonal, tenant-visible",labels_help:"Comma-separated tags for filtering and reporting.",priority_low:"Low",priority_normal:"Normal",priority_high:"High",schedule_type:"Schedule type",interval_days:"Interval (days)",warning_days:"Warning days",earliest_completion_days:"Earliest completion (days before due)",earliest_completion_days_help:"Leave empty to allow completing any time. 0 = only on/after the due date.",last_performed_optional:"Last performed (optional)",interval_anchor:"Interval anchor",anchor_completion:"From completion date",anchor_planned:"From planned date (no drift)",edit_object:"Edit Object",name:"Name",manufacturer_optional:"Manufacturer (optional)",model_optional:"Model (optional)",serial_number_optional:"Serial number (optional)",serial_number_label:"S/N",documentation_url_label:"Manual",object_notes_label:"Notes",sort_due_date:"Due date",sort_object:"Object name",sort_type:"Type",sort_task_name:"Task name",all_objects:"All objects",tasks_lower:"tasks",no_tasks_yet:"No tasks yet",add_first_task:"Add first task",trigger_configuration:"Trigger Configuration",entity_id:"Entity ID",comma_separated:"comma-separated",entity_logic:"Entity logic",entity_logic_any:"Any entity triggers",entity_logic_all:"All entities must trigger",entities:"entities",attribute_optional:"Attribute (optional, blank = state)",use_entity_state:"Use entity state (no attribute)",trigger_above:"Trigger above",trigger_below:"Trigger below",for_at_least_minutes:"For at least (minutes)",safety_interval_days:"Safety interval (days, optional)",safety_interval:"Safety interval (optional)",delta_mode:"Delta mode",from_state_optional:"From state (optional)",to_state_optional:"To state (optional)",documentation_url_optional:"Documentation URL (optional)",object_notes_optional:"Notes (optional)",nfc_tag_id_optional:"NFC Tag ID (optional)",nfc_tags_empty_help:"No NFC tags registered in Home Assistant yet.",nfc_tags_open_settings:"Open Tags settings",nfc_tags_refresh:"Refresh",environmental_entity_optional:"Environmental sensor (optional)",environmental_entity_helper:"e.g. sensor.outdoor_temperature \u2014 adjusts the interval based on environmental conditions",environmental_attribute_optional:"Environmental attribute (optional)",nfc_tag_id:"NFC Tag ID",nfc_linked:"NFC tag linked",nfc_link_hint:"Click to link NFC tag",responsible_user:"Responsible User",shared_with:"Shared with (rotation)",shared_with_help:"Pick multiple people to share this task; the responsible person rotates on each completion.",rotation_strategy:"Rotation",rotation_none:"No rotation",rotation_round_robin:"Round-robin",rotation_least_completed:"Least completed",rotation_random:"Random",no_user_assigned:"(No user assigned)",all_users:"All Users",my_tasks:"My Tasks",tab_calendar:"Calendar",cal_no_events:"No maintenance",cal_window_7:"7 days",cal_window_14:"14 days",cal_window_30:"30 days",cal_window_365:"1 year",cal_every_n_days:"every {n} days",cal_source_time:"Time-based",cal_source_time_adaptive:"Time-based (adaptive)",cal_source_sensor:"Sensor-based",cal_predicted:"predicted",cal_confidence_high:"high confidence",cal_confidence_medium:"medium confidence",cal_confidence_low:"low confidence",budget_monthly:"Monthly budget",budget_yearly:"Yearly budget",groups:"Groups",new_group:"New group",edit_group:"Edit group",no_groups:"No groups yet",delete_group:"Delete group",delete_group_confirm:"Delete group '{name}'?",group_select_tasks:"Select tasks",group_name_required:"Name is required",description_optional:"Description (optional)",selected:"Selected",loading_chart:"Loading chart data...",hide_outliers:"Hide outliers (sensor glitches)",was_maintenance_needed:"Was this maintenance needed?",feedback_needed:"Needed",feedback_not_needed:"Not needed",feedback_not_sure:"Not sure",suggested_interval:"Suggested interval",apply_suggestion:"Apply",reanalyze:"Re-analyze",reanalyze_result:"New analysis",reanalyze_insufficient_data:"Not enough data to produce a recommendation",data_points:"data points",dismiss_suggestion:"Dismiss",confidence_low:"Low",confidence_medium:"Medium",confidence_high:"High",recommended:"recommended",seasonal_awareness:"Seasonal Awareness",edit_seasonal_overrides:"Edit seasonal factors",seasonal_overrides_title:"Seasonal factors (override)",seasonal_overrides_hint:"Factor per month (0.1\u20135.0). Empty = learned automatically.",seasonal_override_invalid:"Invalid value",seasonal_override_range:"Factor must be between 0.1 and 5.0",clear_all:"Clear all",seasonal_chart_title:"Seasonal Factors",seasonal_learned:"Learned",seasonal_manual:"Manual",month_jan:"Jan",month_feb:"Feb",month_mar:"Mar",month_apr:"Apr",month_may:"May",month_jun:"Jun",month_jul:"Jul",month_aug:"Aug",month_sep:"Sep",month_oct:"Oct",month_nov:"Nov",month_dec:"Dec",sensor_prediction:"Sensor Prediction",degradation_trend:"Trend",trend_rising:"Rising",trend_falling:"Falling",trend_stable:"Stable",trend_insufficient_data:"Insufficient data",days_until_threshold:"Days until threshold",threshold_exceeded:"Threshold exceeded",environmental_adjustment:"Environmental factor",sensor_prediction_urgency:"Sensor predicts threshold in ~{days} days",day_short:"day",weibull_reliability_curve:"Reliability Curve",weibull_failure_probability:"Failure Probability",weibull_r_squared:"Fit R\xB2",beta_early_failures:"Early Failures",beta_random_failures:"Random Failures",beta_wear_out:"Wear-out",beta_highly_predictable:"Highly Predictable",confidence_interval:"Confidence Interval",confidence_conservative:"Conservative",confidence_aggressive:"Optimistic",current_interval_marker:"Current interval",recommended_marker:"Recommended",characteristic_life:"Characteristic life",chart_mini_sparkline:"Trend sparkline",chart_history:"Cost and duration history",chart_seasonal:"Seasonal factors, 12 months",chart_weibull:"Weibull reliability curve",chart_sparkline:"Sensor trigger value chart",days_progress:"Days progress",qr_code:"QR Code",qr_generating:"Generating QR code\u2026",qr_error:"Failed to generate QR code.",qr_error_no_url:"No HA URL configured. Please set an external or internal URL in Settings \u2192 System \u2192 Network.",save_error:"Failed to save. Please try again.",qr_print:"Print",qr_download:"Download SVG",qr_action:"Action on scan",qr_action_view:"View maintenance info",qr_action_complete:"Mark maintenance as complete",qr_url_mode:"Link type",qr_mode_companion:"Companion App",qr_mode_local:"Local (mDNS)",qr_mode_server:"Server URL",overview:"Overview",analysis:"Analysis",recent_activities:"Recent Activities",search_notes:"Search notes",avg_cost:"Avg Cost",no_advanced_features:"No advanced features enabled",no_advanced_features_hint:"Enable \u201CAdaptive Intervals\u201D or \u201CSeasonal Patterns\u201D in the integration settings to see analysis data here.",analysis_not_enough_data:"Not enough data for analysis yet.",analysis_not_enough_data_hint:"Weibull analysis requires at least 5 completed maintenances; seasonal patterns become visible after 6+ data points per month.",analysis_manual_task_hint:"Manual tasks without an interval do not generate analysis data.",completions:"completions",current:"Current",shorter:"Shorter",longer:"Longer",normal:"Normal",disabled:"Disabled",compound_logic:"Compound logic",compound:"Compound (multiple conditions)",compound_logic_and:"AND \u2014 all conditions must trigger",compound_logic_or:"OR \u2014 any condition triggers",compound_help:"Combine several sensor conditions into one trigger.",compound_no_conditions:"No conditions yet \u2014 add at least one.",compound_add_condition:"Add condition",compound_condition:"Condition",compound_remove_condition:"Remove condition",card_title:"Title",card_show_header:"Show header with statistics",card_show_actions:"Show action buttons",card_compact:"Compact mode",card_max_items:"Max items (0 = all)",card_filter_status:"Filter by status",card_filter_status_help:"Empty = show all statuses.",card_filter_objects:"Filter by objects",card_filter_objects_help:"Empty = show all objects.",card_filter_entities:"Filter by entities (entity_ids)",card_filter_entities_help:"Pick sensor / binary_sensor entities from this integration. Empty = all.",card_loading_objects:"Loading objects\u2026",card_load_error:"Could not load objects \u2014 check the WebSocket connection.",card_no_tasks_title:"No maintenance tasks yet",card_no_tasks_cta:"\u2192 Create one in the Maintenance panel",no_objects:"No objects yet.",action_error:"Action failed. Please try again.",area_id_optional:"Area (optional)",installation_date_optional:"Installation date (optional)",warranty_expiry_optional:"Warranty expiry (optional)",warranty:"Warranty",warranty_valid_until:"valid until {date}",warranty_expires_in:"expires in {days} days",warranty_expired:"expired",cal_past_windows:"Past windows",cal_forward_windows:"Forward windows",history_edit_title:"Edit history entry",history_edit_timestamp:"Timestamp",manufacturer:"Manufacturer",model:"Model",area:"Area",actions:"Actions",view_mode_label:"View",view_cards:"Card view",view_table:"Table view",objects_table_columns_label:"Objects table columns",objects_table_columns_hint:"Choose which columns appear in the objects table view.",custom_icon_optional:"Icon (optional, e.g. mdi:wrench)",task_enabled:"Task enabled",skip_reason_prompt:"Skip this task?",reason_optional:"Reason (optional)",reset_date_prompt:"Mark task as performed?",reset_date_optional:"Last performed date (optional, defaults to today)",notes_label:"Notes",documentation_label:"Documentation",no_nfc_tag:"\u2014 No tag \u2014",dashboard:"Dashboard",tab_today:"Today",palette_placeholder:"Search objects and tasks\u2026",palette_no_results:"No matches",palette_hint:"\u2191\u2193 to navigate \xB7 Enter to open \xB7 Esc to close",today_all_caught_up:"All caught up! Nothing due this week.",today_overdue:"Overdue",today_due_today:"Due today",today_this_week:"This week",settings:"Settings",settings_features:"Advanced Features",settings_features_desc:"Enable or disable advanced features. Disabling hides them from the UI but does not delete data.",feat_adaptive:"Adaptive Scheduling",feat_adaptive_desc:"Learn optimal intervals from maintenance history",feat_predictions:"Sensor Predictions",feat_predictions_desc:"Predict trigger dates from sensor degradation",feat_seasonal:"Seasonal Adjustments",feat_seasonal_desc:"Adjust intervals based on seasonal patterns",feat_environmental:"Environmental Correlation",feat_environmental_desc:"Correlate intervals with temperature/humidity",feat_budget:"Budget Tracking",feat_budget_desc:"Track monthly and yearly maintenance spending",feat_groups:"Task Groups",feat_groups_desc:"Organize tasks into logical groups",feat_checklists:"Checklists",feat_checklists_desc:"Multi-step procedures for task completion",settings_general:"General",settings_default_warning:"Default warning days",settings_panel_enabled:"Sidebar panel",settings_panel_title:"Sidebar panel title",settings_notifications:"Notifications",settings_notify_service:"Notification service",test_notification:"Test notification",send_test:"Send test",testing:"Sending\u2026",test_notification_success:"Test notification sent",test_notification_failed:"Test notification failed",settings_notify_due_soon:"Notify when due soon",settings_notify_overdue:"Notify when overdue",settings_notify_triggered:"Notify when triggered",settings_interval_hours:"Repeat interval (hours, 0 = once)",settings_quiet_hours:"Quiet hours",settings_quiet_start:"Start",settings_quiet_end:"End",settings_max_per_day:"Max notifications per day (0 = unlimited)",settings_bundling:"Bundle notifications",settings_bundle_threshold:"Bundle threshold",settings_reminder_leads:"Extra reminders (days before due)",settings_reminder_leads_hint:"Comma-separated lead times, e.g. 14, 3, 0 \u2014 one extra reminder fires on each matching day. Empty = off.",settings_actions:"Mobile Action Buttons",settings_action_complete:"Show 'Complete' button",settings_action_skip:"Show 'Skip' button",settings_action_snooze:"Show 'Snooze' button",settings_weekly_digest:"Weekly digest",settings_weekly_digest_hint:"A single summary notification on Monday morning when tasks are due.",settings_warranty_reminder:"Warranty expiry reminder",settings_warranty_reminder_days:"Days before expiry",settings_warranty_reminder_hint:"Notify once when an object's warranty is this many days from expiring.",settings_snooze_hours:"Snooze duration (hours)",settings_budget:"Budget",settings_currency:"Currency",settings_budget_monthly:"Monthly budget",settings_budget_yearly:"Yearly budget",settings_budget_alerts:"Budget alerts",settings_budget_threshold:"Alert threshold (%)",settings_import_export:"Import / Export",settings_export_json:"Export JSON",settings_export_yaml:"Export YAML",settings_export_csv:"Export CSV",settings_import_csv:"Import CSV",settings_import_placeholder:"Paste JSON or CSV content here\u2026",settings_import_btn:"Import",settings_import_success:"{count} objects imported successfully.",settings_export_success:"Export downloaded.",settings_saved:"Setting saved.",settings_include_history:"Include history",sort_alphabetical:"Alphabetical",sort_due_soonest:"Due soonest",sort_task_count:"Task count",sort_area:"Area",sort_assigned_user:"Assigned user",sort_group:"Group",groupby_none:"No grouping",groupby_area:"By area",groupby_group:"By group",groupby_user:"By user",filter_label:"Filter",user_label:"User",sort_label:"Sort",group_by_label:"Group by",state_value_help:'Use the HA state value (usually lowercase, e.g. "on"/"off"). Case is normalised on save.',target_changes_help:"Number of matching transitions before the trigger fires (default: 1).",qr_print_title:"Print QR codes",qr_print_desc:"Generate a printable page of QR codes to cut out and stick on your equipment.",qr_print_load:"Load objects",qr_print_filter:"Filter",qr_print_objects:"Objects",qr_print_actions:"Actions",qr_print_url_mode:"Link type",qr_print_estimate:"Estimated QR codes",qr_print_over_limit:"cap is 200, narrow the filter",qr_print_generate:"Generate QR codes",qr_print_generating:"Generating\u2026",qr_print_ready:"QR codes ready",qr_print_print_button:"Print",qr_print_empty:"Nothing to generate",qr_action_skip:"Skip",vacation_title:"Vacation mode",vacation_active:"active",vacation_ended:"ended",vacation_desc:"Plan a vacation: notifications are paused during the period plus a buffer of days. You can opt specific tasks back in.",vacation_enable:"Enable vacation mode",vacation_start:"Start",vacation_end:"End",vacation_buffer:"Buffer (days)",vacation_exempt_title:"Notify anyway during vacation",vacation_exempt_desc:"Pick tasks that should still notify during vacation (e.g. critical pool chemistry).",vacation_load_tasks:"Load tasks",vacation_preview_btn:"Show preview",vacation_preview_affected:"tasks affected",vacation_event_due_soon:"becomes due soon",vacation_event_overdue:"becomes overdue",vacation_event_triggered_est:"sensor trigger possible",vacation_sensor_based:"(sensor-based)",vacation_action_notify:"Notify anyway",vacation_action_unsilence:"Silence again",vacation_marked_complete:"Marked complete",vacation_marked_skip:"Skipped",vacation_end_now:"End vacation now",add:"Add",show_stats:"Show stats + graphs",hide_stats:"Hide stats",adaptive_no_data:"Not enough completion history yet for adaptive analysis. Complete this task a few more times to unlock interval recommendations and reliability charts.",suggestion_applied:"Suggested interval applied",vacation_mode:"Vacation mode",vacation_status_active:"Active now",vacation_status_scheduled:"Scheduled",vacation_status_inactive:"Inactive",vacation_end_now_confirm:"End vacation immediately?",vacation_exempt_count:"exempt",vacation_advanced:"Advanced\u2026",vacation_open_panel:"Open in panel",enable:"Enable",saved:"Saved",budget_monthly_set:"Set monthly",budget_yearly_set:"Set yearly",budget_advanced:"Currency, alerts\u2026",budget_open_panel:"Open in panel",groups_empty:"No groups yet.",group_new_placeholder:"Add group\u2026",group_delete_confirm:'Delete group "{name}"?',groups_manage_tasks:"Manage task assignments\u2026",groups_open_panel:"Open in panel",unassigned:"Unassigned",no_area:"No area",has_overdue:"Has overdue tasks",object:"Object",settings_panel_access:"Panel access",settings_panel_access_desc:"Admins always have full access. To delegate create, edit and delete to specific non-admins, switch this on and pick them below \u2014 everyone else sees only Complete and Skip.",settings_operator_write:"Allow selected users to create, edit & delete",settings_operator_write_desc:"Off: only admins can change content. On: the selected users below get full access too.",no_non_admin_users:"No non-admin users found. Add some in Settings \u2192 People.",owner_label:"Owner",feat_completion_actions:"Completion actions",feat_completion_actions_desc:"Per-task HA action on complete + quick-complete QR with pre-set values.",on_complete_action_title:"On complete: trigger HA action (optional)",on_complete_action_desc:"Calls an HA service when the task is completed \u2014 e.g. reset a counter on the device.",on_complete_action_service:"Service",on_complete_action_target:"Target entity",on_complete_action_target_hint:"Note: the entity domain must match the service \u2014 e.g. 'button.press' only works on button.*, 'counter.increment' only on counter.*, 'input_button.press' only on input_button.* etc. On a mismatch the action will silently fail (HA logs 'Referenced entities ... missing or not currently available').",on_complete_action_data:"Data (JSON, optional)",on_complete_action_test:"Validate configuration",on_complete_action_test_success:"\u2713 Configuration valid (action will fire only on task completion)",on_complete_action_test_failed:"Failed",quick_complete_defaults_title:"Quick-complete defaults (for QR scans, optional)",quick_complete_defaults_desc:"Pre-set values for quick-complete QR scans. Without these, the QR opens the complete dialog.",quick_complete_defaults_notes:"Notes",quick_complete_defaults_cost:"Cost",quick_complete_defaults_duration:"Duration (minutes)",quick_complete_defaults_feedback_none:"No feedback",quick_complete_defaults_feedback_needed:"Was needed",quick_complete_defaults_feedback_not_needed:"Not needed",quick_complete_success:"Quickly marked complete",show_all_objects:"Show all objects",show_all_tasks:"Clear filter \u2014 show all tasks",filter_to_overdue:"Filter task list to overdue only",filter_to_due_soon:"Filter task list to due-soon only",filter_to_triggered:"Filter task list to triggered only",open_task:"Open task",show_details:"Show history + stats",hide_details:"Hide details",history_empty:"No history yet.",history_edit_button:"Edit entry",total_cost:"Total cost",times_performed:"Performed",older_entries:"older",open_in_panel:"Open in Maintenance panel",skip_reason:"Skip reason (optional)",reset_to_date:"Reset last_performed to",delete_task_confirm:"Delete this task and its history?",delete_object_confirm:"Delete this object and all its tasks?",loading:"Loading\u2026",archive:"Archive",undo:"Undo",task_archived:"Task archived",object_archived:"Object archived",unarchive:"Unarchive",archived:"Archived",show_archived:"Show archived",hide_archived:"Hide archived",confirm_archive_object:"Archive this object and its tasks? They keep their history and can be unarchived later.",settings_archive:"Archive & Retention",settings_archive_desc:"Retire completed one-off tasks without deleting them. Archived items are hidden and inert but keep their history and cost.",settings_archive_oneoff_days:"Auto-archive completed one-off tasks after (days, 0 = off)",settings_delete_archived_oneoff_days:"Auto-delete archived one-off tasks after (days, 0 = never)",archive_object:"Archive object",unarchive_object:"Unarchive object",documents:"Documents",documents_empty:"No documents yet.",doc_upload:"Upload file",doc_uploading:"Uploading\u2026",doc_add_link:"Add link",doc_link_url:"URL (https://\u2026)",doc_link_title:"Title (optional)",doc_open:"Open",doc_delete_confirm:'Delete "{name}"?',doc_too_large:"File is too large (max 25 MB).",doc_upload_failed:"Upload failed.",completion_photo_optional:"Completion photo (optional)",add_photo:"Add photo",uploading:"Uploading\u2026",remove:"Remove",doc_deduped:"Already stored elsewhere \u2014 shared, no extra space used.",doc_dup_in_object:"This file is already attached to this object.",doc_link_invalid:"Only http/https links are allowed.",doc_cat_manual:"Manual",doc_cat_warranty:"Warranty",doc_cat_invoice:"Invoice",doc_cat_spare_parts:"Spare parts",doc_cat_photo:"Photo",doc_cat_other:"Other",doc_link_badge:"Link",doc_storage_title:"Document storage",doc_storage_saved:"Saved via deduplication",doc_storage_refresh:"Refresh",doc_download:"Download",doc_close:"Close",doc_camera:"Take photo",doc_drop_hint:"Drop files here",doc_task_none:"No documents linked to this task.",doc_link_existing:"Link a document\u2026",doc_attach:"Link",doc_unlink:"Unlink",doc_page:"Page",chart_range_7d:"7d",chart_range_30d:"30d",chart_range_90d:"90d",chart_range_1y:"1y",chart_since_service:"since last service",chart_no_stats:"No long-term statistics for this entity \u2014 showing maintenance-event values only",auto_complete_on_recovery:"Auto-complete when the sensor recovers",auto_complete_on_recovery_help:"Records a completion (sets last performed) when the trigger clears itself \u2014 e.g. salt refilled, filter replaced.",doc_search:"Search documents\u2026",doc_search_none:"No matching documents",link_device_optional:"Link to existing device (optional)",parent_object_optional:"Parent object (optional)",parent_none:"(No parent)",paused:"Paused",pause_object:"Pause",resume_object:"Resume",pause_until_prompt:"Freeze this object's schedules \u2014 nothing becomes due and nothing notifies until it is resumed. Optionally set an auto-resume date.",pause_until_label:"Resume on (optional)",object_paused:"Object paused",object_resumed:"Object resumed \u2014 schedules restarted",object_paused_badge:"Paused",paused_until_label:"until",replace_object:"Replace\u2026",replace_object_prompt:"Retire this object and create a successor. History and costs stay archived on the old one; tasks and documents carry over to the new one, counters start fresh.",replace_name_label:"Successor name",object_replaced:"Object replaced \u2014 successor created",reading_unit_label:"Reading unit (e.g. kWh, m\xB3)",reading_unit_help:"Shown next to the recorded value when completing this task.",reading_value_label:"Reading value",reading_label:"Reading",settings_templates_label:"Template gallery",settings_templates_hint:`Untick templates you'll never need \u2014 they disappear from the "From template" pickers (panel and config flow). Nothing else changes; you can re-enable them any time.`,worksheet:"Work sheet",worksheet_scan_view:"Scan to open the task",worksheet_scan_complete:"Scan to complete",worksheet_manual_excerpt:"Manual excerpt",worksheet_pages:"pages",worksheet_printed:"Printed",worksheet_never:"Never"}});function Je(a){return(a||Ye).substring(0,2).toLowerCase()}function r(a,s){let e=Je(s);return Z[e]?.[a]??Z.en[a]??a}function Te(a){let s=Je(a);return s===Ye||s in Z}function Ce(a){let s=Je(a);return s===Ye||s in Z||!Li.has(s)?Promise.resolve():(s in me||(me[s]=fetch(`${Hi}/${s}.json`).then(e=>e.ok?e.json():null).then(e=>{e?Z[s]=e:delete me[s]}).catch(()=>{delete me[s]})),me[s])}function Pt(a){let s=(a||"en").substring(0,2).toLowerCase();return{de:"de-DE",en:"en-US",nl:"nl-NL",fr:"fr-FR",it:"it-IT",es:"es-ES",pt:"pt-PT",ru:"ru-RU",uk:"uk-UA",zh:"zh-CN",da:"da-DK",fi:"fi-FI",nb:"nb-NO",ja:"ja-JP",hi:"hi-IN"}[s]??"en-US"}function Q(a,s){if(!a)return"\u2014";try{let e=a.includes("T")?a:a+"T00:00:00";return new Date(e).toLocaleDateString(Pt(s),{day:"2-digit",month:"2-digit",year:"numeric"})}catch{return a}}function zt(a,s){if(!a)return"\u2014";try{let e=Pt(s),t=new Date(a);return t.toLocaleDateString(e,{day:"2-digit",month:"2-digit",year:"numeric"})+" "+t.toLocaleTimeString(e,{hour:"2-digit",minute:"2-digit"})}catch{return a}}function Rt(a,s,e){return a==null?"\u2014":`${a} ${r("unit_"+(s||"days"),e)}`}function Se(a,s,e="long"){let t=(s||"en").substring(0,2);return new Date(Date.UTC(2024,0,1+a)).toLocaleDateString(t,{weekday:e,timeZone:"UTC"})}function Nt(a,s){let e=a.schedule,t=e?.offset?` ${e.offset>0?"+":"\u2212"}${Math.abs(e.offset)}d`:"";switch(e?.kind){case"weekdays":return((e.weekdays||[]).map(i=>Se(i,s,"short")).join(" & ")||"\u2014")+t;case"nth_weekday":return e.weekday==null||e.nth==null?"\u2014":`${e.nth===-1?r("ord_last",s):r("ord_"+e.nth,s)} ${Se(e.weekday,s,"long")}${t}`;case"day_of_month":return e.day==null?"\u2014":(e.day===-1?r(e.business?"last_business_day_month":"last_day_month",s):`${r("day_word",s)} ${e.day}`)+t;case"one_time":return a.due_date?Q(a.due_date,s):r("one_time",s);case"manual":return r("manual",s);case"interval":return Rt(e.every,e.unit,s)}return a.schedule_type==="one_time"?a.due_date?Q(a.due_date,s):r("one_time",s):a.schedule_type==="manual"?r("manual",s):a.schedule_type==="sensor_based"?r("sensor_based",s):a.interval_days!=null?Rt(a.interval_days,a.interval_unit,s):"\u2014"}function Mt(a,s){a.currentTarget.dispatchEvent(new CustomEvent("hass-more-info",{detail:{entityId:s},bubbles:!0,composed:!0}))}var ae,Ye,jt,Z,Li,Hi,me,Ie,C=y(()=>{"use strict";T();Lt();ae={ok:"var(--success-color, #4caf50)",due_soon:"var(--warning-color, #ff9800)",overdue:"var(--error-color, #f44336)",triggered:"var(--deep-orange-color, #ff5722)",archived:"var(--disabled-color, #9e9e9e)",paused:"var(--info-color, #2196f3)"},Ye="en",jt=(()=>{let a=window;return a.__msLocales||(a.__msLocales={store:{},inflight:{}}),a.__msLocales})(),Z=jt.store;Z.en||(Z.en=Ht);Li=new Set(["de","nl","fr","it","es","pt","ru","uk","pl","cs","sv","zh","da","fi","nb","ja","hi"]),Hi="/maintenance_supporter_locales",me=jt.inflight;Ie=E` + :host { + --maint-ok-color: var(--success-color, #4caf50); + --maint-due-soon-color: var(--warning-color, #ff9800); + --maint-overdue-color: var(--error-color, #f44336); + --maint-triggered-color: #ff5722; + } + + .status-badge { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 4px; + padding: 2px 8px; + border-radius: 12px; + font-size: 12px; + font-weight: 500; + color: white; + white-space: nowrap; + /* Fixed minimum so OK / Due Soon / Overdue / Triggered pills are uniform + width in the task table — keeps the object-name column aligned. */ + min-width: 70px; + box-sizing: border-box; + } + /* Shape icon so status is not conveyed by colour alone (accessibility). */ + .status-badge ha-icon { --mdc-icon-size: 14px; margin-left: -1px; } + + .status-badge.ok { background-color: var(--maint-ok-color); } + .status-badge.due_soon { background-color: var(--maint-due-soon-color); } + .status-badge.overdue { background-color: var(--maint-overdue-color); } + .status-badge.triggered { background-color: var(--maint-triggered-color); } + /* Completed one-time task ("done") — muted blue-grey. */ + .status-badge.done { background-color: var(--maint-done-color, #78909c); } + /* v2.10.0: archived (retire-but-retain) — neutral grey, clearly inert. */ + .status-badge.archived { background-color: var(--disabled-color, #9e9e9e); } + /* v2.20 (N3): paused — frozen but present, info blue. */ + .status-badge.paused { background-color: var(--info-color, #2196f3); } + + /* v1.4.7: 5-column grid so all 5 KPIs (Objects/Tasks/Overdue/Due Soon/ + Triggered) always stay in one row. The previous flex-wrap layout was + wrapping the 5th item (Triggered, the widest label) onto its own row + on narrow viewports because the natural width of the items pushed past + the container width. Grid forces equal 1/5 distribution regardless of + label length. */ + .stats-bar { + display: grid; + grid-template-columns: repeat(5, 1fr); + gap: 16px; + padding: 16px; + } + + .stat-item { + display: flex; + flex-direction: column; + align-items: center; + min-width: 0; + } + .stat-item .stat-label { + /* Allow long labels to ellipsis rather than overflow the grid cell. */ + max-width: 100%; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .stat-item.clickable { cursor: pointer; border-radius: 8px; padding: 4px 8px; transition: background 0.15s, box-shadow 0.15s; } + .stat-item.clickable:hover { background: var(--secondary-background-color); } + /* v2.1.0 — KPIs that map to a status filter highlight when active so the + user can see at a glance which filter is on, even after scrolling away. */ + .stat-item.clickable.active { + background: var(--secondary-background-color); + box-shadow: inset 0 -3px 0 var(--primary-color); + } + + .objects-grid { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(260px, 1fr)); + gap: 16px; + padding: 16px 0; + } + .object-card { + padding: 16px; + background: var(--card-background-color); + border-radius: 8px; + cursor: pointer; + border: 1px solid var(--divider-color); + transition: transform 0.15s, box-shadow 0.15s; + /* Large installs (100+ objects): skip rendering off-screen cards. The + intrinsic size keeps the scrollbar stable while they're skipped. */ + content-visibility: auto; + contain-intrinsic-size: auto 120px; + } + .object-card:hover { transform: translateY(-2px); box-shadow: 0 4px 12px rgba(0,0,0,0.1); } + .object-card-header { display: flex; justify-content: space-between; align-items: center; } + .object-card-name { font-weight: 500; font-size: 16px; } + .object-card-count { color: var(--secondary-text-color); font-size: 13px; } + .object-card-meta { color: var(--secondary-text-color); font-size: 13px; margin-top: 4px; } + .object-card-empty { color: var(--warning-color); font-size: 13px; margin-top: 8px; font-style: italic; } + + /* Overdue indicator dot on object cards (#35) */ + .object-card { position: relative; } + .object-card-overdue { border-left: 3px solid var(--error-color); } + .overdue-dot { + position: absolute; + top: 12px; + right: 12px; + width: 10px; + height: 10px; + border-radius: 50%; + background: var(--error-color); + box-shadow: 0 0 0 2px var(--card-background-color); + } + + /* Group-by collapsible sections (#35 + #36) */ + .group-section { + margin: 12px 0; + border: 1px solid var(--divider-color); + border-radius: 8px; + background: var(--card-background-color); + } + .group-section[open] { padding-bottom: 8px; } + .group-section-header { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 16px; + cursor: pointer; + font-weight: 500; + list-style: none; + user-select: none; + } + .group-section-header::-webkit-details-marker { display: none; } + .group-section-header::before { + content: "▶"; + font-size: 10px; + color: var(--secondary-text-color); + transition: transform 0.15s; + } + .group-section[open] .group-section-header::before { transform: rotate(90deg); } + .group-section-count { + color: var(--secondary-text-color); + font-size: 13px; + font-weight: 400; + } + .group-section .objects-grid, + .group-section .task-table { + padding: 0 12px; + } + + .empty-state-centered { text-align: center; padding: 32px 16px; } + .empty-state-centered ha-button { margin-top: 16px; } + + .stat-value { + font-size: 24px; + font-weight: bold; + color: var(--primary-text-color); + } + + .stat-label { + font-size: 12px; + color: var(--secondary-text-color); + } + + .card-header { + display: flex; + justify-content: space-between; + align-items: center; + padding: 16px; + } + + .card-header h1 { + margin: 0; + font-size: 20px; + font-weight: 500; + } + + .action-buttons { + display: flex; + gap: 8px; + flex-wrap: wrap; + } + + .action-buttons ha-button { + --ha-button-font-size: 13px; + } + + .history-timeline { padding: 0 16px 16px; } + + .history-entry { + display: flex; + gap: 12px; + padding: 8px 0; + border-bottom: 1px solid var(--divider-color); + /* Long histories: skip painting off-screen entries (flex, not subgrid, so + safe — subgrid task rows can't use this without breaking alignment). */ + content-visibility: auto; + contain-intrinsic-size: auto 48px; + } + .history-entry:last-child { border-bottom: none; } + + .history-icon { + flex-shrink: 0; + width: 32px; + height: 32px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + color: white; + } + + .history-icon.completed { background: var(--maint-ok-color); } + .history-icon.skipped { background: var(--secondary-text-color); } + .history-icon.reset { background: var(--info-color, #2196f3); } + .history-icon.triggered { background: var(--maint-triggered-color); } + + .history-content { flex: 1; min-width: 0; } + + /* v2.2.0 — row holds the type label + the small Edit button */ + .history-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + } + .history-edit-btn { + background: transparent; + color: var(--secondary-text-color); + border: none; + border-radius: 4px; + padding: 4px; + cursor: pointer; + display: inline-flex; + align-items: center; + transition: background 0.15s, color 0.15s; + } + .history-edit-btn:hover { + background: var(--secondary-background-color); + color: var(--primary-color); + } + .history-edit-btn ha-icon { --mdc-icon-size: 16px; } + + .history-date { + font-size: 12px; + color: var(--secondary-text-color); + } + + .history-details { + display: flex; + gap: 12px; + font-size: 13px; + color: var(--secondary-text-color); + margin-top: 4px; + } + + /* History filter chips */ + .history-filters { + display: flex; + flex-wrap: wrap; + gap: 8px; + margin-bottom: 12px; + } + + .filter-chip { + display: inline-flex; + align-items: center; + padding: 4px 12px; + border-radius: 16px; + font-size: 12px; + cursor: pointer; + background: var(--secondary-background-color, #f5f5f5); + color: var(--primary-text-color); + border: 1px solid var(--divider-color); + transition: all 0.2s; + user-select: none; + } + + .filter-chip:hover { background: var(--divider-color); } + + .filter-chip.active { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color); + } + + .filter-chip.clear { + font-style: italic; + opacity: 0.7; + } + + /* Cost/Duration history chart */ + .history-chart { + width: 100%; + height: 200px; + display: block; + } + + .chart-legend { + display: flex; + justify-content: center; + gap: 16px; + margin-top: 4px; + font-size: 11px; + color: var(--secondary-text-color); + } + + .legend-item { + display: inline-flex; + align-items: center; + gap: 4px; + } + + .legend-swatch { + display: inline-block; + width: 12px; + height: 12px; + border-radius: 2px; + } + + .empty-state { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + padding: 48px 16px; + color: var(--secondary-text-color); + } + + .empty-state ha-svg-icon { + --mdc-icon-size: 48px; + margin-bottom: 16px; + } + + /* Sparkline chart */ + .sparkline-container { position: relative; margin: 8px 0; } + + .sparkline-svg { + width: 100%; + height: 140px; + display: block; + } + + /* Trigger info card */ + .trigger-card { + background: var(--card-background-color, #fff); + border-radius: 12px; + padding: 12px 16px; + margin: 8px 0; + border: 1px solid var(--divider-color); + } + + .trigger-header { + display: flex; + align-items: center; + gap: 8px; + margin-bottom: 4px; + } + + .trigger-entity-name { font-weight: 500; font-size: 14px; } + .trigger-entity-id { font-size: 11px; color: var(--secondary-text-color); font-family: monospace; } + + .entity-link { + cursor: pointer; + text-decoration: underline dotted; + text-underline-offset: 2px; + } + .entity-link:hover { + color: var(--primary-color); + text-decoration: underline solid; + } + + .trigger-value-row { + display: flex; + align-items: baseline; + gap: 6px; + margin: 4px 0; + } + + .trigger-current { font-size: 28px; font-weight: 700; color: var(--primary-text-color); } + .trigger-current.active { color: var(--maint-triggered-color); } + .trigger-unit { font-size: 14px; color: var(--secondary-text-color); } + + /* Counter progress ("8,507 / 15,000 km · 57 %" + bar) */ + .counter-progress { margin: 6px 0 4px; } + .counter-progress-nums { + display: flex; align-items: baseline; gap: 10px; flex-wrap: wrap; + } + .counter-progress-main { font-size: 26px; font-weight: 700; color: var(--primary-text-color); } + .counter-progress-target { font-size: 15px; font-weight: 500; color: var(--secondary-text-color); } + .counter-progress-pct { font-size: 15px; font-weight: 700; } + .counter-progress-pct.ok { color: var(--success-color, #4caf50); } + .counter-progress-pct.near { color: var(--warning-color, #ff9800); } + .counter-progress-pct.over { color: var(--error-color, #f44336); } + .counter-progress-bar { + height: 8px; border-radius: 4px; margin: 6px 0 4px; overflow: hidden; + background: var(--secondary-background-color, rgba(0, 0, 0, 0.08)); + } + .counter-progress-fill { height: 100%; border-radius: 4px; transition: width 0.3s ease; } + .counter-progress-fill.ok { background: var(--success-color, #4caf50); } + .counter-progress-fill.near { background: var(--warning-color, #ff9800); } + .counter-progress-fill.over { background: var(--error-color, #f44336); } + .counter-progress-caption { font-size: 12px; color: var(--secondary-text-color); } + + /* Note under a chart that fell back to sparse maintenance-event values */ + .chart-note { + display: flex; align-items: center; gap: 6px; margin-top: 2px; + font-size: 12px; color: var(--secondary-text-color); + } + .chart-note ha-icon { --mdc-icon-size: 15px; flex: none; } + + .trigger-limits { + display: flex; + gap: 16px; + font-size: 13px; + color: var(--secondary-text-color); + margin: 6px 0; + flex-wrap: wrap; + } + + .trigger-limit-item { + display: flex; + align-items: center; + gap: 4px; + } + + .trigger-limit-item .dot { + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + .trigger-limit-item .dot.warn { background: var(--error-color, #f44336); } + .trigger-limit-item .dot.range { background: var(--secondary-text-color); } + .trigger-limit-item .dot.ok { background: var(--maint-ok-color); } + + /* Row action buttons */ + .row-actions { + display: flex; + gap: 0; + flex-shrink: 0; + margin-left: auto; + } + + .row-actions mwc-icon-button { + --mdc-icon-button-size: 32px; + --mdc-icon-size: 18px; + } + + .row-actions .btn-complete { color: var(--maint-ok-color); } + .row-actions .btn-skip { color: var(--secondary-text-color); } + + /* Days bar for overview */ + .due-cell { + display: flex; + flex-direction: column; + align-items: flex-end; + min-width: 90px; + gap: 2px; + } + + .due-text { font-size: 13px; } + + .days-bar { + width: 100%; + height: 3px; + background: var(--divider-color); + border-radius: 2px; + overflow: hidden; + } + + .days-bar-fill { + height: 100%; + border-radius: 2px; + transition: width 0.3s; + } + + /* Trigger progress bar (overview rows) */ + .trigger-progress { + display: flex; + flex-direction: column; + gap: 2px; + min-width: 90px; + } + + .trigger-progress-bar { + width: 100%; + height: 6px; + background: var(--divider-color); + border-radius: 3px; + overflow: hidden; + } + + .trigger-progress-fill { + height: 100%; + border-radius: 2px; + transition: width 0.3s; + } + + .trigger-progress-label { + font-size: 12px; + color: var(--secondary-text-color); + text-align: right; + } + + /* Days progress bar (detail view) */ + .days-progress { + margin: 8px 0 16px; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color); + } + + .days-progress-labels { + display: flex; + justify-content: space-between; + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 6px; + } + + .days-progress-bar { + width: 100%; + height: 6px; + background: var(--divider-color); + border-radius: 3px; + overflow: hidden; + } + + .days-progress-fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s; + } + + .days-progress-text { + font-size: 13px; + font-weight: 500; + text-align: center; + margin-top: 6px; + color: var(--primary-text-color); + } + + /* Mini-sparkline in overview rows */ + .mini-sparkline { + width: 60px; + height: 20px; + display: block; + margin-top: 2px; + opacity: 0.7; + } + + /* Overflow indicator for overdue progress bars */ + .days-bar-fill.overflow, + .days-progress-fill.overflow, + .trigger-progress-fill.overflow { + background-image: repeating-linear-gradient( + -45deg, + transparent, + transparent 3px, + rgba(255,255,255,0.2) 3px, + rgba(255,255,255,0.2) 6px + ); + animation: overflow-pulse 2s ease-in-out infinite; + } + + @keyframes overflow-pulse { + 0%, 100% { opacity: 1; } + 50% { opacity: 0.7; } + } + + /* Budget bars */ + .budget-bars { + display: flex; + gap: 16px; + padding: 8px 16px; + flex-wrap: wrap; + } + + .budget-item { + flex: 1; + min-width: 200px; + } + + .budget-label { + display: flex; + justify-content: space-between; + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 4px; + } + + .budget-bar { + width: 100%; + height: 6px; + background: var(--divider-color); + border-radius: 3px; + overflow: hidden; + } + + .budget-bar-fill { + height: 100%; + border-radius: 3px; + transition: width 0.3s; + } + + /* Groups section */ + .groups-section { + padding: 8px 16px 16px; + } + + .groups-section h3 { + font-size: 14px; + font-weight: 500; + color: var(--secondary-text-color); + margin: 0 0 8px; + } + + .groups-grid { + display: flex; + gap: 12px; + flex-wrap: wrap; + } + + .group-card { + background: var(--card-background-color, #fff); + border: 1px solid var(--divider-color); + border-radius: 12px; + padding: 12px 16px; + min-width: 180px; + flex: 1; + max-width: 300px; + cursor: default; + } + + .group-card-head { + display: flex; + justify-content: space-between; + align-items: flex-start; + gap: 8px; + } + + .group-card-name { + font-weight: 500; + font-size: 14px; + margin-bottom: 4px; + } + + .group-card-actions { + display: flex; + gap: 0; + } + .group-card-actions mwc-icon-button { + --mdc-icon-button-size: 28px; + --mdc-icon-size: 16px; + color: var(--secondary-text-color); + } + + .groups-header { + display: flex; + justify-content: space-between; + align-items: center; + gap: 8px; + margin-bottom: 8px; + } + .groups-header h3 { margin: 0; } + + .seasonal-actions { + display: flex; + justify-content: flex-end; + padding: 4px 0; + } + + .group-card-desc { + font-size: 12px; + color: var(--secondary-text-color); + margin-bottom: 8px; + } + + .group-card-tasks { + display: flex; + gap: 6px; + flex-wrap: wrap; + } + + .group-task-chip { + display: inline-flex; + align-items: center; + gap: 4px; + font-size: 11px; + padding: 2px 8px; + border-radius: 10px; + background: var(--secondary-background-color, #f5f5f5); + color: var(--primary-text-color); + } + + /* Adaptive scheduling suggestion badge */ + .suggestion-badge { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 10px; + border-radius: 16px; + font-size: 12px; + font-weight: 500; + background: var(--info-color, #2196f3); + color: white; + margin-left: 8px; + } + + .suggestion-actions { + display: flex; + gap: 8px; + margin-top: 8px; + } + + .suggestion-actions ha-button { + --ha-button-font-size: 12px; + } + + .confidence-dot { + display: inline-block; + width: 8px; + height: 8px; + border-radius: 50%; + flex-shrink: 0; + } + + .confidence-dot.low { background: var(--secondary-text-color); } + .confidence-dot.medium { background: var(--warning-color, #ff9800); } + .confidence-dot.high { background: var(--success-color, #4caf50); } + + /* Feedback toggle buttons in complete dialog */ + .feedback-section { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 0; + border-top: 1px solid var(--divider-color); + } + + .feedback-label { + font-weight: 500; + font-size: 13px; + color: var(--secondary-text-color); + } + + .feedback-buttons { + display: flex; + gap: 8px; + } + + .feedback-btn { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--divider-color); + border-radius: 8px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 13px; + cursor: pointer; + text-align: center; + transition: all 0.2s; + } + + .feedback-btn:hover { + background: var(--secondary-background-color, #f5f5f5); + } + + .feedback-btn.selected { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color); + } + + /* Seasonal chart */ + .seasonal-chart { + padding: 12px 16px; + margin: 8px 0; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color); + } + + .seasonal-chart-title { + font-size: 13px; + font-weight: 500; + color: var(--secondary-text-color); + margin-bottom: 8px; + display: flex; + align-items: center; + gap: 6px; + } + + .seasonal-chart-title .source-tag { + font-size: 10px; + padding: 1px 6px; + border-radius: 8px; + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + font-weight: 400; + } + + .seasonal-chart svg { + width: 100%; + height: 100px; + display: block; + } + + .seasonal-labels { + display: flex; + justify-content: space-between; + padding: 0 2px; + margin-top: 4px; + } + + .seasonal-label { + font-size: 10px; + color: var(--secondary-text-color); + text-align: center; + flex: 1; + } + + .seasonal-label.active-month { + font-weight: 700; + color: var(--primary-color); + } + + .seasonal-factor-tag { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 8px; + border-radius: 10px; + font-size: 11px; + font-weight: 500; + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + margin-left: 6px; + } + + .seasonal-factor-tag.short { + background: rgba(76, 175, 80, 0.15); + color: var(--success-color, #4caf50); + } + + .seasonal-factor-tag.long { + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + } + + /* --- Sensor Prediction Section (Phase 3) --- */ + + .prediction-section { + margin: 16px 0; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color, #e0e0e0); + } + + .prediction-urgency-banner { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + margin-bottom: 12px; + border-radius: 8px; + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + font-size: 13px; + font-weight: 500; + } + .prediction-urgency-banner ha-svg-icon { + --mdc-icon-size: 18px; + flex-shrink: 0; + } + + .prediction-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--primary-text-color); + margin-bottom: 10px; + } + .prediction-title ha-svg-icon { + --mdc-icon-size: 16px; + color: var(--primary-color); + } + + .prediction-grid { + display: flex; + flex-wrap: wrap; + gap: 12px; + } + + .prediction-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--secondary-text-color); + } + .prediction-item ha-svg-icon { + --mdc-icon-size: 14px; + color: var(--secondary-text-color); + flex-shrink: 0; + } + + .prediction-label { + font-weight: 500; + } + + .prediction-value { + font-weight: 600; + color: var(--primary-text-color); + } + .prediction-value.rising { color: var(--error-color, #f44336); } + .prediction-value.falling { color: var(--info-color, #2196f3); } + .prediction-value.stable { color: var(--success-color, #4caf50); } + .prediction-value.exceeded { color: var(--error-color, #f44336); font-weight: 700; } + .prediction-value.urgent { color: var(--warning-color, #ff9800); font-weight: 700; } + + .prediction-rate { + font-size: 11px; + opacity: 0.7; + font-family: monospace; + } + + .prediction-date { + font-size: 11px; + opacity: 0.7; + } + + .prediction-entity { + font-size: 10px; + opacity: 0.6; + font-family: monospace; + } + + /* --- Weibull Reliability Section (Phase 4) --- */ + + .weibull-section { + margin: 16px 0; + padding: 12px 16px; + background: var(--card-background-color, #fff); + border-radius: 12px; + border: 1px solid var(--divider-color, #e0e0e0); + } + + .weibull-title { + display: flex; + align-items: center; + gap: 6px; + font-size: 13px; + font-weight: 600; + color: var(--primary-text-color); + margin-bottom: 10px; + } + .weibull-title ha-svg-icon { + --mdc-icon-size: 16px; + color: var(--primary-color); + } + + .weibull-chart svg { + width: 100%; + height: 160px; + display: block; + } + + .weibull-info-row { + display: flex; + flex-wrap: wrap; + gap: 16px; + margin-top: 10px; + } + + .weibull-info-item { + display: flex; + align-items: center; + gap: 6px; + font-size: 12px; + color: var(--secondary-text-color); + } + + .weibull-info-value { + font-weight: 600; + color: var(--primary-text-color); + } + + /* Beta interpretation badge */ + .beta-badge { + display: inline-flex; + align-items: center; + gap: 4px; + padding: 2px 10px; + border-radius: 12px; + font-size: 11px; + font-weight: 600; + white-space: nowrap; + } + .beta-badge ha-svg-icon { + --mdc-icon-size: 14px; + } + + .beta-badge.early_failures { + background: rgba(244, 67, 54, 0.15); + color: var(--error-color, #f44336); + } + .beta-badge.random_failures { + background: var(--secondary-background-color, #f5f5f5); + color: var(--secondary-text-color); + } + .beta-badge.wear_out { + background: rgba(255, 152, 0, 0.15); + color: var(--warning-color, #ff9800); + } + .beta-badge.highly_predictable { + background: rgba(76, 175, 80, 0.15); + color: var(--success-color, #4caf50); + } + + /* Confidence interval range bar */ + .confidence-range { + margin-top: 12px; + } + + .confidence-range-title { + font-size: 12px; + font-weight: 500; + color: var(--secondary-text-color); + margin-bottom: 6px; + } + + .confidence-bar { + position: relative; + width: 100%; + height: 8px; + background: var(--divider-color, #e0e0e0); + border-radius: 4px; + overflow: visible; + } + + .confidence-fill { + position: absolute; + height: 100%; + border-radius: 4px; + background: var(--primary-color, #03a9f4); + opacity: 0.25; + } + + .confidence-marker { + position: absolute; + top: -4px; + width: 3px; + height: 16px; + border-radius: 1px; + transform: translateX(-50%); + } + .confidence-marker.recommended { + background: var(--success-color, #4caf50); + } + .confidence-marker.current { + background: var(--primary-color, #03a9f4); + } + + .confidence-labels { + display: flex; + justify-content: space-between; + margin-top: 4px; + } + + .confidence-text { + font-size: 10px; + color: var(--secondary-text-color); + } + .confidence-text.low { + text-align: left; + } + .confidence-text.high { + text-align: right; + } + + .task-disabled { opacity: 0.5; } + .badge-disabled { + font-size: 10px; + padding: 1px 6px; + border-radius: 8px; + background: var(--disabled-color, #9e9e9e); + color: white; + } + + /* ── Shared responsive styles (panel + card) ── */ + @media (max-width: 600px) { + .row-actions mwc-icon-button { + --mdc-icon-button-size: 44px; + --mdc-icon-size: 22px; + } + + .due-cell { min-width: 70px; } + + .trigger-card { padding: 10px 12px; } + .trigger-current { font-size: 22px; } + + .prediction-grid { flex-direction: column; gap: 8px; } + + .weibull-info-row { flex-direction: column; gap: 8px; } + + .budget-bars { flex-direction: column; } + .budget-item { min-width: 0; } + + .group-card { min-width: 0; max-width: 100%; } + + .filter-chip { padding: 6px 12px; font-size: 13px; } + + .history-details { flex-wrap: wrap; gap: 6px; } + + .sparkline-container { max-width: 100%; overflow: hidden; } + .sparkline-svg { height: 100px; } + + .stats-bar { gap: 8px; padding: 12px; } + .stat-item { min-width: 60px; } + .stat-value { font-size: 20px; } + } +`});function Pi(a,s){let e=ji[a];if(!e)return a;let t=r(e,s);return t&&t!==e?t:a}function zi(a){let e=a.match(/data\['([^']+)'\]/)?.[1],t;return(t=a.match(/length of value must be at most (\d+)/))?{field:e,rule:"too_long",param:t[1]}:(t=a.match(/length of value must be at least (\d+)/))?{field:e,rule:"too_short",param:t[1]}:(t=a.match(/value must be at most (\S+)/))?{field:e,rule:"value_too_high",param:t[1]}:(t=a.match(/value must be at least (\S+)/))?{field:e,rule:"value_too_low",param:t[1]}:/required key not provided/.test(a)?{field:e,rule:"required"}:(t=a.match(/expected (\w+)/))?{field:e,rule:"wrong_type",param:t[1]}:/value must be one of/.test(a)?{field:e,rule:"invalid_choice"}:/not a valid value/.test(a)?{field:e,rule:"invalid_value"}:{field:e,rule:"unknown"}}function I(a,s,e){if(e=e??r("action_error",s),typeof a=="string")return a;if(typeof a!="object"||a===null)return e;let t=a,i=t.message||t.error?.message||"";if(!i)return e;let o=zi(i),d=o.field?Pi(o.field,s):"",p=c=>r(c,s).replace("{field}",d).replace("{n}",o.param??"");switch(o.rule){case"too_long":return p("err_too_long");case"too_short":return p("err_too_short");case"value_too_high":return p("err_value_too_high");case"value_too_low":return p("err_value_too_low");case"required":return p("err_required");case"wrong_type":return p("err_wrong_type").replace("{type}",o.param??"");case"invalid_choice":return p("err_invalid_choice");case"invalid_value":return p("err_invalid_value");default:return i||e}}var ji,X=y(()=>{"use strict";C();ji={name:"name",task_type:"maintenance_type",schedule_type:"schedule_type",interval_days:"interval_days",interval_anchor:"interval_anchor",warning_days:"warning_days",last_performed:"last_performed_optional",notes:"notes_optional",documentation_url:"documentation_url_optional",custom_icon:"custom_icon_optional",nfc_tag_id:"nfc_tag_id_optional",responsible_user_id:"responsible_user",entity_slug:"entity_slug",entity_id:"entity_id",area_id:"area_id_optional",manufacturer:"manufacturer_optional",model:"model_optional",serial_number:"serial_number_optional",installation_date:"installation_date_optional",warranty_expiry:"warranty_expiry_optional",checklist:"checklist_steps_optional",reason:"reason",feedback:"feedback",cost:"cost",duration:"duration",description:"description_optional",group_name:"name",group_description:"description_optional",environmental_entity:"environmental_entity_optional",environmental_attribute:"environmental_attribute_optional",trigger_above:"trigger_above",trigger_below:"trigger_below",trigger_for_minutes:"trigger_for_minutes"}});var w,Ze=y(()=>{"use strict";T();j();C();X();w=class extends ${constructor(){super(...arguments);this.entryId="";this.taskId="";this.taskName="";this.lang="en";this.checklist=[];this.adaptiveEnabled=!1;this.taskType="";this.readingUnit="";this._open=!1;this._notes="";this._cost="";this._duration="";this._loading=!1;this._error="";this._checklistState={};this._feedback="needed";this._photoDocId="";this._photoPreview="";this._photoUploading=!1;this._readingValue=""}open(){this._open||(this._open=!0,this._notes="",this._cost="",this._duration="",this._error="",this._checklistState={},this._feedback="needed",this._photoDocId="",this._photoPreview="",this._photoUploading=!1,this._readingValue="")}_toggleCheck(e){let t=String(e);this._checklistState={...this._checklistState,[t]:!this._checklistState[t]}}_setFeedback(e){this._feedback=e}async _onPhotoInput(e){let t=e.target,i=t.files?.[0];if(t.value="",!!i){this._photoUploading=!0,this._error="";try{let o=new FormData;o.append("entry_id",this.entryId),o.append("tags","photo"),o.append("file",i,i.name);let d=await fetch("/api/maintenance_supporter/document/upload",{method:"POST",headers:{Authorization:`Bearer ${this.hass.auth?.data?.access_token??""}`},body:o});if(!d.ok){this._error=d.status===413?r("doc_too_large",this.lang):r("doc_upload_failed",this.lang);return}let p=await d.json();p.id&&(this._photoDocId=p.id,this._photoPreview=URL.createObjectURL(i))}catch{this._error=r("doc_upload_failed",this.lang)}finally{this._photoUploading=!1}}}_removePhoto(){this._photoPreview&&URL.revokeObjectURL(this._photoPreview),this._photoDocId="",this._photoPreview=""}async _complete(){this._loading=!0,this._error="";try{let e={type:"maintenance_supporter/task/complete",entry_id:this.entryId,task_id:this.taskId};if(this._notes&&(e.notes=this._notes),this._cost){let t=parseFloat(this._cost);!isNaN(t)&&t>=0&&(e.cost=t)}if(this._duration){let t=parseInt(this._duration,10);!isNaN(t)&&t>=0&&(e.duration=t)}if(this.checklist.length>0&&(e.checklist_state=this._checklistState),this.adaptiveEnabled&&(e.feedback=this._feedback),this._photoDocId&&(e.photo_doc_id=this._photoDocId),this._readingValue!==""){let t=parseFloat(this._readingValue);isNaN(t)||(e.reading_value=t)}await this.hass.connection.sendMessagePromise(e),this._open=!1,this.dispatchEvent(new CustomEvent("task-completed"))}catch(e){this._error=I(e,this.lang,r("save_error",this.lang))}finally{this._loading=!1}}_close(){this._open=!1}render(){if(!this._open)return n``;let e=this.lang||this.hass?.language||"en";return n` + +
    ${r("complete_title",e)}${this.taskName}
    +
    + ${this._error?n`
    ${this._error}
    `:_} + ${this.checklist.length>0?n` +
    + + ${this.checklist.map((t,i)=>n` + + `)} +
    + `:_} + ${this.taskType==="reading"?n` + `:_} + + + + +
    + ${r("completion_photo_optional",e)} + ${this._photoPreview?n` +
    + + +
    `:n` + `} +
    + ${this.adaptiveEnabled?n` + + `:_} +
    +
    + + ${r("cancel",e)} + + + ${this._loading?r("completing",e):r("complete",e)} + +
    +
    + `}};w.styles=E` + .dialog-title { + font-size: 18px; + font-weight: 500; + padding-bottom: 12px; + } + .content { + display: flex; + flex-direction: column; + gap: 16px; + min-width: 300px; + } + .dialog-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 16px; + } + .error { + color: var(--error-color, #f44336); + font-size: 13px; + } + .field { display: flex; flex-direction: column; gap: 4px; } + .field-label { + font-size: 12px; + color: var(--secondary-text-color); + } + .field-input { + padding: 8px 10px; font-size: 14px; + background: var(--secondary-background-color, rgba(0,0,0,0.06)); + color: var(--primary-text-color); + border: 1px solid var(--divider-color); border-radius: 6px; + font-family: inherit; + width: 100%; box-sizing: border-box; + } + .field-input:focus { + outline: none; + border-color: var(--primary-color); + } + .photo-pick { + display: inline-flex; + align-items: center; + gap: 8px; + padding: 8px 12px; + border: 1px dashed var(--divider-color); + border-radius: 8px; + cursor: pointer; + font-size: 13px; + color: var(--secondary-text-color); + width: fit-content; + } + .photo-pick:hover { border-color: var(--primary-color); } + .photo-pick input[type="file"] { display: none; } + .photo-preview { + position: relative; + width: fit-content; + } + .photo-preview img { + max-width: 160px; + max-height: 160px; + border-radius: 8px; + display: block; + } + .photo-remove { + position: absolute; + top: -8px; + right: -8px; + width: 24px; + height: 24px; + border-radius: 50%; + border: none; + background: var(--error-color, #db4437); + color: #fff; + cursor: pointer; + font-size: 12px; + line-height: 1; + } + .checklist-section { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 0; + border-bottom: 1px solid var(--divider-color); + margin-bottom: 4px; + } + .checklist-label { + font-weight: 500; + font-size: 13px; + color: var(--secondary-text-color); + } + .checklist-item { + display: flex; + align-items: center; + gap: 8px; + cursor: pointer; + padding: 4px 0; + font-size: 14px; + } + .checklist-item input[type="checkbox"] { + width: 18px; + height: 18px; + cursor: pointer; + } + .feedback-section { + display: flex; + flex-direction: column; + gap: 8px; + padding: 8px 0; + border-top: 1px solid var(--divider-color); + } + .feedback-label { + font-weight: 500; + font-size: 13px; + color: var(--secondary-text-color); + } + .feedback-buttons { + display: flex; + gap: 8px; + } + .feedback-btn { + flex: 1; + padding: 8px 12px; + border: 1px solid var(--divider-color); + border-radius: 8px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 13px; + cursor: pointer; + text-align: center; + transition: all 0.2s; + } + .feedback-btn:hover { + background: var(--secondary-background-color, #f5f5f5); + } + .feedback-btn.selected { + background: var(--primary-color); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color); + } + `,l([v({attribute:!1})],w.prototype,"hass",2),l([v()],w.prototype,"entryId",2),l([v()],w.prototype,"taskId",2),l([v()],w.prototype,"taskName",2),l([v()],w.prototype,"lang",2),l([v({type:Array})],w.prototype,"checklist",2),l([v({type:Boolean})],w.prototype,"adaptiveEnabled",2),l([v()],w.prototype,"taskType",2),l([v()],w.prototype,"readingUnit",2),l([h()],w.prototype,"_open",2),l([h()],w.prototype,"_notes",2),l([h()],w.prototype,"_cost",2),l([h()],w.prototype,"_duration",2),l([h()],w.prototype,"_loading",2),l([h()],w.prototype,"_error",2),l([h()],w.prototype,"_checklistState",2),l([h()],w.prototype,"_feedback",2),l([h()],w.prototype,"_photoDocId",2),l([h()],w.prototype,"_photoPreview",2),l([h()],w.prototype,"_photoUploading",2),l([h()],w.prototype,"_readingValue",2);customElements.get("maintenance-complete-dialog")||customElements.define("maintenance-complete-dialog",w)});var L,Qe=y(()=>{"use strict";T();j();L=class extends ${constructor(){super(...arguments);this.label="";this.value="";this.placeholder="";this.type="text";this.required=!1;this.disabled=!1}_onInput(e){let t=e.target.value;this.value=t,this.dispatchEvent(new CustomEvent("input",{bubbles:!0,composed:!0,detail:{value:t}}))}render(){return n` + + `}};L.styles=E` + :host { display: block; } + .field { + display: flex; + flex-direction: column; + gap: 4px; + } + .label { + font-size: 12px; + color: var(--secondary-text-color, #888); + font-weight: 500; + } + .req { color: var(--error-color, #f44336); margin-left: 2px; } + input { + padding: 8px 10px; + font-size: 14px; + background: var(--secondary-background-color, rgba(0,0,0,0.06)); + color: var(--primary-text-color); + border: 1px solid var(--divider-color, rgba(255,255,255,0.12)); + border-radius: 6px; + font-family: inherit; + width: 100%; + box-sizing: border-box; + outline: none; + } + input:focus { + border-color: var(--primary-color); + } + input:disabled { opacity: 0.5; cursor: not-allowed; } + .helper { + font-size: 11px; + color: var(--secondary-text-color); + font-style: italic; + } + `,l([v()],L.prototype,"label",2),l([v()],L.prototype,"value",2),l([v()],L.prototype,"placeholder",2),l([v()],L.prototype,"type",2),l([v({type:Boolean})],L.prototype,"required",2),l([v({type:Boolean})],L.prototype,"disabled",2),l([v()],L.prototype,"step",2),l([v()],L.prototype,"min",2),l([v()],L.prototype,"max",2),l([v()],L.prototype,"pattern",2),l([v()],L.prototype,"helper",2);customElements.get("ms-textfield")||customElements.define("ms-textfield",L)});var A,qt=y(()=>{"use strict";T();j();C();X();Qe();A=class extends ${constructor(){super(...arguments);this.objects=[];this._open=!1;this._loading=!1;this._error="";this._name="";this._manufacturer="";this._model="";this._serialNumber="";this._areaId="";this._installationDate="";this._warrantyExpiry="";this._documentationUrl="";this._notes="";this._haDeviceId="";this._parentEntryId="";this._entryId=null}get _lang(){return this.hass?.language??navigator.language.split("-")[0]??"en"}openCreate(){this._entryId=null,this._name="",this._manufacturer="",this._model="",this._serialNumber="",this._areaId="",this._installationDate="",this._warrantyExpiry="",this._documentationUrl="",this._notes="",this._haDeviceId="",this._parentEntryId="",this._error="",this._open=!0}openEdit(e,t){this._entryId=e,this._name=t.name||"",this._manufacturer=t.manufacturer||"",this._model=t.model||"",this._serialNumber=t.serial_number||"",this._areaId=t.area_id||"",this._installationDate=t.installation_date||"",this._warrantyExpiry=t.warranty_expiry||"",this._documentationUrl=t.documentation_url||"",this._notes=t.notes||"",this._haDeviceId=t.ha_device_id||"",this._parentEntryId=t.parent_entry_id||"",this._error="",this._open=!0}async _save(){if(!this._loading&&this._name.trim()){this._loading=!0,this._error="";try{this._entryId?await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/update",entry_id:this._entryId,name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}):await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/object/create",name:this._name,manufacturer:this._manufacturer||null,model:this._model||null,serial_number:this._serialNumber||null,area_id:this._areaId||null,installation_date:this._installationDate||null,warranty_expiry:this._warrantyExpiry||null,documentation_url:this._documentationUrl.trim()||null,notes:this._notes.trim()||null,ha_device_id:this._haDeviceId||null,parent_entry_id:this._parentEntryId||null}),this._open=!1,this.dispatchEvent(new CustomEvent("object-saved"))}catch(e){this._error=I(e,this._lang,r("save_error",this._lang))}finally{this._loading=!1}}}_parentChoices(){return(this.objects||[]).filter(e=>e.entry_id!==this._entryId)}_close(){this._open=!1}render(){if(!this._open)return n``;let e=this._lang,t=this._entryId?r("edit_object",e):r("new_object",e);return n` + +
    ${t}
    +
    + ${this._error?n`
    ${this._error}
    `:_} + this._name=i.target.value} + > + this._manufacturer=i.target.value} + > + this._model=i.target.value} + > + this._serialNumber=i.target.value} + > + this._documentationUrl=i.target.value} + > + this._areaId=i.detail.value||""} + > + this._installationDate=i.target.value} + > + this._warrantyExpiry=i.target.value} + > + r("link_device_optional",e)} + @value-changed=${i=>this._haDeviceId=i.detail.value?.device||""} + > + ${this._parentChoices().length?n``:_} + +
    +
    + + ${r("cancel",this._lang)} + + + ${this._loading?r("saving",this._lang):r("save",this._lang)} + +
    +
    + `}};A.styles=E` + .dialog-title { + font-size: 18px; + font-weight: 500; + padding-bottom: 12px; + } + .content { + display: flex; + flex-direction: column; + gap: 16px; + min-width: 300px; + } + .dialog-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 16px; + } + ms-textfield { + display: block; + } + .textarea-field { + display: flex; flex-direction: column; gap: 4px; + } + .textarea-label { + font-size: 12px; color: var(--secondary-text-color, #888); font-weight: 500; + } + .textarea-field textarea { + padding: 8px 10px; font-size: 14px; font-family: inherit; + background: var(--secondary-background-color, rgba(0,0,0,0.06)); + color: var(--primary-text-color); + border: 1px solid var(--divider-color); border-radius: 6px; + resize: vertical; + } + .textarea-field textarea:focus { + outline: none; border-color: var(--primary-color); + } + .parent-select { + padding: 8px 10px; font-size: 14px; font-family: inherit; + background: var(--secondary-background-color, rgba(0,0,0,0.06)); + color: var(--primary-text-color); + border: 1px solid var(--divider-color); border-radius: 6px; + } + .error { + color: var(--error-color, #f44336); + font-size: 13px; + } + `,l([v({attribute:!1})],A.prototype,"hass",2),l([v({attribute:!1})],A.prototype,"objects",2),l([h()],A.prototype,"_open",2),l([h()],A.prototype,"_loading",2),l([h()],A.prototype,"_error",2),l([h()],A.prototype,"_name",2),l([h()],A.prototype,"_manufacturer",2),l([h()],A.prototype,"_model",2),l([h()],A.prototype,"_serialNumber",2),l([h()],A.prototype,"_areaId",2),l([h()],A.prototype,"_installationDate",2),l([h()],A.prototype,"_warrantyExpiry",2),l([h()],A.prototype,"_documentationUrl",2),l([h()],A.prototype,"_notes",2),l([h()],A.prototype,"_haDeviceId",2),l([h()],A.prototype,"_parentEntryId",2),l([h()],A.prototype,"_entryId",2);customElements.get("maintenance-object-dialog")||customElements.define("maintenance-object-dialog",A)});var Le,Dt=y(()=>{"use strict";Le=class{constructor(s){this.usersCache=null;this.cacheTimestamp=0;this.CACHE_TTL_MS=6e4;this.hass=s}updateHass(s){this.hass=s}async getUsers(s=!1){let e=Date.now();if(!s&&this.usersCache&&e-this.cacheTimestampt.id===s)?.name||null}getUser(s){return!s||!this.usersCache?null:this.usersCache.find(e=>e.id===s)||null}getCurrentUserId(){return this.hass.user?.id||null}isCurrentUser(s){return s?s===this.getCurrentUserId():!1}clearCache(){this.usersCache=null,this.cacheTimestamp=0}}});function Ui(){return{entityIds:"",type:"threshold",above:"",below:"",forMinutes:"0",targetValue:"",deltaMode:!1,fromState:"",toState:"",targetChanges:"",runtimeHours:""}}function Fi(a){return{entityIds:(a.entity_ids||(a.entity_id?[a.entity_id]:[])).join(", "),type:a.type||"threshold",above:a.trigger_above?.toString()??"",below:a.trigger_below?.toString()??"",forMinutes:a.trigger_for_minutes?.toString()??"0",targetValue:a.trigger_target_value?.toString()??"",deltaMode:a.trigger_delta_mode||!1,fromState:a.trigger_from_state||"",toState:a.trigger_to_state||"",targetChanges:a.trigger_target_changes?.toString()??"",runtimeHours:a.trigger_runtime_hours?.toString()??""}}function Wi(a){let s=a.entityIds.split(",").map(t=>t.trim()).filter(Boolean);if(s.length===0)return null;let e={entity_id:s[0],entity_ids:s,type:a.type};if(a.type==="threshold"){let t=parseFloat(a.above);isNaN(t)||(e.trigger_above=t);let i=parseFloat(a.below);isNaN(i)||(e.trigger_below=i);let o=parseInt(a.forMinutes,10);isNaN(o)||(e.trigger_for_minutes=o)}else if(a.type==="counter"){let t=parseFloat(a.targetValue);isNaN(t)||(e.trigger_target_value=t),e.trigger_delta_mode=a.deltaMode}else if(a.type==="state_change"){a.fromState&&(e.trigger_from_state=a.fromState),a.toState&&(e.trigger_to_state=a.toState);let t=parseInt(a.targetChanges,10);isNaN(t)||(e.trigger_target_changes=t)}else if(a.type==="runtime"){let t=parseFloat(a.runtimeHours);isNaN(t)||(e.trigger_runtime_hours=t)}return e}function Vi(a){return Array.from({length:7},(s,e)=>Se(e,a,"short"))}var Ni,Mi,qi,Di,Ot,Oi,u,Ut=y(()=>{"use strict";T();j();C();Dt();X();Qe();Ni=["cleaning","inspection","replacement","calibration","service","reading","custom"],Mi=["low","normal","high"],qi=["time_based","weekdays","nth_weekday","day_of_month","sensor_based","one_time","manual"],Di=["weekdays","nth_weekday","day_of_month"],Ot=["threshold","counter","state_change","runtime"],Oi=[...Ot,"compound"];u=class extends ${constructor(){super(...arguments);this.checklistsEnabled=!1;this.scheduleTimeEnabled=!1;this.completionActionsEnabled=!1;this.defaultWarningDays=7;this._open=!1;this._loading=!1;this._error="";this._entryId="";this._taskId=null;this._objectChoices=[];this._name="";this._type="custom";this._scheduleType="time_based";this._intervalDays="30";this._intervalUnit="days";this._dueDate="";this._warningDays="7";this._earliestCompletionDays="";this._intervalAnchor="completion";this._weekdays=[];this._nth="1";this._nthWeekday="5";this._domDay="1";this._domLastDay=!1;this._domBusiness=!1;this._calOffset="0";this._notes="";this._documentationUrl="";this._customIcon="";this._priority="normal";this._labels="";this._enabled=!0;this._triggerEntityId="";this._triggerEntityIds=[];this._triggerEntityLogic="any";this._triggerAttribute="";this._triggerType="threshold";this._triggerAbove="";this._triggerBelow="";this._triggerForMinutes="0";this._triggerTargetValue="";this._triggerDeltaMode=!1;this._autoCompleteOnRecovery=!1;this._triggerFromState="";this._triggerToState="";this._triggerTargetChanges="";this._triggerRuntimeHours="";this._compoundLogic="AND";this._compoundConditions=[];this._suggestedAttributes=[];this._availableAttributes=[];this._entityDomain="";this._lastPerformed="";this._nfcTagId="";this._readingUnit="";this._availableTags=[];this._responsibleUserId=null;this._assigneePool=[];this._rotationStrategy="";this._availableUsers=[];this._checklistText="";this._scheduleTime="";this._actionService="";this._actionTargetEntity="";this._actionData={};this._actionDataJsonFallback="";this._actionTesting=!1;this._actionTestResult="";this._actionTestError="";this._qcNotes="";this._qcCost="";this._qcDuration="";this._qcFeedback="";this._environmentalEntity="";this._environmentalAttribute="";this._environmentalInitial="";this._environmentalAttributeInitial="";this._userService=null}get _lang(){return this.hass?.language??navigator.language.split("-")[0]??"en"}async openCreate(e,t){this._entryId=e,this._taskId=null,this._error="",!e&&t&&t.length>0?(this._objectChoices=t.map(i=>({entry_id:i.entry_id,name:i.object.name})).sort((i,o)=>i.name.localeCompare(o.name)),this._entryId=this._objectChoices[0].entry_id):this._objectChoices=[],this._resetFields(),await Promise.all([this._loadUsers(),this._loadTags()]),this._open=!0}async openEdit(e,t){this._entryId=e,this._taskId=t.id,this._error="",this._name=t.name,this._type=t.type,this._scheduleType=t.schedule_type,this._intervalDays=t.interval_days!=null?String(t.interval_days):"",this._intervalUnit=t.interval_unit||"days",this._dueDate=t.due_date||"";let i=t.schedule;this._weekdays=i?.kind==="weekdays"?[...i.weekdays??[]]:[],this._nth=i?.kind==="nth_weekday"?String(i.nth??1):"1",this._nthWeekday=i?.kind==="nth_weekday"?String(i.weekday??5):"5",this._domDay=i?.kind==="day_of_month"&&(i.day??1)>=1?String(i.day??1):"1",this._domLastDay=i?.kind==="day_of_month"&&i.day===-1,this._domBusiness=i?.kind==="day_of_month"&&i.business===!0,this._calOffset=i?.offset?String(i.offset):"0",this._warningDays=t.warning_days.toString(),this._earliestCompletionDays=t.earliest_completion_days!=null?String(t.earliest_completion_days):"",this._intervalAnchor=t.interval_anchor||"completion",this._notes=t.notes||"",this._documentationUrl=t.documentation_url||"",this._customIcon=t.custom_icon||"",this._priority=t.priority||"normal",this._labels=(t.labels||[]).join(", "),this._enabled=t.enabled!==!1,this._lastPerformed=t.last_performed||"",this._nfcTagId=t.nfc_tag_id||"",this._readingUnit=t.reading_unit||"",this._responsibleUserId=t.responsible_user_id||null,this._assigneePool=[...t.assignee_pool||[]],this._rotationStrategy=t.rotation_strategy||"",this._checklistText=(t.checklist||[]).join(` +`),this._scheduleTime=t.schedule_time||"";let o=t.on_complete_action;if(o&&o.service){this._actionService=o.service;let c=o.target?.entity_id;this._actionTargetEntity=Array.isArray(c)?c[0]||"":c||"",this._actionData=o.data&&typeof o.data=="object"?{...o.data}:{},this._actionDataJsonFallback=""}else this._actionService="",this._actionTargetEntity="",this._actionData={},this._actionDataJsonFallback="";let d=t.quick_complete_defaults;this._qcNotes=d?.notes||"",this._qcCost=d?.cost!=null?String(d.cost):"",this._qcDuration=d?.duration!=null?String(d.duration):"",this._qcFeedback=d?.feedback||"";let p=t.adaptive_config||{};if(this._environmentalEntity=p.environmental_entity||"",this._environmentalAttribute=p.environmental_attribute||"",this._environmentalInitial=this._environmentalEntity,this._environmentalAttributeInitial=this._environmentalAttribute,t.trigger_config){let c=t.trigger_config;this._triggerEntityId=c.entity_id||"",this._triggerEntityIds=c.entity_ids||(c.entity_id?[c.entity_id]:[]),this._triggerEntityLogic=c.entity_logic||"any",this._triggerAttribute=c.attribute||"",this._triggerType=c.type||"threshold",this._triggerAbove=c.trigger_above?.toString()||"",this._triggerBelow=c.trigger_below?.toString()||"",this._triggerForMinutes=c.trigger_for_minutes?.toString()||"0",this._triggerTargetValue=c.trigger_target_value?.toString()||"",this._triggerDeltaMode=c.trigger_delta_mode||!1,this._autoCompleteOnRecovery=c.auto_complete_on_recovery||!1,this._triggerFromState=c.trigger_from_state||"",this._triggerToState=c.trigger_to_state||"",this._triggerTargetChanges=c.trigger_target_changes?.toString()||"",this._triggerRuntimeHours=c.trigger_runtime_hours?.toString()||"",c.type==="compound"?(this._compoundLogic=c.compound_logic==="OR"?"OR":"AND",this._compoundConditions=(c.conditions||[]).map(Fi)):(this._compoundLogic="AND",this._compoundConditions=[])}else this._resetTriggerFields();this._triggerEntityId&&this._fetchEntityAttributes(this._triggerEntityId),await Promise.all([this._loadUsers(),this._loadTags()]),this._open=!0}_resetFields(){this._name="",this._type="custom",this._scheduleType="time_based",this._intervalDays="30",this._intervalUnit="days",this._dueDate="",this._warningDays=String(this.defaultWarningDays),this._earliestCompletionDays="",this._intervalAnchor="completion",this._weekdays=[],this._nth="1",this._nthWeekday="5",this._domDay="1",this._domLastDay=!1,this._domBusiness=!1,this._calOffset="0",this._notes="",this._documentationUrl="",this._customIcon="",this._priority="normal",this._labels="",this._enabled=!0,this._lastPerformed="",this._nfcTagId="",this._readingUnit="",this._responsibleUserId=null,this._assigneePool=[],this._rotationStrategy="",this._checklistText="",this._scheduleTime="",this._environmentalEntity="",this._environmentalAttribute="",this._environmentalInitial="",this._environmentalAttributeInitial="",this._actionService="",this._actionTargetEntity="",this._actionData={},this._actionDataJsonFallback="",this._actionTesting=!1,this._actionTestResult="",this._qcNotes="",this._qcCost="",this._qcDuration="",this._qcFeedback="",this._resetTriggerFields()}_resetTriggerFields(){this._triggerEntityId="",this._triggerEntityIds=[],this._triggerEntityLogic="any",this._triggerAttribute="",this._suggestedAttributes=[],this._availableAttributes=[],this._entityDomain="",this._triggerType="threshold",this._triggerAbove="",this._triggerBelow="",this._triggerForMinutes="0",this._triggerTargetValue="",this._triggerDeltaMode=!1,this._autoCompleteOnRecovery=!1,this._triggerFromState="",this._triggerToState="",this._triggerTargetChanges="",this._triggerRuntimeHours="",this._compoundLogic="AND",this._compoundConditions=[]}async _loadUsers(){this._userService||(this._userService=new Le(this.hass));try{this._availableUsers=await this._userService.getUsers()}catch(e){console.error("Failed to load users:",e),this._availableUsers=[]}}_toggleAssignee(e){this._assigneePool=this._assigneePool.includes(e)?this._assigneePool.filter(t=>t!==e):[...this._assigneePool,e]}async _testAction(){let e=this._actionService.trim();if(!e||!/^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(e)){this._actionTestResult="error",this._actionTestError="Invalid service format (expected 'domain.service')",setTimeout(()=>{this._actionTestResult="",this._actionTestError=""},5e3);return}let[t,i]=e.split(".");if(!this.hass?.services?.[t]?.[i]){this._actionTestResult="error",this._actionTestError=`Service "${e}" is not registered in Home Assistant. Check spelling and that the integration providing it is loaded.`,setTimeout(()=>{this._actionTestResult="",this._actionTestError=""},8e3);return}let o=this._actionTargetEntity.trim();if(o){let d=o.split(".")[0];if(d!==t&&!new Set(["homeassistant","scene","notify","persistent_notification"]).has(t)){this._actionTestResult="error",this._actionTestError=`Service "${e}" only works on ${t}.* entities; entity "${o}" is in ${d}.* \u2014 pick a service that matches the entity domain (e.g. ${d}.${i})`,setTimeout(()=>{this._actionTestResult="",this._actionTestError=""},8e3);return}if(!this.hass.states?.[o]){this._actionTestResult="error",this._actionTestError=`Target entity "${o}" not found in Home Assistant \u2014 the entity may have been renamed or its integration removed.`,setTimeout(()=>{this._actionTestResult="",this._actionTestError=""},8e3);return}}this._actionTestResult="ok",setTimeout(()=>{this._actionTestResult="",this._actionTestError=""},5e3)}_buildActionData(){if(this._actionDataJsonFallback.trim())try{let e=JSON.parse(this._actionDataJsonFallback);if(e&&typeof e=="object"&&!Array.isArray(e))return e}catch{}return{...this._actionData}}_serviceSchema(){let e=this._actionService.trim();if(!e||!/^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(e))return null;let[t,i]=e.split("."),o=this.hass?.services?.[t]?.[i]?.fields;return!o||Object.keys(o).length===0?null:Object.entries(o).map(([d,p])=>({name:d,required:!!p.required,selector:p.selector||{text:{}}}))}_renderCompletionActionsSection(e){if(!this.completionActionsEnabled)return _;let t=this._serviceSchema();return n` +
    + ${r("on_complete_action_title",e)} +

    ${r("on_complete_action_desc",e)}

    + {this._actionService=i.detail.value||"";let o=this._serviceSchema();if(o){let d=new Set(o.map(p=>p.name));this._actionData=Object.fromEntries(Object.entries(this._actionData).filter(([p])=>d.has(p)))}}} + > + r("on_complete_action_target",e)} + @value-changed=${i=>{let o=i.detail.value;this._actionTargetEntity=o.target_entity||""}} + > +

    + ${r("on_complete_action_target_hint",e)} +

    + ${t?n` + {this._actionData={...i.detail.value}}} + > + `:n` + {this._actionDataJsonFallback=i.target.value}} + > + `} +
    + + ${this._actionTestResult==="ok"?n`${r("on_complete_action_test_success",e)}`:_} + ${this._actionTestResult==="error"?n`
    + ${r("on_complete_action_test_failed",e)} + ${this._actionTestError?n`
    ${this._actionTestError}
    `:_} +
    `:_} +
    +
    + +
    + ${r("quick_complete_defaults_title",e)} +

    ${r("quick_complete_defaults_desc",e)}

    + {this._qcNotes=i.target.value}} + > + {this._qcCost=i.target.value}} + > + {this._qcDuration=i.target.value}} + > + +
    + `}async _loadTags(){try{let e=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/tags/list"});this._availableTags=e.tags||[]}catch{this._availableTags=[]}}async _fetchEntityAttributes(e){if(!e||!this.hass){this._suggestedAttributes=[],this._availableAttributes=[],this._entityDomain="";return}try{let t=await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/entity/attributes",entity_id:e});this._entityDomain=t.domain||"",this._suggestedAttributes=t.suggested_attributes||[],this._availableAttributes=t.available_attributes||[]}catch{this._suggestedAttributes=[],this._availableAttributes=[],this._entityDomain=""}}async _save(){if(!this._loading&&this._name.trim()){this._loading=!0,this._error="";try{let e={type:this._taskId?"maintenance_supporter/task/update":"maintenance_supporter/task/create",entry_id:this._entryId,name:this._name,task_type:this._type,schedule_type:this._scheduleType,warning_days:parseInt(this._warningDays,10)||7},t=this._earliestCompletionDays.trim();if(e.earliest_completion_days=t===""?null:Math.max(0,parseInt(t,10)||0),this._taskId&&(e.task_id=this._taskId),this._scheduleType==="one_time"?(e.due_date=this._dueDate||null,e.interval_days=null):Di.includes(this._scheduleType)?(e.schedule=this._buildSchedule(),e.interval_days=null,this._taskId&&(e.due_date=null)):(this._taskId&&(e.due_date=null),this._scheduleType!=="manual"&&this._intervalDays?(e.interval_days=parseInt(this._intervalDays,10),e.interval_unit=this._intervalUnit,e.interval_anchor=this._intervalAnchor):this._taskId&&(e.interval_days=null,e.interval_anchor="completion")),e.notes=this._notes||null,e.documentation_url=this._documentationUrl||null,e.custom_icon=this._customIcon||null,e.priority=this._priority,e.labels=this._labels.split(",").map(p=>p.trim()).filter(Boolean),e.enabled=this._enabled,e.last_performed=this._lastPerformed||null,e.nfc_tag_id=this._nfcTagId||null,e.reading_unit=this._readingUnit.trim()||null,e.responsible_user_id=this._responsibleUserId,e.assignee_pool=this._assigneePool,e.rotation_strategy=this._assigneePool.length>=2&&this._rotationStrategy?this._rotationStrategy:null,this._scheduleType==="sensor_based"&&this._triggerType==="compound"){let p=this._compoundConditions.map(Wi).filter(c=>c!==null);if(p.length>0){let c={type:"compound",compound_logic:this._compoundLogic,conditions:p};this._autoCompleteOnRecovery&&(c.auto_complete_on_recovery=!0),e.trigger_config=c}else this._taskId&&(e.trigger_config=null)}else if(this._scheduleType==="sensor_based"&&this._triggerEntityId){let p=this._triggerEntityIds.length>0?this._triggerEntityIds:[this._triggerEntityId],c={entity_id:p[0],entity_ids:p,type:this._triggerType};if(this._triggerAttribute&&(c.attribute=this._triggerAttribute),this._autoCompleteOnRecovery&&(c.auto_complete_on_recovery=!0),p.length>1&&(c.entity_logic=this._triggerEntityLogic),this._triggerType==="threshold"){if(this._triggerAbove){let g=parseFloat(this._triggerAbove);isNaN(g)||(c.trigger_above=g)}if(this._triggerBelow){let g=parseFloat(this._triggerBelow);isNaN(g)||(c.trigger_below=g)}if(this._triggerForMinutes){let g=parseInt(this._triggerForMinutes,10);isNaN(g)||(c.trigger_for_minutes=g)}}else if(this._triggerType==="counter"){if(this._triggerTargetValue){let g=parseFloat(this._triggerTargetValue);isNaN(g)||(c.trigger_target_value=g)}c.trigger_delta_mode=this._triggerDeltaMode}else if(this._triggerType==="state_change"){if(this._triggerFromState&&(c.trigger_from_state=this._triggerFromState),this._triggerToState&&(c.trigger_to_state=this._triggerToState),this._triggerTargetChanges){let g=parseInt(this._triggerTargetChanges,10);isNaN(g)||(c.trigger_target_changes=g)}}else if(this._triggerType==="runtime"&&this._triggerRuntimeHours){let g=parseFloat(this._triggerRuntimeHours);isNaN(g)||(c.trigger_runtime_hours=g)}e.trigger_config=c}else this._taskId&&(e.trigger_config=null);if(this.scheduleTimeEnabled&&this._scheduleType==="time_based"){let p=this._scheduleTime.trim();e.schedule_time=/^([01]\d|2[0-3]):[0-5]\d$/.test(p)?p:null}if(this.checklistsEnabled){let p=this._checklistText.split(` +`).map(c=>c.trim()).filter(Boolean).slice(0,100);e.checklist=p.length?p:null}if(this.completionActionsEnabled){let p=this._actionService.trim();if(p&&/^[a-z][a-z0-9_]*\.[a-z0-9_]+$/.test(p)){let m={service:p},x=this._actionTargetEntity.trim();x&&(m.target={entity_id:x});let f=this._buildActionData();Object.keys(f).length>0&&(m.data=f),e.on_complete_action=m}else e.on_complete_action=null;let c={};this._qcNotes.trim()&&(c.notes=this._qcNotes.trim());let g=parseFloat(this._qcCost);!isNaN(g)&&g>=0&&(c.cost=g);let b=parseInt(this._qcDuration,10);!isNaN(b)&&b>=0&&(c.duration=b),this._qcFeedback&&(c.feedback=this._qcFeedback),e.quick_complete_defaults=Object.keys(c).length?c:null}let i=await this.hass.connection.sendMessagePromise(e),o=this._taskId||i?.task_id,d=this._environmentalEntity!==this._environmentalInitial||this._environmentalAttribute!==this._environmentalAttributeInitial;if(o&&this._scheduleType==="sensor_based"&&d)try{await this.hass.connection.sendMessagePromise({type:"maintenance_supporter/task/set_environmental_entity",entry_id:this._entryId,task_id:o,environmental_entity:this._environmentalEntity||null,environmental_attribute:this._environmentalAttribute||null}),this._environmentalInitial=this._environmentalEntity,this._environmentalAttributeInitial=this._environmentalAttribute}catch{}this._open=!1,this.dispatchEvent(new CustomEvent("task-saved"))}catch(e){this._error=I(e,this._lang,r("save_error",this._lang))}finally{this._loading=!1}}}_close(){this._open=!1}_renderTriggerFields(){if(this._scheduleType!=="sensor_based")return _;let e=this._lang,t=this._triggerType==="compound";return n` +

    ${r("trigger_configuration",e)}

    +
    + + +
    + ${t?this._renderCompoundEditor():n` + 0?this._triggerEntityIds.join(", "):this._triggerEntityId} + @input=${i=>{let d=i.target.value.split(",").map(p=>p.trim()).filter(Boolean);this._triggerEntityId=d[0]||"",this._triggerEntityIds=d,d[0]&&this._fetchEntityAttributes(d[0])}} + > + ${this._triggerEntityIds.length>1?n` +
    + + +
    + `:_} + ${this._availableAttributes.length>0?n` +
    + + +
    + `:n` + this._triggerAttribute=i.target.value} + > + `} + ${this._renderTriggerTypeFields()} + `} + +
    ${r("auto_complete_on_recovery_help",e)}
    + this._intervalDays=i.target.value} + > + ${this._intervalDays?this._renderUnitSelect():_} + `}_patchCondition(e,t){this._compoundConditions=this._compoundConditions.map((i,o)=>o===e?{...i,...t}:i)}_addCondition(){this._compoundConditions=[...this._compoundConditions,Ui()]}_removeCondition(e){this._compoundConditions=this._compoundConditions.filter((t,i)=>i!==e)}_renderCompoundEditor(){let e=this._lang;return n` +
    + + +
    +
    ${r("compound_help",e)}
    + ${this._compoundConditions.length===0?n`
    ${r("compound_no_conditions",e)}
    `:this._compoundConditions.map((t,i)=>this._renderCondition(t,i))} + + `}_renderCondition(e,t){let i=this._lang,o=t+1;return n` +
    +
    + ${r("compound_condition",i)} ${o} + +
    + this._patchCondition(t,{entityIds:d.target.value})} + > +
    + + +
    + ${this._renderConditionTypeFields(e,t)} +
    + `}_renderConditionTypeFields(e,t){let i=this._lang;return e.type==="threshold"?n` + this._patchCondition(t,{above:o.target.value})}> + this._patchCondition(t,{below:o.target.value})}> + this._patchCondition(t,{forMinutes:o.target.value})}> + `:e.type==="counter"?n` + this._patchCondition(t,{targetValue:o.target.value})}> + + `:e.type==="state_change"?n` + this._patchCondition(t,{fromState:o.target.value})}> + this._patchCondition(t,{toState:o.target.value})}> + this._patchCondition(t,{targetChanges:o.target.value})}> + `:e.type==="runtime"?n` + this._patchCondition(t,{runtimeHours:o.target.value})}> + `:_}_renderUnitSelect(){let e=this._lang;return n` +
    + + +
    `}_toggleWeekday(e){this._weekdays=this._weekdays.includes(e)?this._weekdays.filter(t=>t!==e):[...this._weekdays,e]}_buildSchedule(){let e=i=>{let o=parseInt(this._calOffset,10)||0;return o&&(i.offset=Math.max(-15,Math.min(o,15))),i};if(this._scheduleType==="weekdays")return e({kind:"weekdays",weekdays:[...this._weekdays].sort((i,o)=>i-o)});if(this._scheduleType==="nth_weekday")return e({kind:"nth_weekday",nth:parseInt(this._nth,10),weekday:parseInt(this._nthWeekday,10)});let t={kind:"day_of_month",day:this._domLastDay?-1:parseInt(this._domDay,10)||1};return this._domBusiness&&(t.business=!0),e(t)}_renderCalendarFields(){let e=this._lang,t=Vi(e);if(this._scheduleType==="weekdays")return n` + +
    + ${t.map((i,o)=>n` + `)} +
    + ${this._renderCalOffsetField()}`;if(this._scheduleType==="nth_weekday"){let i=[["1",r("ord_1",e)],["2",r("ord_2",e)],["3",r("ord_3",e)],["4",r("ord_4",e)],["5",r("ord_5",e)],["-1",r("ord_last",e)]];return n` +
    + + +
    +
    + + +
    + ${this._renderCalOffsetField()}`}return this._scheduleType==="day_of_month"?n` + ${this._domLastDay?_:n` + this._domDay=i.target.value} + >`} + + + ${this._renderCalOffsetField()}`:_}_renderCalOffsetField(){let e=this._lang;return n` + this._calOffset=t.target.value} + >`}_renderTriggerTypeFields(){let e=this._lang;return this._triggerType==="threshold"?n` + this._triggerAbove=t.target.value} + > + this._triggerBelow=t.target.value} + > + this._triggerForMinutes=t.target.value} + > + `:this._triggerType==="counter"?n` + this._triggerTargetValue=t.target.value} + > + + `:this._triggerType==="state_change"?n` + this._triggerFromState=t.target.value} + > +
    ${r("state_value_help",e)}
    + this._triggerToState=t.target.value} + > + this._triggerTargetChanges=t.target.value} + > +
    ${r("target_changes_help",e)}
    + `:this._triggerType==="runtime"?n` + this._triggerRuntimeHours=t.target.value} + > + `:_}render(){if(!this._open)return n``;let e=this._lang,t=this._taskId?r("edit_task",e):r("new_task",e);return n` + +
    ${t}
    +
    + ${this._error?n`
    ${this._error}
    `:_} + ${this._objectChoices.length>0?n` +
    + + +
    + `:_} + this._name=i.target.value} + > +
    + + +
    + ${this._type==="reading"?n` + this._readingUnit=i.target.value} + > +
    ${r("reading_unit_help",e)}
    + `:_} +
    + + +
    +
    + + this._labels=i.target.value} + /> +
    ${r("labels_help",e)}
    +
    +
    + + +
    + ${this._scheduleType==="time_based"?n` + this._intervalDays=i.target.value} + > + ${this._renderUnitSelect()} +
    + + +
    + ${this.scheduleTimeEnabled?n` + this._scheduleTime=i.target.value} + > + `:_} + `:_} + ${this._renderCalendarFields()} + ${this._scheduleType==="one_time"?n` + this._dueDate=i.target.value} + > + `:_} + this._warningDays=i.target.value} + > + this._earliestCompletionDays=i.target.value} + > + ${this.checklistsEnabled?n` +

    ${r("checklist_steps_optional",e)}

    + +
    ${r("checklist_help",e)}
    + `:_} + this._lastPerformed=i.target.value} + > +
    + + +
    + ${this._availableUsers.length>=2?n` +
    + +
    ${r("shared_with_help",e)}
    +
    + ${this._availableUsers.map(i=>n` + `)} +
    +
    + ${this._assigneePool.length>=2?n` +
    + + +
    `:_} + `:_} + ${this._renderTriggerFields()} + ${this._scheduleType==="sensor_based"?n` + this._environmentalEntity=i.target.value.trim()} + > + ${this._environmentalEntity?n` + this._environmentalAttribute=i.target.value.trim()} + > + `:_} + `:_} + this._notes=i.target.value} + > + this._documentationUrl=i.target.value} + > + this._customIcon=i.detail.value||""} + > + ${this._availableTags.length>0?n` +
    + + + +
    + `:n` + this._nfcTagId=i.target.value} + > +
    + ${r("nfc_tags_empty_help",e)} + ${r("nfc_tags_open_settings",e)} + · + +
    + `} + + ${this._renderCompletionActionsSection(e)} +
    +
    + ${r("cancel",e)} + + ${this._loading?r("saving",e):r("save",e)} + +
    +
    + `}};u.styles=E` + .dialog-title { + font-size: 18px; + font-weight: 500; + padding-bottom: 12px; + } + /* v1.3.0: completion-action sections */ + .ca-section { + border: 1px solid var(--divider-color); + border-radius: 6px; + padding: 8px 12px; + margin-top: 8px; + } + .ca-section > summary { + cursor: pointer; + font-weight: 500; + } + .ca-section ms-textfield, + .ca-section ha-entity-picker, + .ca-section ha-service-picker, + .ca-section ha-form, + .ca-section .qc-feedback { + width: 100%; + margin-top: 8px; + display: block; + } + .ca-section .qc-feedback { + padding: 8px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + } + .ca-test-row { + display: flex; + align-items: center; + gap: 12px; + margin-top: 8px; + } + .ca-test-ok { color: var(--success-color, #4caf50); font-size: 13px; } + .ca-test-error { color: var(--error-color, #f44336); font-size: 13px; font-weight: 500; } + .ca-test-error-block { display: flex; flex-direction: column; gap: 4px; flex: 1; min-width: 0; } + .ca-test-error-detail { + font-size: 12px; + color: var(--secondary-text-color); + background: rgba(244, 67, 54, 0.08); + padding: 6px 8px; border-radius: 4px; + line-height: 1.4; + word-break: break-word; + } + .content { + display: flex; + flex-direction: column; + gap: 12px; + min-width: 350px; + max-height: 70vh; + overflow-y: auto; + } + @media (max-width: 600px) { + .content { + min-width: 0; + max-height: none; + } + } + .dialog-actions { + display: flex; + justify-content: flex-end; + gap: 8px; + padding-top: 16px; + } + ms-textfield { + display: block; + } + .field-label { + font-size: 12px; + color: var(--secondary-text-color); + } + .checklist-textarea { + width: 100%; + min-height: 88px; + padding: 8px; + font-family: inherit; + font-size: 14px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color); + color: var(--primary-text-color); + resize: vertical; + box-sizing: border-box; + } + .field-help { + font-size: 12px; + color: var(--secondary-text-color); + } + .field-help a, + .link-button { + background: none; + border: 0; + padding: 0; + color: var(--primary-color); + cursor: pointer; + font: inherit; + text-decoration: underline; + } + .field-help a:hover, + .link-button:hover { + text-decoration: none; + } + /* Smaller refresh icon-button when shown next to the dropdown. */ + .select-row .link-button { + margin-left: 8px; + text-decoration: none; + font-size: 16px; + } + .select-row .link-button:hover { + color: var(--primary-color); + opacity: 0.7; + } + h3 { + margin: 8px 0 0; + font-size: 14px; + color: var(--primary-color); + } + .select-row { + display: flex; + flex-direction: column; + gap: 4px; + } + .assignee-pool { + display: flex; + flex-wrap: wrap; + gap: 6px 14px; + margin-top: 4px; + } + .checkbox-row { + display: inline-flex; + align-items: center; + gap: 8px; + font-size: 13px; + cursor: pointer; + margin: 2px 0; + } + .checkbox-row input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + } + .pool-item { + display: inline-flex; + align-items: center; + gap: 6px; + font-size: 13px; + cursor: pointer; + } + .pool-item input[type="checkbox"] { + width: 16px; + height: 16px; + cursor: pointer; + } + .select-row label { + font-size: 12px; + color: var(--secondary-text-color); + } + .select-row select { + padding: 8px; + border: 1px solid var(--divider-color); + border-radius: 4px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 14px; + } + .field-label { + font-size: 12px; + color: var(--secondary-text-color); + } + .weekday-chips { + display: flex; + flex-wrap: wrap; + gap: 6px; + } + .weekday-chip { + padding: 6px 12px; + border: 1px solid var(--divider-color); + border-radius: 16px; + background: var(--card-background-color, #fff); + color: var(--primary-text-color); + font-size: 13px; + cursor: pointer; + } + .weekday-chip.selected { + background: var(--primary-color, #03a9f4); + color: var(--text-primary-color, #fff); + border-color: var(--primary-color, #03a9f4); + } + .error { + color: var(--error-color, #f44336); + font-size: 13px; + } + .toggle-row { + display: flex; + align-items: center; + gap: 8px; + font-size: 14px; + cursor: pointer; + } + `,l([v({attribute:!1})],u.prototype,"hass",2),l([v({type:Boolean,attribute:"checklists-enabled"})],u.prototype,"checklistsEnabled",2),l([v({type:Boolean,attribute:"schedule-time-enabled"})],u.prototype,"scheduleTimeEnabled",2),l([v({type:Boolean,attribute:"completion-actions-enabled"})],u.prototype,"completionActionsEnabled",2),l([v({type:Number,attribute:"default-warning-days"})],u.prototype,"defaultWarningDays",2),l([h()],u.prototype,"_open",2),l([h()],u.prototype,"_loading",2),l([h()],u.prototype,"_error",2),l([h()],u.prototype,"_entryId",2),l([h()],u.prototype,"_taskId",2),l([h()],u.prototype,"_objectChoices",2),l([h()],u.prototype,"_name",2),l([h()],u.prototype,"_type",2),l([h()],u.prototype,"_scheduleType",2),l([h()],u.prototype,"_intervalDays",2),l([h()],u.prototype,"_intervalUnit",2),l([h()],u.prototype,"_dueDate",2),l([h()],u.prototype,"_warningDays",2),l([h()],u.prototype,"_earliestCompletionDays",2),l([h()],u.prototype,"_intervalAnchor",2),l([h()],u.prototype,"_weekdays",2),l([h()],u.prototype,"_nth",2),l([h()],u.prototype,"_nthWeekday",2),l([h()],u.prototype,"_domDay",2),l([h()],u.prototype,"_domLastDay",2),l([h()],u.prototype,"_domBusiness",2),l([h()],u.prototype,"_calOffset",2),l([h()],u.prototype,"_notes",2),l([h()],u.prototype,"_documentationUrl",2),l([h()],u.prototype,"_customIcon",2),l([h()],u.prototype,"_priority",2),l([h()],u.prototype,"_labels",2),l([h()],u.prototype,"_enabled",2),l([h()],u.prototype,"_triggerEntityId",2),l([h()],u.prototype,"_triggerEntityIds",2),l([h()],u.prototype,"_triggerEntityLogic",2),l([h()],u.prototype,"_triggerAttribute",2),l([h()],u.prototype,"_triggerType",2),l([h()],u.prototype,"_triggerAbove",2),l([h()],u.prototype,"_triggerBelow",2),l([h()],u.prototype,"_triggerForMinutes",2),l([h()],u.prototype,"_triggerTargetValue",2),l([h()],u.prototype,"_triggerDeltaMode",2),l([h()],u.prototype,"_autoCompleteOnRecovery",2),l([h()],u.prototype,"_triggerFromState",2),l([h()],u.prototype,"_triggerToState",2),l([h()],u.prototype,"_triggerTargetChanges",2),l([h()],u.prototype,"_triggerRuntimeHours",2),l([h()],u.prototype,"_compoundLogic",2),l([h()],u.prototype,"_compoundConditions",2),l([h()],u.prototype,"_suggestedAttributes",2),l([h()],u.prototype,"_availableAttributes",2),l([h()],u.prototype,"_entityDomain",2),l([h()],u.prototype,"_lastPerformed",2),l([h()],u.prototype,"_nfcTagId",2),l([h()],u.prototype,"_readingUnit",2),l([h()],u.prototype,"_availableTags",2),l([h()],u.prototype,"_responsibleUserId",2),l([h()],u.prototype,"_assigneePool",2),l([h()],u.prototype,"_rotationStrategy",2),l([h()],u.prototype,"_availableUsers",2),l([h()],u.prototype,"_checklistText",2),l([h()],u.prototype,"_scheduleTime",2),l([h()],u.prototype,"_actionService",2),l([h()],u.prototype,"_actionTargetEntity",2),l([h()],u.prototype,"_actionData",2),l([h()],u.prototype,"_actionDataJsonFallback",2),l([h()],u.prototype,"_actionTesting",2),l([h()],u.prototype,"_actionTestResult",2),l([h()],u.prototype,"_actionTestError",2),l([h()],u.prototype,"_qcNotes",2),l([h()],u.prototype,"_qcCost",2),l([h()],u.prototype,"_qcDuration",2),l([h()],u.prototype,"_qcFeedback",2),l([h()],u.prototype,"_environmentalEntity",2),l([h()],u.prototype,"_environmentalAttribute",2);customElements.get("maintenance-task-dialog")||customElements.define("maintenance-task-dialog",u)});var O,Ft=y(()=>{"use strict";T();j();C();X();O=class extends ${constructor(){super(...arguments);this._open=!1;this._saving=!1;this._error="";this._draft=null;this._originalSnapshot=null}get _lang(){return this.hass?.language||"en"}openEdit(e){this._draft={...e},this._originalSnapshot={...e},this._error="",this._open=!0}close(){this._open=!1,this._error="",this._draft=null,this._originalSnapshot=null}_set(e,t){this._draft&&(this._draft={...this._draft,[e]:t})}async _save(){if(!(!this._draft||!this._originalSnapshot)){this._saving=!0,this._error="";try{let e={type:"maintenance_supporter/task/history/update",entry_id:this._draft.entry_id,task_id:this._draft.task_id,original_timestamp:this._originalSnapshot.original_timestamp};if(this._draft.timestamp!==this._originalSnapshot.timestamp&&(e.timestamp=this._draft.timestamp),this._draft.notes!==this._originalSnapshot.notes&&(e.notes=this._draft.notes),this._draft.cost!==this._originalSnapshot.cost&&(e.cost=this._draft.cost),this._draft.duration!==this._originalSnapshot.duration&&(e.duration=this._draft.duration),this._draft.completed_by!==this._originalSnapshot.completed_by&&(e.completed_by=this._draft.completed_by),Object.keys(e).filter(i=>!["type","entry_id","task_id","original_timestamp"].includes(i)).length===0){this.close();return}await this.hass.connection.sendMessagePromise(e),this.dispatchEvent(new CustomEvent("history-entry-saved",{detail:{entry_id:this._draft.entry_id,task_id:this._draft.task_id,new_timestamp:this._draft.timestamp},bubbles:!0,composed:!0})),this.close()}catch(e){this._error=I(e,this._lang)}finally{this._saving=!1}}}render(){if(!this._open||!this._draft)return _;let e=this._lang,t=this._draft;return n` +
    + + `}};O.styles=E` + :host { display: contents; } + .backdrop { + position: fixed; inset: 0; + background: rgba(0,0,0,0.5); + z-index: 100; + } + .dialog { + position: fixed; left: 50%; top: 50%; + transform: translate(-50%, -50%); + width: 95vw; max-width: 480px; + background: var(--card-background-color, var(--ha-card-background, #1c1c1c)); + color: var(--primary-text-color); + border-radius: 12px; + box-shadow: 0 10px 30px rgba(0,0,0,0.4); + padding: 20px; + display: flex; flex-direction: column; gap: 12px; + z-index: 101; + max-height: 90vh; overflow: auto; + } + h2 { margin: 0; font-size: 18px; } + .entry-type { + display: flex; align-items: center; gap: 6px; + color: var(--secondary-text-color); font-size: 13px; + } + label { display: flex; flex-direction: column; gap: 4px; font-size: 13px; } + label span { color: var(--secondary-text-color); } + .row { display: grid; grid-template-columns: 1fr 1fr; gap: 12px; } + input, textarea { + padding: 8px; font-size: 14px; + background: var(--secondary-background-color, #2c2c2c); + color: var(--primary-text-color); + border: 1px solid var(--divider-color, #444); + border-radius: 6px; + width: 100%; box-sizing: border-box; + font-family: inherit; + } + .actions { + display: flex; gap: 8px; justify-content: flex-end; + margin-top: 8px; + } + button { + padding: 8px 16px; font-size: 14px; + border-radius: 6px; cursor: pointer; + border: none; font-weight: 500; + } + button.cancel { + background: transparent; + color: var(--primary-text-color); + border: 1px solid var(--divider-color); + } + button.save { + background: var(--primary-color); + color: var(--text-primary-color, white); + } + button[disabled] { opacity: 0.5; cursor: wait; } + .error { + color: var(--error-color, #d32f2f); + font-size: 13px; padding: 8px; + background: rgba(211,47,47,0.1); + border-radius: 6px; + } + `,l([v({attribute:!1})],O.prototype,"hass",2),l([h()],O.prototype,"_open",2),l([h()],O.prototype,"_saving",2),l([h()],O.prototype,"_error",2),l([h()],O.prototype,"_draft",2);customElements.get("maintenance-history-edit-dialog")||customElements.define("maintenance-history-edit-dialog",O)});function oe(a){return a.replace(/&/g,"&").replace(//g,">").replace(/"/g,""")}function Wt(a){return!a.startsWith("data:image/svg+xml,")&&!a.startsWith("data:image/png;base64,")?"":oe(a)}function Bi(a){return a.replace(/[/\\:*?"<>|#%]+/g,"").replace(/\s+/g,"-").toLowerCase().substring(0,100)}var H,Vt=y(()=>{"use strict";T();j();C();H=class extends ${constructor(){super(...arguments);this.lang="en";this._open=!1;this._loading=!1;this._error="";this._viewResult=null;this._completeResult=null;this._urlMode="companion";this._entryId="";this._taskId=null;this._objectName="";this._taskName="";this._generateSeq=0}openForObject(e,t){this._entryId=e,this._taskId=null,this._objectName=t,this._taskName="",this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}openForTask(e,t,i,o){this._entryId=e,this._taskId=t,this._objectName=i,this._taskName=o,this._urlMode="companion",this._error="",this._viewResult=null,this._completeResult=null,this._open=!0,this._generate()}async _generate(){let e=++this._generateSeq;this._loading=!0,this._error="",this._viewResult=null,this._completeResult=null;try{let t={type:"maintenance_supporter/qr/generate",entry_id:this._entryId,url_mode:this._urlMode};this._taskId&&(t.task_id=this._taskId);let i=[this.hass.connection.sendMessagePromise({...t,action:"view"})];this._taskId&&i.push(this.hass.connection.sendMessagePromise({...t,action:"complete"}));let o=await Promise.all(i);if(e!==this._generateSeq)return;this._viewResult=o[0],o.length>1&&(this._completeResult=o[1])}catch(t){if(e!==this._generateSeq)return;let i=t?.code,o=t?.message;this._error=i==="no_url"||typeof o=="string"&&o.includes("No Home Assistant URL")?r("qr_error_no_url",this.lang):r("qr_error",this.lang)}finally{e===this._generateSeq&&(this._loading=!1)}}_setUrlMode(e){this._urlMode!==e&&(this._urlMode=e,this._generate())}_print(){if(!this._viewResult)return;let e=this._viewResult,t=e.label.task_name?`${e.label.object_name} \u2014 ${e.label.task_name}`:e.label.object_name,i=[e.label.manufacturer,e.label.model].filter(Boolean).join(" "),o=window.open("","_blank","width=600,height=500");if(!o)return;let d=this.lang||"en",p=oe(t),c=oe(i),g=!!this._completeResult,b=oe(r("qr_action_view",d)),m=oe(r("qr_action_complete",d));o.document.write(` +${p} + +

    ${p}

    +${c?`
    ${c}
    `:""} +
    +
    + QR Info +
    ${b}
    +
    + ${g?`
    + QR Complete +
    ${m}
    +
    `:""} +
    +
    ${oe(this._viewResult.url)}
    + + + + + +Homie Dashboard + + + + + + + + + + +
    +
    + HOMIE DASH +
    +
    +
    + +
    +
    +
    +
    Weather
    +
    + +
    + +
    + + +
    + +
    +
    + +
    + + + + + + + + +
    + + +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +

    WELCOME

    +
    +
    +
    + +
    +
    +
    +
    +
    + Connection lost — retrying… +
    + +
    +
    +
    + Artist + / + Track +
    +
    + +
    +
    + +
    +
    +
    +
    + + + + + + +
    + +
    +
    + + Phone volume only +
    + + 50 +
    +
    + +
    +
    +
    +
    +
    +
    + 0:00 + +
    +
    +
    + +
    +
    +
    +
    + + +
    + + +
    + + +
    + +
    + + +
    + + +
    +
    +
    +
    +
    +

    WELCOME

    +
    +
    +
    + + +
    +
    +
    + +
    +
    + + +
    + + +
    + + + + +
    + + + + +
    + + + + +
    + + +
    + +
    + +
    + + +
    + + +
    +
    + + +
    + + + + +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    + + + Humidity + + +
    +
    + + + Wind + + +
    +
    + + + Pressure + + +
    +
    + + + UV Index + + +
    +
    + + + Cloud Cover + + +
    +
    + +
    +
    +
    5-DAY FORECAST
    +
    +
    +
    +
    HOURLY FORECAST
    +
    +
    +
    + +
    +
    + + +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    + + +
    +
    +
    +
    +
    AQI
    +
    +
    +
    +
    PM2.5
    +
    +
    +
    +
    PM10
    +
    +
    +
    +
    CO
    +
    +
    +
    +
    NO2
    +
    +
    +
    +
    + + +
    + +
    +
    +
    W
    +
    +
    +
    +
    Today
    +
    +
    +
    +
    Solar
    +
    +
    +
    +
    Grid
    +
    +
    +
    +
    + + +
    +
    +
    + +
    + +
    +
    Nothing Playing
    + + + +
    + +
    + +
    + +
    +
    + +
    +
    +
    + +
    + + + +
    + +
    +
    + +
    + + 50 +
    +
    +
    +
    + +
    +
    +
    +
    +
    + 0:00 + +
    +
    +
    +
    + + +
    +
    Good Evening
    +
    + + · + +
    +
    +
    + + +
    +
    +
    +
    +
    + +
    +
    +
    + + +
    + + +
    + DISARMED + +
    + + +
    +
    + + + +
    + +
    + + +
    +
    + + +
    + +
    + + +
    +
    + +
    + +
    +
    +
    + +
    +
    + + +
    +
    + +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    + + +
    +
    + +
    +
    +
    + +
    + + +
    +
    + +
    +
    +
    Loading…
    +
    +
    + +
    +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + +
    +
    + + + + + +
    +
    +
    +
    + +
    + + +
    + + +
    + + +
    +
    +
    --
    +
    + + +
    +
    --
    +
    --
    +
    Weather
    +
    + + +
    + +
    + + +
    + + +
    +
    + + Upcoming +
    + +
    +
    Day
    +
    All Events
    +
    + +
    + +
    +
    Loading…
    +
    + + + + +
    +
    + + Today's Meals +
    +
    +
    Loading…
    +
    +
    +
    + + +
    +
    + + To-Do +
    +
    +
    Loading…
    +
    + + +
    + +
    +
    + + + + Habit Tracker + +
    +
    +
    + + +
    +
    + + + + + + + Mood Tracker + +
    +
    +
    +
    +
    + +
    +
    + + +
    + +
    + +
    + +
    + + +
    +
    +
    +
    + + Motion Events +
    +
    Last 24 hours
    +
    +
    +
    +
    Loading…
    +
    +
    +
    +
    + +
    + +
    + +
    +
    +
    + +
    + + + + + + + +
    + +
    +
    +
    + +
    + + + +
    + +
    + 0 / 0 + +
    +
    +
    + + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + + + +
    + + +
    +
    °
    +
    +
    +
    +
    + + +
    + + +
    +
    + + + +
    +
    +
    AQI
    +
    +
    +
    +
    +
    +
    + PM2.5 + +
    +
    + PM10 + +
    +
    + CO + +
    +
    + NO2 + +
    +
    +
    + + +
    +
    °
    +
    +
    +
    +
    + + +
    +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SUNRISE + + + + SUNSET + + + + + + + + + + + + + + + + + + + + + + +
    +
    +
    + Moon Phase + +
    +
    +
    + +
    + + +
    +
    +
    +
    +
    +
    HUMIDITY
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    WIND
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    UV INDEX
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    PRESSURE
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    CLOUD
    +
    +
    +
    + +
    +
    +
    + + +
    + + +
    +
    +
    5-Day Forecast
    +
    + +
    +
    +
    + + +
    +
    +
    Hourly Forecast
    +
    + +
    +
    +
    + +
    + + +
    +
    +
    +
    + +
    +
    + + +
    +
    + +
    + +
    + + +
    + + + + + +
    +
    + + ° +
    +
    + Target +
    + + ° +
    +
    + +
    + + + + +
    + + +
    + + + + + +
    +
    +
    + + +
    + + + + + +
    +
    +
    +
    + +
    +
    +
    +
    +
    + SETTINGS +
    + +
    + STARTUP + +
    +
    +
    + + + + + + + + + + + + +
    +
    + +
    +
    + THEME + +
    +
    +
    +
    + +
    +
    + TIME OF DAY THEME + +
    +
    +

    Automatically switch the accent colour at set times of day. Leave a slot as Default to keep the current theme.

    +
    +
    + +
    +
    + BLOOM INTENSITY + +
    +
    +
    + + + +
    +
    + +
    +
    + FONTS + +
    +
    +
    +
    + + + + + + + + + + +
    +
    +
    +
    + +
    +
    + LANGUAGE + +
    +
    +
    + + + + + + + + + + + + + + + + +
    +
    + +
    +
    + TOP-LEFT ELEMENTS + +
    +
    +
    + Dashboard Name + +
    +
    + Custom Name + +
    +
    + Clock + +
    +
    + Clock Format + +
    +
    + Toggle on for 12-hour (AM/PM), off for 24-hour +
    +
    + +
    +
    + CENTER ELEMENTS + +
    +
    +
    + Personalised Name + +
    +
    + +
    +
    + TOP-RIGHT ELEMENTS + +
    +
    +
    + Mode Launcher + +
    +
    + Now Playing + +
    +
    + Memories + +
    +
    + My Day + +
    +
    + Cameras + +
    +
    + Thermostat + +
    +
    + Clock + +
    +
    + Weather + +
    +
    + Solar + +
    +
    + Pet Stats + +
    +
    + Security + +
    +
    + +
    +
    + SENSOR ROW + +
    +
    +
    +
    + + + + + + +
    +
    + SCREENSAVER + +
    +
    +
    + Enable Screensaver + +
    +
    + Start after (min) + +
    +
    + Rotate after (min) + +
    +
    + Rotate between + +
    +
    +
    + + + + + + + + + + + + +
    +
    +
    + +
    +
    + GESTURES + +
    +
    +

    Choose how to exit a dashboard and return to the overview screen.

    +
    + + + +
    +
    + + +
    +
    + + +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    + + Habit History +
    +
    +
    +
    Loading…
    +
    +
    +
    + + +
    +
    +
    + + +
    SOLAR ENERGY
    + +
    + + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + SOLAR + + + + + + + + + + + + + + + + + BATTERY + + + + + + + + + + + + + HOME + + + + + + + + + + + + + + + + + + GRID + + + + + EXPORT + CHARGE + +
    + + +
    + +
    +
    + +
    +
    W
    +
    Live Usage
    +
    + +
    +
    + +
    +
    kW
    +
    Today's Usage
    +
    + +
    +
    + +
    +
    +
    Today's Cost
    +
    + +
    +
    + +
    +
    kWh
    +
    Monthly Usage
    +
    + +
    +
    + +
    +
    +
    Monthly Cost
    +
    +
    + + +
    + +
    +
    + +
    +
    kW
    +
    Production
    +
    + +
    +
    + +
    +
    kW
    +
    Grid
    +
    + +
    +
    + +
    +
    %
    +
    Battery
    +
    + +
    +
    + +
    +
    °C
    +
    Inverter Temp
    +
    + +
    +
    + +
    +
    °C
    +
    Outdoor Temp
    +
    +
    + + +
    +
    + HOURLY ENERGY +
    + Solar + Consumption +
    +
    +
    + +
    Loading…
    +
    +
    +
    + +
    +
    + + +
    +
    +
    +
    +
    + + Mood History +
    +
    +
    +
    Loading…
    +
    +
    +
    + + +
    +
    +
    + + + + +
    +
    +
    +
    + Doorbell Camera +
    + + + + + Camera offline +
    +
    +
    + + + + + + + \ No newline at end of file diff --git a/www/community/lovelace-layout-card/layout-card.js b/www/community/lovelace-layout-card/layout-card.js new file mode 100644 index 0000000..eeae97a --- /dev/null +++ b/www/community/lovelace-layout-card/layout-card.js @@ -0,0 +1 @@ +var t,e,n,i,r,o,a,s,u,l,c,d,h,v,f,p,y,g,m,_,b,w,k,E,C;function x(t,e){return e||(e=t.slice(0)),Object.freeze(Object.defineProperties(t,{raw:{value:Object.freeze(e)}}))}function $(t,e){var n=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),n.push.apply(n,i)}return n}function A(t){for(var e=1;e=0;--r){var o=this.tryEntries[r],a=o[4],s=this.prev,u=o[1],l=o[2];if(-1===o[0])return i("end"),!1;if(!u&&!l)throw Error("try statement without catch or finally");if(null!=o[0]&&o[0]<=s){if(s=0;--n){var i=this.tryEntries[n];if(i[0]>-1&&i[0]<=this.prev&&this.prev=0;--e){var n=this.tryEntries[e];if(n[2]===t)return this.complete(n[4],n[3]),E(n),d}},catch:function(t){for(var e=this.tryEntries.length-1;e>=0;--e){var n=this.tryEntries[e];if(n[0]===t){var i=n[4];if("throw"===i.type){var r=i.arg;E(n)}return r}}throw Error("illegal catch attempt")},delegateYield:function(e,n,i){return this.delegate={i:x(e),r:n,n:i},"next"===this.method&&(this.arg=t),d}},e}function U(t,e,n,i,r,o,a){try{var s=t[o](a),u=s.value}catch(t){return void n(t)}s.done?e(u):Promise.resolve(u).then(i,r)}function L(t){return function(){var e=this,n=arguments;return new Promise((function(i,r){var o=t.apply(e,n);function a(t){U(o,i,r,a,s,"next",t)}function s(t){U(o,i,r,a,s,"throw",t)}a(void 0)}))}}function R(t,e,n){return e=G(e),function(t,e){if(e&&("object"==K(e)||"function"==typeof e))return e;if(void 0!==e)throw new TypeError("Derived constructors may only return object or undefined");return H(t)}(t,I()?Reflect.construct(e,n||[],G(t).constructor):e.apply(t,n))}function H(t){if(void 0===t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return t}function N(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function");t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,writable:!0,configurable:!0}}),Object.defineProperty(t,"prototype",{writable:!1}),e&&D(t,e)}function z(t){var e="function"==typeof Map?new Map:void 0;return z=function(t){if(null===t||!function(t){try{return-1!==Function.toString.call(t).indexOf("[native code]")}catch(e){return"function"==typeof t}}(t))return t;if("function"!=typeof t)throw new TypeError("Super expression must either be null or a function");if(void 0!==e){if(e.has(t))return e.get(t);e.set(t,n)}function n(){return function(t,e,n){if(I())return Reflect.construct.apply(null,arguments);var i=[null];i.push.apply(i,e);var r=new(t.bind.apply(t,i));return n&&D(r,n.prototype),r}(t,arguments,G(this).constructor)}return n.prototype=Object.create(t.prototype,{constructor:{value:n,enumerable:!1,writable:!0,configurable:!0}}),D(n,t)},z(t)}function I(){try{var t=!Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){})))}catch(t){}return(I=function(){return!!t})()}function D(t,e){return D=Object.setPrototypeOf?Object.setPrototypeOf.bind():function(t,e){return t.__proto__=e,t},D(t,e)}function G(t){return G=Object.setPrototypeOf?Object.getPrototypeOf.bind():function(t){return t.__proto__||Object.getPrototypeOf(t)},G(t)}function q(t,e){var n="undefined"!=typeof Symbol&&t[Symbol.iterator]||t["@@iterator"];if(!n){if(Array.isArray(t)||(n=B(t))||e&&t&&"number"==typeof t.length){n&&(t=n);var i=0,r=function(){};return{s:r,n:function(){return i>=t.length?{done:!0}:{done:!1,value:t[i++]}},e:function(t){throw t},f:r}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}var o,a=!0,s=!1;return{s:function(){n=n.call(t)},n:function(){var t=n.next();return a=t.done,t},e:function(t){s=!0,o=t},f:function(){try{a||null==n.return||n.return()}finally{if(s)throw o}}}}function B(t,e){if(t){if("string"==typeof t)return W(t,e);var n={}.toString.call(t).slice(8,-1);return"Object"===n&&t.constructor&&(n=t.constructor.name),"Map"===n||"Set"===n?Array.from(t):"Arguments"===n||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)?W(t,e):void 0}}function W(t,e){(null==e||e>t.length)&&(e=t.length);for(var n=0,i=Array(e);n=0;s--)(r=t[s])&&(a=(o<3?r(a):o>3?r(e,n,a):r(e,n))||a);return o>3&&a&&Object.defineProperty(e,n,a),a}"function"==typeof SuppressedError&&SuppressedError;var Z=globalThis,X=Z.ShadowRoot&&(void 0===Z.ShadyCSS||Z.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,tt=Symbol(),et=new WeakMap,nt=function(){return F((function t(e,n,i){if(Q(this,t),this._$cssResult$=!0,i!==tt)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=n}),[{key:"styleSheet",get:function(){var t=this.o,e=this.t;if(X&&void 0===t){var n=void 0!==e&&1===e.length;n&&(t=et.get(e)),void 0===t&&((this.o=t=new CSSStyleSheet).replaceSync(this.cssText),n&&et.set(e,t))}return t}},{key:"toString",value:function(){return this.cssText}}])}(),it=function(t){for(var e=arguments.length,n=new Array(e>1?e-1:0),i=1;i0&&(this._$Ep=e)}},{key:"createRenderRoot",value:function(){var t,e=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return function(t,e){if(X)t.adoptedStyleSheets=e.map((function(t){return t instanceof CSSStyleSheet?t:t.styleSheet}));else{var n,i=q(e);try{for(i.s();!(n=i.n()).done;){var r=n.value,o=document.createElement("style"),a=Z.litNonce;void 0!==a&&o.setAttribute("nonce",a),o.textContent=r.cssText,t.appendChild(o)}}catch(t){i.e(t)}finally{i.f()}}}(e,this.constructor.elementStyles),e}},{key:"connectedCallback",value:function(){var t,e;null!==(t=this.renderRoot)&&void 0!==t||(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(e=this._$EO)||void 0===e||e.forEach((function(t){var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)}))}},{key:"enableUpdating",value:function(t){}},{key:"disconnectedCallback",value:function(){var t;null===(t=this._$EO)||void 0===t||t.forEach((function(t){var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)}))}},{key:"attributeChangedCallback",value:function(t,e,n){this._$AK(t,n)}},{key:"_$ET",value:function(t,e){var n=this.constructor.elementProperties.get(t),i=this.constructor._$Eu(t,n);if(void 0!==i&&!0===n.reflect){var r,o=(void 0!==(null===(r=n.converter)||void 0===r?void 0:r.toAttribute)?n.converter:yt).toAttribute(e,n.type);this._$Em=t,null==o?this.removeAttribute(i):this.setAttribute(i,o),this._$Em=null}}},{key:"_$AK",value:function(t,e){var n=this.constructor,i=n._$Eh.get(t);if(void 0!==i&&this._$Em!==i){var r,o,a,s,u=n.getPropertyOptions(i),l="function"==typeof u.converter?{fromAttribute:u.converter}:void 0!==(null===(r=u.converter)||void 0===r?void 0:r.fromAttribute)?u.converter:yt;this._$Em=i,this[i]=null!==(o=null!==(a=l.fromAttribute(e,u.type))&&void 0!==a?a:null===(s=this._$Ej)||void 0===s?void 0:s.get(i))&&void 0!==o?o:null,this._$Em=null}}},{key:"requestUpdate",value:function(t,e,n){if(void 0!==t){var i,r,o=this.constructor,a=this[t];if(null!=n||(n=o.getPropertyOptions(t)),!((null!==(i=n.hasChanged)&&void 0!==i?i:gt)(a,e)||n.useDefault&&n.reflect&&a===(null===(r=this._$Ej)||void 0===r?void 0:r.get(t))&&!this.hasAttribute(o._$Eu(t,n))))return;this.C(t,e,n)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}},{key:"C",value:function(t,e,n,i){var r,o,a,s=n.useDefault,u=n.reflect,l=n.wrapped;s&&!(null!==(r=this._$Ej)&&void 0!==r?r:this._$Ej=new Map).has(t)&&(this._$Ej.set(t,null!==(o=null!=i?i:e)&&void 0!==o?o:this[t]),!0!==l||void 0!==i)||(this._$AL.has(t)||(this.hasUpdated||s||(e=void 0),this._$AL.set(t,e)),!0===u&&this._$Em!==t&&(null!==(a=this._$Eq)&&void 0!==a?a:this._$Eq=new Set).add(t))}},{key:"_$EP",value:(e=L(T().mark((function t(){var e;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return this.isUpdatePending=!0,t.prev=1,t.next=4,this._$ES;case 4:t.next=9;break;case 6:t.prev=6,t.t0=t.catch(1),Promise.reject(t.t0);case 9:if(e=this.scheduleUpdate(),t.t1=null!=e,!t.t1){t.next=14;break}return t.next=14,e;case 14:return t.abrupt("return",!this.isUpdatePending);case 15:case"end":return t.stop()}}),t,this,[[1,6]])}))),function(){return e.apply(this,arguments)})},{key:"scheduleUpdate",value:function(){return this.performUpdate()}},{key:"performUpdate",value:function(){if(this.isUpdatePending){if(!this.hasUpdated){var t;if(null!==(t=this.renderRoot)&&void 0!==t||(this.renderRoot=this.createRenderRoot()),this._$Ep){var e,n=q(this._$Ep);try{for(n.s();!(e=n.n()).done;){var i=P(e.value,2),r=i[0],o=i[1];this[r]=o}}catch(t){n.e(t)}finally{n.f()}this._$Ep=void 0}var a=this.constructor.elementProperties;if(a.size>0){var s,u=q(a);try{for(u.s();!(s=u.n()).done;){var l=P(s.value,2),c=l[0],d=l[1],h=d.wrapped,v=this[c];!0!==h||this._$AL.has(c)||void 0===v||this.C(c,void 0,d,v)}}catch(t){u.e(t)}finally{u.f()}}}var f=!1,p=this._$AL;try{var y;(f=this.shouldUpdate(p))?(this.willUpdate(p),null!==(y=this._$EO)&&void 0!==y&&y.forEach((function(t){var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(p)):this._$EM()}catch(p){throw f=!1,this._$EM(),p}f&&this._$AE(p)}}},{key:"willUpdate",value:function(t){}},{key:"_$AE",value:function(t){var e;null!==(e=this._$EO)&&void 0!==e&&e.forEach((function(t){var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}},{key:"_$EM",value:function(){this._$AL=new Map,this.isUpdatePending=!1}},{key:"updateComplete",get:function(){return this.getUpdateComplete()}},{key:"getUpdateComplete",value:function(){return this._$ES}},{key:"shouldUpdate",value:function(t){return!0}},{key:"update",value:function(t){var e=this;this._$Eq&&(this._$Eq=this._$Eq.forEach((function(t){return e._$ET(t,e[t])}))),this._$EM()}},{key:"updated",value:function(t){}},{key:"firstUpdated",value:function(t){}}],[{key:"addInitializer",value:function(t){var e;this._$Ei(),(null!==(e=this.l)&&void 0!==e?e:this.l=[]).push(t)}},{key:"observedAttributes",get:function(){return this.finalize(),this._$Eh&&j(this._$Eh.keys())}},{key:"createProperty",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:mt;if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){var n=Symbol(),i=this.getPropertyDescriptor(t,n,e);void 0!==i&&at(this.prototype,t,i)}}},{key:"getPropertyDescriptor",value:function(t,e,n){var i,r=null!==(i=st(this.prototype,t))&&void 0!==i?i:{get:function(){return this[e]},set:function(t){this[e]=t}},o=r.get,a=r.set;return{get:o,set:function(e){var i=null==o?void 0:o.call(this);null!=a&&a.call(this,e),this.requestUpdate(t,i,n)},configurable:!0,enumerable:!0}}},{key:"getPropertyOptions",value:function(t){var e;return null!==(e=this.elementProperties.get(t))&&void 0!==e?e:mt}},{key:"_$Ei",value:function(){if(!this.hasOwnProperty(pt("elementProperties"))){var t=ct(this);t.finalize(),void 0!==t.l&&(this.l=j(t.l)),this.elementProperties=new Map(t.elementProperties)}}},{key:"finalize",value:function(){if(!this.hasOwnProperty(pt("finalized"))){if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(pt("properties"))){var t,e=this.properties,n=q([].concat(j(ut(e)),j(lt(e))));try{for(n.s();!(t=n.n()).done;){var i=t.value;this.createProperty(i,e[i])}}catch(t){n.e(t)}finally{n.f()}}var r=this[Symbol.metadata];if(null!==r){var o=litPropertyMetadata.get(r);if(void 0!==o){var a,s=q(o);try{for(s.s();!(a=s.n()).done;){var u=P(a.value,2),l=u[0],c=u[1];this.elementProperties.set(l,c)}}catch(t){s.e(t)}finally{s.f()}}}this._$Eh=new Map;var d,h=q(this.elementProperties);try{for(h.s();!(d=h.n()).done;){var v=P(d.value,2),f=v[0],p=v[1],y=this._$Eu(f,p);void 0!==y&&this._$Eh.set(y,f)}}catch(t){h.e(t)}finally{h.f()}this.elementStyles=this.finalizeStyles(this.styles)}}},{key:"finalizeStyles",value:function(t){var e=[];if(Array.isArray(t)){var n,i=q(new Set(t.flat(1/0).reverse()));try{for(i.s();!(n=i.n()).done;){var r=n.value;e.unshift(rt(r))}}catch(t){i.e(t)}finally{i.f()}}else void 0!==t&&e.push(rt(t));return e}},{key:"_$Eu",value:function(t,e){var n=e.attribute;return!1===n?void 0:"string"==typeof n?n:"string"==typeof t?t.toLowerCase():void 0}}]);var e}();_t.elementStyles=[],_t.shadowRootOptions={mode:"open"},_t[pt("elementProperties")]=new Map,_t[pt("finalized")]=new Map,null!=ft&&ft({ReactiveElement:_t}),(null!==(n=dt.reactiveElementVersions)&&void 0!==n?n:dt.reactiveElementVersions=[]).push("2.1.0");var bt=globalThis,wt=bt.trustedTypes,kt=wt?wt.createPolicy("lit-html",{createHTML:function(t){return t}}):void 0,Et="$lit$",Ct="lit$".concat(Math.random().toFixed(9).slice(2),"$"),xt="?"+Ct,$t="<".concat(xt,">"),At=document,St=function(){return At.createComment("")},Ot=function(t){return null===t||"object"!=K(t)&&"function"!=typeof t},Mt=Array.isArray,jt="[ \t\n\f\r]",Pt=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,Tt=/-->/g,Ut=/>/g,Lt=RegExp(">|".concat(jt,"(?:([^\\s\"'>=/]+)(").concat(jt,"*=").concat(jt,"*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)"),"g"),Rt=/'/g,Ht=/"/g,Nt=/^(?:script|style|textarea|title)$/i,zt=function(t){return function(e){for(var n=arguments.length,i=new Array(n>1?n-1:0),r=1;r":3===e?"":"",a=Pt,s=0;s"===c[0]?(a=null!=n?n:Pt,d=-1):void 0===c[1]?d=-2:(d=a.lastIndex-c[2].length,l=c[1],a=void 0===c[3]?Lt:'"'===c[3]?Ht:Rt):a===Ht||a===Rt?a=Lt:a===Tt||a===Ut?a=Pt:(a=Lt,n=void 0);var v=a===Lt&&t[s+1].startsWith("/>")?" ":"";o+=a===Pt?u+$t:d>=0?(r.push(l),u.slice(0,d)+Et+u.slice(d)+Ct+v):u+Ct+(-2===d?s:v)}return[Bt(t,o+(t[i]||"")+(2===e?"":3===e?"":"")),r]},Qt=function(){return F((function t(e,n){var i,r=e.strings,o=e._$litType$;Q(this,t),this.parts=[];var a=0,s=0,u=r.length-1,l=this.parts,c=P(Wt(r,o),2),d=c[0],h=c[1];if(this.el=t.createElement(d,n),qt.currentNode=this.el.content,2===o||3===o){var v=this.el.content.firstChild;v.replaceWith.apply(v,j(v.childNodes))}for(;null!==(i=qt.nextNode())&&l.length0){i.textContent=wt?wt.emptyScript:"";for(var k=0;k2&&void 0!==arguments[2]?arguments[2]:t,u=arguments.length>3?arguments[3]:void 0;if(e===It)return e;var l=void 0!==u?null===(n=s._$Co)||void 0===n?void 0:n[u]:s._$Cl,c=Ot(e)?void 0:e._$litDirective$;return(null===(i=l)||void 0===i?void 0:i.constructor)!==c&&(null!==(r=l)&&void 0!==r&&null!==(o=r._$AO)&&void 0!==o&&o.call(r,!1),void 0===c?l=void 0:(l=new c(t))._$AT(t,s,u),void 0!==u?(null!==(a=s._$Co)&&void 0!==a?a:s._$Co=[])[u]=l:s._$Cl=l),void 0!==l&&(e=Vt(t,l._$AS(t,e.values),l,u)),e}var Ft=function(){return F((function t(e,n){Q(this,t),this._$AV=[],this._$AN=void 0,this._$AD=e,this._$AM=n}),[{key:"parentNode",get:function(){return this._$AM.parentNode}},{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"u",value:function(t){var e,n=this._$AD,i=n.el.content,r=n.parts,o=(null!==(e=null==t?void 0:t.creationScope)&&void 0!==e?e:At).importNode(i,!0);qt.currentNode=o;for(var a=qt.nextNode(),s=0,u=0,l=r[0];void 0!==l;){var c;if(s===l.index){var d=void 0;2===l.type?d=new Jt(a,a.nextSibling,this,t):1===l.type?d=new l.ctor(a,l.name,l.strings,this,t):6===l.type&&(d=new te(a,this,t)),this._$AV.push(d),l=r[++u]}s!==(null===(c=l)||void 0===c?void 0:c.index)&&(a=qt.nextNode(),s++)}return qt.currentNode=At,o}},{key:"p",value:function(t){var e,n=0,i=q(this._$AV);try{for(i.s();!(e=i.n()).done;){var r=e.value;void 0!==r&&(void 0!==r.strings?(r._$AI(t,r,n),n+=r.strings.length-2):r._$AI(t[n])),n++}}catch(t){i.e(t)}finally{i.f()}}}])}(),Jt=function(){function t(e,n,i,r){var o;Q(this,t),this.type=2,this._$AH=Dt,this._$AN=void 0,this._$AA=e,this._$AB=n,this._$AM=i,this.options=r,this._$Cv=null===(o=null==r?void 0:r.isConnected)||void 0===o||o}return F(t,[{key:"_$AU",get:function(){var t,e;return null!==(t=null===(e=this._$AM)||void 0===e?void 0:e._$AU)&&void 0!==t?t:this._$Cv}},{key:"parentNode",get:function(){var t,e=this._$AA.parentNode,n=this._$AM;return void 0!==n&&11===(null===(t=e)||void 0===t?void 0:t.nodeType)&&(e=n.parentNode),e}},{key:"startNode",get:function(){return this._$AA}},{key:"endNode",get:function(){return this._$AB}},{key:"_$AI",value:function(t){t=Vt(this,t,arguments.length>1&&void 0!==arguments[1]?arguments[1]:this),Ot(t)?t===Dt||null==t||""===t?(this._$AH!==Dt&&this._$AR(),this._$AH=Dt):t!==this._$AH&&t!==It&&this._(t):void 0!==t._$litType$?this.$(t):void 0!==t.nodeType?this.T(t):function(t){return Mt(t)||"function"==typeof(null==t?void 0:t[Symbol.iterator])}(t)?this.k(t):this._(t)}},{key:"O",value:function(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}},{key:"T",value:function(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}},{key:"_",value:function(t){this._$AH!==Dt&&Ot(this._$AH)?this._$AA.nextSibling.data=t:this.T(At.createTextNode(t)),this._$AH=t}},{key:"$",value:function(t){var e,n=t.values,i=t._$litType$,r="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=Qt.createElement(Bt(i.h,i.h[0]),this.options)),i);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===r)this._$AH.p(n);else{var o=new Ft(r,this),a=o.u(this.options);o.p(n),this.T(a),this._$AH=o}}},{key:"_$AC",value:function(t){var e=Gt.get(t.strings);return void 0===e&&Gt.set(t.strings,e=new Qt(t)),e}},{key:"k",value:function(e){Mt(this._$AH)||(this._$AH=[],this._$AR());var n,i,r=this._$AH,o=0,a=q(e);try{for(a.s();!(i=a.n()).done;){var s=i.value;o===r.length?r.push(n=new t(this.O(St()),this.O(St()),this,this.options)):n=r[o],n._$AI(s),o++}}catch(t){a.e(t)}finally{a.f()}o0&&void 0!==arguments[0]?arguments[0]:this._$AA.nextSibling,e=arguments.length>1?arguments[1]:void 0;for(null===(n=this._$AP)||void 0===n||n.call(this,!1,!0,e);t&&t!==this._$AB;){var n,i=t.nextSibling;t.remove(),t=i}}},{key:"setConnected",value:function(t){var e;void 0===this._$AM&&(this._$Cv=t,null===(e=this._$AP)||void 0===e||e.call(this,t))}}])}(),Kt=function(){return F((function t(e,n,i,r,o){Q(this,t),this.type=1,this._$AH=Dt,this._$AN=void 0,this.element=e,this.name=n,this._$AM=r,this.options=o,i.length>2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=Dt}),[{key:"tagName",get:function(){return this.element.tagName}},{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"_$AI",value:function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:this,n=arguments.length>2?arguments[2]:void 0,i=arguments.length>3?arguments[3]:void 0,r=this.strings,o=!1;if(void 0===r)t=Vt(this,t,e,0),(o=!Ot(t)||t!==this._$AH&&t!==It)&&(this._$AH=t);else{var a,s,u=t;for(t=r[0],a=0;a1&&void 0!==arguments[1]?arguments[1]:this,0))&&void 0!==e?e:Dt)!==It){var n=this._$AH,i=t===Dt&&n!==Dt||t.capture!==n.capture||t.once!==n.once||t.passive!==n.passive,r=t!==Dt&&(n===Dt||i);i&&this.element.removeEventListener(this.name,this,n),r&&this.element.addEventListener(this.name,this,t),this._$AH=t}}},{key:"handleEvent",value:function(t){var e,n;"function"==typeof this._$AH?this._$AH.call(null!==(e=null===(n=this.options)||void 0===n?void 0:n.host)&&void 0!==e?e:this.element,t):this._$AH.handleEvent(t)}}])}(),te=function(){return F((function t(e,n,i){Q(this,t),this.element=e,this.type=6,this._$AN=void 0,this._$AM=n,this.options=i}),[{key:"_$AU",get:function(){return this._$AM._$AU}},{key:"_$AI",value:function(t){Vt(this,t)}}])}(),ee=bt.litHtmlPolyfillSupport;null!=ee&&ee(Qt,Jt),(null!==(i=bt.litHtmlVersions)&&void 0!==i?i:bt.litHtmlVersions=[]).push("3.3.0");var ne=globalThis,ie=function(){function t(){var e;return Q(this,t),(e=R(this,t,arguments)).renderOptions={host:H(e)},e._$Do=void 0,e}return N(t,_t),F(t,[{key:"createRenderRoot",value:function(){var e,n,i=O(t,"createRenderRoot",this,3)([]);return null!==(n=(e=this.renderOptions).renderBefore)&&void 0!==n||(e.renderBefore=i.firstChild),i}},{key:"update",value:function(e){var n=this.render();this.hasUpdated||(this.renderOptions.isConnected=this.isConnected),O(t,"update",this,3)([e]),this._$Do=function(t,e,n){var i,r=null!==(i=null==n?void 0:n.renderBefore)&&void 0!==i?i:e,o=r._$litPart$;if(void 0===o){var a,s=null!==(a=null==n?void 0:n.renderBefore)&&void 0!==a?a:null;r._$litPart$=o=new Jt(e.insertBefore(St(),s),s,void 0,null!=n?n:{})}return o._$AI(t),o}(n,this.renderRoot,this.renderOptions)}},{key:"connectedCallback",value:function(){var e;O(t,"connectedCallback",this,3)([]),null===(e=this._$Do)||void 0===e||e.setConnected(!0)}},{key:"disconnectedCallback",value:function(){var e;O(t,"disconnectedCallback",this,3)([]),null===(e=this._$Do)||void 0===e||e.setConnected(!1)}},{key:"render",value:function(){return It}}])}();ie._$litElement$=!0,ie.finalized=!0,null===(r=ne.litElementHydrateSupport)||void 0===r||r.call(ne,{LitElement:ie});var re=ne.litElementPolyfillSupport;null==re||re({LitElement:ie}),(null!==(o=ne.litElementVersions)&&void 0!==o?o:ne.litElementVersions=[]).push("4.2.0");var oe={attribute:!0,type:String,converter:yt,reflect:!1,hasChanged:gt},ae=function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:oe,e=arguments.length>1?arguments[1]:void 0,n=arguments.length>2?arguments[2]:void 0,i=n.kind,r=n.metadata,o=globalThis.litPropertyMetadata.get(r);if(void 0===o&&globalThis.litPropertyMetadata.set(r,o=new Map),"setter"===i&&((t=Object.create(t)).wrapped=!0),o.set(n.name,t),"accessor"===i){var a=n.name;return{set:function(n){var i=e.get.call(this);e.set.call(this,n),this.requestUpdate(a,i,t)},init:function(e){return void 0!==e&&this.C(a,void 0,t,e),e}}}if("setter"===i){var s=n.name;return function(n){var i=this[s];e.call(this,n),this.requestUpdate(s,i,t)}}throw Error("Unsupported decorator location: "+i)};function se(t){return function(e,n){return"object"==K(n)?ae(t,e,n):function(t,e,n){var i=e.hasOwnProperty(n);return e.constructor.createProperty(n,t),i?Object.getOwnPropertyDescriptor(e,n):void 0}(t,e,n)}}function ue(t){return se(A(A({},t),{},{state:!0,attribute:!1}))}var le=function(){function t(){var e;return Q(this,t),(e=R(this,t,arguments)).cards=[],e._editMode=!1,e._editorLoaded=!1,e}return N(t,ie),F(t,[{key:"setConfig",value:(n=L(T().mark((function t(e){return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:this._config=Object.assign({},e),this._config.view_layout&&void 0===this._config.layout&&(this._config.layout=this._config.view_layout);case 2:case"end":return t.stop()}}),t,this)}))),function(t){return n.apply(this,arguments)})},{key:"updated",value:(e=L(T().mark((function t(e){var n,i,r,o,a,s,u=this;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:e.has("lovelace")&&(null===(n=this.lovelace)||void 0===n?void 0:n.editMode)!=(null===(i=e.get("lovelace"))||void 0===i?void 0:i.editMode)&&((null===(r=this.lovelace)||void 0===r?void 0:r.editMode)&&!this._editorLoaded&&(this._editorLoaded=!0,(s=document.createElement("hui-masonry-view")).lovelace={editMode:!0},s.willUpdate(new Map)),this.cards.forEach((function(t){var e;return t.editMode=null===(e=u.lovelace)||void 0===e?void 0:e.editMode})),this._editMode=null!==(a=null===(o=this.lovelace)||void 0===o?void 0:o.editMode)&&void 0!==a&&a);case 1:case"end":return t.stop()}}),t,this)}))),function(t){return e.apply(this,arguments)})},{key:"_shouldShow",value:function(t,e,n){var i,r,o,a,s,u,l,c;return"always"===(null===(i=e.view_layout)||void 0===i?void 0:i.show)||"never"!==(null===(r=e.view_layout)||void 0===r?void 0:r.show)&&(("shown"!==(null===(a=null===(o=e.view_layout)||void 0===o?void 0:o.show)||void 0===a?void 0:a.sidebar)||"auto"!==(null===(s=this.hass)||void 0===s?void 0:s.dockedSidebar)&&!this.narrow)&&!("hidden"===(null===(l=null===(u=e.view_layout)||void 0===u?void 0:u.show)||void 0===l?void 0:l.sidebar)&&"docked"===(null===(c=this.hass)||void 0===c?void 0:c.dockedSidebar)&&!this.narrow))}},{key:"getCardElement",value:function(t){var e;if(!(null===(e=this.lovelace)||void 0===e?void 0:e.editMode))return t.card;var n=document.createElement("hui-card-options");return n.hass=this.hass,n.lovelace=this.lovelace,n.path=[this.index,t.index],t.card.editMode=!0,n.appendChild(t.card),!1===t.show&&(n.style.border="1px solid red"),n}},{key:"_addCard",value:function(){this.dispatchEvent(new CustomEvent("ll-create-card"))}},{key:"_render_fab",value:function(){var t;return!0==!(null===(t=this.lovelace)||void 0===t?void 0:t.editMode)?zt(a||(a=x([""]))):zt(s||(s=x(["\n \n ',"\n "])),this._render_fab())}}],[{key:"styles",get:function(){return[this._fab_styles,it(c||(c=x(["\n :host {\n display: block;\n height: 100%;\n box-sizing: border-box;\n overflow-y: var(--layout-overflow);\n }\n\n #columns {\n display: grid;\n grid-auto-columns: minmax(\n var(--column-width),\n var(--column-max-width)\n );\n grid-template-columns: var(--column-widths);\n justify-content: center;\n justify-items: center;\n margin: var(--layout-margin);\n padding: var(--layout-padding);\n height: var(--layout-height);\n overflow-y: var(--layout-overflow);\n }\n .column {\n grid-row: 1/2;\n max-width: var(--column-max-width);\n width: 100%;\n }\n .column > * {\n display: block;\n margin: var(--card-margin);\n }\n "])))]}}]);var e,n,i,r,o,a,s}();Y([se()],ce.prototype,"_columns",void 0),Y([se()],ce.prototype,"_config",void 0);var de=function(){function t(){return Q(this,t),R(this,t,arguments)}return N(t,ce),F(t,[{key:"_placeColumnCards",value:(e=L(T().mark((function t(e,n){var i,r,o,a,s,u,l;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:o=function(){for(var t=0,n=0;n\n ',""])),this._render_fab())}}],[{key:"styles",get:function(){return[this._fab_styles,it(h||(h=x(["\n :host {\n height: 100%;\n box-sizing: border-box;\n }\n #root {\n display: grid;\n justify-content: stretch;\n margin: var(--layout-margin);\n padding: var(--layout-padding);\n height: var(--layout-height);\n overflow-y: var(--layout-overflow);\n }\n #root > * {\n margin: var(--masonry-view-card-margin, 4px 4px 8px);\n }\n "])))]}}]);var e,n,i}();customElements.define("grid-layout",ye);var ge=function(){function t(){var e;return Q(this,t),(e=R(this,t,arguments)).editMode=!1,e.isPanel=!1,e._cards=[],e}return N(t,ie),F(t,[{key:"setConfig",value:function(t){this._config=Object.assign({},t),this._config.entities&&(this._config.cards=this._config.entities.map((function(t){return t.type?t:Object.assign(Object.assign({},t),{type:"entity"})})));var e=t.layout_type;e?((null==e?void 0:e.endsWith("-layout"))||(e+="-layout"),e.startsWith("custom:")&&(e=e.substring(7))):e="hui-masonry-view",this._layoutType=e}},{key:"updated",value:(i=L(T().mark((function e(n){var i,r,o,a=this;return T().wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(O(t,"updated",this,3)([n]),!n.has("_layoutType")&&!n.has("_config")){e.next=12;break}return r={type:this._layoutType,layout:this._config.layout||this._config.layout_options,cards:this._config.cards},o=document.createElement(this._layoutType),null===(i=o.setConfig)||void 0===i||i.call(o,r),this._layoutElement=o,e.next=8,this._createCards();case 8:this._layoutElement.hass=this.hass,this._layoutElement.narrow=!1,this._layoutElement.lovelace=Object.assign(Object.assign({},this._getLovelace()),{editMode:!1}),this._layoutElement.index=1;case 12:n.has("hass")&&(this._cards.forEach((function(t){t.hass=a.hass})),this._layoutElement&&(this._layoutElement.hass=this.hass)),n.has("_cards")&&this._layoutElement&&(this._layoutElement.cards=this._cards),n.has("editMode")&&this._layoutElement&&(this._layoutElement.lovelace=Object.assign(Object.assign({},this._getLovelace()),{editMode:!1}));case 15:case"end":return e.stop()}}),e,this)}))),function(t){return i.apply(this,arguments)})},{key:"_getLovelace",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:this;return t.lovelace?t.lovelace:"home-assistant"!==t.localName?t.parentElement&&t.parentElement.host?this._getLovelace(t.parentElement.host):t.parentNode&&t.parentNode.host?this._getLovelace(t.parentNode.host):t.parentElement?this._getLovelace(t.parentElement):t.parentNode?this._getLovelace(t.parentNode):void 0:void 0}},{key:"_createCard",value:function(t,e){var n=this,i=e.createCardElement(t);return i.addEventListener("ll-rebuild",(function(e){e.stopPropagation(),n._rebuildCard(i,t)})),i.hass=this.hass,i}},{key:"_createCards",value:(n=L(T().mark((function t(){var e,n=this;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,window.loadCardHelpers();case 2:e=t.sent,this._cards=this._config.cards.map((function(t){return n._createCard(t,e)}));case 4:case"end":return t.stop()}}),t,this)}))),function(){return n.apply(this,arguments)})},{key:"_rebuildCard",value:(e=L(T().mark((function t(e,n){var i,r;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,window.loadCardHelpers();case 2:i=t.sent,r=this._createCard(n,i),e.parentElement&&e.parentElement.replaceChild(r,e),this._cards=this._cards.map((function(t){return t===e?r:t}));case 6:case"end":return t.stop()}}),t,this)}))),function(t,n){return e.apply(this,arguments)})},{key:"render",value:function(){return zt(v||(v=x(["",""])),this._layoutElement)}}],[{key:"styles",get:function(){return it(f||(f=x(["\n :host(:not(:first-child)) {\n margin-top: 0 !important;\n }\n :host(:not(:last-child)) {\n margin-bottom: 0 !important;\n }\n "])))}},{key:"getConfigElement",value:function(){return document.createElement("layout-card-editor")}},{key:"getStubConfig",value:function(){return{layout_type:"masonry",layout:{},cards:[]}}}]);var e,n,i}();Y([se()],ge.prototype,"hass",void 0),Y([se()],ge.prototype,"editMode",void 0),Y([se()],ge.prototype,"isPanel",void 0),Y([se()],ge.prototype,"_config",void 0),Y([se()],ge.prototype,"_cards",void 0),Y([se()],ge.prototype,"_layoutElement",void 0),Y([se()],ge.prototype,"_layoutType",void 0),customElements.define("layout-card",ge),window.customCards=window.customCards||[],window.customCards.push({type:"layout-card",name:"Layout Card",preview:!1,description:"Like a stack card, but with way more control."});var me=function(){var t=L(T().mark((function t(){var e,n,i,r;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(!customElements.get("ha-form")){t.next=2;break}return t.abrupt("return");case 2:return t.next=4,null===(n=(e=window).loadCardHelpers)||void 0===n?void 0:n.call(e);case 4:if(i=t.sent){t.next=7;break}return t.abrupt("return");case 7:return t.next=9,i.createCardElement({type:"entity"});case 9:if(r=t.sent){t.next=12;break}return t.abrupt("return");case 12:return t.next=14,r.getConfigElement();case 14:case"end":return t.stop()}}),t)})));return function(){return t.apply(this,arguments)}}(),_e=[{value:"custom:masonry-layout",label:"Masonry (layout-card)"},{value:"custom:horizontal-layout",label:"Horizontal (layout-card)"},{value:"custom:vertical-layout",label:"Vertical (layout-card)"},{value:"custom:grid-layout",label:"Grid (layout-card)"}],be=["masonry","sidebar","panel"],we=function(){function t(){var e;return Q(this,t),(e=R(this,t,arguments))._selectedTab=0,e._selectedCard=0,e._cardGUIMode=!0,e._cardGUIModeAvailable=!0,e._schema=function(t){return[{name:"layout_type",selector:{select:{options:[].concat(j(be.map((function(e){return{value:e,label:t("ui.panel.lovelace.editor.edit_view.types.".concat(e))}}))),_e)}}},{name:"layout",selector:{object:{}}}]},e}return N(t,ie),F(t,[{key:"setConfig",value:function(t){this._config=t}},{key:"firstUpdated",value:(n=L(T().mark((function t(){return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return me(),t.next=3,this._preloadCardEditors();case 3:case"end":return t.stop()}}),t,this)}))),function(){return n.apply(this,arguments)})},{key:"_preloadCardEditors",value:(e=L(T().mark((function t(){var e,n,i,r,o;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,null===(n=(e=window).loadCardHelpers)||void 0===n?void 0:n.call(e);case 2:if(r=t.sent){t.next=5;break}return t.abrupt("return");case 5:return t.next=7,r.createCardElement({type:"vertical-stack",cards:[]});case 7:return o=t.sent,t.next=10,customElements.whenDefined("hui-vertical-stack-card");case 10:if(!(null===(i=null==o?void 0:o.constructor)||void 0===i?void 0:i.getConfigElement)){t.next=13;break}return t.next=13,o.constructor.getConfigElement();case 13:case"end":return t.stop()}}),t)}))),function(){return e.apply(this,arguments)})},{key:"_handleSwitchTab",value:function(t){t.stopPropagation(),this._selectedTab=parseInt(t.detail.name,10)}},{key:"_editCard",value:function(t){t.stopPropagation(),"add-card"!==t.detail.name?(this._cardGUIMode=!0,this._cardEditorEl&&(this._cardEditorEl.GUImode=!0),this._cardGUIModeAvailable=!0,this._selectedCard=parseInt(t.detail.name,10)):this._selectedCard=this._config.cards.length}},{key:"_addCard",value:function(t){t.stopPropagation();var e=j(this._config.cards);e.push(t.detail.config),this._config=Object.assign(Object.assign({},this._config),{cards:e}),this._selectedCard=this._config.cards.length-1,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}},{key:"_updateCard",value:function(t){t.stopPropagation();var e=j(this._config.cards);e[this._selectedCard]=t.detail.config,this._config=Object.assign(Object.assign({},this._config),{cards:e}),this._cardGUIModeAvailable=t.detail.guiModeAvailable,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}},{key:"_GUIModeChange",value:function(t){t.stopPropagation(),this._cardGUIMode=t.detail.guiMode,this._cardGUIModeAvailable=t.detail.guiModeAvailable}},{key:"_toggleMode",value:function(t){this._cardEditorEl.toggleMode()}},{key:"_moveCard",value:function(t){var e=this._selectedCard,n=e+t.currentTarget.move,i=j(this._config.cards),r=i.splice(e,1)[0];i.splice(n,0,r),this._config=Object.assign(Object.assign({},this._config),{cards:i}),this._selectedCard=n,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}},{key:"_deleteCard",value:function(){var t=j(this._config.cards);t.splice(this._selectedCard,1),this._config=Object.assign(Object.assign({},this._config),{cards:t}),this._selectedCard=Math.max(0,this._selectedCard-1),this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:this._config}}))}},{key:"_valueChanged",value:function(t){t.stopPropagation();var e=t.detail.value;this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:e}}))}},{key:"_computeLabel",value:function(t){return"layout_type"===t.name?this.hass.localize("ui.panel.lovelace.editor.edit_view.type"):"layout"===t.name?"Layout options (layout-card)":void 0}},{key:"render",value:function(){return this.hass&&this._config?zt(y||(y=x(['\n
    \n \n \n Layout\n \n \n Cards\n \n \n
    \n ',"\n
    \n
    \n "])),this._handleSwitchTab,0==this._selectedTab,0,1==this._selectedTab,1,[this._renderLayoutEditor,this._renderCardsEditor][this._selectedTab].bind(this)()):zt(p||(p=x([""])))}},{key:"_renderLayoutEditor",value:function(){var t=this._schema(this.hass.localize),e=Object.assign({},this._config);return zt(g||(g=x(['\n

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

    \n \n \n \n \n \n
    \n ',"\n
    \n \n "])),this._editCard,this._config.cards.map((function(e,n){return zt(b||(b=x(['\n \n \n ',"\n \n \n \n \n \n \n \n \n \n \n \n \n "])),this._toggleMode,!this._cardGUIModeAvailable,this.hass.localize(this._cardEditorEl||this._cardGUIMode?"ui.panel.lovelace.editor.edit_card.show_code_editor":"ui.panel.lovelace.editor.edit_card.show_visual_editor"),0===t,this._moveCard,-1,"mdi:arrow-left",t===e-1,this._moveCard,1,"mdi:arrow-right",this._deleteCard,"mdi:delete",this.hass,this._config.cards[t],this.lovelace,this._updateCard,this._GUIModeChange):zt(k||(k=x(["\n \n "])),this.hass,this.lovelace,this._addCard))}}],[{key:"styles",get:function(){return[it(E||(E=x(['\n mwc-tab-bar {\n border-bottom: 1px solid var(--divider-color);\n }\n\n .layout,\n .cards #editor {\n margin-top: 8px;\n border: 1px solid var(--divider-color);\n padding: 12px;\n }\n\n #add-card {\n max-width: 32px;\n padding: 0;\n }\n ha-tab-group {\n margin-top: -16px;\n margin-bottom: 16px;\n }\n ha-tab-group-tab {\n flex: 1;\n }\n ha-tab-group-tab::part(base) {\n width: 100%;\n justify-content: center;\n }\n ha-tab-group-tab[panel="?"] {\n flex: 0;\n }\n\n .cards .card-options {\n display: flex;\n justify-content: flex-end;\n width: 100%;\n }\n #editor {\n border: 1px solid var(--divider-color);\n padding: 12px;\n }\n .gui-mode-button {\n margin-right: auto;\n }\n\n a {\n color: var(--primary-color);\n }\n '])))]}}]);var e,n}();Y([se()],we.prototype,"_config",void 0),Y([se()],we.prototype,"lovelace",void 0),Y([se()],we.prototype,"hass",void 0),Y([ue()],we.prototype,"_selectedTab",void 0),Y([ue()],we.prototype,"_selectedCard",void 0),Y([ue()],we.prototype,"_cardGUIMode",void 0),Y([ue()],we.prototype,"_cardGUIModeAvailable",void 0),Y([function(t){return function(e,n,i){return function(t,e,n){return n.configurable=!0,n.enumerable=!0,Reflect.decorate&&"object"!=K(e)&&Object.defineProperty(t,e,n),n}(e,n,{get:function(){return function(e){var n,i;return null!==(n=null===(i=e.renderRoot)||void 0===i?void 0:i.querySelector(t))&&void 0!==n?n:null}(this)}})}}("hui-card-element-editor")],we.prototype,"_cardEditorEl",void 0),customElements.define("layout-card-editor",we),customElements.whenDefined("hui-card-element-editor").then((function(){var t=customElements.get("hui-card-element-editor"),e=t.prototype.getConfigElement;t.prototype.getConfigElement=L(T().mark((function t(){var n,i;return T().wrap((function(t){for(;;)switch(t.prev=t.next){case 0:return t.next=2,e.bind(this)();case 2:if(n=t.sent){i=n.setConfig;try{n.setConfig=function(t){var e=JSON.parse(JSON.stringify(t));this._layoutData=e.view_layout,delete e.view_layout,i.bind(this)(e)}}catch(t){console.warn(t)}}return t.abrupt("return",n);case 5:case"end":return t.stop()}}),t,this)})));var n=t.prototype._handleUIConfigChanged;t.prototype._handleUIConfigChanged=function(t){this._configElement&&this._configElement._layoutData&&(t.detail.config.view_layout=this._configElement._layoutData),n.bind(this)(t)}})),customElements.whenDefined("hui-view-editor").then((function(){var t=customElements.get("hui-view-editor"),e=t.prototype.firstUpdated;t.prototype.firstUpdated=function(){var t=this;null==e||e.bind(this)(),this._oldSchema=this._schema,this._schema=function(){var e,n=t._oldSchema.apply(t,arguments),i=n.find((function(t){return"type"==t.name}));if("layout"===i.name)return n;i.selector.select.options.find((function(t){return t.value===_e[0].value}))||(e=i.selector.select.options).push.apply(e,_e);return void 0===n.find((function(t){return"layout"===t.name}))&&n.push({name:"layout",selector:{object:{}}}),n};var n=document.createElement("p");n.innerHTML='\n You have layout-card installed which adds some options to this dialog.
    \n Please see\n layout-card on GitHubfor usage instructions.\n \n ',this.shadowRoot.appendChild(n),this.requestUpdate()}}));var ke=function(){function t(){return Q(this,t),R(this,t,arguments)}return N(t,z(HTMLElement)),F(t,[{key:"setConfig",value:function(t){var e,n;this.height=null!==(e=t.height)&&void 0!==e?e:50,this.size=null!==(n=t.size)&&void 0!==n?n:Math.ceil(this.height/50),this.style.cssText="\n display: block;\n height: ".concat(this.height,"px;\n ")}},{key:"getCardSize",value:function(){return this.size}}],[{key:"getConfigElement",value:function(){return document.createElement("gap-card-editor")}},{key:"getStubConfig",value:function(){return{}}}])}();customElements.define("gap-card",ke),window.customCards=window.customCards||[],window.customCards.push({type:"gap-card",name:"Gap Card",preview:!1,description:"Add a customizable gap in the layout."});var Ee=function(){function t(){return Q(this,t),R(this,t,arguments)}return N(t,ie),F(t,[{key:"setConfig",value:function(t){this._config=t}},{key:"heightChanged",value:function(t){var e=Object.assign({},this._config);delete e.height,t.detail.value&&(e.height=parseInt(t.detail.value)),this._config=e,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:e}}))}},{key:"sizeChanged",value:function(t){var e=Object.assign({},this._config);delete e.size,t.detail.value&&(e.size=parseInt(t.detail.value)),this._config=e,this.dispatchEvent(new CustomEvent("config-changed",{detail:{config:e}}))}},{key:"render",value:function(){return zt(C||(C=x(["\n {if(e)i.adoptedStyleSheets=n.map(t=>t instanceof CSSStyleSheet?t:t.styleSheet);else for(const e of n){const n=document.createElement("style"),r=t.litNonce;void 0!==r&&n.setAttribute("nonce",r),n.textContent=e.cssText,i.appendChild(n)}},a=e?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new r("string"==typeof t?t:t+"",void 0,i))(e)})(t):t,{is:c,defineProperty:u,getOwnPropertyDescriptor:l,getOwnPropertyNames:h,getOwnPropertySymbols:f,getPrototypeOf:d}=Object,p=globalThis,m=p.trustedTypes,y=m?m.emptyScript:"",b=p.reactiveElementPolyfillSupport,_=(t,e)=>t,v={toAttribute(t,e){switch(e){case Boolean:t=t?y: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)=>!c(t,e),$={attribute:!0,type:String,converter:v,reflect:!1,useDefault:!1,hasChanged:g};Symbol.metadata??=Symbol("metadata"),p.litPropertyMetadata??=new WeakMap;class w extends HTMLElement{static addInitializer(t){this._$Ei(),(this.l??=[]).push(t)}static get observedAttributes(){return this.finalize(),this._$Eh&&[...this._$Eh.keys()]}static createProperty(t,e=$){if(e.state&&(e.attribute=!1),this._$Ei(),this.prototype.hasOwnProperty(t)&&((e=Object.create(e)).wrapped=!0),this.elementProperties.set(t,e),!e.noAccessor){const i=Symbol(),n=this.getPropertyDescriptor(t,i,e);void 0!==n&&u(this.prototype,t,n)}}static getPropertyDescriptor(t,e,i){const{get:n,set:r}=l(this.prototype,t)??{get(){return this[e]},set(t){this[e]=t}};return{get:n,set(e){const o=n?.call(this);r?.call(this,e),this.requestUpdate(t,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)??$}static _$Ei(){if(this.hasOwnProperty(_("elementProperties")))return;const t=d(this);t.finalize(),void 0!==t.l&&(this.l=[...t.l]),this.elementProperties=new Map(t.elementProperties)}static finalize(){if(this.hasOwnProperty(_("finalized")))return;if(this.finalized=!0,this._$Ei(),this.hasOwnProperty(_("properties"))){const t=this.properties,e=[...h(t),...f(t)];for(const i of e)this.createProperty(i,t[i])}const t=this[Symbol.metadata];if(null!==t){const e=litPropertyMetadata.get(t);if(void 0!==e)for(const[t,i]of e)this.elementProperties.set(t,i)}this._$Eh=new Map;for(const[t,e]of this.elementProperties){const i=this._$Eu(t,e);void 0!==i&&this._$Eh.set(i,t)}this.elementStyles=this.finalizeStyles(this.styles)}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 _$Eu(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}constructor(){super(),this._$Ep=void 0,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Em=null,this._$Ev()}_$Ev(){this._$ES=new Promise(t=>this.enableUpdating=t),this._$AL=new Map,this._$E_(),this.requestUpdate(),this.constructor.l?.forEach(t=>t(this))}addController(t){(this._$EO??=new Set).add(t),void 0!==this.renderRoot&&this.isConnected&&t.hostConnected?.()}removeController(t){this._$EO?.delete(t)}_$E_(){const t=new Map,e=this.constructor.elementProperties;for(const i of e.keys())this.hasOwnProperty(i)&&(t.set(i,this[i]),delete this[i]);t.size>0&&(this._$Ep=t)}createRenderRoot(){const t=this.shadowRoot??this.attachShadow(this.constructor.shadowRootOptions);return s(t,this.constructor.elementStyles),t}connectedCallback(){this.renderRoot??=this.createRenderRoot(),this.enableUpdating(!0),this._$EO?.forEach(t=>t.hostConnected?.())}enableUpdating(t){}disconnectedCallback(){this._$EO?.forEach(t=>t.hostDisconnected?.())}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$ET(t,e){const i=this.constructor.elementProperties.get(t),n=this.constructor._$Eu(t,i);if(void 0!==n&&!0===i.reflect){const r=(void 0!==i.converter?.toAttribute?i.converter:v).toAttribute(e,i.type);this._$Em=t,null==r?this.removeAttribute(n):this.setAttribute(n,r),this._$Em=null}}_$AK(t,e){const i=this.constructor,n=i._$Eh.get(t);if(void 0!==n&&this._$Em!==n){const t=i.getPropertyOptions(n),r="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==t.converter?.fromAttribute?t.converter:v;this._$Em=n;const o=r.fromAttribute(e,t.type);this[n]=o??this._$Ej?.get(n)??o,this._$Em=null}}requestUpdate(t,e,i,n=!1,r){if(void 0!==t){const o=this.constructor;if(!1===n&&(r=this[t]),i??=o.getPropertyOptions(t),!((i.hasChanged??g)(r,e)||i.useDefault&&i.reflect&&r===this._$Ej?.get(t)&&!this.hasAttribute(o._$Eu(t,i))))return;this.C(t,e,i)}!1===this.isUpdatePending&&(this._$ES=this._$EP())}C(t,e,{useDefault:i,reflect:n,wrapped:r},o){i&&!(this._$Ej??=new Map).has(t)&&(this._$Ej.set(t,o??e??this[t]),!0!==r||void 0!==o)||(this._$AL.has(t)||(this.hasUpdated||i||(e=void 0),this._$AL.set(t,e)),!0===n&&this._$Em!==t&&(this._$Eq??=new Set).add(t))}async _$EP(){this.isUpdatePending=!0;try{await this._$ES}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){if(!this.isUpdatePending)return;if(!this.hasUpdated){if(this.renderRoot??=this.createRenderRoot(),this._$Ep){for(const[t,e]of this._$Ep)this[t]=e;this._$Ep=void 0}const t=this.constructor.elementProperties;if(t.size>0)for(const[e,i]of t){const{wrapped:t}=i,n=this[e];!0!==t||this._$AL.has(e)||void 0===n||this.C(e,void 0,i,n)}}let t=!1;const e=this._$AL;try{t=this.shouldUpdate(e),t?(this.willUpdate(e),this._$EO?.forEach(t=>t.hostUpdate?.()),this.update(e)):this._$EM()}catch(e){throw t=!1,this._$EM(),e}t&&this._$AE(e)}willUpdate(t){}_$AE(t){this._$EO?.forEach(t=>t.hostUpdated?.()),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EM(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$ES}shouldUpdate(t){return!0}update(t){this._$Eq&&=this._$Eq.forEach(t=>this._$ET(t,this[t])),this._$EM()}updated(t){}firstUpdated(t){}}w.elementStyles=[],w.shadowRootOptions={mode:"open"},w[_("elementProperties")]=new Map,w[_("finalized")]=new Map,b?.({ReactiveElement:w}),(p.reactiveElementVersions??=[]).push("2.1.2");const A=globalThis,O=t=>t,S=A.trustedTypes,E=S?S.createPolicy("lit-html",{createHTML:t=>t}):void 0,P="$lit$",j=`lit$${Math.random().toFixed(9).slice(2)}$`,x="?"+j,k=`<${x}>`,C=document,D=()=>C.createComment(""),N=t=>null===t||"object"!=typeof t&&"function"!=typeof t,T=Array.isArray,M="[ \t\n\f\r]",U=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,H=/-->/g,R=/>/g,I=RegExp(`>|${M}(?:([^\\s"'>=/]+)(${M}*=${M}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),F=/'/g,z=/"/g,L=/^(?:script|style|textarea|title)$/i,B=t=>(e,...i)=>({_$litType$:t,strings:e,values:i}),W=B(1),V=(B(2),B(3),Symbol.for("lit-noChange")),q=Symbol.for("lit-nothing"),Z=new WeakMap,J=C.createTreeWalker(C,129);function K(t,e){if(!T(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==E?E.createHTML(e):e}const Y=(t,e)=>{const i=t.length-1,n=[];let r,o=2===e?"":3===e?"":"",s=U;for(let e=0;e"===c[0]?(s=r??U,u=-1):void 0===c[1]?u=-2:(u=s.lastIndex-c[2].length,a=c[1],s=void 0===c[3]?I:'"'===c[3]?z:F):s===z||s===F?s=I:s===H||s===R?s=U:(s=I,r=void 0);const h=s===I&&t[e+1].startsWith("/>")?" ":"";o+=s===U?i+k:u>=0?(n.push(a),i.slice(0,u)+P+i.slice(u)+j+h):i+j+(-2===u?e:h)}return[K(t,o+(t[i]||"")+(2===e?"":3===e?"":"")),n]};class G{constructor({strings:t,_$litType$:e},i){let n;this.parts=[];let r=0,o=0;const s=t.length-1,a=this.parts,[c,u]=Y(t,e);if(this.el=G.createElement(c,i),J.currentNode=this.el.content,2===e||3===e){const t=this.el.content.firstChild;t.replaceWith(...t.childNodes)}for(;null!==(n=J.nextNode())&&a.length0){n.textContent=S?S.emptyScript:"";for(let i=0;iT(t)||"function"==typeof t?.[Symbol.iterator])(t)?this.k(t):this._(t)}O(t){return this._$AA.parentNode.insertBefore(t,this._$AB)}T(t){this._$AH!==t&&(this._$AR(),this._$AH=this.O(t))}_(t){this._$AH!==q&&N(this._$AH)?this._$AA.nextSibling.data=t:this.T(C.createTextNode(t)),this._$AH=t}$(t){const{values:e,_$litType$:i}=t,n="number"==typeof i?this._$AC(t):(void 0===i.el&&(i.el=G.createElement(K(i.h,i.h[0]),this.options)),i);if(this._$AH?._$AD===n)this._$AH.p(e);else{const t=new X(n,this),i=t.u(this.options);t.p(e),this.T(i),this._$AH=t}}_$AC(t){let e=Z.get(t.strings);return void 0===e&&Z.set(t.strings,e=new G(t)),e}k(t){T(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,n=0;for(const r of t)n===e.length?e.push(i=new tt(this.O(D()),this.O(D()),this,this.options)):i=e[n],i._$AI(r),n++;n2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=q}_$AI(t,e=this,i,n){const r=this.strings;let o=!1;if(void 0===r)t=Q(this,t,e,0),o=!N(t)||t!==this._$AH&&t!==V,o&&(this._$AH=t);else{const n=t;let s,a;for(t=r[0],s=0;s{const n=i?.renderBefore??e;let r=n._$litPart$;if(void 0===r){const t=i?.renderBefore??null;n._$litPart$=r=new tt(e.insertBefore(D(),t),t,void 0,i??{})}return r._$AI(t),r})(e,this.renderRoot,this.renderOptions)}connectedCallback(){super.connectedCallback(),this._$Do?.setConnected(!0)}disconnectedCallback(){super.disconnectedCallback(),this._$Do?.setConnected(!1)}render(){return V}}ct._$litElement$=!0,ct.finalized=!0,at.litElementHydrateSupport?.({LitElement:ct});const ut=at.litElementPolyfillSupport;var lt,ht;ut?.({LitElement:ct}),(at.litElementVersions??=[]).push("4.2.2"),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"}(lt||(lt={})),function(t){t.language="language",t.system="system",t.am_pm="12",t.twenty_four="24"}(ht||(ht={}));const ft=["closed","locked","off"],dt=(new Set(["fan","input_boolean","light","switch","group","automation"]),(t,e,i,n)=>{n=n||{},i=i??{};const r=new Event(e,{bubbles:void 0===n.bubbles||n.bubbles,cancelable:Boolean(n.cancelable),composed:void 0===n.composed||n.composed});return r.detail=i,t.dispatchEvent(r),r});new Set(["call-service","divider","section","weblink","cast","select"]);const pt=t=>{dt(window,"haptic",t)};var mt="unavailable",yt="unknown",bt=[mt,yt],_t="last-changed",vt="last-updated",gt=["relative","total","date","time","datetime"],$t=["entity-id","last-changed","last-updated","last-triggered","position","tilt-position","brightness"],wt="language",At=function(t){return t<10?"0".concat(t):t};function Ot(t){var e=Math.floor(t/3600),i=Math.floor(t%3600/60),n=Math.floor(t%3600%60);return e>0?"".concat(e,":").concat(At(i),":").concat(At(n)):i>0?"".concat(i,":").concat(At(n)):n>0?""+n:null}function St(t){return St="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},St(t)}function Et(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 Pt(t,e,i){return(e=function(t){var e=function(t){if("object"!=St(t)||!t)return t;var e=t[Symbol.toPrimitive];if(void 0!==e){var i=e.call(t,"string");if("object"!=St(i))return i;throw new TypeError("@@toPrimitive must return a primitive value.")}return String(t)}(t);return"symbol"==St(e)?e:e+""}(e))in t?Object.defineProperty(t,e,{value:i,enumerable:!0,configurable:!0,writable:!0}):t[e]=i,t}var jt=function(t,e,i){var n=e?function(t){switch(t.number_format){case"comma_decimal":return["en-US","en"];case"decimal_comma":return["de","es","it"];case"space_comma":return["fr","sv","cs"];case"system":return;default:return t.language}}(e):void 0;if(Number.isNaN=Number.isNaN||function t(e){return"number"==typeof e&&t(e)},"none"!==(null==e?void 0:e.number_format)&&!Number.isNaN(Number(t))&&Intl)try{return new Intl.NumberFormat(n,xt(t,i)).format(Number(t))}catch(e){return console.error(e),new Intl.NumberFormat(void 0,xt(t,i)).format(Number(t))}return"string"==typeof t?t:"".concat(function(t){var e=arguments.length>1&&void 0!==arguments[1]?arguments[1]:2;return Math.round(t*Math.pow(10,e))/Math.pow(10,e)}(t,null==i?void 0:i.maximumFractionDigits).toString()).concat("currency"===(null==i?void 0:i.style)?" ".concat(i.currency):"")},xt=function(t,e){var i=function(t){for(var e=1;e-1?t.split(".")[1].length:0;i.minimumFractionDigits=n,i.maximumFractionDigits=n}return i},kt=Number.isNaN||function(t){return"number"==typeof t&&t!=t};function Ct(t,e){return t===e||!(!kt(t)||!kt(e))}function Dt(t,e){if(t.length!==e.length)return!1;for(var i=0;ie.hide_if.above)return!0;e.hide_if.value&&(n=n.concat(e.hide_if.value))}else n=n.concat(e.hide_if);return n.some(function(t){return"number"==typeof t?t===+i:t===i})},Vt=["formatEntityName","formatEntityState","formatEntityAttributeValue"];function qt(t){return qt="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},qt(t)}function Zt(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 Jt(t){for(var e=1;e\n
    \n ',"","\n
    \n "])),this._hass,this.config,this.renderSecondaryInfo(),!1,this.config.column?"entities-column":"entities-row",this.entities.map(function(e){return t.renderEntity(e.stateObj,e)}),this.renderMainEntity()):this.renderWarning():W(Gt||(Gt=ye([""])))}},{key:"renderSecondaryInfo",value:function(){if(!this.config.secondary_info||"string"==typeof(t=this.config.secondary_info)&&$t.includes(t)||Wt(this.info,this.config.secondary_info))return null;var t;if("string"==typeof this.config.secondary_info)return W(Xt||(Xt=ye(["",""])),this.config.secondary_info);var e=de(this.info,this.config.secondary_info);return W(te||(te=ye([""," ",""])),e,this.renderValue(this.info,this.config.secondary_info))}},{key:"renderMainEntity",value:function(){return!1===this.config.show_state?null:W(ee||(ee=ye(['
    \n ',"\n
    ","
    \n
    "])),pe(this.config),this.onRowClick,this.config.state_header&&W(ie||(ie=ye(["",""])),this.config.state_header),this.renderValue(this.stateObj,this.config))}},{key:"renderEntity",value:function(t,e){if(!t||Wt(t,e))return e.default?W(ne||(ne=ye(['
    \n ',"\n
    ","
    \n
    "])),pe(e),e.name,e.default):null;var i=this.clickHandler(t.entity_id,e.tap_action);return W(re||(re=ye(['
    \n ',"\n
    ","
    \n
    "])),pe(e),i,de(t,e),e.icon?this.renderIcon(t,e):this.renderValue(t,e))}},{key:"renderValue",value:function(t,e){if(!0===e.toggle)return W(oe||(oe=ye([''])),t,this._hass);var i;if(e.attribute&&[_t,vt].includes(e.attribute))return W(se||(se=ye([""])),this._hass,t[null===(i=e.attribute)||void 0===i?void 0:i.replace("-","_")]);if(e.format&>.includes(e.format)){var n,r=e.attribute?null!==(n=t.attributes[e.attribute])&&void 0!==n?n:t[e.attribute]:t.state,o=new Date(r);return o instanceof Date&&!isNaN(o.getTime())?W(ae||(ae=ye([""])),this._hass,o,e.format):r}return function(t,e,i){var n="function"==typeof t.formatEntityState,r="function"==typeof t.formatEntityAttributeValue;if(Bt(e))return n?t.formatEntityState(e):t.localize("state.default.".concat(e.state));var o=i.attribute?e.attributes[i.attribute]:e.state,s=!1===i.unit?void 0:void 0!==i.attribute?i.unit:i.unit||e.attributes.unit_of_measurement;if(i.format){if(null==o&&(o=0),isNaN(parseFloat(o))||!isFinite(o));else if("brightness"===i.format)o=Math.round(o/255*100),s="%";else if("duration"===i.format){var a;o=null!==(a=Ot(o))&&void 0!==a?a:"0",s=void 0}else if("duration-m"===i.format){var c;o=null!==(c=Ot(o/1e3))&&void 0!==c?c:"0",s=void 0}else if("duration-h"===i.format){var u;o=null!==(u=Ot(3600*o))&&void 0!==u?u:"0",s=void 0}else if(i.format.startsWith("precision")){var l=parseInt(i.format.slice(-1),10);o=jt(parseFloat(o),t.locale,{minimumFractionDigits:l,maximumFractionDigits:l})}else if(i.format.startsWith("kilo"))o=fe(o,1e3,i.format.slice(4),t.locale);else if(i.format.startsWith("mega"))o=fe(o,1e6,i.format.slice(4),t.locale);else if(i.format.startsWith("milli"))o=fe(o,.001,i.format.slice(5),t.locale);else if("invert"===i.format){var h=he(o);o=jt(o-2*o,t.locale,void 0!==h?{minimumFractionDigits:h,maximumFractionDigits:h}:void 0)}else if("position"===i.format){var f=he(o);o=jt(100-o,t.locale,void 0!==f?{minimumFractionDigits:f,maximumFractionDigits:f}:void 0)}else"celsius_to_fahrenheit"===i.format?o=jt(1.8*o+32,t.locale,{maximumFractionDigits:0}):"fahrenheit_to_celsius"===i.format&&(o=jt(5*(o-32)/9,t.locale,{maximumFractionDigits:1}));return"".concat(o).concat(s?" ".concat(s):"")}var d=Jt(Jt({},e),{},{attributes:Jt(Jt({},e.attributes),{},{unit_of_measurement:s})});if(i.attribute){if(r)return t.formatEntityAttributeValue(d,i.attribute);var p=null==o?"":isNaN(o)?o:jt(o,t.locale);return"".concat(p).concat(s?" ".concat(s):"")}return n?t.formatEntityState(d):function(t,e,i,n){var r=e.state;if(r===yt||r===mt)return t("state.default.".concat(r));if(function(t){return!!t.attributes.unit_of_measurement||!!t.attributes.state_class}(e)){if("monetary"===e.attributes.device_class&&/^[A-Za-z]{3}$/.test(e.attributes.unit_of_measurement))try{return jt(r,i,{style:"currency",currency:e.attributes.unit_of_measurement})}catch(t){}return"".concat(jt(r,i)).concat(e.attributes.unit_of_measurement?" "+e.attributes.unit_of_measurement:"")}var o,s=function(t){return t.entity_id.substr(0,t.entity_id.indexOf("."))}(e);if("input_datetime"===s)return e.attributes.has_date&&e.attributes.has_time?(o=new Date(e.attributes.year,e.attributes.month-1,e.attributes.day,e.attributes.hour,e.attributes.minute),Ht(o,i)):e.attributes.has_date?(o=new Date(e.attributes.year,e.attributes.month-1,e.attributes.day),Tt(o,i)):e.attributes.has_time?((o=new Date).setHours(e.attributes.hour,e.attributes.minute),It(o,i)):e.state;if("humidifier"===s&&"on"===r&&e.attributes.humidity)return"".concat(e.attributes.humidity," %");if("counter"===s||"number"===s||"input_number"===s)return jt(r,i);if("button"===s||"sensor"===s&&"timestamp"===e.attributes.device_class)return Ht(new Date(r),i);var a=n&&n[e.entity_id];return(null==a?void 0:a.translation_key)&&t("component.".concat(a.platform,".entity.").concat(s,".").concat(a.translation_key,".state.").concat(r))||e.attributes.device_class&&t("component.".concat(s,".entity_component.").concat(e.attributes.device_class,".state.").concat(r))||t("component.".concat(s,".entity_component._.state.").concat(r))||r}(t.localize,d,t.locale,t.entities)}(this._hass,t,e)}},{key:"renderIcon",value:function(t,e){return W(ce||(ce=ye([''])),this._hass,t,!0===e.icon?t.attributes.icon||null:e.icon,e.state_color)}},{key:"renderWarning",value:function(){return W(ue||(ue=ye(["\n ","\n "])),this._hass.localize("ui.panel.lovelace.warning.entity_not_found","entity",this.config.entity))}},{key:"clickHandler",value:function(t,e){var i=this;return function(){return((t,e,i)=>{let n;if(i.tap_action&&(n=i.tap_action),n||(n={action:"more-info"}),!n.confirmation||n.confirmation.exemptions&&n.confirmation.exemptions.some(t=>t.user===e.user.id)||confirm(n.confirmation.text||`Are you sure you want to ${n.action}?`))switch(n.action){case"more-info":(n.entity||i.entity||i.camera_image)&&(dt(t,"hass-more-info",{entityId:n.entity?n.entity:i.entity?i.entity:i.camera_image}),n.haptic&&pt(n.haptic));break;case"navigate":n.navigation_path&&(((t,e,i=!1)=>{i?history.replaceState(null,"",e):history.pushState(null,"",e),dt(window,"location-changed",{replace:i})})(0,n.navigation_path),n.haptic&&pt(n.haptic));break;case"url":n.url_path&&window.open(n.url_path),n.haptic&&pt(n.haptic);break;case"toggle":i.entity&&(((t,e)=>{((t,e,i=!0)=>{const n=function(t){return t.substr(0,t.indexOf("."))}(e),r="group"===n?"homeassistant":n;let o;switch(n){case"lock":o=i?"unlock":"lock";break;case"cover":o=i?"open_cover":"close_cover";break;default:o=i?"turn_on":"turn_off"}t.callService(r,o,{entity_id:e})})(t,e,ft.includes(t.states[e].state))})(e,i.entity),n.haptic&&pt(n.haptic));break;case"call-service":{if(!n.service)return;const[t,r]=n.service.split(".",2),o=Object.assign({},n.service_data);"entity"===o.entity_id&&(o.entity_id=i.entity),e.callService(t,r,o,n.target),n.haptic&&pt(n.haptic);break}case"fire-dom-event":dt(t,"ll-custom",n),n.haptic&&pt(n.haptic)}})(i,i._hass,{entity:t,tap_action:e})}}}],[{key:"properties",get:function(){return{_hass:Object,config:Object,stateObj:Object}}},{key:"styles",get:function(){return o(Yt||(Yt=function(t,e){return e||(e=t.slice(0)),Object.freeze(Object.defineProperties(t,{raw:{value:Object.freeze(e)}}))}(["\n .icon-small {\n width: auto;\n }\n .entity {\n text-align: center;\n cursor: pointer;\n }\n .entity span {\n font-size: 10px;\n color: var(--secondary-text-color);\n }\n .entities-row {\n flex-direction: row;\n display: inline-flex;\n justify-content: space-between;\n align-items: center;\n }\n .entities-row .entity {\n margin-right: 16px;\n }\n .entities-row .entity:last-of-type {\n margin-right: 0;\n }\n .entities-column {\n flex-direction: column;\n display: flex;\n align-items: flex-end;\n justify-content: space-evenly;\n }\n .entities-column .entity div {\n display: inline-block;\n vertical-align: middle;\n }\n"])))}}])}(ct);customElements.define("multiple-entity-row",Se)})(); \ No newline at end of file diff --git a/www/community/lovelace-multiple-entity-row/multiple-entity-row.js.gz b/www/community/lovelace-multiple-entity-row/multiple-entity-row.js.gz new file mode 100644 index 0000000..f3fb1bf Binary files /dev/null and b/www/community/lovelace-multiple-entity-row/multiple-entity-row.js.gz differ diff --git a/www/community/lovelace-thermostat-pro-timeline/thermostat-pro-timeline.js b/www/community/lovelace-thermostat-pro-timeline/thermostat-pro-timeline.js new file mode 100644 index 0000000..3208a6d --- /dev/null +++ b/www/community/lovelace-thermostat-pro-timeline/thermostat-pro-timeline.js @@ -0,0 +1,19795 @@ +// Note: avoid ESM imports so the card works when loaded as "javascript" resource in HA. +// ---- thermostat-timeline i18n (namespaced) ---- +const TT_I18N = { + en: { + // Card + 'card.title_default': 'Thermostat Timeline', + 'err.config_entities': "Config must have 'entities: [climate.xxx, ...]'.", + // Modal / editor (inline) + 'ui.edit_block': 'Edit block', + 'ui.add_block': 'Add block', + 'ui.from': 'From', + 'ui.to': 'To', + 'ui.temperature': 'Temperature', + 'ui.delete': 'Delete', + 'ui.cancel': 'Cancel', + 'ui.close': 'Close', + 'ui.unsaved_confirm': 'You have unsaved changes. Close anyway?', + 'ui.discard_changes': 'Discard changes', + 'ui.keep_editing': 'Keep editing', + 'ui.save': 'Save', + 'ui.error_prefix': 'Error: ', + 'ui.overlap_msg': 'You selected overlapping times — overlap is {start} – {end}.', + 'ui.auto_fix': 'Auto-fix', + 'ui.fix_start_to': 'Set start to {time}', + 'ui.fix_end_to': 'Set end to {time}', + 'ui.current_temp': 'Current temperature:', + 'ui.copy_plan': 'Copy plan', + 'ui.paste_plan': 'Paste plan', + 'ui.copy_day': 'Copy day', + 'ui.paste_day': 'Paste day', + + 'editor.title_label': 'Title', + 'editor.add_entity': 'Add room', + 'editor.entity_placeholder': 'Select an entity', + 'editor.drag_reorder': 'Drag to reorder', + 'editor.remove': 'Remove', + 'editor.default_c': 'Default °C', + 'editor.row_height_px': 'Row height (px)', + 'editor.max_c': 'Max °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Auto apply', + 'editor.auto_apply.desc': 'When enabled, the card tries to set temperature on the selected climate entities to match the scheduled value for “now”.', + 'editor.apply_edit.title': 'On edit', + 'editor.apply_edit.desc': 'When you edit blocks and it affects the current time, apply the new temperature immediately.', + 'editor.apply_default.title': 'On default temperature change', + 'editor.apply_default.desc': 'When “Default °C” changes and it affects the current period, apply the new temperature immediately.', + 'editor.perroom.title': 'Default °C per room', + 'editor.perroom.desc': 'Shows a Default °C field on each row. When off, the global Default °C is used for all rooms.', + + 'editor.show_room_temp.title': 'Show room temperature', + 'editor.show_room_temp.desc': 'Show the current room temperature bubble on each row in the timeline header.', + + 'editor.room_mode.title': 'Room entity type', + 'editor.room_mode.desc': 'When enabled, select an input_number (target temperature). When disabled, select a climate entity.', + + 'editor.room_temp_sensor_override.title': 'Use room temperature sensor', + 'editor.room_temp_sensor_override.desc': 'When enabled, the shown room temperature (and boiler logic) uses the selected temperature sensor instead of climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Temperature sensor', + + 'editor.turn_on.title': 'Send turn_on command', + 'editor.turn_on.desc': 'Some thermostats require a turn_on command in addition to set_temperature.', + 'editor.turn_on.order': 'Order', + 'editor.turn_on.order.before': 'Before set_temperature', + 'editor.turn_on.order.after': 'After set_temperature', + + 'editor.merge_label': 'Merge with (add extra thermostat)', + 'editor.display_name_optional': 'Room name (optional)', + 'editor.merged_with': 'Merged with', + + 'editor.store_enable.title': 'Shared storage + background control', + 'editor.store_enable.short': 'Stores schedules in the selected sensor and enables background control.', + 'editor.store_enable.desc': 'Stores schedules in the selected sensor and enables set_temperature in the background. Thermostats are also updated when the card is closed. Shares schedules and card settings across users/devices. When off: commands are only sent while the card is visible on a device and must remain open.', + 'editor.store_missing': 'The integration “Thermostat Pro Timeline Sync” is not installed. Shared storage is unavailable.', + 'editor.open_in_hacs': 'Open in HACS', + 'editor.clear_store': 'Clear storage', + 'editor.clear_store_confirm': 'This will delete all stored schedules. Continue?', + 'editor.migrate_to_store': 'Transfer data to storage', + 'editor.migrate_confirm': 'Transfer your browser-saved schedules to the selected sensor? This overwrites existing schedules in storage.', + 'editor.migrate_pull_confirm': 'Import schedules from storage and overwrite local browser data?', + 'editor.select_storage_first': 'Select a storage sensor first.', + 'editor.no_local_data': 'No local browser data was found.', + 'editor.test_tool': 'Test tool', + 'editor.test_select_entity': 'Select entity', + 'editor.test_empty': 'Select an entity to see available commands…', + 'editor.test_export': 'Export .md', + 'editor.test_no_fields': 'No fields', + 'editor.test_select_placeholder': '— Select —', + 'editor.error_prefix': 'Error: ', + 'editor.clear_all': 'Clear all data', + 'editor.clear_all_confirm': 'This deletes both storage and local/browser schedules. Continue?', + 'editor.clear_storage_only': 'Clear storage data only', + 'editor.clear_storage_only_confirm': 'This deletes schedules from the selected storage sensor. Continue?', + 'editor.clear_local_only': 'Clear ALL local data (local only)', + 'editor.clear_local_only_confirm': 'This deletes only schedules saved in your browser. Continue?', + 'editor.reset.desc': 'What this does:\n\n• Clear all data: Resets backend storage (.storage) for Thermostat Timeline and removes ALL local browser caches/drafts.\n• Clear ALL local data (local only): Only clears data stored in this browser (localStorage).\n\nTip: After a full reset, reload the page to avoid old drafts reappearing.', + + 'editor.select_entities': 'Rooms', + 'editor.colors.empty': 'No rooms added yet. Add a room to define colors.', + 'editor.colors.go_add': 'Add room', + + 'week.enable': 'Enable weekdays', + 'week.enable.desc': 'Enable per-weekday planning. When off, the same plan is used every day.', + 'week.mode': 'Weekday mode', + // removed same_all option from UI + 'week.mode.weekday_weekend': 'Weekdays + Weekend', + 'week.mode.weekday_sat_sun': 'Weekdays + Saturday + Sunday', + 'week.mode.all_7': 'All days', + 'week.button': 'Weekdays', + 'week.title': 'Weekdays', + 'week.group.weekdays': 'Weekdays', + 'week.group.weekend': 'Weekend', + 'week.days_full.mon': 'Monday', + 'week.days_full.tue': 'Tuesday', + 'week.days_full.wed': 'Wednesday', + 'week.days_full.thu': 'Thursday', + 'week.days_full.fri': 'Friday', + 'week.days_full.sat': 'Saturday', + 'week.days_full.sun': 'Sunday', + 'week.days.mon': 'Mon', + 'week.days.tue': 'Tue', + 'week.days.wed': 'Wed', + 'week.days.thu': 'Thu', + 'week.days.fri': 'Fri', + 'week.days.sat': 'Sat', + 'week.days.sun': 'Sun', + 'week.editor.title': 'Weekday schedule', + 'week.editor.preview_note': 'Preview only. Changes are applied when you press Save.', + 'week.open_editor.desc': 'Open the weekdays editor popup.', + 'week.open_editor.btn': 'Open', + 'week.view.title': 'Weekdays view', + 'week.view.rooms_one_day': 'All rooms • one day', + 'week.view.days_one_room': 'All days • one room', + 'week.view_switch.title': 'Switch view from timeline', + 'week.view_switch.desc': 'Show a toggle in the timeline header to switch between the two Weekdays view modes.', + 'week.select_room': 'Select room', + 'week.copy_to_rooms': 'Copy weekdays to rooms', + 'week.copy_to_rooms.title': 'Copy weekdays to rooms', + 'week.copy_to_rooms.select': 'Select rooms', + 'week.copy_to_rooms.apply': 'Apply to rooms', + + 'editor.timefmt.title': 'Time format', + 'editor.timefmt.desc': 'Choose 24-hour or 12-hour (AM/PM) display for times.', + 'editor.tempfmt.title': 'Temperature unit', + 'editor.tempfmt.desc': 'Choose Celsius (°C) or Fahrenheit (°F) for display in the card and editors.', + 'editor.timesrc.title': 'Time source', + 'editor.timesrc.desc': 'Choose whether the card uses the browser time or the Home Assistant time zone for “now”.', + 'editor.timesrc.browser': 'Browser', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'Configuration ID', + 'editor.instance.desc': 'Keep multiple independent schedules/settings under different IDs.', + 'editor.instance.new_id': 'New ID', + + 'editor.tabs.settings': 'Settings', + 'editor.tabs.pause': 'Pause', + 'editor.tabs.weekdays': 'Weekdays', + 'editor.tabs.rooms': 'Rooms', + 'editor.tabs.boiler': 'Boiler control', + 'editor.tabs.colors': 'Colors', + 'editor.tabs.presence_sensor': 'Presence sensor', + 'editor.tabs.owd': 'Open Window Detection', + 'editor.tabs.away': 'Away from home', + 'editor.tabs.reset': 'Reset', + 'editor.tabs.sync': 'Sync Engine', + 'editor.tabs.backup': 'Backup', + + 'editor.presence_sensor.enable.title': 'Presence sensor', + 'editor.presence_sensor.enable.desc': 'Enable per-room presence sensor configuration.', + + 'editor.presence_sensor.entity.title': 'Presence entity', + 'editor.presence_sensor.entity.desc': 'Select the binary_sensor that indicates presence in this room.', + + 'editor.presence_sensor.on_temp.title': 'Temperature when presence is ON', + 'editor.presence_sensor.on_temp.desc': 'If the sensor is ON, this room will use this temperature instead of the schedule.', + + 'editor.presence_sensor.on_delay.title': 'Delay before applying presence', + 'editor.presence_sensor.on_delay.desc': 'How long the sensor must stay ON before the override temperature is used. Value uses the selected unit.', + 'editor.presence_sensor.off_delay.title': 'Delay before resuming schedule', + 'editor.presence_sensor.off_delay.desc': 'How long the sensor must stay OFF before the schedule is resumed. Value uses the selected unit.', + + 'owd.enable': 'Open Window Detection', + 'owd.enable.desc': 'Turns off thermostats in a room when selected window/door sensors are open.', + 'owd.sensors': 'Window/Door sensors', + 'owd.delays.title': 'Delays', + 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).', + 'owd.open_delay': 'After open (min)', + 'owd.close_delay': 'After closed (min)', + + 'boiler.switch': 'Boiler switch', + 'boiler.switch_type': 'Entity type', + 'boiler.switch_type.switch': 'Switch', + 'boiler.switch_type.input_boolean': 'Input boolean', + 'boiler.rooms': 'Rooms included', + 'boiler.rooms.desc': 'Select which rooms should be included in boiler control.', + 'boiler.enable': 'Enable boiler control', + 'boiler.enable.desc': 'Show boiler settings in this card editor.', + 'boiler.temp_sensor': 'Boiler temperature sensor', + 'boiler.temp_sensor.desc': 'Temperature sensor that controls the boiler switch.', + 'boiler.min_temp': 'Min boiler temp (°C)', + 'boiler.max_temp': 'Max boiler temp (°C)', + 'boiler.offsets': 'Offsets', + 'boiler.offsets.desc': 'ON offset: how far below the scheduled target before turning ON. OFF offset: how far above the scheduled target before turning OFF.', + 'boiler.on_offset': 'ON offset (°C)', + 'boiler.off_offset': 'OFF offset (°C)', + + 'sync.running': 'Sync in progress…', + 'sync.completed': 'Sync completed', + + 'backup.title': 'Backup', + 'backup.desc': 'Copy the full content of your sync sensor to the backup sensor and restore it when needed.', + 'backup.now': 'Backup now', + 'backup.restore': 'Restore', + 'backup.restore.confirm_overwrite': 'Restore from backup? This will overwrite current schedules and settings.', + 'backup.delete': 'Delete backup', + 'backup.delete.confirm': 'Delete selected backup?', + 'backup.new': 'New backup', + 'backup.auto': 'Auto backup', + 'backup.auto.desc': 'Create backups automatically at a fixed interval.', + 'backup.interval': 'Interval (days)', + 'backup.interval.desc': 'How many days between automatic backups (min 1).', + 'backup.sensor': 'Backup sensor', + 'backup.last': 'Last backup', + 'backup.slot.title': 'Select backup', + 'backup.slot.desc': 'Select which backup slot to restore.', + 'backup.slot.empty': 'No backups yet', + 'backup.select': 'Select what to back up', + 'backup.select.desc': 'Checked items are included in backup/restore', + 'backup.opt.all': 'All', + 'backup.opt.main': 'Main schedules', + 'backup.opt.weekday': 'Weekday schedules', + 'backup.opt.presence': 'Presence schedules', + 'backup.opt.settings': 'Editor settings', + 'backup.opt.holiday': 'Holiday schedules', + 'backup.opt.colors': 'Color blocks', + + 'backup.import.title': 'Import thermostat_timeline.json', + 'backup.import.desc': 'Upload a thermostat_timeline.json file and import it. A new backup is created first.', + 'backup.import.btn': 'Import file', + 'backup.import.err_no_hass': 'Home Assistant not ready', + 'backup.import.err_storage_off': 'Shared storage is OFF. Turn it on first.', + 'backup.import.err_no_integration': 'Integration thermostat_timeline not available', + 'backup.import.err_json': 'Invalid JSON', + 'backup.import.err_invalid': 'Invalid file: missing data.schedules', + + 'away.enable': 'Enable Away mode', + 'away.desc': 'When none of the selected persons are home, thermostats are automatically lowered to the specified temperature.', + 'away.temp': 'Away temperature', + 'away.persons': 'Persons', + 'away.add_person': 'Add person', + 'away.resume_schedule': 'Resume schedule', + 'away.resume_away': 'Resume away temperature', + + 'editor.heat_colors.title': 'Colors for heat blocks', + 'editor.heat_colors.add': 'Add color interval', + 'editor.colors.col_from': 'From °C', + 'editor.colors.col_to': 'To °C', + 'editor.colors.col_color': 'Color', + 'editor.colors.refresh': 'Refresh schedule', + 'editor.colors.clear_local': 'Clear local color data', + 'editor.colors.clear_local_confirm': 'This deletes color intervals stored in your browser for this card. Continue?', + 'editor.colors.clear_shared': 'Clear shared color storage', + 'editor.colors.clear_shared_confirm': 'This deletes color intervals in the selected storage sensor. Continue?', + 'editor.colors.clear_all': 'Clear all color data', + 'editor.colors.clear_all_confirm': 'This deletes all color intervals both locally and in the selected storage sensor. Continue?', + 'editor.colors.mode': 'Color mode', + 'editor.colors.mode.desc': 'Choose per-room colors or one set for all rooms.', + 'editor.colors.mode_per_room': 'Per room', + 'editor.colors.mode_global': 'All rooms', + 'editor.colors.global_title': 'All rooms', + + 'editor.reload_editor': 'Reload editor', + 'editor.reload_dashboard': 'Reload dashboard', + + 'onboard.title': 'Welcome to Thermostat Timeline', + 'onboard.next': 'Next', + 'onboard.prev': 'Back', + 'onboard.skip': 'Skip', + 'onboard.done': 'Done', + 'onboard.dont_show': 'Don’t show again', + 'onboard.video_not_found': 'Video not found', + 'onboard.image_not_found': 'Image not found', + 'onboard.step_main': 'This is the main timeline. It shows target temperatures over 24 hours for each room.', + 'onboard.step_add': 'Add a heat block via + Add block or by double-clicking the timeline.', + 'onboard.step_edit': 'Edit or delete a block by double-clicking it. Use Weekdays for different days.', + 'onboard.reset': 'Show guide again', + 'onboard.reset_ok': 'The guide will show next time.', + + 'pause.menu_title': 'Pause auto-apply', + 'pause.off_1h': 'Pause for 1 hour', + 'pause.off_5h': 'Pause for 5 hours', + 'pause.off_custom': 'Custom duration…', + 'pause.prompt_minutes': 'Enter duration (minutes):', + 'pause.off_indef': 'Pause until I resume', + 'pause.resume': 'Resume now', + 'pause.close': 'Close', + 'pause.active_until': 'Paused until {time}', + 'pause.active_indef': 'Paused', + + 'editor.pausebtn.title': 'Pause button', + 'editor.pausebtn.desc': 'Show a global pause button in the card header. When pause is active, all set_temperature commands are suppressed.', + 'editor.pause_sensor.title': 'Pause via binary sensor', + 'editor.pause_sensor.desc': 'Automatically pause while the selected binary sensor is on.', + 'editor.pause_sensor.entity_title': 'Pause sensor', + 'editor.pause_sensor.entity_desc': 'Binary sensor that controls pause (on = pause, off = resume).', + 'editor.integration.connected': 'Connected to integration', + 'editor.integration.disconnected': 'Not connected to integration', + + 'profiles.enable': 'Enable profiles (override)', + 'profiles.enable.desc': 'Allow named day schedules that can be activated to override the main plan.', + 'profiles.button': 'Profiles', + 'profiles.editor.title': 'Profiles', + 'profiles.add': 'Add profile', + 'profiles.rename': 'Rename', + 'profiles.delete': 'Delete', + 'profiles.activate': 'Activate', + 'profiles.deactivate': 'Deactivate', + 'profiles.active': 'Active', + 'profiles.none': 'None', + 'profiles.name_placeholder': 'Profile name', + 'profiles.name_exists': 'Name exists', + 'profiles.edit': 'Edit', + 'profiles.preview_note': 'Preview only. Profiles become active when you activate them.', + 'profiles.active_label': 'Active profile:', + 'profiles.save_new': 'Save new profile', + 'profiles.save_existing': 'Save changes to selected profile', + + 'editor.tabs.holidays': 'Holidays', + 'holidays.enable': 'Enable holidays', + 'holidays.desc': 'Use a special day schedule on holidays.', + 'holidays.source': 'Holiday source', + 'holidays.source.calendar': 'Calendar', + 'holidays.source.manual': 'Manual dates', + 'holidays.entity': 'Holiday entity', + 'holidays.entity_desc': 'Select a calendar that only provides on/off. It must be on on holidays and off on other days.', + 'holidays.dates': 'Holiday dates', + 'holidays.add_date': 'Add date', + 'holidays.add_range': 'Add range', + 'holidays.date_in_range': 'Date already covered by a range', + 'holidays.ranges': 'Holiday ranges', + 'holidays.single_dates': 'Single dates', + 'holidays.editor.title': 'Holiday schedule', + 'holidays.edit': 'Edit holiday schedule', + 'holidays.button': 'Holidays', + // Holiday copy-from popup + 'holidays.copy_from': 'Copy from…', + 'holidays.copy.title': 'Copy into holiday', + 'holidays.copy.select': 'Select source', + 'holidays.copy.apply': 'Insert', + 'holidays.copy.replace_confirm': 'Replace existing holiday blocks?', + 'holidays.copy.group.main': 'Main plan', + 'holidays.copy.group.weekdays': 'Weekdays', + 'holidays.copy.group.profiles': 'Profiles', + 'holidays.copy.group.presence': 'Presence', + + 'presence.button': 'Presence schedules', + 'presence.editor.title': 'Presence schedules', + 'presence.enable_advanced': 'Enable advanced presence schedules', + 'presence.enable_advanced.desc': 'Use combinations and schedules based on who is home/away.', + 'presence.live_header': 'Show live presence in header', + 'presence.live_header.desc': 'Chips are shown in the right corner.', + // Away delay + 'away.delay_enable': 'Delay Away actions', + 'away.delay_desc': 'Wait before applying Away changes.', + 'away.delay_value': 'Delay', + 'away.delay_unit': 'Unit', + 'away.delay_unit.seconds': 'Seconds', + 'away.delay_unit.minutes': 'Minutes', + 'presence.combos': 'Combinations', + 'presence.enable_combo': 'Enable this combination', + 'presence.all_home': 'All home', + 'presence.none_home': 'Nobody home', + 'presence.home': 'Home: {names}', + 'presence.away': 'Away: {names}', + 'presence.manage': 'Manage combinations', + 'presence.who_home': 'Who is home', + 'presence.who_away': 'Who is away' + }, + da: { + // Card + 'card.title_default': 'Thermostat Timeline', + 'err.config_entities': "Config skal have 'entities: [climate.xxx, ...]'.", + // Modal / editor (inline) + 'ui.edit_block': 'Edit block', + 'ui.add_block': 'Add block', + 'ui.from': 'From', + 'ui.to': 'To', + 'ui.temperature': 'Temperatur', + 'ui.delete': 'Slet', + 'ui.cancel': 'Annuller', + 'ui.close': 'Luk', + 'ui.unsaved_confirm': 'Du har ændringer, der ikke er gemt. Vil du lukke alligevel?', + 'ui.discard_changes': 'Forkast ændringer', + 'ui.keep_editing': 'Fortsæt redigering', + 'ui.save': 'Gem', + 'ui.error_prefix': 'Fejl: ', + 'ui.overlap_msg': 'Du har valgt overlappende tider — overlap er {start} – {end}.', + 'ui.auto_fix': 'Ret automatisk', + 'ui.fix_start_to': 'Sæt start til {time}', + 'ui.fix_end_to': 'Sæt slut til {time}', + 'ui.current_temp': 'Nuværende temperatur:', + 'ui.copy_plan': 'Kopiér plan', + 'ui.paste_plan': 'Indsæt plan', + 'ui.copy_day': 'Kopiér dag', + 'ui.paste_day': 'Indsæt dag', + 'editor.title_label': 'Titel', + 'editor.store_enable.title': 'Delt lager + baggrundsstyring', + 'editor.store_enable.short': 'Gemmer tidsplaner i den valgte sensor og aktiverer baggrundsstyring.', + 'editor.store_enable.desc': 'Gemmer tidsplaner i den valgte sensor og aktiverer set_temperature i baggrunden. Termostater opdateres også når kortet er lukket. Deler tidsplaner og kortindstillinger på tværs af brugere/enheder. Når slået fra: kommandoer sendes kun mens kortet er synligt på en enhed og skal forblive åbent.', + 'editor.store_missing': 'Integrationen “Thermostat Pro Timeline Sync” er ikke installeret. Delt lager er ikke tilgængeligt.', + 'editor.clear_store': 'Ryd lager', + 'editor.clear_store_confirm': 'Dette vil slette alle gemte tidsplaner. Vil du fortsætte?', + 'editor.migrate_to_store': 'Overfør data til lager', + 'editor.migrate_confirm': 'Overfør dine browser-gemte tidsplaner til den valgte sensor? Dette overskriver eksisterende planer i lageret.', + 'editor.migrate_pull_confirm': 'Importér tidsplaner fra lager og overskriv lokale browserdata?', + 'editor.select_storage_first': 'Vælg først en lagersensor.', + 'editor.no_local_data': 'Ingen lokale browserdata blev fundet.', + 'editor.clear_all': 'Ryd alt data', + 'editor.clear_all_confirm': 'Dette sletter både lager og lokale/browser tidsplaner. Vil du fortsætte?', + 'editor.clear_storage_only': 'Ryd kun lagerdata', + 'editor.clear_storage_only_confirm': 'Dette sletter tidsplaner fra den valgte lagersensor. Vil du fortsætte?', + 'editor.clear_local_only': 'Ryd ALLE lokale data (kun lokalt)', + 'editor.clear_local_only_confirm': 'Dette sletter kun tidsplaner gemt i din browser. Vil du fortsætte?', + 'editor.reset.desc': 'Hvad gør det her:\n\n• Ryd alt data: Nulstiller backend storage (.storage) for Thermostat Timeline og fjerner ALLE lokale caches/drafts i denne browser.\n• Ryd ALLE lokale data (kun lokalt): Rydder kun data gemt i denne browser (localStorage).\n\nTip: Efter en fuld reset kan en genindlæsning forhindre at gamle drafts dukker op igen.', + 'editor.temp_sensor': 'Temperatursensor', + 'editor.test_tool': 'Testværktøj', + 'editor.test_select_entity': 'Vælg entitet', + 'editor.test_empty': 'Vælg en entitet for at se tilgængelige kommandoer…', + 'editor.test_export': 'Export .md', + 'editor.test_no_fields': 'Ingen felter', + 'editor.test_select_placeholder': '— Vælg —', + 'editor.error_prefix': 'Fejl: ', + 'editor.open_in_hacs': 'Åbn i HACS', + 'editor.add_entity': 'Tilføj rum', + 'editor.entity_placeholder': 'Vælg en entitet', + 'editor.drag_reorder': 'Træk for at ændre rækkefølge', + 'editor.remove': 'Fjern', + 'editor.default_c': 'Standard °C', + 'editor.row_height_px': 'Rækkehøjde (px)', + 'editor.min_c': 'Min °C', + 'editor.max_c': 'Maks °C', + 'editor.auto_apply.title': 'Auto apply', + 'editor.auto_apply.desc': 'Når slået til forsøger kortet at sætte temperaturen på de valgte climate‑entiteter til den planlagte værdi for “nu”.', + 'editor.apply_edit.title': 'Ved redigering', + 'editor.apply_edit.desc': 'Når du redigerer blokke og det påvirker nuværende tidspunkt, så anvend den nye temperatur med det samme.', + 'editor.apply_default.title': 'Ved ændring af Standard °C', + 'editor.apply_default.desc': 'Når “Standard °C” ændres og det påvirker den aktuelle periode, så anvend den nye temperatur med det samme.', + 'editor.perroom.title': 'Standard °C pr. rum', + 'editor.perroom.desc': 'Vis et Standard °C‑felt på hver række. Når slået fra, bruges global Standard °C for alle rum.', + + 'editor.show_room_temp.title': 'Vis rumtemperatur', + 'editor.show_room_temp.desc': 'Vis boblen med nuværende rumtemperatur på hver række i tidslinjens header.', + 'editor.room_mode.title': 'Rumtype (entitet)', + 'editor.room_mode.desc': 'Når slået til vælger du en input_number (måltemperatur). Når slået fra vælger du en climate‑entitet.', + + 'editor.room_temp_sensor_override.title': 'Brug temperatur-sensor til rumtemp', + 'editor.room_temp_sensor_override.desc': 'Når slået til bruges den valgte temperatursensor til vist rumtemp (og kedel-logik) i stedet for climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Temperatursensor', + + 'editor.turn_on.title': 'Send turn_on kommando', + 'editor.turn_on.desc': 'Nogle termostater kræver en turn_on kommando ud over set_temperature.', + 'editor.turn_on.order': 'Rækkefølge', + 'editor.turn_on.order.before': 'Før set_temperature', + 'editor.turn_on.order.after': 'Efter set_temperature', + 'editor.display_name_optional': 'Rum navn (valgfri)', + 'editor.merge_label': 'Flet med (tilføj ekstra termostat)', + 'editor.merged_with': 'Flettet med', + 'editor.colors.empty': 'Der er endnu ikke oprettet rum. Tilføj et rum for at definere farver.', + 'editor.colors.go_add': 'Tilføj rum', + 'editor.select_entities': 'Rum', + 'week.enable': 'Aktivér ugedage', + 'week.mode': 'Ugedags-tilstand', + 'week.mode.weekday_weekend': 'Hverdage + Weekend', + 'week.mode.weekday_sat_sun': 'Hverdage + Lørdag + Søndag', + 'week.mode.all_7': 'Alle dage', + 'week.button': 'Ugedage', + 'week.title': 'Ugedage', + 'week.group.weekdays': 'Hverdage', + 'week.group.weekend': 'Weekend', + 'week.days_full.mon': 'Mandag', + 'week.days_full.tue': 'Tirsdag', + 'week.days_full.wed': 'Onsdag', + 'week.days_full.thu': 'Torsdag', + 'week.days_full.fri': 'Fredag', + 'week.days_full.sat': 'Lørdag', + 'week.days_full.sun': 'Søndag', + 'week.days.mon': 'Man', + 'week.days.tue': 'Tir', + 'week.days.wed': 'Ons', + 'week.days.thu': 'Tor', + 'week.days.fri': 'Fre', + 'week.days.sat': 'Lør', + 'week.days.sun': 'Søn', + 'week.editor.title': 'Ugedags-tidsplan', + 'week.editor.preview_note': 'Kun forhåndsvisning. Ændringer gemmes først når du trykker Gem.', + 'week.open_editor.desc': 'Åbn ugedags-editoren.', + 'week.open_editor.btn': 'Åbn', + 'week.view.title': 'Ugedage-visning', + 'week.view.rooms_one_day': 'Alle rum • én dag', + 'week.view.days_one_room': 'Alle dage • ét rum', + 'week.view_switch.title': 'Skift visning fra tidslinjen', + 'week.view_switch.desc': 'Vis en toggle i tidslinjens header, så du kan skifte ugedags-visning uden at åbne indstillinger.', + 'week.select_room': 'Vælg rum', + 'week.copy_to_rooms': 'Kopiér ugedage til rum', + 'week.copy_to_rooms.title': 'Kopiér ugedage til rum', + 'week.copy_to_rooms.select': 'Vælg rum', + 'week.copy_to_rooms.apply': 'Indsæt i rum', + 'week.enable.desc': 'Aktivér plan per ugedag. Når slået fra, bruges samme plan hver dag.', + 'editor.timefmt.title': 'Tidsformat', + 'editor.timefmt.desc': 'Vælg 24-timers eller 12-timers (AM/PM) visning for tider på kortet.', + 'editor.tempfmt.title': 'Temperaturenhed', + 'editor.tempfmt.desc': 'Vælg Celsius (°C) eller Fahrenheit (°F) til visning i kortet og editorer.', + 'editor.timesrc.title': 'Tidskilde', + 'editor.timesrc.desc': 'Vælg om kortet skal bruge browserens tid eller Home Assistant-tidszone til “nu”.', + 'editor.timesrc.browser': 'Browser', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'Konfigurations-ID', + 'editor.instance.desc': 'Hold flere uafhængige tidsplaner/indstillinger under forskellige ID’er.', + 'editor.instance.new_id': 'Nyt ID', + 'editor.tabs.settings': 'Indstillinger', + 'editor.tabs.pause': 'Pause', + 'editor.tabs.weekdays': 'Ugedage', + 'editor.tabs.rooms': 'Rum', + 'editor.tabs.boiler': 'Kedelstyring', + 'editor.tabs.colors': 'Farver', + 'editor.tabs.presence_sensor': 'Tilstedeværelsessensor', + 'editor.presence_sensor.enable.title': 'Tilstedeværelsessensor', + 'editor.presence_sensor.enable.desc': 'Aktivér konfiguration af tilstedeværelsessensor per rum.', + 'editor.presence_sensor.entity.title': 'Tilstedeværelses-entity', + 'editor.presence_sensor.entity.desc': 'Vælg den binary_sensor der indikerer tilstedeværelse i dette rum.', + 'editor.presence_sensor.on_temp.title': 'Temperatur når der er tilstedeværelse', + 'editor.presence_sensor.on_temp.desc': 'Hvis sensoren er ON, bruger rummet denne temperatur i stedet for skemaet.', + 'editor.presence_sensor.on_delay.title': 'Forsinkelse før tilstedeværelse bruges', + 'editor.presence_sensor.on_delay.desc': 'Hvor længe sensoren skal være ON før overstyringstemperaturen bruges. Værdien bruger valgt enhed.', + 'editor.presence_sensor.off_delay.title': 'Forsinkelse før skema genoptages', + 'editor.presence_sensor.off_delay.desc': 'Hvor længe sensoren skal være OFF før skemaet genoptages. Værdien bruger valgt enhed.', + 'editor.tabs.owd': 'Open Window Detection', + 'owd.enable': 'Open Window Detection', + 'owd.enable.desc': 'Slår termostaterne fra i et rum når valgte vindue-/dørsensorer er åbne.', + 'owd.sensors': 'Vindue-/dørsensorer', + 'owd.delays.title': 'Forsinkelser', + 'owd.delays.desc': 'Minutter før der slukkes (åben) og før skemaet genoptages (lukket).', + 'owd.open_delay': 'Efter åbnet (min)', + 'owd.close_delay': 'Efter lukket (min)', + 'editor.tabs.away': 'Væk fra hjemme', + 'editor.tabs.reset': 'Nulstil', + 'editor.tabs.sync': 'Sync Engine', + 'boiler.switch': 'Boiler switch', + 'boiler.switch_type': 'Entitetstype', + 'boiler.switch_type.switch': 'Switch', + 'boiler.switch_type.input_boolean': 'Input boolean', + 'boiler.rooms': 'Rum der er med', + 'boiler.rooms.desc': 'Vælg hvilke rum der skal være med i kedelstyringen.', + 'boiler.enable': 'Aktivér kedelstyring', + 'boiler.enable.desc': 'Vis kedelindstillinger i denne cards editor.', + 'boiler.temp_sensor': 'Kedel temperatursensor', + 'boiler.temp_sensor.desc': 'Temperatursensor som styrer kedel‑switchen.', + 'boiler.min_temp': 'Min kedeltemp (°C)', + 'boiler.max_temp': 'Maks kedeltemp (°C)', + 'boiler.offsets': 'Offset', + 'boiler.offsets.desc': 'ON offset: hvor langt under skema-temperaturen den må gå før den tænder. OFF offset: hvor langt over skema-temperaturen den må gå før den slukker.', + 'boiler.on_offset': 'ON offset (°C)', + 'boiler.off_offset': 'OFF offset (°C)', + 'sync.running': 'Igangværende sync…', + 'sync.completed': 'Sync gennemført', + 'editor.tabs.backup': 'Backup', + 'backup.title': 'Backup', + 'backup.desc': 'Kopiér hele indholdet af din sync‑sensor til backup‑sensoren og gendan den igen ved behov.', + 'backup.now': 'Backup nu', + 'backup.restore': 'Gendan', + 'backup.restore.confirm_overwrite': 'Gendan fra backup? Dette overskriver nuværende tidsplaner og indstillinger.', + 'backup.delete': 'Slet backup', + 'backup.delete.confirm': 'Slet valgt backup?', + 'backup.new': 'Ny backup', + 'backup.auto': 'Auto backup', + 'backup.auto.desc': 'Lav automatisk backup med faste intervaller.', + 'backup.interval': 'Interval (dage)', + 'backup.interval.desc': 'Hvor mange dage der skal gå mellem automatiske backups (min. 1).', + 'backup.sensor': 'Backup sensor', + 'backup.last': 'Sidste backup', + 'backup.slot.title': 'Vælg backup', + 'backup.slot.desc': 'Vælg hvilken backup-slot der skal gendannes.', + 'backup.slot.empty': 'Ingen backups endnu', + 'backup.select': 'Vælg hvad der skal tages backup af', + 'backup.select.desc': 'Markerede punkter tages med i backup/gendannelse', + 'backup.opt.all': 'Alt', + 'backup.opt.main': 'Hovedskemaer', + 'backup.opt.weekday': 'Ugedagsskemaer', + 'backup.opt.presence': 'Tilstede skemaer', + 'backup.opt.settings': 'Editor-indstillinger', + 'backup.opt.holiday': 'Helligdags-skemaer', + 'backup.opt.colors': 'Farveblokke', + 'backup.import.title': 'Importér thermostat_timeline.json', + 'backup.import.desc': 'Upload en thermostat_timeline.json fil og importér den. Der laves først en ny backup.', + 'backup.import.btn': 'Importér fil', + 'backup.import.err_no_hass': 'Home Assistant er ikke klar', + 'backup.import.err_storage_off': 'Delt lager er slået fra. Slå det til først.', + 'backup.import.err_no_integration': 'Integrationen thermostat_timeline er ikke tilgængelig', + 'backup.import.err_json': 'Ugyldigt JSON', + 'backup.import.err_invalid': 'Ugyldig fil: mangler data.schedules', + 'away.enable': 'Aktivér væk-tilstand', + 'away.desc': 'Når ingen af de valgte personer er hjemme, sænkes termostaterne automatisk til den angivne temperatur.', + 'away.temp': 'Væk-temperatur', + 'away.persons': 'Personer', + 'away.add_person': 'Tilføj person', + 'away.resume_schedule': 'Genoptag skema', + 'away.resume_away': 'Genoptag væk‑temperatur', + 'editor.heat_colors.title': 'Farver for varmeblokke', + 'editor.heat_colors.add': 'Tilføj farveinterval', + 'editor.colors.col_from': 'Fra °C', + 'editor.colors.col_to': 'Til °C', + 'editor.colors.col_color': 'Farve', + 'editor.colors.refresh': 'Opdater skema', + 'editor.colors.clear_local': 'Ryd lokale farvedata', + 'editor.colors.clear_local_confirm': 'Dette sletter farveintervaller gemt i din browser for dette kort. Vil du fortsætte?', + 'editor.colors.clear_shared': 'Ryd delt farvelager', + 'editor.colors.clear_shared_confirm': 'Dette sletter farveintervaller i den valgte lagersensor. Vil du fortsætte?', + 'editor.colors.clear_all': 'Ryd alle farvedata', + 'editor.colors.clear_all_confirm': 'Dette sletter alle farveintervaller både lokalt og i den valgte lagersensor. Vil du fortsætte?', + 'editor.colors.mode': 'Farvetilstand', + 'editor.colors.mode.desc': 'Vælg om farver defineres pr. rum eller ens for alle rum.', + 'editor.colors.mode_per_room': 'Pr. rum', + 'editor.colors.mode_global': 'Samme for alle', + 'editor.colors.global_title': 'Alle rum', + 'editor.reload_editor': 'Genindlæs editor', + 'editor.reload_dashboard': 'Genindlæs dashboard', + 'onboard.title': 'Velkommen til Termostat Tidslinje', + 'onboard.next': 'Næste', + 'onboard.prev': 'Tilbage', + 'onboard.skip': 'Spring over', + 'onboard.done': 'Færdig', + 'onboard.dont_show': 'Vis ikke igen', + 'onboard.step_main': 'Dette er hovedtidslinjen. Den viser ønskede temperaturer over 24 timer for hvert rum.', + 'onboard.step_add': 'Tilføj en varmeblok via + Tilføj blok eller ved at dobbeltklikke på tidslinjen.', + 'onboard.step_edit': 'Redigér eller slet en blok ved dobbeltklik. Brug Ugedage til forskellige dage.', + 'onboard.reset': 'Vis guide igen', + 'onboard.reset_ok': 'Guiden vises næste gang.', + 'onboard.video_not_found': 'Video ikke fundet', + 'onboard.image_not_found': 'Billede ikke fundet', + 'pause.menu_title': 'Pause auto-apply', + 'pause.off_1h': 'Sluk i 1 time', + 'pause.off_5h': 'Sluk i 5 timer', + 'pause.off_custom': 'Tilpasset varighed…', + 'pause.prompt_minutes': 'Angiv varighed (minutter):', + 'pause.off_indef': 'Sluk til jeg tænder', + 'pause.resume': 'Tænd igen nu', + 'pause.close': 'Luk', + 'pause.active_until': 'Slukket til {time}', + 'pause.active_indef': 'Slukket', + 'editor.pausebtn.title': 'Pause‑knap', + 'editor.pausebtn.desc': 'Vis en global pauseknap i kortets header. Når pause er aktiv, undertrykkes alle set_temperature‑kommandoer.', + 'editor.pause_sensor.title': 'Pause via binary sensor', + 'editor.pause_sensor.desc': 'Sæt automatisk pause mens den valgte binary sensor er on.', + 'editor.pause_sensor.entity_title': 'Pause sensor', + 'editor.pause_sensor.entity_desc': 'Binary sensor som styrer pause (on = pause, off = genoptag).', + 'editor.integration.connected': 'Integration klar', + 'editor.integration.disconnected': 'Integration ikke tilgængelig', + 'profiles.enable': 'Aktivér skema overrule', + 'profiles.enable.desc': 'Tillad navngivne dagsskemaer som kan aktiveres og overrule hovedplanen.', + 'profiles.button': 'Manuelt skema', + 'profiles.editor.title': 'Skemaer', + 'profiles.add': 'Tilføj skema', + 'profiles.rename': 'Omdøb', + 'profiles.delete': 'Slet', + 'profiles.activate': 'Aktivér', + 'profiles.deactivate': 'Deaktivér', + 'profiles.active': 'Aktiv', + 'profiles.none': 'Ingen', + 'profiles.name_placeholder': 'Skemanavn', + 'profiles.name_exists': 'Navnet findes allerede', + 'profiles.edit': 'Redigér', + 'profiles.preview_note': 'Kun forhåndsvisning. Skemaerne bliver først aktive, når profilen aktiveres.', + 'profiles.active_label': 'Manuelt skema aktivt:', + 'profiles.save_new': 'Gem ny profil', + 'profiles.save_existing': 'Gem ændringer til valgt profil', + 'editor.tabs.holidays': 'Helligdage', + 'holidays.enable': 'Aktivér helligdage', + 'holidays.desc': 'Brug et særligt dagsskema på helligdage.', + 'holidays.source': 'Kilde til helligdage', + 'holidays.source.calendar': 'Kalender', + 'holidays.source.manual': 'Manuelle datoer', + 'holidays.entity': 'Helligdags‑entitet', + 'holidays.entity_desc': 'Vælg en kalender, der kun giver on/off. Den skal være on på helligdage og off på andre dage.', + 'holidays.dates': 'Helligdagsdatoer', + 'holidays.add_date': 'Tilføj dato', + 'holidays.add_range': 'Tilføj periode', + 'holidays.date_in_range': 'Dato findes allerede i en periode', + 'holidays.ranges': 'Ferieperioder', + 'holidays.single_dates': 'Enkeltdatoer', + 'holidays.editor.title': 'Helligdags‑skema', + 'holidays.edit': 'Redigér helligdags‑skema', + 'holidays.button': 'Helligdage', + // Holiday copy-from popup + 'holidays.copy_from': 'Kopiér fra…', + 'holidays.copy.title': 'Kopiér til helligdags‑skema', + 'holidays.copy.select': 'Vælg kilde', + 'holidays.copy.apply': 'Indsæt', + 'holidays.copy.replace_confirm': 'Erstat eksisterende helligdags‑blokke?', + 'holidays.copy.group.main': 'Hovedskema', + 'holidays.copy.group.weekdays': 'Ugedage', + 'holidays.copy.group.profiles': 'Skemaer', + 'holidays.copy.group.presence': 'Tilstede skemaer', + 'presence.button': 'Tilstede skemaer', + 'presence.editor.title': 'Tilstede skemaer', + 'presence.enable_advanced': 'Aktivér avancerede tilstede‑skemaer', + 'presence.enable_advanced.desc': 'Brug kombinationer og skemaer baseret på hvem der er hjemme/ude.', + // Away delay + 'away.delay_enable': 'Forsink Away‑handlinger', + 'away.delay_desc': 'Vent før Away ændringer anvendes.', + 'away.delay_value': 'Forsinkelse', + 'away.delay_unit': 'Enhed', + 'away.delay_unit.seconds': 'Sekunder', + 'away.delay_unit.minutes': 'Minutter', + 'presence.live_header': 'Vis live personstatus i header', + 'presence.live_header.desc': 'Chips vises i højre hjørne.', + 'presence.combos': 'Kombinationer', + 'presence.enable_combo': 'Aktivér denne kombination', + 'presence.all_home': 'Alle hjemme', + 'presence.none_home': 'Ingen hjemme', + 'presence.home': 'Hjemme: {names}', + 'presence.away': 'Ude: {names}', + 'presence.manage': 'Administrér kombinationer', + 'presence.who_home': 'Hvem er hjemme', + 'presence.who_away': 'Hvem er ikke hjemme' + }, + sv: { + 'card.title_default': 'Termostat-tidslinje', + 'ui.edit_block': 'Redigera block', + 'ui.add_block': 'Lägg till block', + 'ui.from': 'Från', + 'ui.to': 'Till', + 'ui.temperature': 'Temperatur', + 'ui.delete': 'Ta bort', + 'ui.cancel': 'Avbryt', + 'ui.close': 'Stäng', + 'ui.unsaved_confirm': 'Du har osparade ändringar. Vill du stänga ändå?', + 'ui.discard_changes': 'Ignorera ändringar', + 'ui.keep_editing': 'Fortsätt redigera', + 'ui.save': 'Spara', + 'ui.error_prefix': 'Fel: ', + 'ui.overlap_msg': 'Du har valt överlappande tider — det överlappar under {start} – {end}.', + 'ui.auto_fix': 'Åtgärda automatiskt', + 'ui.fix_start_to': 'Justera start till {time}', + 'ui.fix_end_to': 'Justera slut till {time}', + // Inline meta + 'ui.current_temp': 'Nuvarande temperatur:', + + 'editor.title_label': 'Titel', + 'editor.add_entity': 'Lägg till rum', + 'editor.entity_placeholder': 'Välj en entitet', + 'editor.drag_reorder': 'Dra för att ändra ordning', + 'editor.remove': 'Ta bort', + 'editor.default_c': 'Standard °C', + 'editor.row_height_px': 'Radhöjd (px)', + 'editor.max_c': 'Max °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Automatisk tillämpning', + 'editor.auto_apply.desc': 'När det är aktiverat försöker kortet automatiskt ställa in temperaturen på valda climate-entiteter så att den matchar det planerade värdet för ”just nu”.', + 'editor.apply_edit.title': 'Vid redigering', + 'editor.apply_edit.desc': 'När du ändrar tidslinjeblock och ändringen påverkar aktuell tid, tillämpas den nya temperaturen omedelbart.', + 'editor.apply_default.title': 'Vid ändring av Standard °C', + 'editor.apply_default.desc': 'När ”Standard °C” ändras och det påverkar den aktuella perioden, tillämpas den nya temperaturen omedelbart.', + + 'editor.show_room_temp.title': 'Visa rumstemperatur', + 'editor.show_room_temp.desc': 'Visa bubblan med aktuell rumstemperatur på varje rad i tidslinjens header.', + 'editor.room_mode.title': 'Typ av rumentitet', + 'editor.room_mode.desc': 'När aktiverat, välj en input_number (måltemperatur). När av, välj en climate-entitet.', + + 'editor.room_temp_sensor_override.title': 'Använd rumstemperatursensor', + 'editor.room_temp_sensor_override.desc': 'När den är aktiverad används den valda temperatursensorn för visad rumstemperatur (och pannlogik) i stället för climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Temperatursensor', + 'ui.copy_plan': 'Kopiera schema', + 'ui.paste_plan': 'Klistra in schema', + 'ui.copy_day': 'Kopiera dag', + 'ui.paste_day': 'Klistra in dag', + 'editor.merge_label': 'Flet med (vælg en ekstra radiatortermostat)', + 'editor.display_name_optional': 'Rumsnamn (valfritt)', + 'editor.merged_with': 'Sammanslagen med', + 'editor.store_enable.title': 'Delad lagring + bakgrundsstyrning', + 'editor.store_enable.short': 'Sparar scheman i vald sensor och aktiverar bakgrundsstyrning.', + 'editor.store_enable.desc': 'Sparar scheman i vald sensor och aktiverar set_temperature i bakgrunden. Termostater uppdateras även när kortet är stängt. Delar också scheman (värmeblock) och kortinställningar mellan användare/enheter. När av: kommandon skickas bara när kortet är aktivt synligt på en enhet (surfplatta/dator/mobil) och måste förbli öppet.', + 'editor.store_missing': 'Integrationen "Thermostat Pro Timeline Sync" är inte installerad. Delad lagring är inte tillgänglig.', + 'editor.clear_store': 'Rensa lagring', + 'editor.clear_store_confirm': 'Detta tar bort alla sparade scheman. Vill du fortsätta?', + 'editor.migrate_to_store': 'Överför data till lagring', + 'editor.migrate_confirm': 'Överför dina webbläsar-sparade scheman till vald sensor? Detta skriver över befintliga scheman i lagringen.', + 'editor.migrate_pull_confirm': 'Importera scheman från lagring och skriv över lokala webbläsardata?', + 'editor.select_storage_first': 'Välj först en lagringssensor.', + 'editor.no_local_data': 'Inga lokala webbläsardata hittades.', + 'editor.clear_all': 'Rensa all data', + 'editor.clear_all_confirm': 'Detta tar bort både lagring och lokala webblässarscheman. Vill du fortsätta?', + 'editor.clear_storage_only': 'Rensa endast lagringsdata', + 'editor.clear_storage_only_confirm': 'Detta tar bort scheman från vald lagringssensor. Vill du fortsätta?', + 'editor.clear_local_only': 'Rensa endast lokala data', + 'editor.clear_local_only_confirm': 'Detta tar endast bort scheman som sparats i din webbläsare. Vill du fortsätta?', + 'editor.reset.desc': 'Det här gör:\n\n• Rensa all data: Återställer backend-lagring (.storage) för Thermostat Timeline och tar bort ALLA lokala cache/drafts i den här webbläsaren.\n• Rensa endast lokala data: Rensar bara data som är lagrad i den här webbläsaren (localStorage).\n\nTips: Efter en full reset kan en siduppdatering förhindra att gamla drafts kommer tillbaka.', + 'week.enable': 'Aktivera veckodagar', + 'week.mode': 'Veckoläge', + 'week.mode.same_all': 'Samma alla dagar (1)', + 'week.mode.weekday_weekend': 'Vardagar + Helg', + 'week.mode.weekday_sat_sun': 'Vardagar + Lördag + Söndag', + 'week.mode.all_7': 'Alla dagar', + 'week.button': 'Veckodagar', + 'week.days.mon': 'Mån', + 'week.days.tue': 'Tis', + 'week.days.wed': 'Ons', + 'week.days.thu': 'Tor', + 'week.days.fri': 'Fre', + 'week.days.sat': 'Lör', + 'week.days.sun': 'Sön', + 'week.editor.title': 'Veckoschema', + 'week.editor.preview_note': 'Endast förhandsvisning. Ändringar tillämpas när du trycker Spara.', + 'week.open_editor.desc': 'Öppna veckodagsredigeraren.', + 'week.open_editor.btn': 'Öppna', + 'week.copy_to_rooms': 'Kopiera vecka till rum', + 'week.copy_to_rooms.title': 'Kopiera vecka till rum', + 'week.copy_to_rooms.select': 'Välj rum', + 'week.copy_to_rooms.apply': 'Klistra in i rum', + 'week.group.weekdays': 'Vardagar', + 'week.group.weekend': 'Helg', + 'week.days_full.mon': 'Måndag', + 'week.days_full.tue': 'Tisdag', + 'week.days_full.wed': 'Onsdag', + 'week.days_full.thu': 'Torsdag', + 'week.days_full.fri': 'Fredag', + 'week.days_full.sat': 'Lördag', + 'week.days_full.sun': 'Söndag', + 'editor.timefmt.title': 'Tidsformat', + 'editor.timefmt.desc': 'Välj 24-timmars eller 12-timmars (AM/PM) visning på kortet.', + 'editor.tempfmt.title': 'Temperaturenhet', + 'editor.tempfmt.desc': 'Välj Celsius (°C) eller Fahrenheit (°F) för visning i kortet och editorer.', + 'editor.timesrc.title': 'Tidskälla', + 'editor.timesrc.desc': 'Välj om kortet ska använda webbläsarens tid eller Home Assistant-tidszon för ”nu”.', + 'editor.timesrc.browser': 'Webbläsare', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'Konfigurations-ID', + 'editor.instance.desc': 'Ha flera oberoende scheman/inställningar under olika ID:n.', + 'editor.instance.new_id': 'Nytt ID', + 'editor.heat_colors.title': 'Färger för värmeblock', + 'editor.heat_colors.add': 'Lägg till färgintervall', + 'editor.colors.col_from': 'Från °C', + 'editor.colors.col_to': 'Till °C', + 'editor.colors.col_color': 'Färg', + 'editor.colors.empty': 'Inga rum tillagda ännu. Lägg till ett rum för att definiera färger.', + 'editor.colors.go_add': 'Lägg till rum', + 'editor.colors.refresh': 'Uppdatera schema', + 'editor.colors.clear_local': 'Rensa lokala färgdata', + 'editor.colors.clear_local_confirm': 'Detta tar bort färgintervall som sparats i din webbläsare för detta kort. Vill du fortsätta?', + 'editor.colors.clear_shared': 'Rensa delad färglagring', + 'editor.colors.clear_shared_confirm': 'Detta tar bort färgintervall i den valda lagringssensorn. Vill du fortsätta?', + 'editor.colors.mode': 'Färgläge', + 'editor.colors.mode.desc': 'Välj färger per rum eller ett set för alla rum.', + 'editor.colors.mode_per_room': 'Per rum', + 'editor.colors.mode_global': 'Alla rum', + 'editor.colors.global_title': 'Alla rum', + 'editor.colors.clear_all': 'Rensa all färgdata', + 'editor.colors.clear_all_confirm': 'Detta tar bort alla färgintervall både lokalt och i vald lagringssensor. Vill du fortsätta?', + 'editor.reload_editor': 'Ladda om redigerare', + 'editor.reload_dashboard': 'Ladda om instrumentpanel', + 'onboard.title': 'Välkommen till Termostat‑tidslinje', + 'onboard.next': 'Nästa', + 'onboard.prev': 'Tillbaka', + 'onboard.skip': 'Hoppa över', + 'onboard.done': 'Klar', + 'onboard.dont_show': 'Visa inte igen', + 'onboard.step_main': 'Detta är huvudtidslinjen. Den visar önskad temperatur under dygnet för varje rum.', + 'onboard.step_add': 'Lägg till ett värmeblock via + Lägg till block eller dubbelklicka på tidslinjen.', + 'onboard.step_edit': 'Redigera eller ta bort ett block genom att dubbelklicka. Använd veckodagar för olika dagar.', + 'onboard.reset': 'Visa guiden igen', + 'onboard.reset_ok': 'Guiden visas nästa gång.', + 'editor.select_entities': 'Rum', + 'editor.tabs.rooms': 'Rum', + 'editor.test_tool': 'Testverktyg', + 'editor.test_select_entity': 'Välj enhet', + 'editor.test_empty': 'Välj en enhet för att se tillgängliga kommandon…', + 'editor.test_export': 'Exportera .md', + 'editor.test_no_fields': 'Inga fält', + 'editor.test_select_placeholder': '— Välj —', + 'editor.error_prefix': 'Fel: ', + 'editor.open_in_hacs': 'Öppna i HACS', + 'week.enable.desc': 'Aktivera schema per veckodag. När det är av, används samma schema varje dag.', + 'editor.tabs.sync': 'Sync Engine', + 'sync.running': 'Synkronisering pågår…', + 'sync.completed': 'Synkronisering klar', + 'editor.tabs.backup': 'Backup', + 'backup.title': 'Backup', + 'backup.desc': 'Kopiera innehållet i din sync‑sensor till backup‑sensorn och återställ vid behov.', + 'backup.now': 'Säkerhetskopiera nu', + 'backup.restore': 'Återställ', + 'backup.delete': 'Ta bort backup', + 'backup.delete.confirm': 'Ta bort vald backup?', + 'backup.new': 'Ny backup', + 'backup.auto': 'Automatisk backup', + 'backup.auto.desc': 'Gör automatiska säkerhetskopior med intervall.', + 'backup.interval': 'Intervall (dagar)', + 'backup.interval.desc': 'Hur många dagar mellan automatiska säkerhetskopior (min. 1).', + 'backup.sensor': 'Backup‑sensor', + 'backup.last': 'Senaste backup', + 'backup.slot.title': 'Välj backup', + 'backup.slot.desc': 'Välj vilken backupplats som ska återställas.', + 'backup.slot.empty': 'Inga backups ännu', + 'pause.menu_title': 'Pausa auto‑apply', + 'pause.off_1h': 'Pausa 1 timme', + 'pause.off_5h': 'Pausa 5 timmar', + 'pause.off_custom': 'Anpassad varaktighet…', + 'pause.off_indef': 'Pausa tills jag återupptar', + 'pause.resume': 'Återuppta nu', + 'pause.close': 'Stäng', + 'pause.active_until': 'Pausad till {time}', + 'pause.active_indef': 'Pausad', + 'editor.pausebtn.title': 'Pausknapp', + 'editor.pausebtn.desc': 'Visa en global pausknapp i kortets huvud. När paus är aktiv blockeras alla set_temperature‑kommandon.', + 'editor.integration.connected': 'Integration klar', + 'editor.integration.disconnected': 'Integration inte tillgänglig' + , 'profiles.button': 'Manuellt schema' + , 'profiles.editor.title': 'Profiler' + , 'profiles.add': 'Lägg till profil' + , 'profiles.rename': 'Byt namn' + , 'profiles.delete': 'Ta bort' + , 'profiles.activate': 'Aktivera' + , 'profiles.deactivate': 'Inaktivera' + , 'profiles.active': 'Aktiv' + , 'profiles.none': 'Ingen' + , 'profiles.name_placeholder': 'Profilnamn' + , 'profiles.edit': 'Redigera' + , 'profiles.preview_note': 'Endast förhandsvisning. Scheman blir aktiva när du aktiverar profilen.' + , 'editor.tabs.holidays': 'Helgdagar' + , 'holidays.enable': 'Aktivera helgdagar' + , 'holidays.desc': 'Använd ett särskilt dagsschema på helgdagar.' + , 'holidays.source': 'Källa för helgdagar' + , 'holidays.source.calendar': 'Kalender' + , 'holidays.source.manual': 'Manuella datum' + , 'holidays.entity': 'Helgdags‑entitet' + , 'holidays.entity_desc': 'Välj en kalender som bara ger on/off. Den ska vara on på helgdagar och off övriga dagar.' + , 'holidays.dates': 'Helgdagsdatum' + , 'holidays.add_date': 'Lägg till datum' + , 'holidays.editor.title': 'Helgdags‑schema' + , 'holidays.edit': 'Redigera helgdags‑schema' + , 'holidays.button': 'Helgdagar' + , 'editor.perroom.title': 'Standard °C per rum' + , 'editor.perroom.desc': 'Visa ett Standard °C‑fält på varje rad. När av, används global Standard °C för alla rum.' + , 'away.resume_schedule': 'Återuppta schema' + , 'away.resume_away': 'Återuppta bortatemperatur' + , 'presence.button': 'Närvaroscheman' + , 'presence.editor.title': 'Närvaroscheman' + , 'presence.enable_advanced': 'Aktivera avancerade närvaroscheman' + , 'presence.enable_advanced.desc': 'Använd kombinationer och scheman baserade på vem som är hemma/borta.' + , 'away.delay_enable': 'Fördröj borta‑åtgärder' + , 'away.delay_desc': 'Vänta innan borta‑ändringar tillämpas.' + , 'away.delay_value': 'Fördröjning' + , 'away.delay_unit': 'Enhet' + , 'away.delay_unit.seconds': 'Sekunder' + , 'away.delay_unit.minutes': 'Minuter' + , 'presence.combos': 'Kombinationer' + , 'presence.enable_combo': 'Aktivera denna kombination' + , 'presence.all_home': 'Alla hemma' + , 'presence.none_home': 'Ingen hemma' + , 'presence.home': 'Hemma: {names}' + , 'presence.away': 'Borta: {names}' + , 'presence.manage': 'Hantera kombinationer' + , 'presence.who_home': 'Vem är hemma' + , 'presence.who_away': 'Vem är inte hemma' + , 'away.enable': 'Aktivera bortaläge' + , 'away.desc': 'När ingen av de valda personerna är hemma, sänks termostaterna automatiskt till den angivna temperaturen.' + , 'away.temp': 'Bortatemperatur' + , 'away.persons': 'Personer' + , 'away.add_person': 'Lägg till person' + , 'backup.select': 'Välj vad som ska säkerhetskopieras' + , 'backup.select.desc': 'Markerade objekt inkluderas i backup/återställning' + , 'backup.opt.all': 'Alla' + , 'backup.opt.main': 'Huvudscheman' + , 'backup.opt.weekday': 'Veckodagsscheman' + , 'backup.opt.presence': 'Närvaroscheman' + , 'backup.opt.settings': 'Redigeringsinställningar' + , 'backup.opt.holiday': 'Helgdags‑scheman' + , 'backup.opt.colors': 'Färgblock' + , 'editor.tabs.settings': 'Inställningar' + , 'editor.tabs.pause': 'Paus' + , 'editor.tabs.weekdays': 'Veckodagar' + , 'editor.tabs.colors': 'Färger' + , 'editor.tabs.presence_sensor': 'Närvarosensor' + , 'editor.presence_sensor.enable.title': 'Närvarosensor' + , 'editor.presence_sensor.enable.desc': 'Aktivera konfiguration av närvarosensor per rum.' + , 'editor.presence_sensor.entity.title': 'Närvaroentitet' + , 'editor.presence_sensor.entity.desc': 'Välj den binary_sensor som indikerar närvaro i detta rum.' + , 'editor.presence_sensor.on_temp.title': 'Temperatur när närvaro är PÅ' + , 'editor.presence_sensor.on_temp.desc': 'Om sensorn är PÅ använder rummet denna temperatur istället för schemat.' + , 'editor.presence_sensor.on_delay.title': 'Fördröjning innan närvaro används' + , 'editor.presence_sensor.on_delay.desc': 'Hur länge sensorn måste vara PÅ innan överstyrningstemperaturen används. Värdet använder vald enhet.' + , 'editor.presence_sensor.off_delay.title': 'Fördröjning innan schema återupptas' + , 'editor.presence_sensor.off_delay.desc': 'Hur länge sensorn måste vara AV innan schemat återupptas. Värdet använder vald enhet.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Borta hemifrån' + , 'editor.tabs.reset': 'Återställ' + , 'editor.tabs.boiler': 'Pannstyrning' + , 'boiler.switch': 'Pannbrytare' + , 'boiler.switch_type': 'Entitetstyp' + , 'boiler.switch_type.switch': 'Switch' + , 'boiler.switch_type.input_boolean': 'Input boolean' + , 'boiler.rooms': 'Rum som ingår' + , 'boiler.rooms.desc': 'Välj vilka rum som ska ingå i pannstyrningen.' + , 'boiler.enable': 'Aktivera pannstyrning' + , 'boiler.enable.desc': 'Visa panninställningar i den här korteditorn.' + , 'boiler.temp_sensor': 'Pannans temperatursensor' + , 'boiler.temp_sensor.desc': 'Temperatursensor som styr pannbrytaren.' + , 'boiler.min_temp': 'Min pann‑temp (°C)' + , 'boiler.max_temp': 'Max pann‑temp (°C)' + , 'boiler.offsets': 'Offset' + , 'boiler.offsets.desc': 'ON‑offset: hur långt under schemamålet innan den slår PÅ. OFF‑offset: hur långt över schemamålet innan den slår AV.' + , 'boiler.on_offset': 'ON offset (°C)' + , 'boiler.off_offset': 'OFF offset (°C)' + , 'editor.temp_sensor': 'Temperatursensor' + , 'presence.live_header': 'Visa live‑närvaro i rubriken' + , 'presence.live_header.desc': 'Chips visas i högra hörnet.' + , 'profiles.enable': 'Aktivera profiler (åsidosättning)' + , 'profiles.enable.desc': 'Tillåt namngivna dagsscheman som kan aktiveras för att åsidosätta huvudplanen.' + , 'profiles.active_label': 'Manuell profil aktiv:' + , 'profiles.save_new': 'Spara ny profil' + , 'profiles.save_existing': 'Spara ändringar i vald profil' + , 'week.view.title': 'Veckovy' + , 'week.view.rooms_one_day': 'Alla rum • en dag' + , 'week.view.days_one_room': 'Alla dagar • ett rum' + , 'week.view_switch.title': 'Byt vy från tidslinjen' + , 'week.view_switch.desc': 'Visa en växlare i tidslinjens rubrik så att du kan byta mellan de två veckodagsvisningarna.' + , 'week.title': 'Veckodagar' + , 'week.select_room': 'Välj rum' + , 'err.config_entities': "Konfigurationen måste ha 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Video hittades inte' + , 'onboard.image_not_found': 'Bild hittades inte' + , 'pause.prompt_minutes': 'Ange varaktighet (minuter):' + , 'editor.test_tool': 'Testverktyg' + , 'editor.test_select_entity': 'Välj en entitet' + , 'editor.test_empty': 'Inget att exportera' + , 'editor.test_export': 'Exportera' + , 'editor.test_no_fields': 'Inga fält att visa' + , 'editor.test_select_placeholder': 'Välj...' + , 'editor.error_prefix': 'Fel: ' + , 'backup.restore.confirm_overwrite': 'Återställ från säkerhetskopia? Detta kommer att skriva över nuvarande scheman och inställningar.' + , 'backup.import.err_no_hass': 'Home Assistant är inte redo' + , 'backup.import.err_storage_off': 'Delad lagring är AV. Slå på den först.' + , 'backup.import.err_no_integration': 'Integrationen thermostat_timeline är inte tillgänglig' + , 'backup.import.err_json': 'Ogiltig JSON' + , 'profiles.name_exists': 'Namnet finns redan' + }, + nb: { + 'card.title_default': 'Termostat tidslinje', + 'ui.edit_block': 'Rediger blokk', + 'ui.add_block': 'Legg til blokk', + 'ui.from': 'Fra', + 'ui.to': 'Til', + 'ui.temperature': 'Temperatur', + 'ui.delete': 'Slett', + 'ui.cancel': 'Avbryt', + 'ui.close': 'Lukk', + 'ui.unsaved_confirm': 'Du har ulagrede endringer. Vil du lukke likevel?', + 'ui.discard_changes': 'Forkast endringer', + 'ui.keep_editing': 'Fortsett redigering', + 'ui.save': 'Lagre', + 'ui.error_prefix': 'Feil: ', + 'ui.overlap_msg': 'Du har valgt overlappende tider — det overlapper i perioden {start} – {end}.', + 'ui.auto_fix': 'Rett automatisk', + 'ui.fix_start_to': 'Sett start til {time}', + 'ui.fix_end_to': 'Sett slutt til {time}', + // Inline meta + 'ui.current_temp': 'Aktuell temperatur:', + + 'editor.title_label': 'Tittel', + 'editor.add_entity': 'Legg til rom', + 'editor.entity_placeholder': 'Velg en entitet', + 'editor.drag_reorder': 'Dra for å endre rekkefølge', + 'editor.remove': 'Fjern', + 'editor.default_c': 'Standard °C', + 'editor.row_height_px': 'Radhøyde (px)', + 'editor.max_c': 'Maks °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Automatisk bruk', + 'editor.auto_apply.desc': 'Når aktivert forsøker kortet å sette temperaturen på valgte climate-entiteter slik at den samsvarer med planlagt verdi for «nå».', + 'editor.apply_edit.title': 'Ved redigering', + 'editor.apply_edit.desc': 'Når du endrer blokker i tidslinjen og endringen påvirker aktuell tid, tilpasses den nye temperaturen umiddelbart.', + 'editor.apply_default.title': 'Ved Standard °C-endring', + 'editor.apply_default.desc': 'Når «Standard °C» endres og det påvirker gjeldende periode, brukes den nye temperaturen umiddelbart.', + + 'editor.show_room_temp.title': 'Vis romtemperatur', + 'editor.show_room_temp.desc': 'Vis boblen med aktuell romtemperatur på hver rad i tidslinjens toppfelt.', + 'editor.room_mode.title': 'Type rom-entitet', + 'editor.room_mode.desc': 'Når aktivert, velg en input_number (måltemperatur). Når deaktivert, velg en climate-entitet.', + + 'editor.room_temp_sensor_override.title': 'Bruk romtemperatursensor', + 'editor.room_temp_sensor_override.desc': 'Når aktivert brukes den valgte temperatursensoren for vist romtemperatur (og kjel-logikk) i stedet for climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Temperatursensor', + 'ui.copy_plan': 'Kopier plan', + 'ui.paste_plan': 'Lim inn plan', + 'ui.copy_day': 'Kopier dag', + 'ui.paste_day': 'Lim inn dag', + 'editor.merge_label': 'Flet med (vælg en ekstra radiatortermostat)', + 'editor.display_name_optional': 'Romnavn (valgfritt)', + 'editor.merged_with': 'Flettet med', + 'editor.store_enable.title': 'Delt lagring + bakgrunnsstyring', + 'editor.store_enable.short': 'Lagrer tidsplaner i valgt sensor og aktiverer bakgrunnsstyring.', + 'editor.store_enable.desc': 'Lagrer tidsplaner i valgt sensor og aktiverer set_temperature i bakgrunnen. Termostater oppdateres også når kortet er lukket. Deler også tidsplaner (varmeblokker) og kortinnstillinger på tvers av brukere/enheter. Når av: kommandoer sendes kun mens kortet er aktivt synlig på en enhet (nettbrett/PC/mobil) og må forbli åpent.', + 'editor.store_missing': 'Integrasjonen "Thermostat Pro Timeline Sync" er ikke installert. Delt lagring er ikke tilgjengelig.', + 'editor.clear_store': 'Tøm lagring', + 'editor.clear_store_confirm': 'Dette vil slette alle lagrede tidsplaner. Fortsette?', + 'editor.migrate_to_store': 'Overfør data til lagring', + 'editor.migrate_confirm': 'Overfør nettleserlagrede tidsplaner til valgt sensor? Dette overskriver eksisterende tidsplaner i lagringen.', + 'editor.migrate_pull_confirm': 'Importer tidsplaner fra lagring og overskriv lokale nettleserdata?', + 'editor.select_storage_first': 'Velg først en lagringssensor.', + 'editor.no_local_data': 'Ingen lokale nettleserdata ble funnet.', + 'editor.clear_all': 'Tøm all data', + 'editor.clear_all_confirm': 'Dette sletter både lagring og lokale nettleserplaner. Fortsette?', + 'editor.clear_storage_only': 'Tøm kun lagringsdata', + 'editor.clear_storage_only_confirm': 'Dette sletter tidsplaner fra valgt lagringssenso r. Fortsette?', + 'editor.clear_local_only': 'Tøm kun lokale data', + 'editor.clear_local_only_confirm': 'Dette sletter kun tidsplaner som er lagret i nettleseren. Fortsette?', + 'editor.reset.desc': 'Dette gjør:\n\n• Tøm all data: Tilbakestiller backend-lagring (.storage) for Thermostat Timeline og sletter ALLE lokale cache/drafts i denne nettleseren.\n• Tøm kun lokale data: Tømmer kun data lagret i denne nettleseren (localStorage).\n\nTips: Etter en full reset kan en oppdatering av siden hindre at gamle drafts dukker opp igjen.', + 'week.enable': 'Aktiver ukedager', + 'week.mode': 'Ukedagsmodus', + 'week.mode.same_all': 'Samme hver dag (1)', + 'week.mode.weekday_weekend': 'Hverdager + Helg', + 'week.mode.weekday_sat_sun': 'Hverdager + Lørdag + Søndag', + 'week.mode.all_7': 'Alle dager', + 'week.button': 'Ukedager', + 'week.days.mon': 'Man', + 'week.days.tue': 'Tir', + 'week.days.wed': 'Ons', + 'week.days.thu': 'Tor', + 'week.days.fri': 'Fre', + 'week.days.sat': 'Lør', + 'week.days.sun': 'Søn', + 'week.editor.title': 'Ukeplan', + 'week.editor.preview_note': 'Kun forhåndsvisning. Endringer brukes når du trykker Lagre.', + 'week.open_editor.desc': 'Åpne ukedagseditoren.', + 'week.open_editor.btn': 'Åpne', + 'week.copy_to_rooms': 'Kopier ukeplan til rom', + 'week.copy_to_rooms.title': 'Kopier ukeplan til rom', + 'week.copy_to_rooms.select': 'Velg rom', + 'week.copy_to_rooms.apply': 'Sett inn i rom', + 'week.group.weekdays': 'Hverdager', + 'week.group.weekend': 'Helg', + 'week.days_full.mon': 'Mandag', + 'week.days_full.tue': 'Tirsdag', + 'week.days_full.wed': 'Onsdag', + 'week.days_full.thu': 'Torsdag', + 'week.days_full.fri': 'Fredag', + 'week.days_full.sat': 'Lørdag', + 'week.days_full.sun': 'Søndag', + 'editor.timefmt.title': 'Tidsformat', + 'editor.timefmt.desc': 'Velg 24-timers eller 12-timers (AM/PM) visning for tider på kortet.', + 'editor.tempfmt.title': 'Temperaturenhet', + 'editor.tempfmt.desc': 'Velg Celsius (°C) eller Fahrenheit (°F) for visning i kortet og editorer.', + 'editor.timesrc.title': 'Tidskilde', + 'editor.timesrc.desc': 'Velg om kortet skal bruke nettleserens tid eller Home Assistant-tidssone for «nå».', + 'editor.timesrc.browser': 'Nettleser', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'Konfigurasjons-ID', + 'editor.instance.desc': 'Behold flere uavhengige tidsplaner/innstillinger under forskjellige ID-er.', + 'editor.instance.new_id': 'Nytt ID', + 'editor.heat_colors.title': 'Farger for varmeblokker', + 'editor.heat_colors.add': 'Legg til fargeintervall', + 'editor.colors.col_from': 'Fra °C', + 'editor.colors.col_to': 'Til °C', + 'editor.colors.col_color': 'Farge', + 'editor.colors.empty': 'Ingen rom lagt til ennå. Legg til et rom for å definere farger.', + 'editor.colors.go_add': 'Legg til rom', + 'editor.colors.refresh': 'Oppdater tidsplan', + 'editor.colors.clear_local': 'Tøm lokale fargedata', + 'editor.colors.clear_local_confirm': 'Dette sletter fargeintervaller lagret i nettleseren for dette kortet. Fortsette?', + 'editor.colors.clear_shared': 'Tøm delt fargelagring', + 'editor.colors.clear_shared_confirm': 'Dette sletter fargeintervaller i den valgte lagringssensoren. Fortsette?', + 'editor.colors.mode': 'Fargemodus', + 'editor.colors.mode.desc': 'Velg farger per rom eller ett felles sett for alle rom.', + 'editor.colors.mode_per_room': 'Per rom', + 'editor.colors.mode_global': 'Alle rom', + 'editor.colors.global_title': 'Alle rom', + 'editor.colors.clear_all': 'Tøm alle fargedata', + 'editor.colors.clear_all_confirm': 'Dette sletter alle fargeintervaller både lokalt og i valgt lagringssenso r. Fortsette?', + 'editor.reload_editor': 'Last inn editor på nytt', + 'editor.reload_dashboard': 'Last inn dashboard på nytt', + 'onboard.title': 'Velkommen til Termostat tidslinje', + 'onboard.next': 'Neste', + 'onboard.prev': 'Tilbake', + 'onboard.skip': 'Hopp over', + 'onboard.done': 'Ferdig', + 'onboard.dont_show': 'Ikke vis igjen', + 'onboard.step_main': 'Dette er hovedtidslinjen. Den viser ønsket temperatur gjennom døgnet for hvert rom.', + 'onboard.step_add': 'Legg til en varmeblokk via + Legg til blokk eller ved å dobbeltklikke på tidslinjen.', + 'onboard.step_edit': 'Rediger eller slett en blokk ved å dobbeltklikke. Bruk ukedager for forskjellige dager.', + 'onboard.reset': 'Vis guiden på nytt', + 'onboard.reset_ok': 'Velkomst vises neste gang.', + 'editor.select_entities': 'Rom', + 'editor.tabs.rooms': 'Rum', + 'editor.test_tool': 'Testverktøy', + 'editor.test_select_entity': 'Velg enhet', + 'editor.test_empty': 'Velg en enhet for å se tilgjengelige kommandoer…', + 'editor.test_export': 'Eksporter .md', + 'editor.test_no_fields': 'Ingen felt', + 'editor.test_select_placeholder': '— Velg —', + 'editor.error_prefix': 'Feil: ', + 'editor.open_in_hacs': 'Åpne i HACS', + 'week.enable.desc': 'Aktivera schema per veckodag. När det är av, används samma schema varje dag.', + 'editor.tabs.sync': 'Sync Engine', + 'sync.running': 'Synkronisering pågår…', + 'sync.completed': 'Synkronisering fullført', + 'pause.menu_title': 'Pause auto‑apply', + 'pause.off_1h': 'Pause i 1 time', + 'pause.off_5h': 'Pause i 5 timer', + 'pause.off_custom': 'Egendefinert varighet…', + 'pause.off_indef': 'Pause til jeg gjenopptar', + 'pause.resume': 'Gjenoppta nå', + 'pause.close': 'Lukk', + 'pause.active_until': 'Pauset til {time}', + 'pause.active_indef': 'Pauset', + 'editor.pausebtn.title': 'Pauseknapp', + 'editor.pausebtn.desc': 'Vis en global pauseknapp i kortets header. Når pause er aktiv, undertrykkes alle set_temperature‑kommandoer.', + 'editor.tabs.sync': 'Synk‑motor', + 'editor.tabs.backup': 'Backup', + 'backup.title': 'Backup', + 'backup.desc': 'Kopier innholdet i synk‑sensoren til backup‑sensoren og gjenopprett ved behov.', + 'backup.now': 'Ta backup nå', + 'backup.restore': 'Gjenopprett', + 'backup.delete': 'Slett backup', + 'backup.delete.confirm': 'Slett valgt backup?', + 'backup.new': 'Ny backup', + 'backup.auto': 'Automatisk backup', + 'backup.auto.desc': 'Lag automatiske sikkerhetskopier med intervaller.', + 'backup.interval': 'Intervall (dager)', + 'backup.interval.desc': 'Antall dager mellom automatiske sikkerhetskopier (min. 1).', + 'backup.sensor': 'Backup‑sensor', + 'backup.last': 'Siste backup', + 'backup.slot.title': 'Velg sikkerhetskopi', + 'backup.slot.desc': 'Velg hvilken backup-slot som skal gjenopprettes.', + 'backup.slot.empty': 'Ingen sikkerhetskopier ennå', + 'editor.integration.connected': 'Integrasjon klar', + 'editor.integration.disconnected': 'Integrasjon utilgjengelig' + , 'profiles.button': 'Manuelt skjema' + , 'profiles.editor.title': 'Profiler' + , 'profiles.add': 'Legg til profil' + , 'profiles.rename': 'Gi nytt navn' + , 'profiles.delete': 'Slett' + , 'profiles.activate': 'Aktiver' + , 'profiles.deactivate': 'Deaktiver' + , 'profiles.active': 'Aktiv' + , 'profiles.none': 'Ingen' + , 'profiles.name_placeholder': 'Profilnavn' + , 'profiles.edit': 'Rediger' + , 'profiles.preview_note': 'Kun forhåndsvisning. Skjemaer blir aktive når profilen aktiveres.' + , 'editor.tabs.holidays': 'Helligdager' + , 'holidays.enable': 'Aktiver helligdager' + , 'holidays.desc': 'Bruk en egen dagsplan på helligdager.' + , 'holidays.source': 'Kilde for helligdager' + , 'holidays.source.calendar': 'Kalender' + , 'holidays.source.manual': 'Manuelle datoer' + , 'holidays.entity': 'Helligdags‑entitet' + , 'holidays.entity_desc': 'Velg en kalender som kun gir on/off. Den skal være on på helligdager og off ellers.' + , 'holidays.dates': 'Helligdagsdatoer' + , 'holidays.add_date': 'Legg til dato' + , 'holidays.editor.title': 'Helligdags‑skjema' + , 'holidays.edit': 'Rediger helligdags‑skjema' + , 'holidays.button': 'Helligdager' + , 'editor.perroom.title': 'Standard °C per rom' + , 'editor.perroom.desc': 'Vis et Standard °C‑felt på hver rad. Når av, brukes global Standard °C for alle rom.' + , 'away.resume_schedule': 'Gjenoppta skjema' + , 'away.resume_away': 'Gjenoppta borte‑temperatur' + , 'presence.button': 'Tilstede‑skjemaer' + , 'presence.editor.title': 'Tilstede‑skjemaer' + , 'presence.enable_advanced': 'Aktiver avanserte tilstede‑skjemaer' + , 'presence.enable_advanced.desc': 'Bruk kombinasjoner og skjemaer basert på hvem som er hjemme/borte.' + , 'away.delay_enable': 'Forsink borte‑handlinger' + , 'away.delay_desc': 'Vent før borte‑endringer brukes.' + , 'away.delay_value': 'Forsinkelse' + , 'away.delay_unit': 'Enhet' + , 'away.delay_unit.seconds': 'Sekunder' + , 'away.delay_unit.minutes': 'Minutter' + , 'presence.combos': 'Kombinasjoner' + , 'presence.enable_combo': 'Aktiver denne kombinasjonen' + , 'presence.all_home': 'Alle hjemme' + , 'presence.none_home': 'Ingen hjemme' + , 'presence.home': 'Hjemme: {names}' + , 'presence.away': 'Borte: {names}' + , 'presence.manage': 'Administrer kombinasjoner' + , 'presence.who_home': 'Hvem er hjemme' + , 'presence.who_away': 'Hvem er ikke hjemme' + , 'away.enable': 'Aktiver borte‑modus' + , 'away.desc': 'Når ingen av de valgte personene er hjemme, senkes termostatene automatisk til den angitte temperaturen.' + , 'away.temp': 'Borte‑temperatur' + , 'away.persons': 'Personer' + , 'away.add_person': 'Legg til person' + , 'backup.select': 'Velg hva som skal sikkerhetskopieres' + , 'backup.select.desc': 'Avmerkede elementer tas med i sikkerhetskopi/gjenoppretting' + , 'backup.opt.all': 'Alle' + , 'backup.opt.main': 'Hovedskjemaer' + , 'backup.opt.weekday': 'Ukedagsskjemaer' + , 'backup.opt.presence': 'Tilstede‑skjemaer' + , 'backup.opt.settings': 'Redigeringsinnstillinger' + , 'backup.opt.holiday': 'Helligdags‑skjemaer' + , 'backup.opt.colors': 'Fargeblokker' + , 'editor.tabs.settings': 'Innstillinger' + , 'editor.tabs.pause': 'Pause' + , 'editor.tabs.weekdays': 'Ukedager' + , 'editor.tabs.colors': 'Farger' + , 'editor.tabs.presence_sensor': 'Tilstedeværelsessensor' + , 'editor.presence_sensor.enable.title': 'Tilstedeværelsessensor' + , 'editor.presence_sensor.enable.desc': 'Aktiver konfigurasjon av tilstedeværelsessensor per rom.' + , 'editor.presence_sensor.entity.title': 'Tilstedeværelses‑entitet' + , 'editor.presence_sensor.entity.desc': 'Velg binary_sensor som indikerer tilstedeværelse i dette rommet.' + , 'editor.presence_sensor.on_temp.title': 'Temperatur når tilstedeværelse er PÅ' + , 'editor.presence_sensor.on_temp.desc': 'Hvis sensoren er PÅ, bruker rommet denne temperaturen i stedet for skjemaet.' + , 'editor.presence_sensor.on_delay.title': 'Forsinkelse før tilstedeværelse brukes' + , 'editor.presence_sensor.on_delay.desc': 'Hvor lenge sensoren må være PÅ før overstyringstemperaturen brukes. Verdien bruker valgt enhet.' + , 'editor.presence_sensor.off_delay.title': 'Forsinkelse før skjema gjenopptas' + , 'editor.presence_sensor.off_delay.desc': 'Hvor lenge sensoren må være AV før skjemaet gjenopptas. Verdien bruker valgt enhet.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Borte fra hjemmet' + , 'editor.tabs.reset': 'Tilbakestill' + , 'editor.tabs.boiler': 'Kjelstyring' + , 'boiler.switch': 'Kjelbryter' + , 'boiler.switch_type': 'Enhetstype' + , 'boiler.switch_type.switch': 'Switch' + , 'boiler.switch_type.input_boolean': 'Input boolean' + , 'boiler.rooms': 'Rom som er med' + , 'boiler.rooms.desc': 'Velg hvilke rom som skal være med i kjelstyringen.' + , 'boiler.enable': 'Aktiver kjelstyring' + , 'boiler.enable.desc': 'Vis kjelinnstillinger i denne kort‑editoren.' + , 'boiler.temp_sensor': 'Kjelens temperatursensor' + , 'boiler.temp_sensor.desc': 'Temperatursensor som styrer kjelbryteren.' + , 'boiler.min_temp': 'Min kjeltemperatur (°C)' + , 'boiler.max_temp': 'Maks kjeltemperatur (°C)' + , 'boiler.offsets': 'Offset' + , 'boiler.offsets.desc': 'ON offset: hvor langt under planlagt temperatur før den slår PÅ. OFF offset: hvor langt over planlagt temperatur før den slår AV.' + , 'boiler.on_offset': 'ON offset (°C)' + , 'boiler.off_offset': 'OFF offset (°C)' + , 'editor.temp_sensor': 'Temperatursensor' + , 'presence.live_header': 'Vis live tilstedeværelse i headeren' + , 'presence.live_header.desc': 'Chips vises i høyre hjørne.' + , 'profiles.enable': 'Aktiver profiler (overstyring)' + , 'profiles.enable.desc': 'Tillat navngitte dagsskjemaer som kan aktiveres for å overstyre hovedplanen.' + , 'profiles.active_label': 'Manuell profil aktiv:' + , 'profiles.save_new': 'Lagre ny profil' + , 'profiles.save_existing': 'Lagre endringer i valgt profil' + , 'week.view.title': 'Ukevisning' + , 'week.view.rooms_one_day': 'Alle rom • én dag' + , 'week.view.days_one_room': 'Alle dager • ett rom' + , 'week.view_switch.title': 'Bytt visning fra tidslinjen' + , 'week.view_switch.desc': 'Vis en bryter i tidslinjens toppfelt for å bytte mellom de to ukedagsvisningene.' + , 'week.title': 'Ukedager' + , 'week.select_room': 'Velg rom' + , 'err.config_entities': "Konfig må ha 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Video ble ikke funnet' + , 'onboard.image_not_found': 'Bilde ble ikke funnet' + , 'pause.prompt_minutes': 'Angi varighet (minutter):' + , 'editor.test_tool': 'Testverktøy' + , 'editor.test_select_entity': 'Velg en entitet' + , 'editor.test_empty': 'Ingenting å eksportere' + , 'editor.test_export': 'Eksporter' + , 'editor.test_no_fields': 'Ingen felter å vise' + , 'editor.test_select_placeholder': 'Velg...' + , 'editor.error_prefix': 'Feil: ' + , 'backup.restore.confirm_overwrite': 'Gjenopprett fra sikkerhetskopi? Dette vil overskrive nåværende tidsplaner og innstillinger.' + , 'backup.import.err_no_hass': 'Home Assistant er ikke klar' + , 'backup.import.err_storage_off': 'Delt lagring er AV. Slå den på først.' + , 'backup.import.err_no_integration': 'Integrasjonen thermostat_timeline er ikke tilgjengelig' + , 'backup.import.err_json': 'Ugyldig JSON' + , 'profiles.name_exists': 'Navnet finnes allerede' + }, + de: { + 'card.title_default': 'Thermostat-Zeitlinie', + 'ui.edit_block': 'Block bearbeiten', + 'ui.add_block': 'Block hinzufügen', + 'ui.from': 'Von', + 'ui.to': 'Bis', + 'ui.temperature': 'Temperatur', + 'ui.delete': 'Löschen', + 'ui.cancel': 'Abbrechen', + 'ui.close': 'Schließen', + 'ui.unsaved_confirm': 'Es gibt ungespeicherte Änderungen. Trotzdem schließen?', + 'ui.discard_changes': 'Änderungen verwerfen', + 'ui.keep_editing': 'Weiter bearbeiten', + 'ui.save': 'Speichern', + 'ui.error_prefix': 'Fehler: ', + 'ui.overlap_msg': 'Sie haben sich überschneidende Zeiten ausgewählt — Überschneidung zwischen {start} – {end}.', + 'ui.auto_fix': 'Automatisch korrigieren', + 'ui.fix_start_to': 'Beginn auf {time} korrigieren', + 'ui.fix_end_to': 'Ende auf {time} korrigieren', + // Inline meta + 'ui.current_temp': 'Aktuelle Temperatur:', + + 'editor.title_label': 'Titel', + 'editor.add_entity': 'Raum hinzufügen', + 'editor.entity_placeholder': 'Entität auswählen', + 'editor.drag_reorder': 'Zum Neuordnen ziehen', + 'editor.remove': 'Entfernen', + 'editor.default_c': 'Standard °C', + 'editor.row_height_px': 'Zeilenhöhe (px)', + 'editor.max_c': 'Max °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Automatisch anwenden', + 'editor.auto_apply.desc': 'Wenn aktiviert, versucht die Karte, die Temperatur der ausgewählten climate-Entitäten automatisch so einzustellen, dass sie dem geplanten Wert für „jetzt“ entspricht.', + 'editor.apply_edit.title': 'Beim Bearbeiten', + 'editor.apply_edit.desc': 'Wenn Sie Blöcke der Zeitlinie ändern und die Änderung die aktuelle Zeit betrifft, wird die neue Temperatur sofort angewendet.', + 'editor.apply_default.title': 'Bei Änderung von Standard °C', + 'editor.apply_default.desc': 'Wenn „Standard °C“ geändert wird und dies den aktuellen Zeitraum betrifft, wird die neue Temperatur sofort angewendet.', + + 'editor.show_room_temp.title': 'Raumtemperatur anzeigen', + 'editor.show_room_temp.desc': 'Zeigt die Blase mit der aktuellen Raumtemperatur pro Zeile in der Kopfzeile der Zeitlinie an.', + 'editor.room_mode.title': 'Raum-Entitätstyp', + 'editor.room_mode.desc': 'Wenn aktiviert, wählen Sie eine input_number (Zieltemperatur). Wenn deaktiviert, wählen Sie eine climate-Entität.', + + 'editor.room_temp_sensor_override.title': 'Raumtemperatursensor verwenden', + 'editor.room_temp_sensor_override.desc': 'Wenn aktiviert, verwendet die angezeigte Raumtemperatur (und die Kessel-Logik) den ausgewählten Temperatursensor statt climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Temperatursensor', + 'ui.copy_plan': 'Plan kopieren', + 'ui.paste_plan': 'Plan einfügen', + 'ui.copy_day': 'Tag kopieren', + 'ui.paste_day': 'Tag einfügen', + 'editor.merge_label': 'Mit zusammenführen (zusätzlichen Thermostat hinzufügen)', + 'editor.display_name_optional': 'Raumname (optional)', + 'editor.merged_with': 'Zusammengeführt mit', + 'editor.store_enable.title': 'Geteilter Speicher + Hintergrundsteuerung', + 'editor.store_enable.short': 'Speichert Pläne im ausgewählten Sensor und aktiviert die Hintergrundsteuerung.', + 'editor.store_enable.desc': 'Speichert Pläne im ausgewählten Sensor und aktiviert set_temperature im Hintergrund. Thermostate werden auch aktualisiert, wenn die Karte geschlossen ist. Teilt zudem Zeitpläne (Heizblöcke) und Karten-Einstellungen zwischen Benutzern/Geräten. Wenn aus: Befehle werden nur gesendet, solange die Karte auf einem Gerät (Tablet/PC/Mobil) aktiv sichtbar ist und geöffnet bleibt.', + 'editor.store_missing': 'Die Integration „Thermostat Pro Timeline Sync“ ist nicht installiert. Geteilter Speicher ist nicht verfügbar.', + 'editor.clear_store': 'Speicher leeren', + 'editor.clear_store_confirm': 'Dadurch werden alle gespeicherten Zeitpläne gelöscht. Fortfahren?', + 'editor.migrate_to_store': 'Daten in Speicher übertragen', + 'editor.migrate_confirm': 'Ihre im Browser gespeicherten Pläne in den ausgewählten Sensor übertragen? Vorhandene gespeicherte Pläne werden überschrieben.', + 'editor.migrate_pull_confirm': 'Pläne aus dem Speicher importieren und lokale Browserdaten überschreiben?', + 'editor.select_storage_first': 'Bitte wähle zuerst einen Speichersensor aus.', + 'editor.no_local_data': 'Keine lokalen Browserdaten gefunden.', + 'editor.clear_all': 'Alle Daten löschen', + 'editor.clear_all_confirm': 'Dies löscht sowohl Speicher- als auch lokale Browser-Zeitpläne. Fortfahren?', + 'editor.clear_storage_only': 'Nur Speicherdaten löschen', + 'editor.clear_storage_only_confirm': 'Dies löscht Zeitpläne vom ausgewählten Speichersensor. Fortfahren?', + 'editor.clear_local_only': 'Nur lokale Daten löschen', + 'editor.clear_local_only_confirm': 'Dies löscht nur im Browser gespeicherte Zeitpläne. Fortfahren?', + 'editor.reset.desc': 'Was das macht:\n\n• Alle Daten löschen: Setzt den Backend-Speicher (.storage) für Thermostat Timeline zurück und löscht ALLE lokalen Browser-Caches/Entwürfe.\n• Nur lokale Daten löschen: Löscht nur Daten, die in diesem Browser gespeichert sind (localStorage).\n\nTipp: Nach einem vollständigen Reset die Seite neu laden, damit alte Entwürfe nicht wieder auftauchen.', + 'week.enable': 'Wochentage aktivieren', + 'week.mode': 'Wochentagsmodus', + 'week.mode.same_all': 'Jeden Tag gleich (1)', + 'week.mode.weekday_weekend': 'Werktage + Wochenende', + 'week.mode.weekday_sat_sun': 'Werktage + Samstag + Sonntag', + 'week.mode.all_7': 'Alle Tage', + 'week.button': 'Wochentage', + 'week.days.mon': 'Mo', + 'week.days.tue': 'Di', + 'week.days.wed': 'Mi', + 'week.days.thu': 'Do', + 'week.days.fri': 'Fr', + 'week.days.sat': 'Sa', + 'week.days.sun': 'So', + 'week.editor.title': 'Wochenschema', + 'week.editor.preview_note': 'Nur Vorschau. Änderungen werden erst beim Speichern angewendet.', + 'week.open_editor.desc': 'Wochentags-Editor öffnen.', + 'week.open_editor.btn': 'Öffnen', + 'week.copy_to_rooms': 'Wochenschema in Räume kopieren', + 'week.copy_to_rooms.title': 'Wochenschema in Räume kopieren', + 'week.copy_to_rooms.select': 'Räume auswählen', + 'week.copy_to_rooms.apply': 'In Räume einfügen', + 'week.group.weekdays': 'Werktage', + 'week.group.weekend': 'Wochenende', + 'week.days_full.mon': 'Montag', + 'week.days_full.tue': 'Dienstag', + 'week.days_full.wed': 'Mittwoch', + 'week.days_full.thu': 'Donnerstag', + 'week.days_full.fri': 'Freitag', + 'week.days_full.sat': 'Samstag', + 'week.days_full.sun': 'Sonntag', + 'editor.timefmt.title': 'Zeitformat', + 'editor.timefmt.desc': '24-Stunden- oder 12-Stunden-Anzeige (AM/PM) für Zeiten auf der Karte wählen.', + 'editor.tempfmt.title': 'Temperatureinheit', + 'editor.tempfmt.desc': 'Celsius (°C) oder Fahrenheit (°F) für Anzeige in Karte und Editoren wählen.', + 'editor.timesrc.title': 'Zeitquelle', + 'editor.timesrc.desc': 'Wählen, ob die Karte die Browser‑Zeit oder die Home‑Assistant‑Zeitzone für „jetzt“ verwendet.', + 'editor.timesrc.browser': 'Browser', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'Konfigurations-ID', + 'editor.instance.desc': 'Mehrere unabhängige Zeitpläne/Einstellungen unter verschiedenen IDs verwalten.', + 'editor.instance.new_id': 'Neue ID', + 'editor.heat_colors.title': 'Farben für Heizblöcke', + 'editor.heat_colors.add': 'Farbintervall hinzufügen', + 'editor.colors.col_from': 'Von °C', + 'editor.colors.col_to': 'Bis °C', + 'editor.colors.col_color': 'Farbe', + 'editor.colors.empty': 'Noch keine Räume hinzugefügt. Fügen Sie einen Raum hinzu, um Farben zu definieren.', + 'editor.colors.go_add': 'Raum hinzufügen', + 'editor.colors.refresh': 'Zeitplan aktualisieren', + 'editor.colors.clear_local': 'Lokale Farb-Daten löschen', + 'editor.colors.clear_local_confirm': 'Dadurch werden in Ihrem Browser für diese Karte gespeicherte Farbbereiche gelöscht. Fortfahren?', + 'editor.colors.clear_shared': 'Geteilten Farbspeicher löschen', + 'editor.colors.clear_shared_confirm': 'Dadurch werden Farbbereiche im ausgewählten Speichersensor gelöscht. Fortfahren?', + 'editor.colors.mode': 'Farbmodus', + 'editor.colors.mode.desc': 'Farben pro Raum oder ein Satz für alle Räume wählen.', + 'editor.colors.mode_per_room': 'Pro Raum', + 'editor.colors.mode_global': 'Alle Räume', + 'editor.colors.global_title': 'Alle Räume', + 'editor.colors.clear_all': 'Alle Farbdaten löschen', + 'editor.colors.clear_all_confirm': 'Dies löscht alle Farbbereiche sowohl lokal als auch im ausgewählten Speichersensor. Fortfahren?', + 'editor.reload_editor': 'Editor neu laden', + 'editor.reload_dashboard': 'Dashboard neu laden', + 'onboard.title': 'Willkommen bei Thermostat‑Zeitlinie', + 'onboard.next': 'Weiter', + 'onboard.prev': 'Zurück', + 'onboard.skip': 'Überspringen', + 'onboard.done': 'Fertig', + 'onboard.dont_show': 'Nicht erneut anzeigen', + 'onboard.step_main': 'Dies ist die Hauptzeitlinie. Sie zeigt Solltemperaturen über 24 Stunden für jeden Raum.', + 'onboard.step_add': 'Fügen Sie einen Heizblock über + Block hinzufügen oder per Doppelklick hinzu.', + 'onboard.step_edit': 'Bearbeiten/Löschen per Doppelklick. Verwenden Sie Wochentage für verschiedene Tage.', + 'onboard.reset': 'Anleitung erneut anzeigen', + 'onboard.reset_ok': 'Willkommen wird beim nächsten Mal angezeigt.', + 'editor.select_entities': 'Räume', + 'editor.tabs.rooms': 'Räume', + 'editor.test_tool': 'Testwerkzeug', + 'editor.test_select_entity': 'Entität auswählen', + 'editor.test_empty': 'Wählen Sie eine Entität, um verfügbare Befehle anzuzeigen…', + 'editor.test_export': 'Exportieren .md', + 'editor.test_no_fields': 'Keine Felder', + 'editor.test_select_placeholder': '— Auswählen —', + 'editor.error_prefix': 'Fehler: ', + 'editor.open_in_hacs': 'In HACS öffnen', + 'week.enable.desc': 'Pro-Wochentag-Plan aktivieren. Wenn aus, wird jeden Tag derselbe Plan verwendet.', + 'editor.tabs.sync': 'Sync‑Engine', + 'sync.running': 'Synchronisierung läuft…', + 'sync.completed': 'Synchronisierung abgeschlossen', + 'editor.tabs.backup': 'Backup', + 'backup.title': 'Backup', + 'backup.desc': 'Kopiere den Inhalt deines Sync‑Sensors in den Backup‑Sensor und bei Bedarf wiederherstellen.', + 'backup.now': 'Jetzt sichern', + 'backup.restore': 'Wiederherstellen', + 'backup.delete': 'Backup löschen', + 'backup.delete.confirm': 'Ausgewähltes Backup löschen?', + 'backup.new': 'Neues Backup', + 'backup.auto': 'Automatisches Backup', + 'backup.auto.desc': 'Automatische Sicherungen in festen Intervallen erstellen.', + 'backup.interval': 'Intervall (Tage)', + 'backup.interval.desc': 'Anzahl der Tage zwischen automatischen Sicherungen (min. 1).', + 'backup.sensor': 'Backup‑Sensor', + 'backup.last': 'Letztes Backup', + 'backup.slot.title': 'Backup auswählen', + 'backup.slot.desc': 'Wählen Sie, welcher Backup-Slot wiederhergestellt werden soll.', + 'backup.slot.empty': 'Noch keine Backups', + 'pause.menu_title': 'Auto‑Anwendung pausieren', + 'pause.off_1h': '1 Stunde pausieren', + 'pause.off_5h': '5 Stunden pausieren', + 'pause.off_custom': 'Benutzerdefinierte Dauer…', + 'pause.off_indef': 'Pausieren bis ich fortsetze', + 'pause.resume': 'Jetzt fortsetzen', + 'pause.close': 'Schließen', + 'pause.active_until': 'Pausiert bis {time}', + 'pause.active_indef': 'Pausiert', + 'editor.pausebtn.title': 'Pause‑Schaltfläche', + 'editor.pausebtn.desc': 'Zeigt eine globale Pause‑Schaltfläche in der Kopfzeile. Während der Pause werden alle set_temperature‑Befehle unterdrückt.', + 'editor.integration.connected': 'Integration bereit', + 'editor.integration.disconnected': 'Integration nicht verfügbar' + , 'profiles.button': 'Manueller Zeitplan' + , 'profiles.editor.title': 'Profile' + , 'profiles.add': 'Profil hinzufügen' + , 'profiles.rename': 'Umbenennen' + , 'profiles.delete': 'Löschen' + , 'profiles.activate': 'Aktivieren' + , 'profiles.deactivate': 'Deaktivieren' + , 'profiles.active': 'Aktiv' + , 'profiles.none': 'Keins' + , 'profiles.name_placeholder': 'Profilname' + , 'profiles.edit': 'Bearbeiten' + , 'profiles.preview_note': 'Nur Vorschau. Pläne werden aktiv, wenn Sie das Profil aktivieren.' + , 'editor.tabs.holidays': 'Feiertage' + , 'holidays.enable': 'Feiertage aktivieren' + , 'holidays.desc': 'An Feiertagen einen speziellen Tagesplan verwenden.' + , 'holidays.source': 'Feiertagsquelle' + , 'holidays.source.calendar': 'Kalender' + , 'holidays.source.manual': 'Manuelle Daten' + , 'holidays.entity': 'Feiertags‑Entität' + , 'holidays.entity_desc': 'Wähle einen Kalender, der nur Ein/Aus meldet. An Feiertagen sollte er „on“ sein, an anderen Tagen „off“.' + , 'holidays.dates': 'Feiertagsdaten' + , 'holidays.add_date': 'Datum hinzufügen' + , 'holidays.editor.title': 'Feiertags‑Zeitplan' + , 'holidays.edit': 'Feiertags‑Zeitplan bearbeiten' + , 'holidays.button': 'Feiertage' + , 'editor.perroom.title': 'Standard‑°C pro Raum' + , 'editor.perroom.desc': 'Zeigt ein Standard‑°C‑Feld in jeder Zeile. Wenn aus, wird die globale Standard‑°C für alle Räume verwendet.' + , 'away.resume_schedule': 'Zeitplan fortsetzen' + , 'away.resume_away': 'Abwesenheits‑Temperatur fortsetzen' + , 'presence.button': 'Anwesenheitspläne' + , 'presence.editor.title': 'Anwesenheitspläne' + , 'presence.enable_advanced': 'Erweiterte Anwesenheitspläne aktivieren' + , 'presence.enable_advanced.desc': 'Verwende Kombinationen und Zeitpläne basierend darauf, wer zu Hause/abwesend ist.' + , 'away.delay_enable': 'Abwesenheits‑Aktionen verzögern' + , 'away.delay_desc': 'Vor Anwendung der Abwesenheitsänderungen warten.' + , 'away.delay_value': 'Verzögerung' + , 'away.delay_unit': 'Einheit' + , 'away.delay_unit.seconds': 'Sekunden' + , 'away.delay_unit.minutes': 'Minuten' + , 'presence.combos': 'Kombinationen' + , 'presence.enable_combo': 'Diese Kombination aktivieren' + , 'presence.all_home': 'Alle zu Hause' + , 'presence.none_home': 'Niemand zu Hause' + , 'presence.home': 'Zu Hause: {names}' + , 'presence.away': 'Abwesend: {names}' + , 'presence.manage': 'Kombinationen verwalten' + , 'presence.who_home': 'Wer ist zu Hause' + , 'presence.who_away': 'Wer ist nicht zu Hause' + , 'away.enable': 'Abwesenheitsmodus aktivieren' + , 'away.desc': 'Wenn keine der ausgewählten Personen zu Hause ist, werden die Thermostate automatisch auf die eingestellte Temperatur abgesenkt.' + , 'away.temp': 'Abwesenheits‑Temperatur' + , 'away.persons': 'Personen' + , 'away.add_person': 'Person hinzufügen' + , 'backup.select': 'Auswählen, was gesichert wird' + , 'backup.select.desc': 'Markierte Elemente werden in Sicherung/Wiederherstellung einbezogen.' + , 'backup.opt.all': 'Alle' + , 'backup.opt.main': 'Hauptzeitpläne' + , 'backup.opt.weekday': 'Wochentags‑Zeitpläne' + , 'backup.opt.presence': 'Anwesenheitspläne' + , 'backup.opt.settings': 'Editor‑Einstellungen' + , 'backup.opt.holiday': 'Feiertags‑Zeitpläne' + , 'backup.opt.colors': 'Farbblöcke' + , 'editor.tabs.settings': 'Einstellungen' + , 'editor.tabs.pause': 'Pause' + , 'editor.tabs.weekdays': 'Wochentage' + , 'editor.tabs.colors': 'Farben' + , 'editor.tabs.presence_sensor': 'Anwesenheitssensor' + , 'editor.presence_sensor.enable.title': 'Anwesenheitssensor' + , 'editor.presence_sensor.enable.desc': 'Aktiviert die Anwesenheitssensor‑Konfiguration pro Raum.' + , 'editor.presence_sensor.entity.title': 'Anwesenheitsentität' + , 'editor.presence_sensor.entity.desc': 'Wähle den binary_sensor, der Anwesenheit in diesem Raum anzeigt.' + , 'editor.presence_sensor.on_temp.title': 'Temperatur bei Anwesenheit (EIN)' + , 'editor.presence_sensor.on_temp.desc': 'Wenn der Sensor EIN ist, nutzt dieser Raum diese Temperatur statt dem Zeitplan.' + , 'editor.presence_sensor.on_delay.title': 'Verzögerung bis Anwesenheit gilt' + , 'editor.presence_sensor.on_delay.desc': 'Wie lange der Sensor EIN sein muss, bevor die Override-Temperatur gilt. Wert verwendet die gewählte Einheit.' + , 'editor.presence_sensor.off_delay.title': 'Verzögerung bis Zeitplan fortgesetzt wird' + , 'editor.presence_sensor.off_delay.desc': 'Wie lange der Sensor AUS sein muss, bevor der Zeitplan fortgesetzt wird. Wert verwendet die gewählte Einheit.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Abwesend von zu Hause' + , 'editor.tabs.reset': 'Zurücksetzen' + , 'editor.tabs.boiler': 'Kesselsteuerung' + , 'boiler.switch': 'Kesselschalter' + , 'boiler.switch_type': 'Entitätstyp' + , 'boiler.switch_type.switch': 'Schalter' + , 'boiler.switch_type.input_boolean': 'Input Boolean' + , 'boiler.rooms': 'Einbezogene Räume' + , 'boiler.rooms.desc': 'Wähle, welche Räume in der Kesselsteuerung berücksichtigt werden.' + , 'boiler.enable': 'Kesselsteuerung aktivieren' + , 'boiler.enable.desc': 'Kesseleinstellungen in diesem Karten‑Editor anzeigen.' + , 'boiler.temp_sensor': 'Kessel‑Temperatursensor' + , 'boiler.temp_sensor.desc': 'Temperatursensor zur Steuerung des Kesselschalters.' + , 'boiler.min_temp': 'Min. Kesseltemperatur (°C)' + , 'boiler.max_temp': 'Max. Kesseltemperatur (°C)' + , 'boiler.offsets': 'Offsets' + , 'boiler.offsets.desc': 'ON‑Offset: wie weit unter dem Zeitplan‑Sollwert bevor EIN geschaltet wird. OFF‑Offset: wie weit über dem Zeitplan‑Sollwert bevor AUS geschaltet wird.' + , 'boiler.on_offset': 'ON‑Offset (°C)' + , 'boiler.off_offset': 'OFF‑Offset (°C)' + , 'editor.temp_sensor': 'Temperatursensor' + , 'presence.live_header': 'Live‑Anwesenheit in Kopfzeile anzeigen' + , 'presence.live_header.desc': 'Chips werden in der rechten Ecke angezeigt.' + , 'profiles.enable': 'Profile aktivieren (Überschreiben)' + , 'profiles.enable.desc': 'Benannte Tagespläne erlauben, die den Hauptplan bei Aktivierung überschreiben.' + , 'profiles.active_label': 'Manuelles Profil aktiv:' + , 'profiles.save_new': 'Neues Profil speichern' + , 'profiles.save_existing': 'Änderungen im ausgewählten Profil speichern' + , 'week.view.title': 'Wochenansicht' + , 'week.view.rooms_one_day': 'Alle Räume • ein Tag' + , 'week.view.days_one_room': 'Alle Tage • ein Raum' + , 'week.view_switch.title': 'Ansicht in der Zeitleiste wechseln' + , 'week.view_switch.desc': 'Zeigt einen Umschalter in der Kopfzeile der Zeitleiste, um zwischen den beiden Wochentagsansichten zu wechseln.' + , 'week.title': 'Wochentage' + , 'week.select_room': 'Raum auswählen' + , 'err.config_entities': "Die Konfiguration muss 'entities: [climate.xxx, ...]' enthalten." + , 'onboard.video_not_found': 'Video nicht gefunden' + , 'onboard.image_not_found': 'Bild nicht gefunden' + , 'pause.prompt_minutes': 'Dauer eingeben (Minuten):' + , 'editor.test_tool': 'Testwerkzeug' + , 'editor.test_select_entity': 'Entität auswählen' + , 'editor.test_empty': 'Nichts zu exportieren' + , 'editor.test_export': 'Exportieren' + , 'editor.test_no_fields': 'Keine Felder zum Anzeigen' + , 'editor.test_select_placeholder': 'Auswählen...' + , 'editor.error_prefix': 'Fehler: ' + , 'backup.restore.confirm_overwrite': 'Aus Backup wiederherstellen? Dadurch werden aktuelle Zeitpläne und Einstellungen überschrieben.' + , 'backup.import.err_no_hass': 'Home Assistant ist nicht bereit' + , 'backup.import.err_storage_off': 'Gemeinsamer Speicher ist AUS. Bitte zuerst aktivieren.' + , 'backup.import.err_no_integration': 'Integration thermostat_timeline ist nicht verfügbar' + , 'backup.import.err_json': 'Ungültiges JSON' + , 'profiles.name_exists': 'Name existiert bereits' + }, + es: { + 'card.title_default': 'Línea de tiempo del termostato', + 'ui.edit_block': 'Editar bloque', + 'ui.add_block': 'Agregar bloque', + 'ui.from': 'Desde', + 'ui.to': 'Hasta', + 'ui.temperature': 'Temperatura', + 'ui.delete': 'Eliminar', + 'ui.cancel': 'Cancelar', + 'ui.close': 'Cerrar', + 'ui.unsaved_confirm': 'Tienes cambios sin guardar. ¿Cerrar igualmente?', + 'ui.discard_changes': 'Descartar cambios', + 'ui.keep_editing': 'Seguir editando', + 'ui.save': 'Guardar', + 'ui.error_prefix': 'Error: ', + 'ui.overlap_msg': 'Has seleccionado horas superpuestas — se solapa durante {start} – {end}.', + 'ui.auto_fix': 'Corregir automáticamente', + 'ui.fix_start_to': 'Ajustar inicio a {time}', + 'ui.fix_end_to': 'Ajustar fin a {time}', + // Inline meta + 'ui.current_temp': 'Temperatura actual:', + + 'editor.title_label': 'Título', + 'editor.add_entity': 'Añadir habitación', + 'editor.entity_placeholder': 'Selecciona una entidad', + 'editor.drag_reorder': 'Arrastrar para reordenar', + 'editor.remove': 'Eliminar', + 'editor.default_c': 'Predeterminado °C', + 'editor.row_height_px': 'Altura de fila (px)', + 'editor.max_c': 'Máx °C', + 'editor.min_c': 'Mín °C', + 'editor.auto_apply.title': 'Aplicar automáticamente', + 'editor.auto_apply.desc': 'Cuando está activado, la tarjeta intentará establecer la temperatura en las entidades climate seleccionadas para que coincida con el valor planificado para “ahora mismo”.', + 'editor.apply_edit.title': 'Al editar', + 'editor.apply_edit.desc': 'Cuando cambias bloques de la línea de tiempo y el cambio afecta a la hora actual, la nueva temperatura se aplica inmediatamente.', + 'editor.apply_default.title': 'Al cambiar Predeterminado °C', + 'editor.apply_default.desc': 'Cuando “Predeterminado °C” cambia y afecta al período actual, la nueva temperatura se aplica inmediatamente.', + + 'editor.show_room_temp.title': 'Mostrar temperatura de la habitación', + 'editor.show_room_temp.desc': 'Muestra la burbuja de temperatura actual de la habitación en cada fila del encabezado de la línea de tiempo.', + 'editor.room_mode.title': 'Tipo de entidad de habitación', + 'editor.room_mode.desc': 'Cuando está activado, selecciona un input_number (temperatura objetivo). Cuando está desactivado, selecciona una entidad climate.', + + 'editor.room_temp_sensor_override.title': 'Usar sensor de temperatura de la habitación', + 'editor.room_temp_sensor_override.desc': 'Cuando está activado, la temperatura mostrada de la habitación (y la lógica de la caldera) usa el sensor de temperatura seleccionado en lugar de climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Sensor de temperatura', + 'ui.copy_plan': 'Copiar plan', + 'ui.paste_plan': 'Pegar plan', + 'ui.copy_day': 'Copiar día', + 'ui.paste_day': 'Pegar día', + 'editor.merge_label': 'Combinar con (añadir termostato extra)', + 'editor.display_name_optional': 'Nombre de la habitación (opcional)', + 'editor.merged_with': 'Combinado con', + 'editor.store_enable.title': 'Almacenamiento compartido + control en segundo plano', + 'editor.store_enable.short': 'Guarda horarios en el sensor seleccionado y habilita el control en segundo plano.', + 'editor.store_enable.desc': 'Guarda horarios en el sensor seleccionado y habilita set_temperature en segundo plano. Los termostatos se actualizan incluso con la tarjeta cerrada. Además comparte horarios (bloques de calor) y la configuración de la tarjeta entre usuarios/dispositivos. Si está desactivado: los comandos se envían solo mientras la tarjeta está visible en un dispositivo (tableta/PC/móvil) y debe permanecer abierta.', + 'editor.store_missing': 'La integración "Thermostat Pro Timeline Sync" no está instalada. El almacenamiento compartido no está disponible.', + 'editor.clear_store': 'Vaciar almacenamiento', + 'editor.clear_store_confirm': 'Esto eliminará todos los horarios guardados. ¿Continuar?', + 'editor.migrate_to_store': 'Transferir datos al almacenamiento', + 'editor.migrate_confirm': '¿Transferir los horarios guardados en tu navegador al sensor seleccionado? Esto sobrescribirá los horarios existentes en el almacenamiento.', + 'editor.migrate_pull_confirm': '¿Importar horarios desde el almacenamiento y sobrescribir los datos locales del navegador?', + 'editor.select_storage_first': 'Selecciona primero un sensor de almacenamiento.', + 'editor.no_local_data': 'No se encontraron datos locales del navegador.', + 'editor.clear_all': 'Borrar todos los datos', + 'editor.clear_all_confirm': 'Esto eliminará los horarios tanto del almacenamiento como del navegador local. ¿Continuar?', + 'editor.clear_storage_only': 'Borrar solo datos de almacenamiento', + 'editor.clear_storage_only_confirm': 'Esto eliminará los horarios del sensor de almacenamiento seleccionado. ¿Continuar?', + 'editor.clear_local_only': 'Borrar solo datos locales', + 'editor.clear_local_only_confirm': 'Esto eliminará solo los horarios guardados en tu navegador. ¿Continuar?', + 'editor.reset.desc': 'Qué hace esto:\n\n• Borrar todos los datos: Restablece el almacenamiento del backend (.storage) de Thermostat Timeline y borra TODA la caché/borradores locales del navegador.\n• Borrar solo datos locales: Borra solo los datos guardados en este navegador (localStorage).\n\nConsejo: Después de un reset completo, recarga la página para evitar que reaparezcan borradores antiguos.', + 'week.enable': 'Habilitar días de la semana', + 'week.mode': 'Modo por días', + 'week.mode.same_all': 'Igual todos los días (1)', + 'week.mode.weekday_weekend': 'Laborables + Fin de semana', + 'week.mode.weekday_sat_sun': 'Laborables + Sábado + Domingo', + 'week.mode.all_7': 'Todos los días', + 'week.button': 'Días de la semana', + 'week.days.mon': 'Lun', + 'week.days.tue': 'Mar', + 'week.days.wed': 'Mié', + 'week.days.thu': 'Jue', + 'week.days.fri': 'Vie', + 'week.days.sat': 'Sáb', + 'week.days.sun': 'Dom', + 'week.editor.title': 'Programación por días', + 'week.editor.preview_note': 'Solo vista previa. Los cambios se aplican al pulsar Guardar.', + 'week.open_editor.desc': 'Abrir el editor de días de la semana.', + 'week.open_editor.btn': 'Abrir', + 'week.copy_to_rooms': 'Copiar semana a habitaciones', + 'week.copy_to_rooms.title': 'Copiar semana a habitaciones', + 'week.copy_to_rooms.select': 'Seleccionar habitaciones', + 'week.copy_to_rooms.apply': 'Insertar en habitaciones', + 'week.group.weekdays': 'Laborables', + 'week.group.weekend': 'Fin de semana', + 'week.days_full.mon': 'Lunes', + 'week.days_full.tue': 'Martes', + 'week.days_full.wed': 'Miércoles', + 'week.days_full.thu': 'Jueves', + 'week.days_full.fri': 'Viernes', + 'week.days_full.sat': 'Sábado', + 'week.days_full.sun': 'Domingo', + 'editor.timefmt.title': 'Formato de hora', + 'editor.timefmt.desc': 'Elige visualización de 24 horas o 12 horas (AM/PM) para la tarjeta.', + 'editor.tempfmt.title': 'Unidad de temperatura', + 'editor.tempfmt.desc': 'Elige Celsius (°C) o Fahrenheit (°F) para mostrar en la tarjeta y editores.', + 'editor.timesrc.title': 'Fuente de tiempo', + 'editor.timesrc.desc': 'Elige si la tarjeta usa la hora del navegador o la zona horaria de Home Assistant para “ahora”.', + 'editor.timesrc.browser': 'Navegador', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'ID de configuración', + 'editor.instance.desc': 'Mantén varios horarios/ajustes independientes bajo IDs diferentes.', + 'editor.instance.new_id': 'Nuevo ID', + 'editor.heat_colors.title': 'Colores de bloques de calor', + 'editor.heat_colors.add': 'Añadir intervalo de color', + 'editor.colors.col_from': 'Desde °C', + 'editor.colors.col_to': 'Hasta °C', + 'editor.colors.col_color': 'Color', + 'editor.colors.empty': 'Aún no se han añadido habitaciones. Añade una para definir colores.', + 'editor.colors.go_add': 'Añadir habitación', + 'editor.colors.refresh': 'Actualizar programación', + 'editor.colors.clear_local': 'Borrar datos de color locales', + 'editor.colors.clear_local_confirm': 'Esto eliminará los intervalos de color guardados en tu navegador para esta tarjeta. ¿Continuar?', + 'editor.colors.clear_shared': 'Borrar almacenamiento de color compartido', + 'editor.colors.clear_shared_confirm': 'Esto eliminará los intervalos de color en el sensor de almacenamiento seleccionado. ¿Continuar?', + 'editor.colors.mode': 'Modo de color', + 'editor.colors.mode.desc': 'Elige colores por habitación o un conjunto para todas.', + 'editor.colors.mode_per_room': 'Por habitación', + 'editor.colors.mode_global': 'Todas las habitaciones', + 'editor.colors.global_title': 'Todas las habitaciones', + 'editor.colors.clear_all': 'Borrar todos los datos de color', + 'editor.colors.clear_all_confirm': 'Esto borrará todos los intervalos de color tanto locales como del sensor de almacenamiento seleccionado. ¿Continuar?', + 'editor.reload_editor': 'Recargar editor', + 'editor.reload_dashboard': 'Recargar panel', + 'onboard.title': 'Bienvenido a la línea de tiempo del termostato', + 'onboard.next': 'Siguiente', + 'onboard.prev': 'Atrás', + 'onboard.skip': 'Omitir', + 'onboard.done': 'Listo', + 'onboard.dont_show': 'No mostrar de nuevo', + 'onboard.step_main': 'Esta es la línea principal. Muestra las temperaturas deseadas a lo largo del día por habitación.', + 'onboard.step_add': 'Añade un bloque con + Agregar bloque o con doble clic en la línea de tiempo.', + 'onboard.step_edit': 'Edita o elimina un bloque con doble clic. Usa días de la semana para distintos días.', + 'onboard.reset': 'Mostrar la guía otra vez', + 'onboard.reset_ok': 'La bienvenida se mostrará la próxima vez.', + 'editor.select_entities': 'Habitaciones', + 'editor.tabs.rooms': 'Habitaciones', + 'editor.test_tool': 'Herramienta de prueba', + 'editor.test_select_entity': 'Seleccionar entidad', + 'editor.test_empty': 'Seleccione una entidad para ver los comandos disponibles…', + 'editor.test_export': 'Exportar .md', + 'editor.test_no_fields': 'Sin campos', + 'editor.test_select_placeholder': '— Seleccionar —', + 'editor.error_prefix': 'Error: ', + 'editor.open_in_hacs': 'Abrir en HACS', + 'week.enable.desc': 'Habilitar programación por día. Si está desactivado, se usa el mismo plan todos los días.', + 'editor.tabs.sync': 'Motor de sincronización', + 'sync.running': 'Sincronización en curso…', + 'sync.completed': 'Sincronización completada', + 'editor.tabs.backup': 'Copia de seguridad', + 'backup.title': 'Copia de seguridad', + 'backup.desc': 'Copia el contenido de tu sensor de sincronización al sensor de copia de seguridad y restaura cuando sea necesario.', + 'backup.now': 'Hacer copia ahora', + 'backup.restore': 'Restaurar', + 'backup.delete': 'Eliminar copia de seguridad', + 'backup.delete.confirm': '¿Eliminar la copia seleccionada?', + 'backup.new': 'Nueva copia de seguridad', + 'backup.auto': 'Copia de seguridad automática', + 'backup.auto.desc': 'Realiza copias de seguridad automáticas a intervalos.', + 'backup.interval': 'Intervalo (días)', + 'backup.interval.desc': 'Cuántos días entre copias de seguridad automáticas (mín. 1).', + 'backup.sensor': 'Sensor de copia de seguridad', + 'backup.last': 'Última copia de seguridad', + 'backup.slot.title': 'Seleccionar copia de seguridad', + 'backup.slot.desc': 'Selecciona qué ranura de copia de seguridad restaurar.', + 'backup.slot.empty': 'Aún no hay copias de seguridad', + 'pause.menu_title': 'Pausar auto‑aplicación', + 'pause.off_1h': 'Pausar 1 hora', + 'pause.off_5h': 'Pausar 5 horas', + 'pause.off_custom': 'Duración personalizada…', + 'pause.off_indef': 'Pausar hasta reanudar', + 'pause.resume': 'Reanudar ahora', + 'pause.close': 'Cerrar', + 'pause.active_until': 'Pausado hasta {time}', + 'pause.active_indef': 'Pausado', + 'editor.pausebtn.title': 'Botón de pausa', + 'editor.pausebtn.desc': 'Muestra un botón de pausa global en la cabecera. Mientras está pausado, se suprimen todos los comandos set_temperature.', + 'editor.integration.connected': 'Integración lista', + 'editor.integration.disconnected': 'Integración no disponible' + , 'profiles.button': 'Horario manual' + , 'profiles.editor.title': 'Perfiles' + , 'profiles.add': 'Agregar perfil' + , 'profiles.rename': 'Renombrar' + , 'profiles.delete': 'Eliminar' + , 'profiles.activate': 'Activar' + , 'profiles.deactivate': 'Desactivar' + , 'profiles.active': 'Activo' + , 'profiles.none': 'Ninguno' + , 'profiles.name_placeholder': 'Nombre del perfil' + , 'profiles.edit': 'Editar' + , 'profiles.preview_note': 'Solo vista previa. Los horarios se activan al activar el perfil.' + , 'editor.tabs.holidays': 'Festivos' + , 'holidays.enable': 'Habilitar festivos' + , 'holidays.desc': 'Usa un horario especial en días festivos.' + , 'holidays.source': 'Fuente de festivos' + , 'holidays.source.calendar': 'Calendario' + , 'holidays.source.manual': 'Fechas manuales' + , 'holidays.entity': 'Entidad de festivos' + , 'holidays.entity_desc': 'Selecciona un calendario que solo informe on/off. Debe estar en on en festivos y en off el resto de días.' + , 'holidays.dates': 'Fechas de festivos' + , 'holidays.add_date': 'Agregar fecha' + , 'holidays.editor.title': 'Horario de festivos' + , 'holidays.edit': 'Editar horario de festivos' + , 'holidays.button': 'Festivos' + , 'editor.perroom.title': '°C predeterminado por habitación' + , 'editor.perroom.desc': 'Muestra un campo de °C predeterminado en cada fila. Si está desactivado, se usa el °C predeterminado global para todas las habitaciones.' + , 'away.resume_schedule': 'Reanudar horario' + , 'away.resume_away': 'Reanudar temperatura de ausencia' + , 'presence.button': 'Horarios de presencia' + , 'presence.editor.title': 'Horarios de presencia' + , 'presence.enable_advanced': 'Habilitar horarios avanzados de presencia' + , 'presence.enable_advanced.desc': 'Usa combinaciones y horarios según quién esté en casa/ausente.' + , 'away.delay_enable': 'Retrasar acciones de ausencia' + , 'away.delay_desc': 'Espera antes de aplicar cambios de ausencia.' + , 'away.delay_value': 'Retraso' + , 'away.delay_unit': 'Unidad' + , 'away.delay_unit.seconds': 'Segundos' + , 'away.delay_unit.minutes': 'Minutos' + , 'presence.combos': 'Combinaciones' + , 'presence.enable_combo': 'Habilitar esta combinación' + , 'presence.all_home': 'Todos en casa' + , 'presence.none_home': 'Nadie en casa' + , 'presence.home': 'En casa: {names}' + , 'presence.away': 'Fuera: {names}' + , 'presence.manage': 'Gestionar combinaciones' + , 'presence.who_home': 'Quién está en casa' + , 'presence.who_away': 'Quién no está en casa' + , 'away.enable': 'Habilitar modo ausente' + , 'away.desc': 'Cuando ninguna de las personas seleccionadas está en casa, los termostatos bajan automáticamente a la temperatura establecida.' + , 'away.temp': 'Temperatura de ausencia' + , 'away.persons': 'Personas' + , 'away.add_person': 'Agregar persona' + , 'backup.select': 'Elegir qué copiar en la copia de seguridad' + , 'backup.select.desc': 'Los elementos marcados se incluyen en la copia/restauración' + , 'backup.opt.all': 'Todo' + , 'backup.opt.main': 'Horarios principales' + , 'backup.opt.weekday': 'Horarios por día' + , 'backup.opt.presence': 'Horarios de presencia' + , 'backup.opt.settings': 'Ajustes del editor' + , 'backup.opt.holiday': 'Horarios de festivos' + , 'backup.opt.colors': 'Bloques de color' + , 'editor.tabs.settings': 'Ajustes' + , 'editor.tabs.pause': 'Pausa' + , 'editor.tabs.weekdays': 'Días de la semana' + , 'editor.tabs.colors': 'Colores' + , 'editor.tabs.presence_sensor': 'Sensor de presencia' + , 'editor.presence_sensor.enable.title': 'Sensor de presencia' + , 'editor.presence_sensor.enable.desc': 'Habilita la configuración de sensor de presencia por habitación.' + , 'editor.presence_sensor.entity.title': 'Entidad de presencia' + , 'editor.presence_sensor.entity.desc': 'Selecciona el binary_sensor que indica presencia en esta habitación.' + , 'editor.presence_sensor.on_temp.title': 'Temperatura cuando hay presencia (ENCENDIDO)' + , 'editor.presence_sensor.on_temp.desc': 'Si el sensor está ENCENDIDO, esta habitación usa esta temperatura en lugar del horario.' + , 'editor.presence_sensor.on_delay.title': 'Retraso antes de aplicar presencia' + , 'editor.presence_sensor.on_delay.desc': 'Tiempo que el sensor debe permanecer ENCENDIDO antes de usar la temperatura de presencia. El valor usa la unidad seleccionada.' + , 'editor.presence_sensor.off_delay.title': 'Retraso antes de reanudar el horario' + , 'editor.presence_sensor.off_delay.desc': 'Tiempo que el sensor debe permanecer APAGADO antes de reanudar el horario. El valor usa la unidad seleccionada.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Fuera de casa' + , 'editor.tabs.reset': 'Restablecer' + , 'editor.tabs.boiler': 'Control de caldera' + , 'boiler.switch': 'Interruptor de caldera' + , 'boiler.switch_type': 'Tipo de entidad' + , 'boiler.switch_type.switch': 'Interruptor' + , 'boiler.switch_type.input_boolean': 'Booleano de entrada' + , 'boiler.rooms': 'Habitaciones incluidas' + , 'boiler.rooms.desc': 'Selecciona qué habitaciones deben incluirse en el control de la caldera.' + , 'boiler.enable': 'Activar control de caldera' + , 'boiler.enable.desc': 'Mostrar ajustes de caldera en el editor de esta tarjeta.' + , 'boiler.temp_sensor': 'Sensor de temperatura de caldera' + , 'boiler.temp_sensor.desc': 'Sensor de temperatura usado para controlar el interruptor de la caldera.' + , 'boiler.min_temp': 'Temp. mínima de caldera (°C)' + , 'boiler.max_temp': 'Temp. máxima de caldera (°C)' + , 'boiler.offsets': 'Offsets' + , 'boiler.offsets.desc': 'Offset ON: cuánto por debajo del objetivo programado antes de encender. Offset OFF: cuánto por encima del objetivo programado antes de apagar.' + , 'boiler.on_offset': 'Offset ON (°C)' + , 'boiler.off_offset': 'Offset OFF (°C)' + , 'editor.temp_sensor': 'Sensor de temperatura' + , 'presence.live_header': 'Mostrar presencia en vivo en la cabecera' + , 'presence.live_header.desc': 'Las fichas se muestran en la esquina derecha.' + , 'profiles.enable': 'Habilitar perfiles (sobrescribir)' + , 'profiles.enable.desc': 'Permitir horarios diarios con nombre que puedas activar para sobrescribir el plan principal.' + , 'profiles.active_label': 'Perfil manual activo:' + , 'profiles.save_new': 'Guardar perfil nuevo' + , 'profiles.save_existing': 'Guardar cambios en el perfil seleccionado' + , 'week.view.title': 'Vista semanal' + , 'week.view.rooms_one_day': 'Todas las habitaciones • un día' + , 'week.view.days_one_room': 'Todos los días • una habitación' + , 'week.view_switch.title': 'Cambiar vista desde la línea de tiempo' + , 'week.view_switch.desc': 'Muestra un selector en el encabezado de la línea de tiempo para alternar entre los dos modos de vista de los días de la semana.' + , 'week.title': 'Días de la semana' + , 'week.select_room': 'Seleccionar habitación' + , 'err.config_entities': "La configuración debe tener 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Video no encontrado' + , 'onboard.image_not_found': 'Imagen no encontrada' + , 'pause.prompt_minutes': 'Introduce la duración (minutos):' + , 'editor.test_tool': 'Herramienta de prueba' + , 'editor.test_select_entity': 'Selecciona una entidad' + , 'editor.test_empty': 'Nada que exportar' + , 'editor.test_export': 'Exportar' + , 'editor.test_no_fields': 'No hay campos para mostrar' + , 'editor.test_select_placeholder': 'Seleccionar...' + , 'editor.error_prefix': 'Error: ' + , 'backup.restore.confirm_overwrite': '¿Restaurar desde una copia de seguridad? Esto sobrescribirá los horarios y ajustes actuales.' + , 'backup.import.err_no_hass': 'Home Assistant no está listo' + , 'backup.import.err_storage_off': 'El almacenamiento compartido está DESACTIVADO. Actívalo primero.' + , 'backup.import.err_no_integration': 'La integración thermostat_timeline no está disponible' + , 'backup.import.err_json': 'JSON no válido' + , 'profiles.name_exists': 'El nombre ya existe' + }, + fr: { + 'card.title_default': 'Chronologie du thermostat', + 'ui.edit_block': 'Modifier le bloc', + 'ui.add_block': 'Ajouter un bloc', + 'ui.from': 'De', + 'ui.to': 'À', + 'ui.temperature': 'Température', + 'ui.delete': 'Supprimer', + 'ui.cancel': 'Annuler', + 'ui.close': 'Fermer', + 'ui.unsaved_confirm': 'Des modifications non enregistrées existent. Fermer quand même ?', + 'ui.discard_changes': 'Ignorer les modifications', + 'ui.keep_editing': 'Continuer l’édition', + 'ui.save': 'Enregistrer', + 'ui.error_prefix': 'Erreur : ', + 'ui.overlap_msg': 'Vous avez sélectionné des heures qui se chevauchent — chevauchement entre {start} – {end}.', + 'ui.auto_fix': 'Corriger automatiquement', + 'ui.fix_start_to': 'Ajuster le début à {time}', + 'ui.fix_end_to': 'Ajuster la fin à {time}', + // Inline meta + 'ui.current_temp': 'Température actuelle :', + + 'editor.title_label': 'Titre', + 'editor.add_entity': 'Ajouter une pièce', + 'editor.entity_placeholder': 'Sélectionnez une entité', + 'editor.room_mode.title': 'Type d’entité de pièce', + 'editor.room_mode.desc': 'Activé : sélectionnez un input_number (température cible). Désactivé : sélectionnez une entité climate.', + + 'editor.room_temp_sensor_override.title': 'Utiliser un capteur de température de la pièce', + 'editor.room_temp_sensor_override.desc': 'Lorsque c’est activé, la température de la pièce affichée (et la logique de la chaudière) utilise le capteur de température sélectionné au lieu de climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Capteur de température', + 'editor.drag_reorder': 'Glisser pour réorganiser', + 'editor.remove': 'Supprimer', + 'editor.default_c': 'Par défaut °C', + 'editor.row_height_px': 'Hauteur de ligne (px)', + 'editor.max_c': 'Max °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Application automatique', + 'editor.auto_apply.desc': 'Lorsqu’il est activé, la carte essaie de régler la température des entités climate sélectionnées pour qu’elle corresponde à la valeur prévue pour « maintenant »', + 'editor.apply_edit.title': 'Lors de l’édition', + 'editor.apply_edit.desc': 'Quand vous modifiez des blocs de la chronologie et que la modification affecte l’heure actuelle, la nouvelle température est appliquée immédiatement.', + 'editor.apply_default.title': 'Lors d’un changement de °C par défaut', + 'editor.apply_default.desc': 'Lorsque « °C par défaut » change et que cela affecte la période en cours, la nouvelle température est appliquée immédiatement.', + + 'editor.show_room_temp.title': 'Afficher la température de la pièce', + 'editor.show_room_temp.desc': 'Affiche la bulle de température actuelle de la pièce sur chaque ligne dans l’en-tête de la frise.', + 'ui.copy_plan': 'Copier le plan', + 'ui.paste_plan': 'Coller le plan', + 'ui.copy_day': 'Copier le jour', + 'ui.paste_day': 'Coller le jour', + 'editor.merge_label': 'Fusionner avec (ajouter un thermostat supplémentaire)', + 'editor.display_name_optional': 'Nom de la pièce (facultatif)', + 'editor.merged_with': 'Fusionné avec', + 'editor.store_enable.title': 'Stockage partagé + contrôle en arrière‑plan', + 'editor.store_enable.short': 'Enregistre les plannings dans le capteur sélectionné et active le contrôle en arrière‑plan.', + 'editor.store_enable.desc': 'Enregistre les plannings dans le capteur sélectionné et active set_temperature en arrière‑plan. Les thermostats se mettent à jour même lorsque la carte est fermée. Partage également les plannings (blocs de chauffe) et les paramètres de la carte entre utilisateurs/appareils. Désactivé : commandes envoyées uniquement lorsque la carte est visible sur un appareil (tablette/PC/mobile) et elle doit rester ouverte.', + 'editor.store_missing': 'L’intégration « Thermostat Pro Timeline Sync » n’est pas installée. Le stockage partagé n’est pas disponible.', + 'editor.clear_store': 'Vider le stockage', + 'editor.clear_store_confirm': 'Cela supprimera tous les plannings enregistrés. Continuer ?', + 'editor.migrate_to_store': 'Transférer les données vers le stockage', + 'editor.migrate_confirm': 'Transférer vos plannings enregistrés dans le navigateur vers le capteur sélectionné ? Cela remplacera les plannings existants dans le stockage.', + 'editor.migrate_pull_confirm': 'Importer les plannings depuis le stockage et remplacer les données locales du navigateur ?', + 'editor.select_storage_first': 'Veuillez d’abord sélectionner un capteur de stockage.', + 'editor.no_local_data': 'Aucune donnée locale du navigateur trouvée.', + 'editor.clear_all': 'Effacer toutes les données', + 'editor.clear_all_confirm': 'Cela supprimera les plannings du stockage et du navigateur local. Continuer ?', + 'editor.clear_storage_only': 'Effacer uniquement les données du stockage', + 'editor.clear_storage_only_confirm': 'Cela supprimera les plannings du capteur de stockage sélectionné. Continuer ?', + 'editor.clear_local_only': 'Effacer uniquement les données locales', + 'editor.clear_local_only_confirm': 'Cela supprimera uniquement les plannings enregistrés dans votre navigateur. Continuer ?', + 'editor.reset.desc': 'Ce que cela fait :\n\n• Effacer toutes les données : Réinitialise le stockage backend (.storage) de Thermostat Timeline et supprime TOUS les caches/brouillons locaux du navigateur.\n• Effacer uniquement les données locales : Supprime uniquement les données enregistrées dans ce navigateur (localStorage).\n\nAstuce : Après une réinitialisation complète, rechargez la page pour éviter que d’anciens brouillons réapparaissent.', + 'week.enable': 'Activer les jours de semaine', + 'week.mode': 'Mode jours de semaine', + 'week.mode.same_all': 'Identique chaque jour (1)', + 'week.mode.weekday_weekend': 'Jours ouvrés + Week-end', + 'week.mode.weekday_sat_sun': 'Jours ouvrés + Samedi + Dimanche', + 'week.mode.all_7': 'Tous les jours', + 'week.button': 'Jours de semaine', + 'week.days.mon': 'Lun', + 'week.days.tue': 'Mar', + 'week.days.wed': 'Mer', + 'week.days.thu': 'Jeu', + 'week.days.fri': 'Ven', + 'week.days.sat': 'Sam', + 'week.days.sun': 'Dim', + 'week.editor.title': 'Planification par jour', + 'week.editor.preview_note': 'Aperçu uniquement. Les modifications s’appliquent lors de l’enregistrement.', + 'week.open_editor.desc': 'Ouvrir l’éditeur des jours.', + 'week.open_editor.btn': 'Ouvrir', + 'week.copy_to_rooms': 'Copier la semaine vers des pièces', + 'week.copy_to_rooms.title': 'Copier la semaine vers des pièces', + 'week.copy_to_rooms.select': 'Sélectionner des pièces', + 'week.copy_to_rooms.apply': 'Insérer dans les pièces', + 'week.group.weekdays': 'Jours ouvrés', + 'week.group.weekend': 'Week-end', + 'week.days_full.mon': 'Lundi', + 'week.days_full.tue': 'Mardi', + 'week.days_full.wed': 'Mercredi', + 'week.days_full.thu': 'Jeudi', + 'week.days_full.fri': 'Vendredi', + 'week.days_full.sat': 'Samedi', + 'week.days_full.sun': 'Dimanche', + 'editor.timefmt.title': 'Format de l’heure', + 'editor.timefmt.desc': 'Choisir affichage 24 h ou 12 h (AM/PM) pour les heures sur la carte.', + 'editor.tempfmt.title': 'Unité de température', + 'editor.tempfmt.desc': 'Choisir Celsius (°C) ou Fahrenheit (°F) pour l’affichage dans la carte et les éditeurs.', + 'editor.timesrc.title': 'Source de l’heure', + 'editor.timesrc.desc': 'Choisir si la carte utilise l’heure du navigateur ou le fuseau horaire de Home Assistant pour « maintenant ».', + 'editor.timesrc.browser': 'Navigateur', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'ID de configuration', + 'editor.instance.desc': 'Conservez plusieurs plannings/paramètres indépendants sous différents identifiants.', + 'editor.instance.new_id': 'Nouvel ID', + 'editor.heat_colors.title': 'Couleurs des blocs de chauffe', + 'editor.heat_colors.add': 'Ajouter un intervalle de couleur', + 'editor.colors.col_from': 'De °C', + 'editor.colors.col_to': 'À °C', + 'editor.colors.col_color': 'Couleur', + 'editor.colors.empty': 'Aucune pièce ajoutée. Ajoutez une pièce pour définir des couleurs.', + 'editor.colors.go_add': 'Ajouter une pièce', + 'editor.colors.refresh': 'Mettre à jour le planning', + 'editor.colors.clear_local': 'Effacer les données de couleur locales', + 'editor.colors.clear_local_confirm': 'Cela supprimera les intervalles de couleur enregistrés dans votre navigateur pour cette carte. Continuer ?', + 'editor.colors.clear_shared': 'Effacer le stockage de couleur partagé', + 'editor.colors.clear_shared_confirm': 'Cela supprimera les intervalles de couleur dans le capteur de stockage sélectionné. Continuer ?', + 'editor.colors.mode': 'Mode de couleur', + 'editor.colors.mode.desc': 'Choisir des couleurs par pièce ou un jeu pour toutes les pièces.', + 'editor.colors.mode_per_room': 'Par pièce', + 'editor.colors.mode_global': 'Toutes les pièces', + 'editor.colors.global_title': 'Toutes les pièces', + 'editor.colors.clear_all': 'Effacer toutes les données de couleur', + 'editor.colors.clear_all_confirm': 'Ceci supprimera tous les intervalles de couleur à la fois localement et sur le capteur de stockage sélectionné. Continuer ?', + 'editor.reload_editor': 'Recharger l’éditeur', + 'editor.reload_dashboard': 'Recharger le tableau de bord', + 'onboard.title': 'Bienvenue dans la Chronologie du thermostat', + 'onboard.next': 'Suivant', + 'onboard.prev': 'Précédent', + 'onboard.skip': 'Ignorer', + 'onboard.done': 'Terminer', + 'onboard.dont_show': 'Ne plus afficher', + 'onboard.step_main': 'Voici la chronologie principale. Elle affiche les températures souhaitées sur 24 h pour chaque pièce.', + 'onboard.step_add': 'Ajoutez un bloc via + Ajouter un bloc ou double‑cliquez sur la chronologie.', + 'onboard.step_edit': 'Modifiez/Supprimez un bloc par double‑clic. Utilisez les jours de semaine pour des jours différents.', + 'onboard.reset': 'Afficher le guide à nouveau', + 'onboard.reset_ok': 'La présentation s’affichera la prochaine fois.', + 'editor.select_entities': 'Pièces', + 'editor.tabs.rooms': 'Pièces', + 'week.enable.desc': 'Activer la planification par jour de semaine. Désactivé : même plan chaque jour.', + 'editor.tabs.sync': 'Moteur de synchro', + 'sync.running': 'Synchronisation en cours…', + 'sync.completed': 'Synchronisation terminée', + 'editor.tabs.backup': 'Sauvegarde', + 'backup.title': 'Sauvegarde', + 'backup.desc': 'Copie le contenu de votre capteur de synchro vers le capteur de sauvegarde et restaure si besoin.', + 'backup.now': 'Sauvegarder maintenant', + 'backup.restore': 'Restaurer', + 'backup.delete': 'Supprimer la sauvegarde', + 'backup.delete.confirm': 'Supprimer la sauvegarde sélectionnée ?', + 'backup.new': 'Nouvelle sauvegarde', + 'backup.auto': 'Sauvegarde automatique', + 'backup.auto.desc': 'Effectuer des sauvegardes automatiques à intervalle régulier.', + 'backup.interval': 'Intervalle (jours)', + 'backup.interval.desc': 'Nombre de jours entre les sauvegardes automatiques (min. 1).', + 'backup.sensor': 'Capteur de sauvegarde', + 'backup.last': 'Dernière sauvegarde', + 'backup.slot.title': 'Sélectionner une sauvegarde', + 'backup.slot.desc': 'Sélectionnez l’emplacement de sauvegarde à restaurer.', + 'backup.slot.empty': 'Aucune sauvegarde pour l’instant', + 'pause.menu_title': 'Suspendre l’application automatique', + 'pause.off_1h': 'Suspendre 1 heure', + 'pause.off_5h': 'Suspendre 5 heures', + 'pause.off_custom': 'Durée personnalisée…', + 'pause.off_indef': 'Suspendre jusqu’à reprise', + 'pause.resume': 'Reprendre maintenant', + 'pause.close': 'Fermer', + 'pause.active_until': 'Suspendu jusqu’à {time}', + 'pause.active_indef': 'Suspendu', + 'editor.pausebtn.title': 'Bouton Pause', + 'editor.pausebtn.desc': 'Affiche un bouton de pause global dans l’en‑tête de la carte. En pause, toutes les commandes set_temperature sont bloquées.', + 'editor.integration.connected': 'Intégration prête', + 'editor.integration.disconnected': 'Intégration indisponible' + , 'editor.open_in_hacs': 'Ouvrir dans HACS' + , 'profiles.button': 'Programme manuel' + , 'profiles.editor.title': 'Profils' + , 'profiles.add': 'Ajouter un profil' + , 'profiles.rename': 'Renommer' + , 'profiles.delete': 'Supprimer' + , 'profiles.activate': 'Activer' + , 'profiles.deactivate': 'Désactiver' + , 'profiles.active': 'Actif' + , 'profiles.none': 'Aucun' + , 'profiles.name_placeholder': 'Nom du profil' + , 'profiles.edit': 'Modifier' + , 'profiles.preview_note': 'Aperçu uniquement. Les plannings deviennent actifs lorsque vous activez le profil.' + , 'editor.tabs.holidays': 'Jours fériés' + , 'holidays.enable': 'Activer les jours fériés' + , 'holidays.desc': 'Utiliser un planning spécifique les jours fériés.' + , 'holidays.source': 'Source des jours fériés' + , 'holidays.source.calendar': 'Calendrier' + , 'holidays.source.manual': 'Dates manuelles' + , 'holidays.entity': 'Entité jours fériés' + , 'holidays.entity_desc': 'Sélectionnez un calendrier ne fournissant que on/off. Il doit être « on » les jours fériés et « off » les autres jours.' + , 'holidays.dates': 'Dates des jours fériés' + , 'holidays.add_date': 'Ajouter une date' + , 'holidays.editor.title': 'Planning jours fériés' + , 'holidays.edit': 'Modifier le planning jours fériés' + , 'holidays.button': 'Jours fériés' + , 'editor.perroom.title': '°C par défaut par pièce' + , 'editor.perroom.desc': 'Affiche un champ °C par défaut sur chaque ligne. Désactivé : utilise le °C par défaut global pour toutes les pièces.' + , 'away.resume_schedule': 'Reprendre le programme' + , 'away.resume_away': 'Reprendre la température d’absence' + , 'presence.button': 'Programmes de présence' + , 'presence.editor.title': 'Programmes de présence' + , 'presence.enable_advanced': 'Activer les plannings de présence avancés' + , 'presence.enable_advanced.desc': 'Utilisez des combinaisons et des plannings selon qui est présent/absent.' + , 'away.delay_enable': 'Retarder les actions d’absence' + , 'away.delay_desc': 'Attendre avant d’appliquer les changements d’absence.' + , 'away.delay_value': 'Délai' + , 'away.delay_unit': 'Unité' + , 'away.delay_unit.seconds': 'Secondes' + , 'away.delay_unit.minutes': 'Minutes' + , 'presence.combos': 'Combinaisons' + , 'presence.enable_combo': 'Activer cette combinaison' + , 'presence.all_home': 'Tous à la maison' + , 'presence.none_home': 'Personne à la maison' + , 'presence.home': 'À la maison : {names}' + , 'presence.away': 'Absents : {names}' + , 'presence.manage': 'Gérer les combinaisons' + , 'presence.who_home': 'Qui est à la maison' + , 'presence.who_away': 'Qui n’est pas à la maison' + , 'away.enable': 'Activer le mode absence' + , 'away.desc': 'Lorsque aucune des personnes sélectionnées n’est à la maison, les thermostats sont automatiquement abaissés à la température définie.' + , 'away.temp': 'Température d’absence' + , 'away.persons': 'Personnes' + , 'away.add_person': 'Ajouter une personne' + , 'backup.select': 'Choisir quoi sauvegarder' + , 'backup.select.desc': 'Les éléments cochés sont inclus dans la sauvegarde/restauration' + , 'backup.opt.all': 'Tout' + , 'backup.opt.main': 'Plannings principaux' + , 'backup.opt.weekday': 'Plannings par jour' + , 'backup.opt.presence': 'Plannings de présence' + , 'backup.opt.settings': 'Paramètres de l’éditeur' + , 'backup.opt.holiday': 'Plannings jours fériés' + , 'backup.opt.colors': 'Blocs de couleur' + , 'editor.tabs.settings': 'Paramètres' + , 'editor.tabs.pause': 'Pause' + , 'editor.tabs.weekdays': 'Jours de semaine' + , 'editor.tabs.colors': 'Couleurs' + , 'editor.tabs.presence_sensor': 'Capteur de présence' + , 'editor.presence_sensor.enable.title': 'Capteur de présence' + , 'editor.presence_sensor.enable.desc': 'Activer la configuration des capteurs de présence par pièce.' + , 'editor.presence_sensor.entity.title': 'Entité de présence' + , 'editor.presence_sensor.entity.desc': 'Sélectionnez le binary_sensor qui indique la présence dans cette pièce.' + , 'editor.presence_sensor.on_temp.title': 'Température quand présence est activée' + , 'editor.presence_sensor.on_temp.desc': 'Si le capteur est activé, cette pièce utilise cette température au lieu du planning.' + , 'editor.presence_sensor.on_delay.title': 'Délai avant d’appliquer la présence' + , 'editor.presence_sensor.on_delay.desc': 'Durée pendant laquelle le capteur doit rester activé avant d’utiliser la température de présence. La valeur utilise l’unité sélectionnée.' + , 'editor.presence_sensor.off_delay.title': 'Délai avant de reprendre le planning' + , 'editor.presence_sensor.off_delay.desc': 'Durée pendant laquelle le capteur doit rester désactivé avant de reprendre le planning. La valeur utilise l’unité sélectionnée.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Absence du domicile' + , 'editor.tabs.reset': 'Réinitialiser' + , 'editor.tabs.boiler': 'Contrôle chaudière' + , 'boiler.switch': 'Interrupteur de chaudière' + , 'boiler.switch_type': "Type d’entité" + , 'boiler.switch_type.switch': 'Interrupteur' + , 'boiler.switch_type.input_boolean': "Booléen d’entrée" + , 'boiler.rooms': 'Pièces incluses' + , 'boiler.rooms.desc': 'Sélectionnez les pièces à inclure dans le contrôle de la chaudière.' + , 'boiler.enable': 'Activer le contrôle chaudière' + , 'boiler.enable.desc': 'Afficher les réglages de chaudière dans l’éditeur de cette carte.' + , 'boiler.temp_sensor': 'Capteur de température chaudière' + , 'boiler.temp_sensor.desc': 'Capteur de température utilisé pour piloter l’interrupteur de la chaudière.' + , 'boiler.min_temp': 'Temp. min. chaudière (°C)' + , 'boiler.max_temp': 'Temp. max. chaudière (°C)' + , 'boiler.offsets': 'Décalages' + , 'boiler.offsets.desc': 'Décalage ON : à combien en dessous de la consigne planifiée avant d’allumer. Décalage OFF : à combien au‑dessus de la consigne planifiée avant d’éteindre.' + , 'boiler.on_offset': 'Décalage ON (°C)' + , 'boiler.off_offset': 'Décalage OFF (°C)' + , 'editor.temp_sensor': 'Capteur de température' + , 'presence.live_header': 'Afficher la présence en direct dans l’en‑tête' + , 'presence.live_header.desc': 'Les puces sont affichées dans le coin droit.' + , 'profiles.enable': 'Activer les profils (remplacement)' + , 'profiles.enable.desc': 'Autoriser des plannings journaliers nommés pouvant remplacer le plan principal lorsque vous les activez.' + , 'profiles.active_label': 'Profil manuel actif :' + , 'profiles.save_new': 'Enregistrer un nouveau profil' + , 'profiles.save_existing': 'Enregistrer les modifications du profil sélectionné' + , 'week.view.title': 'Vue semaine' + , 'week.view.rooms_one_day': 'Toutes les pièces • un jour' + , 'week.view.days_one_room': 'Tous les jours • une pièce' + , 'week.view_switch.title': 'Changer la vue depuis la frise' + , 'week.view_switch.desc': 'Affiche un sélecteur dans l’en-tête de la frise pour basculer entre les deux modes de vue des jours de semaine.' + , 'week.title': 'Jours de semaine' + , 'week.select_room': 'Sélectionner une pièce' + , 'err.config_entities': "La configuration doit contenir 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Vidéo introuvable' + , 'onboard.image_not_found': 'Image introuvable' + , 'pause.prompt_minutes': 'Saisir la durée (minutes) :' + , 'editor.test_tool': 'Outil de test' + , 'editor.test_select_entity': 'Sélectionnez une entité' + , 'editor.test_empty': 'Rien à exporter' + , 'editor.test_export': 'Exporter' + , 'editor.test_no_fields': 'Aucun champ à afficher' + , 'editor.test_select_placeholder': 'Sélectionner...' + , 'editor.error_prefix': 'Erreur : ' + , 'backup.restore.confirm_overwrite': 'Restaurer depuis une sauvegarde ? Cela écrasera les plannings et paramètres actuels.' + , 'backup.import.err_no_hass': "Home Assistant n'est pas prêt" + , 'backup.import.err_storage_off': 'Le stockage partagé est DÉSACTIVÉ. Activez-le d’abord.' + , 'backup.import.err_no_integration': "L'intégration thermostat_timeline n'est pas disponible" + , 'backup.import.err_json': 'JSON invalide' + , 'profiles.name_exists': 'Le nom existe déjà' + }, + it: { + 'card.title_default': 'Cronologia del termostato', + 'ui.edit_block': 'Modifica blocco', + 'ui.add_block': 'Aggiungi blocco', + 'ui.from': 'Da', + 'ui.to': 'A', + 'ui.temperature': 'Temperatura', + 'ui.delete': 'Elimina', + 'ui.cancel': 'Annulla', + 'ui.close': 'Chiudi', + 'ui.unsaved_confirm': 'Hai modifiche non salvate. Chiudere comunque?', + 'ui.discard_changes': 'Scarta modifiche', + 'ui.keep_editing': 'Continua a modificare', + 'ui.save': 'Salva', + 'ui.error_prefix': 'Errore: ', + 'ui.overlap_msg': 'Hai selezionato orari sovrapposti — si sovrappongono tra {start} e {end}.', + 'ui.auto_fix': 'Correzione automatica', + 'ui.fix_start_to': 'Allinea inizio a {time}', + 'ui.fix_end_to': 'Allinea fine a {time}', + // Inline meta + 'ui.current_temp': 'Temperatura attuale:', + + 'editor.title_label': 'Titolo', + 'editor.add_entity': 'Aggiungi stanza', + 'editor.entity_placeholder': 'Seleziona un\'entità', + 'editor.room_mode.title': 'Tipo di entità stanza', + 'editor.room_mode.desc': 'Quando è attivo, seleziona un input_number (temperatura obiettivo). Quando è disattivo, seleziona un\'entità climate.', + + 'editor.room_temp_sensor_override.title': 'Usa sensore temperatura stanza', + 'editor.room_temp_sensor_override.desc': 'Quando abilitato, la temperatura stanza mostrata (e la logica della caldaia) usa il sensore di temperatura selezionato invece di climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Sensore di temperatura', + 'editor.drag_reorder': 'Trascina per riordinare', + 'editor.remove': 'Rimuovi', + 'editor.default_c': '°C predefinita', + 'editor.perroom.title': '°C predefinita per stanza', + 'editor.perroom.desc': 'Mostra un campo °C predefinita su ogni riga. Se disattivato, viene usata la °C predefinita globale per tutte le stanze.', + 'editor.row_height_px': 'Altezza riga (px)', + 'editor.max_c': '°C max', + 'editor.min_c': '°C min', + 'editor.auto_apply.title': 'Applicazione automatica', + 'editor.auto_apply.desc': 'Quando è attivo, la scheda prova a impostare la temperatura sulle entità climate selezionate così da corrispondere al valore pianificato per "adesso".', + 'editor.apply_edit.title': 'Durante la modifica', + 'editor.apply_edit.desc': 'Quando modifichi i blocchi della timeline e la modifica influisce sull\'ora corrente, la nuova temperatura viene applicata immediatamente.', + 'editor.apply_default.title': 'Al cambio di °C predefinita', + 'editor.apply_default.desc': 'Quando la "°C predefinita" cambia e interessa il periodo corrente, la nuova temperatura viene applicata subito.', + + 'editor.show_room_temp.title': 'Mostra temperatura stanza', + 'editor.show_room_temp.desc': 'Mostra la bolla della temperatura attuale della stanza su ogni riga nell’intestazione della timeline.', + 'ui.copy_plan': 'Copia pianificazione', + 'ui.paste_plan': 'Incolla pianificazione', + 'ui.copy_day': 'Copia giorno', + 'ui.paste_day': 'Incolla giorno', + 'editor.merge_label': 'Unisci con (aggiungi un altro termostato)', + 'editor.display_name_optional': 'Nome stanza (facoltativo)', + 'editor.merged_with': 'Unito con', + 'editor.store_enable.title': 'Archiviazione condivisa + controllo in background', + 'editor.store_enable.short': 'Salva i programmi nel sensore selezionato e abilita il controllo in background.', + 'editor.store_enable.desc': 'Salva i programmi nel sensore selezionato e abilita i comandi set_temperature in background tramite l\'integrazione Thermostat Timeline. I termostati si aggiornano anche quando la scheda è chiusa. Condivide anche programmi (blocchi di calore) e impostazioni tra utenti/dispositivi. Quando è disattivo, i comandi vengono inviati solo mentre la scheda è visibile su un dispositivo (tablet/PC/mobile) e deve restare aperta.', + 'editor.store_missing': 'L\'integrazione "Thermostat Pro Timeline Sync" non è installata. L\'archiviazione condivisa non è disponibile.', + 'editor.clear_store': 'Svuota archiviazione', + 'editor.clear_store_confirm': 'Questo eliminerà tutti i programmi salvati. Continuare?', + 'editor.migrate_to_store': 'Trasferisci dati nell\'archiviazione', + 'editor.migrate_confirm': 'Trasferire i programmi salvati nel browser al sensore selezionato? Eventuali programmi esistenti verranno sovrascritti.', + 'editor.migrate_pull_confirm': 'Importare i programmi dall\'archiviazione e sovrascrivere i dati locali del browser?', + 'editor.select_storage_first': 'Seleziona prima un sensore di archiviazione.', + 'editor.no_local_data': 'Nessun dato locale del browser trovato.', + 'editor.clear_all': 'Cancella tutti i dati', + 'editor.clear_all_confirm': 'Questo eliminerà sia l\'archiviazione sia i programmi salvati nel browser locale. Continuare?', + 'editor.clear_storage_only': 'Cancella solo i dati dell\'archiviazione', + 'editor.clear_storage_only_confirm': 'Questo eliminerà i programmi dal sensore di archiviazione selezionato. Continuare?', + 'editor.clear_local_only': 'Cancella solo i dati locali', + 'editor.clear_local_only_confirm': 'Questo eliminerà solo i programmi salvati nel tuo browser. Continuare?', + 'editor.reset.desc': 'Cosa fa:\n\n• Cancella tutti i dati: Reimposta lo storage backend (.storage) di Thermostat Timeline ed elimina TUTTE le cache/bozze locali del browser.\n• Cancella solo i dati locali: Elimina solo i dati salvati in questo browser (localStorage).\n\nSuggerimento: Dopo un reset completo, ricarica la pagina per evitare che vecchie bozze ricompaiano.', + 'editor.temp_sensor': 'Sensore di temperatura', + 'editor.test_tool': 'Strumento di test', + 'editor.test_select_entity': 'Seleziona entità', + 'editor.test_empty': 'Seleziona un\'entità per vedere i comandi disponibili…', + 'editor.test_export': 'Esporta .md', + 'editor.test_no_fields': 'Nessun campo', + 'editor.test_select_placeholder': '— Seleziona —', + 'editor.error_prefix': 'Errore: ', + 'editor.open_in_hacs': 'Apri in HACS' + , 'week.enable': 'Abilita giorni della settimana' + , 'week.mode': 'Modalità giorni' + , 'week.mode.weekday_weekend': 'Feriali + Weekend' + , 'week.mode.weekday_sat_sun': 'Feriali + Sabato + Domenica' + , 'week.mode.all_7': 'Tutti i giorni' + , 'week.button': 'Giorni feriali' + , 'week.group.weekdays': 'Feriali' + , 'week.group.weekend': 'Weekend' + , 'week.days_full.mon': 'Lunedì' + , 'week.days_full.tue': 'Martedì' + , 'week.days_full.wed': 'Mercoledì' + , 'week.days_full.thu': 'Giovedì' + , 'week.days_full.fri': 'Venerdì' + , 'week.days_full.sat': 'Sabato' + , 'week.days_full.sun': 'Domenica' + , 'week.days.mon': 'Lun' + , 'week.days.tue': 'Mar' + , 'week.days.wed': 'Mer' + , 'week.days.thu': 'Gio' + , 'week.days.fri': 'Ven' + , 'week.days.sat': 'Sab' + , 'week.days.sun': 'Dom' + , 'week.editor.title': 'Programma settimanale' + , 'week.editor.preview_note': 'Solo anteprima. Le modifiche si applicano quando premi Salva.' + , 'week.open_editor.desc': 'Apri l’editor dei giorni della settimana.' + , 'week.open_editor.btn': 'Apri' + , 'week.view.title': 'Vista settimanale' + , 'week.view.rooms_one_day': 'Tutte le stanze • un giorno' + , 'week.view.days_one_room': 'Tutti i giorni • una stanza' + , 'week.view_switch.title': 'Cambia vista dalla timeline' + , 'week.view_switch.desc': 'Mostra un selettore nell’intestazione della timeline per passare tra le due modalità di visualizzazione dei giorni feriali.' + , 'week.title': 'Giorni feriali' + , 'week.select_room': 'Seleziona stanza' + , 'week.copy_to_rooms': 'Copia settimana nelle stanze' + , 'week.copy_to_rooms.title': 'Copia settimana nelle stanze' + , 'week.copy_to_rooms.select': 'Seleziona stanze' + , 'week.copy_to_rooms.apply': 'Inserisci nelle stanze' + , 'week.enable.desc': 'Abilita la pianificazione per giorno. Quando è disattivo viene usato lo stesso piano ogni giorno.' + , 'editor.timefmt.title': 'Formato orario' + , 'editor.timefmt.desc': 'Scegli 24 ore o 12 ore (AM/PM) per gli orari mostrati sulla scheda.' + , 'editor.tempfmt.title': 'Unità di temperatura' + , 'editor.tempfmt.desc': 'Scegli Celsius (°C) o Fahrenheit (°F) per la scheda e gli editor.' + , 'editor.timesrc.title': 'Origine orario' + , 'editor.timesrc.desc': 'Scegli se la scheda usa l\'ora del browser o il fuso di Home Assistant per "adesso".' + , 'editor.timesrc.browser': 'Browser' + , 'editor.timesrc.ha': 'Home Assistant' + , 'editor.instance.title': 'ID configurazione' + , 'editor.instance.desc': 'Mantieni più pianificazioni/impostazioni indipendenti con ID diversi.' + , 'editor.instance.new_id': 'Nuovo ID' + , 'editor.tabs.settings': 'Impostazioni' + , 'editor.tabs.pause': 'Pausa' + , 'editor.tabs.weekdays': 'Giorni feriali' + , 'editor.tabs.rooms': 'Stanze' + , 'editor.tabs.colors': 'Colori' + , 'editor.tabs.presence_sensor': 'Sensore di presenza' + , 'editor.presence_sensor.enable.title': 'Sensore di presenza' + , 'editor.presence_sensor.enable.desc': 'Abilita la configurazione del sensore di presenza per stanza.' + , 'editor.presence_sensor.entity.title': 'Entità di presenza' + , 'editor.presence_sensor.entity.desc': 'Seleziona il binary_sensor che indica la presenza in questa stanza.' + , 'editor.presence_sensor.on_temp.title': 'Temperatura quando presenza è ON' + , 'editor.presence_sensor.on_temp.desc': 'Se il sensore è ON, questa stanza usa questa temperatura invece del programma.' + , 'editor.presence_sensor.on_delay.title': 'Ritardo prima di applicare presenza' + , 'editor.presence_sensor.on_delay.desc': 'Quanto tempo il sensore deve restare ON prima di usare la temperatura di presenza. Il valore usa l’unità selezionata.' + , 'editor.presence_sensor.off_delay.title': 'Ritardo prima di riprendere il programma' + , 'editor.presence_sensor.off_delay.desc': 'Quanto tempo il sensore deve restare OFF prima di riprendere il programma. Il valore usa l’unità selezionata.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Fuori casa' + , 'editor.tabs.reset': 'Reimposta' + , 'editor.tabs.boiler': 'Controllo caldaia' + , 'boiler.switch': 'Interruttore caldaia' + , 'boiler.switch_type': 'Tipo di entità' + , 'boiler.switch_type.switch': 'Interruttore' + , 'boiler.switch_type.input_boolean': 'Booleano di input' + , 'boiler.rooms': 'Stanze incluse' + , 'boiler.rooms.desc': 'Seleziona quali stanze includere nel controllo della caldaia.' + , 'boiler.enable': 'Attiva controllo caldaia' + , 'boiler.enable.desc': 'Mostra le impostazioni caldaia nell’editor di questa scheda.' + , 'boiler.temp_sensor': 'Sensore temperatura caldaia' + , 'boiler.temp_sensor.desc': 'Sensore di temperatura usato per controllare l’interruttore della caldaia.' + , 'boiler.min_temp': 'Temperatura min. caldaia (°C)' + , 'boiler.max_temp': 'Temperatura max. caldaia (°C)' + , 'boiler.offsets': 'Offset' + , 'boiler.offsets.desc': 'Offset ON: quanto sotto l’obiettivo programmato prima di accendere. Offset OFF: quanto sopra l’obiettivo programmato prima di spegnere.' + , 'boiler.on_offset': 'Offset ON (°C)' + , 'boiler.off_offset': 'Offset OFF (°C)' + , 'editor.tabs.sync': 'Motore di sincronizzazione' + , 'sync.running': 'Sincronizzazione in corso…' + , 'sync.completed': 'Sincronizzazione completata' + , 'away.enable': 'Abilita modalità assenza' + , 'away.desc': 'Quando nessuna delle persone selezionate è in casa, abbassa automaticamente i termostati alla temperatura impostata.' + , 'away.temp': 'Temperatura assenza' + , 'away.persons': 'Persone' + , 'away.add_person': 'Aggiungi persona' + , 'away.resume_schedule': 'Riprendi programma' + , 'away.resume_away': 'Ripristina temperatura assenza' + , 'editor.heat_colors.title': 'Colori dei blocchi di calore' + , 'editor.heat_colors.add': 'Aggiungi intervallo colore' + , 'editor.colors.col_from': 'Da °C' + , 'editor.colors.col_to': 'A °C' + , 'editor.colors.col_color': 'Colore' + , 'editor.colors.empty': 'Nessuna stanza aggiunta. Aggiungi una stanza per definire i colori.' + , 'editor.colors.go_add': 'Aggiungi stanza' + , 'editor.select_entities': 'Stanze' + , 'editor.colors.refresh': 'Aggiorna programma' + , 'editor.colors.clear_local': 'Pulisci dati colore locali' + , 'editor.colors.clear_local_confirm': 'Questo eliminerà gli intervalli colore salvati nel tuo browser per questa scheda. Continuare?' + , 'editor.colors.clear_shared': 'Pulisci archivio colori condiviso' + , 'editor.colors.clear_shared_confirm': 'Questo eliminerà gli intervalli colore nel sensore di archiviazione selezionato. Continuare?' + , 'editor.colors.clear_all': 'Pulisci tutti i dati colore' + , 'editor.colors.clear_all_confirm': 'Questo eliminerà tutti gli intervalli colore sia locali sia nel sensore selezionato. Continuare?' + , 'editor.colors.mode': 'Modalità colore' + , 'editor.colors.mode.desc': 'Scegli colori per stanza o un set per tutte le stanze.' + , 'editor.colors.mode_per_room': 'Per stanza' + , 'editor.colors.mode_global': 'Tutte le stanze' + , 'editor.colors.global_title': 'Tutte le stanze' + , 'editor.reload_editor': 'Ricarica editor' + , 'editor.reload_dashboard': 'Ricarica dashboard' + , 'onboard.title': 'Benvenuto in Thermostat Timeline' + , 'onboard.next': 'Avanti' + , 'onboard.prev': 'Indietro' + , 'onboard.skip': 'Salta' + , 'onboard.done': 'Fine' + , 'onboard.dont_show': 'Non mostrare più' + , 'onboard.step_main': 'Questa è la timeline principale. Mostra le temperature desiderate sulle 24 ore per ogni stanza.' + , 'onboard.step_add': 'Aggiungi un blocco di calore con + Aggiungi blocco o facendo doppio clic sulla timeline.' + , 'onboard.step_edit': 'Modifica o elimina un blocco con un doppio clic. Usa Giorni feriali per pianificare giorni diversi.' + , 'onboard.reset': 'Mostra di nuovo la guida' + , 'onboard.reset_ok': 'La schermata di benvenuto verrà mostrata la prossima volta.' + , 'pause.menu_title': 'Pausa applicazione automatica' + , 'pause.off_1h': 'Pausa 1 ora' + , 'pause.off_5h': 'Pausa 5 ore' + , 'pause.off_custom': 'Durata personalizzata…' + , 'pause.off_indef': 'Pausa finché non riprendo' + , 'pause.resume': 'Riprendi ora' + , 'pause.close': 'Chiudi' + , 'pause.active_until': 'In pausa fino alle {time}' + , 'pause.active_indef': 'In pausa' + , 'editor.pausebtn.title': 'Pulsante Pausa' + , 'editor.pausebtn.desc': 'Mostra un pulsante di pausa globale nell\'intestazione della scheda. Durante la pausa, tutti i comandi set_temperature sono sospesi.' + , 'editor.integration.connected': 'Integrazione pronta' + , 'editor.integration.disconnected': 'Integrazione non disponibile' + , 'profiles.enable': 'Abilita profili (override)' + , 'profiles.enable.desc': 'Consenti programmi giornalieri nominati che possono sostituire il piano principale.' + , 'profiles.button': 'Programma manuale' + , 'profiles.editor.title': 'Profili' + , 'profiles.add': 'Aggiungi profilo' + , 'profiles.rename': 'Rinomina' + , 'profiles.delete': 'Elimina' + , 'profiles.activate': 'Attiva' + , 'profiles.deactivate': 'Disattiva' + , 'profiles.active': 'Attivo' + , 'profiles.none': 'Nessuno' + , 'profiles.name_placeholder': 'Nome profilo' + , 'profiles.edit': 'Modifica' + , 'profiles.preview_note': 'Solo anteprima. I programmi diventano attivi quando attivi il profilo.' + , 'profiles.active_label': 'Profilo manuale attivo:' + , 'profiles.save_new': 'Salva nuovo profilo' + , 'profiles.save_existing': 'Salva modifiche al profilo selezionato' + , 'editor.tabs.holidays': 'Festività' + , 'editor.tabs.backup': 'Backup' + , 'backup.title': 'Backup' + , 'backup.desc': 'Copia programmi e impostazioni nel backup e ripristinali al bisogno.' + , 'backup.now': 'Esegui backup ora' + , 'backup.restore': 'Ripristina' + , 'backup.delete': 'Elimina backup' + , 'backup.delete.confirm': 'Eliminare il backup selezionato?' + , 'backup.new': 'Nuovo backup' + , 'backup.auto': 'Backup automatico' + , 'backup.auto.desc': 'Esegui backup automatici pianificati.' + , 'backup.interval': 'Intervallo (giorni)' + , 'backup.interval.desc': 'Numero di giorni tra i backup automatici (min. 1).' + , 'backup.sensor': 'Sensore di backup' + , 'backup.last': 'Ultimo backup' + , 'backup.slot.title': 'Seleziona backup' + , 'backup.slot.desc': 'Seleziona quale slot di backup ripristinare.' + , 'backup.slot.empty': 'Nessun backup ancora' + , 'backup.select': 'Scegli cosa includere nel backup' + , 'backup.select.desc': 'Gli elementi selezionati sono inclusi nel backup/ripristino' + , 'backup.opt.all': 'Tutto' + , 'backup.opt.main': 'Programmi principali' + , 'backup.opt.weekday': 'Programmi feriali' + , 'backup.opt.presence': 'Programmi presenza' + , 'backup.opt.settings': 'Impostazioni editor' + , 'backup.opt.holiday': 'Programmi festività' + , 'backup.opt.colors': 'Blocchi colore' + , 'holidays.enable': 'Abilita festività' + , 'holidays.desc': 'Usa un programma giornaliero speciale nei giorni festivi.' + , 'holidays.source': 'Fonte festività' + , 'holidays.source.calendar': 'Calendario' + , 'holidays.source.manual': 'Date manuali' + , 'holidays.entity': 'Entità festività' + , 'holidays.entity_desc': 'Seleziona un calendario che segnala solo acceso/spento. Deve essere acceso nei giorni festivi e spento negli altri.' + , 'holidays.dates': 'Date festività' + , 'holidays.add_date': 'Aggiungi data' + , 'holidays.editor.title': 'Programma festività' + , 'holidays.edit': 'Modifica programma festività' + , 'holidays.button': 'Festività' + , 'presence.button': 'Programmi presenza' + , 'presence.editor.title': 'Programmi presenza' + , 'presence.enable_advanced': 'Abilita pianificazioni di presenza avanzate' + , 'presence.enable_advanced.desc': 'Usa combinazioni e pianificazioni in base a chi è in casa/fuori.' + , 'away.delay_enable': 'Ritarda azioni di assenza' + , 'away.delay_desc': 'Attendi prima di applicare i cambi di assenza.' + , 'away.delay_value': 'Ritardo' + , 'away.delay_unit': 'Unità' + , 'away.delay_unit.seconds': 'Secondi' + , 'away.delay_unit.minutes': 'Minuti' + , 'presence.live_header': 'Mostra presenza live nell\'intestazione' + , 'presence.live_header.desc': 'Le chip vengono visualizzate nell’angolo destro.' + , 'presence.combos': 'Combinazioni' + , 'presence.enable_combo': 'Abilita questa combinazione' + , 'presence.all_home': 'Tutti a casa' + , 'presence.none_home': 'Nessuno a casa' + , 'presence.home': 'A casa: {names}' + , 'presence.away': 'Fuori: {names}' + , 'presence.manage': 'Gestisci combinazioni' + , 'presence.who_home': 'Chi è a casa' + , 'presence.who_away': 'Chi non è a casa' + , 'err.config_entities': "La configurazione deve avere 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Video non trovato' + , 'onboard.image_not_found': 'Immagine non trovata' + , 'pause.prompt_minutes': 'Inserisci la durata (minuti):' + , 'editor.test_tool': 'Strumento di test' + , 'editor.test_select_entity': "Seleziona un'entità" + , 'editor.test_empty': 'Niente da esportare' + , 'editor.test_export': 'Esporta' + , 'editor.test_no_fields': 'Nessun campo da mostrare' + , 'editor.test_select_placeholder': 'Seleziona...' + , 'editor.error_prefix': 'Errore: ' + , 'backup.restore.confirm_overwrite': 'Ripristinare da backup? Questo sovrascriverà pianificazioni e impostazioni correnti.' + , 'backup.import.err_no_hass': 'Home Assistant non è pronto' + , 'backup.import.err_storage_off': "L'archiviazione condivisa è DISATTIVATA. Attivala prima." + , 'backup.import.err_no_integration': "L'integrazione thermostat_timeline non è disponibile" + , 'backup.import.err_json': 'JSON non valido' + , 'profiles.name_exists': 'Il nome esiste già' + }, + fi: { + 'card.title_default': 'Termostaatin aikajana', + 'ui.edit_block': 'Muokkaa lohkoa', + 'ui.add_block': 'Lisää lohko', + 'ui.from': 'Alkaen', + 'ui.to': 'Asti', + 'ui.temperature': 'Lämpötila', + 'ui.delete': 'Poista', + 'ui.cancel': 'Peruuta', + 'ui.close': 'Sulje', + 'ui.unsaved_confirm': 'Sinulla on tallentamattomia muutoksia. Suljetaanko silti?', + 'ui.discard_changes': 'Hylkää muutokset', + 'ui.keep_editing': 'Jatka muokkausta', + 'ui.save': 'Tallenna', + 'ui.error_prefix': 'Virhe: ', + 'ui.overlap_msg': 'Valitsit päällekkäisiä aikoja — ne menevät päällekkäin välillä {start} – {end}.', + 'ui.auto_fix': 'Korjaa automaattisesti', + 'ui.fix_start_to': 'Korjaa alku ajaksi {time}', + 'ui.fix_end_to': 'Korjaa loppu ajaksi {time}', + // Inline meta + 'ui.current_temp': 'Nykyinen lämpötila:', + + 'editor.title_label': 'Otsikko', + 'editor.add_entity': 'Lisää huone', + 'editor.entity_placeholder': 'Valitse entiteetti', + 'editor.drag_reorder': 'Vedä järjestelläksesi', + 'editor.remove': 'Poista', + 'editor.default_c': 'Oletus °C', + 'editor.row_height_px': 'Rivin korkeus (px)', + 'editor.max_c': 'Maksi °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Automaattinen käyttö', + 'editor.auto_apply.desc': 'Kun käytössä, kortti yrittää asettaa valittujen climate-entiteettien lämpötilan vastaamaan suunniteltua arvoa tälle hetkelle.', + 'editor.apply_edit.title': 'Muokkauksen yhteydessä', + 'editor.apply_edit.desc': 'Kun muutat aikajanan lohkoja ja muutos koskee nykyhetkeä, uusi lämpötila otetaan käyttöön heti.', + 'editor.apply_default.title': 'Kun Oletus °C muuttuu', + 'editor.apply_default.desc': 'Kun ”Oletus °C” muuttuu ja se vaikuttaa meneillään olevaan ajanjaksoon, uusi lämpötila otetaan käyttöön heti.', + + 'editor.show_room_temp.title': 'Näytä huonelämpötila', + 'editor.show_room_temp.desc': 'Näyttää nykyisen huonelämpötilan kuplan jokaisella rivillä aikajanan otsikossa.', + 'editor.room_mode.title': 'Huoneen entiteettityyppi', + 'editor.room_mode.desc': 'Kun käytössä, valitse input_number (tavoitelämpötila). Kun pois päältä, valitse climate-entiteetti.', + + 'editor.room_temp_sensor_override.title': 'Käytä huonelämpötila-anturia', + 'editor.room_temp_sensor_override.desc': 'Kun käytössä, näytetty huonelämpötila (ja kattilalogiikka) käyttää valittua lämpötila-anturia climate current_temperature -arvon sijaan.', + 'editor.room_temp_sensor_override.entity': 'Lämpötila-anturi', + 'ui.copy_plan': 'Kopioi aikataulu', + 'ui.paste_plan': 'Liitä aikataulu', + 'ui.copy_day': 'Kopioi päivä', + 'ui.paste_day': 'Liitä päivä', + 'editor.merge_label': 'Yhdistä (lisää toinen termostaatti)', + 'editor.display_name_optional': 'Huoneen nimi (valinnainen)', + 'editor.merged_with': 'Yhdistetty kohteeseen', + 'editor.store_enable.title': 'Jaettu tallennus + taustasäätö', + 'editor.store_enable.short': 'Tallentaa aikataulut valittuun sensoriin ja ottaa käyttöön taustasäädön.', + 'editor.store_enable.desc': 'Tallentaa aikataulut valittuun sensoriin ja ottaa käyttöön taustalla tapahtuvan set_temperature‑ohjauksen. Termostaatit päivittyvät myös, kun kortti ei ole auki. Jakaa myös aikataulut (lämpöblokit) ja kortin asetukset käyttäjien/laitteiden välillä. Kun pois päältä: komennot lähetetään vain, kun kortti on näkyvissä laitteella (tabletti/tietokone/puhelin) ja sen on pysyttävä auki.', + 'editor.store_missing': 'Integraatiota "Thermostat Pro Timeline Sync" ei ole asennettu. Jaettu tallennus ei ole käytettävissä.', + 'editor.clear_store': 'Tyhjennä tallennus', + 'editor.clear_store_confirm': 'Tämä poistaa kaikki tallennetut aikataulut. Jatketaanko?', + 'editor.migrate_to_store': 'Siirrä tiedot tallennukseen', + 'editor.migrate_confirm': 'Siirretäänkö selaimeen tallennetut aikataulut valittuun sensoriin? Tämä korvaa nykyiset aikataulut tallennuksessa.', + 'editor.migrate_pull_confirm': 'Tuo aikataulut tallennuksesta ja korvaa paikalliset selaintiedot?', + 'editor.select_storage_first': 'Valitse ensin tallennussensori.', + 'editor.no_local_data': 'Paikallisia selaintietoja ei löytynyt.', + 'editor.clear_all': 'Tyhjennä kaikki tiedot', + 'editor.clear_all_confirm': 'Tämä poistaa sekä tallennuksesta että selaimesta aikataulut. Jatketaanko?', + 'editor.clear_storage_only': 'Tyhjennä vain tallennusdata', + 'editor.clear_storage_only_confirm': 'Tämä poistaa aikataulut valitusta sensorista. Jatketaanko?', + 'editor.clear_local_only': 'Tyhjennä vain paikalliset tiedot', + 'editor.clear_local_only_confirm': 'Tämä poistaa vain selaimeen tallennetut aikataulut. Jatketaanko?', + 'editor.reset.desc': 'Mitä tämä tekee:\n\n• Tyhjennä kaikki tiedot: Nollaa Thermostat Timeline -integraation backend-tallennuksen (.storage) ja poistaa KAIKKI paikalliset välimuistit/luonnokset tältä selaimelta.\n• Tyhjennä vain paikalliset tiedot: Tyhjentää vain tässä selaimessa tallennetut tiedot (localStorage).\n\nVinkki: Täyden resetoinnin jälkeen lataa sivu uudelleen, jotta vanhat luonnokset eivät ilmesty takaisin.', + 'week.enable': 'Ota viikonpäivät käyttöön', + 'week.mode': 'Viikkotila', + 'week.mode.same_all': 'Sama joka päivä (1)', + 'week.mode.weekday_weekend': 'Arkipäivät + Viikonloppu', + 'week.mode.weekday_sat_sun': 'Arkipäivät + Lauantai + Sunnuntai', + 'week.mode.all_7': 'Kaikki päivät', + 'week.button': 'Viikonpäivät', + 'week.days.mon': 'Ma', + 'week.days.tue': 'Ti', + 'week.days.wed': 'Ke', + 'week.days.thu': 'To', + 'week.days.fri': 'Pe', + 'week.days.sat': 'La', + 'week.days.sun': 'Su', + 'week.editor.title': 'Viikkosuunnitelma', + 'week.editor.preview_note': 'Vain esikatselu. Muutokset otetaan käyttöön, kun painat Tallenna.', + 'week.open_editor.desc': 'Avaa viikonpäiväeditori.', + 'week.open_editor.btn': 'Avaa', + 'week.copy_to_rooms': 'Kopioi viikkosuunnitelma huoneisiin', + 'week.copy_to_rooms.title': 'Kopioi viikkosuunnitelma huoneisiin', + 'week.copy_to_rooms.select': 'Valitse huoneet', + 'week.copy_to_rooms.apply': 'Lisää huoneisiin', + 'week.group.weekdays': 'Arkipäivät', + 'week.group.weekend': 'Viikonloppu', + 'week.days_full.mon': 'Maanantai', + 'week.days_full.tue': 'Tiistai', + 'week.days_full.wed': 'Keskiviikko', + 'week.days_full.thu': 'Torstai', + 'week.days_full.fri': 'Perjantai', + 'week.days_full.sat': 'Lauantai', + 'week.days_full.sun': 'Sunnuntai', + 'editor.timefmt.title': 'Aikamuoto', + 'editor.timefmt.desc': 'Valitse 24 tunnin tai 12 tunnin (AM/PM) näyttö kortin ajoille.', + 'editor.tempfmt.title': 'Lämpötilayksikkö', + 'editor.tempfmt.desc': 'Valitse Celsius (°C) tai Fahrenheit (°F) kortin ja editorien näyttöön.', + 'editor.timesrc.title': 'Ajan lähde', + 'editor.timesrc.desc': 'Valitse käytetäänkö “nyt”‑hetkeen selaimen aikaa vai Home Assistantin aikavyöhykettä.', + 'editor.timesrc.browser': 'Selain', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'Kokoonpano-ID', + 'editor.instance.desc': 'Pidä useita erillisiä aikatauluja/asetuksia eri tunnisteilla.', + 'editor.instance.new_id': 'Uusi ID', + 'editor.heat_colors.title': 'Lämmityslohkojen värit', + 'editor.heat_colors.add': 'Lisää värihaarukka', + 'editor.colors.col_from': 'Alkaen °C', + 'editor.colors.col_to': 'Asti °C', + 'editor.colors.col_color': 'Väri', + 'editor.colors.empty': 'Huoneita ei ole lisätty vielä. Lisää huone määrittääksesi värit.', + 'editor.colors.go_add': 'Lisää huone', + 'editor.colors.refresh': 'Päivitä aikataulu', + 'editor.colors.clear_local': 'Tyhjennä paikalliset väritiedot', + 'editor.colors.clear_local_confirm': 'Tämä poistaa tähän korttiin selaimeen tallennetut värihaarukat. Jatketaanko?', + 'editor.colors.clear_shared': 'Tyhjennä jaettu värivarasto', + 'editor.colors.clear_shared_confirm': 'Tämä poistaa värihaarukat valitusta tallennussensorista. Jatketaanko?', + 'editor.colors.mode': 'Väritila', + 'editor.colors.mode.desc': 'Valitse värit huoneittain tai yksi yhteinen kaikille.', + 'editor.colors.mode_per_room': 'Huoneittain', + 'editor.colors.mode_global': 'Kaikille', + 'editor.colors.global_title': 'Kaikki huoneet', + 'editor.colors.clear_all': 'Tyhjennä kaikki väritiedot', + 'editor.colors.clear_all_confirm': 'Tämä poistaa kaikki värihaarukat sekä paikallisesti että valitusta tallennussensorista. Jatketaanko?', + 'editor.reload_editor': 'Lataa editori uudelleen', + 'editor.reload_dashboard': 'Lataa koontinäyttö uudelleen', + 'onboard.title': 'Tervetuloa Termostaatin aikajanaan', + 'onboard.next': 'Seuraava', + 'onboard.prev': 'Edellinen', + 'onboard.skip': 'Ohita', + 'onboard.done': 'Valmis', + 'onboard.dont_show': 'Älä näytä uudelleen', + 'onboard.step_main': 'Tämä on pääaikajana. Se näyttää toivotut lämpötilat 24 tunnin aikana huoneittain.', + 'onboard.step_add': 'Lisää lämpölohko painamalla + Lisää lohko tai kaksoisnapsauttamalla aikajanaa.', + 'onboard.step_edit': 'Muokkaa tai poista lohko kaksoisnapsauttamalla. Käytä viikonpäiviä eri päiville.', + 'onboard.reset': 'Näytä opas uudelleen', + 'onboard.reset_ok': 'Tervetuloa-näyttö avautuu seuraavalla kerralla.', + 'editor.select_entities': 'Huoneet', + 'editor.tabs.rooms': 'Huoneet', + 'editor.test_tool': 'Testityökalu', + 'editor.test_select_entity': 'Valitse entiteetti', + 'editor.test_empty': 'Valitse entiteetti nähdäksesi käytettävissä olevat komennot…', + 'editor.test_export': 'Vie .md', + 'editor.test_no_fields': 'Ei kenttiä', + 'editor.test_select_placeholder': '— Valitse —', + 'editor.error_prefix': 'Virhe: ', + 'editor.open_in_hacs': 'Avaa HACSissa', + 'week.enable.desc': 'Ota viikonpäiväkohtainen aikataulu käyttöön. Kun pois päältä, käytetään samaa aikataulua joka päivä.', + 'editor.tabs.sync': 'Synkronointi‑moottori', + 'sync.running': 'Synkronointi käynnissä…', + 'sync.completed': 'Synkronointi valmis', + 'editor.tabs.backup': 'Varmuuskopio', + 'backup.title': 'Varmuuskopio', + 'backup.desc': 'Kopioi sync‑anturin sisältö varmuuskopioanturiin ja palauta tarvittaessa.', + 'backup.now': 'Varmuuskopioi nyt', + 'backup.restore': 'Palauta', + 'backup.delete': 'Poista varmuuskopio', + 'backup.delete.confirm': 'Poistetaanko valittu varmuuskopio?', + 'backup.new': 'Uusi varmuuskopio', + 'backup.auto': 'Automaattinen varmuuskopiointi', + 'backup.auto.desc': 'Tee varmuuskopioita automaattisesti tietyin välein.', + 'backup.interval': 'Väli (päivää)', + 'backup.interval.desc': 'Kuinka monta päivää automaattisten varmuuskopioiden välillä (min. 1).', + 'backup.sensor': 'Varmuuskopioanturi', + 'backup.last': 'Viimeisin varmuuskopio', + 'backup.slot.title': 'Valitse varmuuskopio', + 'backup.slot.desc': 'Valitse mikä varmuuskopiointipaikka palautetaan.', + 'backup.slot.empty': 'Ei varmuuskopioita vielä', + 'pause.menu_title': 'Keskeytä automaattinen asetus', + 'pause.off_1h': 'Keskeytä 1 tunniksi', + 'pause.off_5h': 'Keskeytä 5 tunniksi', + 'pause.off_custom': 'Mukautettu kesto…', + 'pause.off_indef': 'Keskeytä kunnes jatkan', + 'pause.resume': 'Jatka nyt', + 'pause.close': 'Sulje', + 'pause.active_until': 'Keskeytetty asti {time}', + 'pause.active_indef': 'Keskeytetty', + 'editor.pausebtn.title': 'Taukopainike', + 'editor.pausebtn.desc': 'Näytä globaali taukopainike kortin yläreunassa. Tauon aikana kaikki set_temperature‑komennot estetään.', + 'editor.integration.connected': 'Integraatio valmis', + 'editor.integration.disconnected': 'Integraatio ei käytettävissä' + , 'profiles.button': 'Manuaalinen aikataulu' + , 'profiles.editor.title': 'Profiilit' + , 'profiles.add': 'Lisää profiili' + , 'profiles.rename': 'Nimeä uudelleen' + , 'profiles.delete': 'Poista' + , 'profiles.activate': 'Aktivoi' + , 'profiles.deactivate': 'Poista käytöstä' + , 'profiles.active': 'Aktiivinen' + , 'profiles.none': 'Ei mikään' + , 'profiles.name_placeholder': 'Profiilin nimi' + , 'profiles.edit': 'Muokkaa' + , 'profiles.preview_note': 'Vain esikatselu. Aikataulut aktivoituvat, kun aktivoit profiilin.' + , 'editor.tabs.holidays': 'Pyhäpäivät' + , 'holidays.enable': 'Ota pyhäpäivät käyttöön' + , 'holidays.desc': 'Käytä erityistä päiväaikataulua pyhäpäivinä.' + , 'holidays.source': 'Pyhäpäivien lähde' + , 'holidays.source.calendar': 'Kalenteri' + , 'holidays.source.manual': 'Manuaaliset päivämäärät' + , 'holidays.entity': 'Pyhäpäivä‑entiteetti' + , 'holidays.entity_desc': 'Valitse kalenteri, joka ilmoittaa vain on/off. Sen tulee olla on pyhäpäivinä ja off muina päivinä.' + , 'holidays.dates': 'Pyhäpäivien päivämäärät' + , 'holidays.add_date': 'Lisää päivämäärä' + , 'holidays.editor.title': 'Pyhäpäivä‑aikataulu' + , 'holidays.edit': 'Muokkaa pyhäpäivä‑aikataulua' + , 'holidays.button': 'Pyhäpäivät' + , 'editor.perroom.title': 'Oletus °C huoneittain' + , 'editor.perroom.desc': 'Näytä oletus °C -kenttä jokaisella rivillä. Kun pois päältä, käytetään globaalia oletus °C:ta kaikissa huoneissa.' + , 'away.resume_schedule': 'Jatka aikataulua' + , 'away.resume_away': 'Jatka poissaololämpötilaa' + , 'presence.button': 'Läsnäoloaikataulut' + , 'presence.editor.title': 'Läsnäoloaikataulut' + , 'presence.enable_advanced': 'Ota käyttöön kehittyneet läsnäoloaikataulut' + , 'presence.enable_advanced.desc': 'Käytä yhdistelmiä ja aikatauluja sen mukaan kuka on kotona/poissa.' + , 'away.delay_enable': 'Viivästytä poissaolo‑toimia' + , 'away.delay_desc': 'Odotetaan ennen poissaolomuutosten soveltamista.' + , 'away.delay_value': 'Viive' + , 'away.delay_unit': 'Yksikkö' + , 'away.delay_unit.seconds': 'Sekunnit' + , 'away.delay_unit.minutes': 'Minuutit' + , 'presence.combos': 'Yhdistelmät' + , 'presence.enable_combo': 'Ota tämä yhdistelmä käyttöön' + , 'presence.all_home': 'Kaikki kotona' + , 'presence.none_home': 'Ei ketään kotona' + , 'presence.home': 'Kotona: {names}' + , 'presence.away': 'Poissa: {names}' + , 'presence.manage': 'Hallinnoi yhdistelmiä' + , 'presence.who_home': 'Kuka on kotona' + , 'presence.who_away': 'Kuka ei ole kotona' + , 'away.enable': 'Ota poissaolotila käyttöön' + , 'away.desc': 'Kun yksikään valituista henkilöistä ei ole kotona, termostaatit lasketaan automaattisesti asetettuun lämpötilaan.' + , 'away.temp': 'Poissaololämpötila' + , 'away.persons': 'Henkilöt' + , 'away.add_person': 'Lisää henkilö' + , 'backup.select': 'Valitse, mitä varmuuskopioidaan' + , 'backup.select.desc': 'Valitut kohteet sisällytetään varmuuskopiointiin/palautukseen' + , 'backup.opt.all': 'Kaikki' + , 'backup.opt.main': 'Pääaikataulut' + , 'backup.opt.weekday': 'Viikonpäiväaikataulut' + , 'backup.opt.presence': 'Läsnäoloaikataulut' + , 'backup.opt.settings': 'Editorin asetukset' + , 'backup.opt.holiday': 'Pyhäpäiväaikataulut' + , 'backup.opt.colors': 'Värilohkot' + , 'editor.tabs.settings': 'Asetukset' + , 'editor.tabs.pause': 'Tauko' + , 'editor.tabs.weekdays': 'Viikonpäivät' + , 'editor.tabs.colors': 'Värit' + , 'editor.tabs.presence_sensor': 'Läsnäoloanturi' + , 'editor.presence_sensor.enable.title': 'Läsnäoloanturi' + , 'editor.presence_sensor.enable.desc': 'Ota käyttöön läsnäoloanturin määritys huoneittain.' + , 'editor.presence_sensor.entity.title': 'Läsnäoloentiteetti' + , 'editor.presence_sensor.entity.desc': 'Valitse binary_sensor, joka ilmaisee läsnäolon tässä huoneessa.' + , 'editor.presence_sensor.on_temp.title': 'Lämpötila, kun läsnäolo on PÄÄLLÄ' + , 'editor.presence_sensor.on_temp.desc': 'Jos sensori on PÄÄLLÄ, tämä huone käyttää tätä lämpötilaa aikataulun sijaan.' + , 'editor.presence_sensor.on_delay.title': 'Viive ennen läsnäolon käyttöä' + , 'editor.presence_sensor.on_delay.desc': 'Kuinka kauan sensorin täytyy olla PÄÄLLÄ ennen kuin läsnäololämpötila otetaan käyttöön. Arvo käyttää valittua yksikköä.' + , 'editor.presence_sensor.off_delay.title': 'Viive ennen aikatauluun palaamista' + , 'editor.presence_sensor.off_delay.desc': 'Kuinka kauan sensorin täytyy olla POIS PÄÄLTÄ ennen kuin aikataulu palautuu. Arvo käyttää valittua yksikköä.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Poissa kotoa' + , 'editor.tabs.reset': 'Palauta' + , 'editor.tabs.boiler': 'Kattilan ohjaus' + , 'boiler.switch': 'Kattilakytkin' + , 'boiler.switch_type': 'Entiteettityyppi' + , 'boiler.switch_type.switch': 'Kytkin' + , 'boiler.switch_type.input_boolean': 'Input boolean' + , 'boiler.rooms': 'Mukana olevat huoneet' + , 'boiler.rooms.desc': 'Valitse, mitkä huoneet otetaan mukaan kattilan ohjaukseen.' + , 'boiler.enable': 'Ota kattilan ohjaus käyttöön' + , 'boiler.enable.desc': 'Näytä kattila-asetukset tämän kortin editorissa.' + , 'boiler.temp_sensor': 'Kattilan lämpötila‑anturi' + , 'boiler.temp_sensor.desc': 'Lämpötila‑anturi, joka ohjaa kattilakytkintä.' + , 'boiler.min_temp': 'Kattilan minimi (°C)' + , 'boiler.max_temp': 'Kattilan maksimi (°C)' + , 'boiler.offsets': 'Offsetit' + , 'boiler.offsets.desc': 'ON‑offset: kuinka paljon alle aikataulun tavoitteen ennen kuin kytkeytyy päälle. OFF‑offset: kuinka paljon yli aikataulun tavoitteen ennen kuin kytkeytyy pois.' + , 'boiler.on_offset': 'ON offset (°C)' + , 'boiler.off_offset': 'OFF offset (°C)' + , 'editor.temp_sensor': 'Lämpötila‑anturi' + , 'presence.live_header': 'Näytä live‑läsnäolo yläreunassa' + , 'presence.live_header.desc': 'Siruja näytetään oikeassa kulmassa.' + , 'profiles.enable': 'Ota profiilit käyttöön (ohitus)' + , 'profiles.enable.desc': 'Salli nimettyjä päiväaikatauluja, jotka voit aktivoida ohittaaksesi pääsuunnitelman.' + , 'profiles.active_label': 'Manuaalinen profiili aktiivinen:' + , 'profiles.save_new': 'Tallenna uusi profiili' + , 'profiles.save_existing': 'Tallenna muutokset valittuun profiiliin' + , 'week.view.title': 'Viikkonäkymä' + , 'week.view.rooms_one_day': 'Kaikki huoneet • yksi päivä' + , 'week.view.days_one_room': 'Kaikki päivät • yksi huone' + , 'week.view_switch.title': 'Vaihda näkymää aikajanasta' + , 'week.view_switch.desc': 'Näyttää valitsimen aikajanan otsikossa, jolla voi vaihtaa kahden viikonpäivänäkymän välillä.' + , 'week.title': 'Viikonpäivät' + , 'week.select_room': 'Valitse huone' + , 'err.config_entities': "Määrityksessä täytyy olla 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Videota ei löytynyt' + , 'onboard.image_not_found': 'Kuvaa ei löytynyt' + , 'pause.prompt_minutes': 'Anna kesto (minuuttia):' + , 'editor.test_tool': 'Testityökalu' + , 'editor.test_select_entity': 'Valitse entiteetti' + , 'editor.test_empty': 'Ei mitään vietävää' + , 'editor.test_export': 'Vie' + , 'editor.test_no_fields': 'Ei kenttiä näytettäväksi' + , 'editor.test_select_placeholder': 'Valitse...' + , 'editor.error_prefix': 'Virhe: ' + , 'backup.restore.confirm_overwrite': 'Palautetaanko varmuuskopiosta? Tämä korvaa nykyiset aikataulut ja asetukset.' + , 'backup.import.err_no_hass': 'Home Assistant ei ole valmis' + , 'backup.import.err_storage_off': 'Jaettu tallennus on POIS PÄÄLTÄ. Ota se ensin käyttöön.' + , 'backup.import.err_no_integration': 'Integraatio thermostat_timeline ei ole käytettävissä' + , 'backup.import.err_json': 'Virheellinen JSON' + , 'profiles.name_exists': 'Nimi on jo olemassa' + }, + cs: { + // Karta + 'card.title_default': 'Časová osa termostatu', + // Modal / editor (inline) + 'ui.edit_block': 'Upravit blok', + 'ui.add_block': 'Přidat blok', + 'ui.from': 'Od', + 'ui.to': 'Do', + 'ui.temperature': 'Teplota', + 'ui.delete': 'Smazat', + 'ui.cancel': 'Zrušit', + 'ui.close': 'Zavřít', + 'ui.unsaved_confirm': 'Máte neuložené změny. Opravdu zavřít?', + 'ui.discard_changes': 'Zahodit změny', + 'ui.keep_editing': 'Pokračovat v úpravách', + 'ui.save': 'Uložit', + 'ui.error_prefix': 'Chyba: ', + 'ui.overlap_msg': 'Vybrali jste překrývající se časy — překrývá se mezi {start} – {end}.', + 'ui.auto_fix': 'Opravit automaticky', + 'ui.fix_start_to': 'Upravit začátek na {time}', + 'ui.fix_end_to': 'Upravit konec na {time}', + // Inline meta + 'ui.current_temp': 'Aktuální teplota:', + + // GUI editor panel + 'editor.title_label': 'Nadpis', + 'editor.add_entity': 'Přidat místnost', + 'editor.entity_placeholder': 'Vyberte entitu', + 'editor.room_mode.title': 'Typ entity místnosti', + 'editor.room_mode.desc': 'Když je zapnuto, vyberte input_number (cílová teplota). Když je vypnuto, vyberte entitu climate.', + + 'editor.room_temp_sensor_override.title': 'Použít senzor teploty v místnosti', + 'editor.room_temp_sensor_override.desc': 'Je-li zapnuto, zobrazená teplota v místnosti (a logika kotle) používá vybraný teplotní senzor místo climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Senzor teploty', + 'editor.drag_reorder': 'Přetažením změňte pořadí', + 'editor.remove': 'Odebrat', + 'editor.default_c': 'Výchozí °C', + 'editor.row_height_px': 'Výška řádku (px)', + 'editor.max_c': 'Max °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Automatické použití', + 'editor.auto_apply.desc': 'Je-li zapnuto, karta se pokusí nastavit teplotu vybraných climate entit tak, aby odpovídala plánované hodnotě pro „právě teď“.', + 'editor.apply_edit.title': 'Při úpravě', + 'editor.apply_edit.desc': 'Když změníte bloky v časové ose a změna ovlivní aktuální čas, nová teplota se použije okamžitě.', + 'editor.apply_default.title': 'Při změně Výchozí °C', + 'editor.apply_default.desc': 'Když se změní „Výchozí °C“ a ovlivní aktuální období, nová teplota se použije okamžitě.', + + 'editor.show_room_temp.title': 'Zobrazit teplotu v místnosti', + 'editor.show_room_temp.desc': 'Zobrazí bublinu s aktuální teplotou v místnosti na každém řádku v záhlaví časové osy.', + 'ui.copy_plan': 'Kopírovat plán', + 'ui.paste_plan': 'Vložit plán', + 'ui.copy_day': 'Kopírovat den', + 'ui.paste_day': 'Vložit den', + 'editor.merge_label': 'Sloučit s (přidat další termostat)', + 'editor.display_name_optional': 'Název místnosti (volitelné)', + 'editor.merged_with': 'Sloučeno s', + 'editor.store_enable.title': 'Sdílené úložiště + řízení na pozadí', + 'editor.store_enable.short': 'Ukládá rozvrhy do zvoleného senzoru a umožňuje řízení na pozadí.', + 'editor.store_enable.desc': 'Ukládá rozvrhy do zvoleného senzoru a zapíná set_temperature na pozadí přes integraci Thermostat Timeline. Termostaty se aktualizují i když karta není otevřená. Také sdílí rozvrhy (tepelné bloky) a nastavení karty mezi uživateli/zařízeními. Když je vypnuto, příkazy se posílají jen když je karta aktivně viditelná na zařízení (tablet/PC/mobil) — musí zůstat otevřená na obrazovce.', + 'editor.store_missing': 'Integrace „Thermostat Pro Timeline Sync“ není nainstalována. Sdílené úložiště není k dispozici.', + 'editor.clear_store': 'Vymazat úložiště', + 'editor.clear_store_confirm': 'Tímto smažete všechny uložené rozvrhy. Pokračovat?', + 'editor.migrate_to_store': 'Převést data do úložiště', + 'editor.migrate_confirm': 'Převést vaše rozvrhy uložené v prohlížeči do zvoleného senzoru? Tím se přepíší existující rozvrhy v úložišti.', + 'editor.migrate_pull_confirm': 'Importovat rozvrhy z úložiště a přepsat lokální data v prohlížeči?', + 'editor.select_storage_first': 'Nejprve vyberte senzor úložiště.', + 'editor.no_local_data': 'Nebyla nalezena žádná lokální data v prohlížeči.', + 'editor.clear_all': 'Smazat všechna data', + 'editor.clear_all_confirm': 'Tímto smažete rozvrhy jak v úložišti, tak v lokálním prohlížeči. Pokračovat?', + 'editor.clear_storage_only': 'Smazat jen data v úložišti', + 'editor.clear_storage_only_confirm': 'Tímto smažete rozvrhy z vybraného senzoru úložiště. Pokračovat?', + 'editor.clear_local_only': 'Smazat jen lokální data', + 'editor.clear_local_only_confirm': 'Tímto smažete pouze rozvrhy uložené ve vašem prohlížeči. Pokračovat?', + 'editor.reset.desc': 'Co to dělá:\n\n• Smazat všechna data: Resetuje backend úložiště (.storage) pro Thermostat Timeline a smaže VŠECHNY lokální cache/návrhy v tomto prohlížeči.\n• Smazat jen lokální data: Smaže pouze data uložená v tomto prohlížeči (localStorage).\n\nTip: Po plném resetu stránku obnovte, aby se znovu neobjevily staré návrhy.', + 'editor.temp_sensor': 'Teplotní senzor', + + // Týdny + 'week.enable': 'Povolit pracovní dny', + 'week.mode': 'Režim dní v týdnu', + // removed same_all option from UI + 'week.mode.weekday_weekend': 'Pracovní dny + Víkend', + 'week.mode.weekday_sat_sun': 'Pracovní dny + Sobota + Neděle', + 'week.mode.all_7': 'Všechny dny', + 'week.button': 'Dny v týdnu', + 'week.group.weekdays': 'Pracovní dny', + 'week.group.weekend': 'Víkend', + 'week.days_full.mon': 'Pondělí', + 'week.days_full.tue': 'Úterý', + 'week.days_full.wed': 'Středa', + 'week.days_full.thu': 'Čtvrtek', + 'week.days_full.fri': 'Pátek', + 'week.days_full.sat': 'Sobota', + 'week.days_full.sun': 'Neděle', + 'week.days.mon': 'Po', + 'week.days.tue': 'Út', + 'week.days.wed': 'St', + 'week.days.thu': 'Čt', + 'week.days.fri': 'Pá', + 'week.days.sat': 'So', + 'week.days.sun': 'Ne', + 'week.editor.title': 'Týdenní rozvrh', + 'week.editor.preview_note': 'Pouze náhled. Změny se použijí po stisknutí Uložit.', + 'week.open_editor.desc': 'Otevřít editor dnů v týdnu.', + 'week.open_editor.btn': 'Otevřít', + 'week.copy_to_rooms': 'Kopírovat týden do místností', + 'week.copy_to_rooms.title': 'Kopírovat týden do místností', + 'week.copy_to_rooms.select': 'Vyberte místnosti', + 'week.copy_to_rooms.apply': 'Vložit do místností', + 'week.enable.desc': 'Povolit plán podle dní v týdnu. Když je vypnuto, používá se stejný plán každý den.', + + // Formáty + 'editor.timefmt.title': 'Formát času', + 'editor.timefmt.desc': 'Zvolte 24hodinový nebo 12hodinový (AM/PM) formát pro časy na kartě.', + 'editor.tempfmt.title': 'Jednotka teploty', + 'editor.tempfmt.desc': 'Zvolte Celsia (°C) nebo Fahrenheit (°F) pro zobrazení na kartě a v editorech.', + 'editor.timesrc.title': 'Zdroj času', + 'editor.timesrc.desc': 'Zvolte, zda má karta používat čas prohlížeče nebo časové pásmo Home Assistantu pro „nyní“.', + 'editor.timesrc.browser': 'Prohlížeč', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'ID konfigurace', + 'editor.instance.desc': 'Udržujte více nezávislých plánů/nastavení pod různými ID.', + 'editor.instance.new_id': 'Nové ID', + + // Záložky + 'editor.tabs.settings': 'Nastavení', + 'editor.tabs.pause': 'Pauza', + 'editor.tabs.rooms': 'Místnosti', + 'editor.tabs.boiler': 'Ovládání kotle', + 'boiler.switch': 'Spínač kotle', + 'boiler.switch_type': 'Typ entity', + 'boiler.switch_type.switch': 'Spínač', + 'boiler.switch_type.input_boolean': 'Vstupní boolean', + 'boiler.rooms': 'Zahrnuté místnosti', + 'boiler.rooms.desc': 'Vyberte, které místnosti mají být zahrnuty do ovládání kotle.', + 'editor.tabs.presence_sensor': 'Senzor přítomnosti', + 'boiler.enable': 'Povolit ovládání kotle', + 'boiler.enable.desc': 'Zobrazit nastavení kotle v editoru této karty.', + 'boiler.temp_sensor': 'Teplotní senzor kotle', + 'boiler.temp_sensor.desc': 'Teplotní senzor používaný k ovládání spínače kotle.', + + 'editor.presence_sensor.enable.title': 'Senzor přítomnosti', + 'editor.presence_sensor.enable.desc': 'Povolit konfiguraci senzoru přítomnosti pro jednotlivé místnosti.', + + 'editor.presence_sensor.entity.title': 'Entita přítomnosti', + 'editor.presence_sensor.entity.desc': 'Vyberte binary_sensor, který indikuje přítomnost v této místnosti.', + 'editor.presence_sensor.on_temp.title': 'Teplota při přítomnosti (ZAP)', + 'editor.presence_sensor.on_temp.desc': 'Pokud je senzor ZAP, tato místnost použije tuto teplotu místo rozvrhu.', + 'editor.presence_sensor.on_delay.title': 'Zpoždění před použitím přítomnosti', + 'editor.presence_sensor.on_delay.desc': 'Jak dlouho musí být senzor ZAP, než se použije teplota přítomnosti. Hodnota používá vybranou jednotku.', + 'editor.presence_sensor.off_delay.title': 'Zpoždění před návratem na rozvrh', + 'editor.presence_sensor.off_delay.desc': 'Jak dlouho musí být senzor VYP, než se obnoví rozvrh. Hodnota používá vybranou jednotku.', + 'boiler.min_temp': 'Min. teplota kotle (°C)', + 'boiler.max_temp': 'Max. teplota kotle (°C)', + 'boiler.offsets': 'Offsety', + 'boiler.offsets.desc': 'ON offset: o kolik pod plánovanou teplotu před zapnutím. OFF offset: o kolik nad plánovanou teplotu před vypnutím.', + 'boiler.on_offset': 'ON offset (°C)', + 'boiler.off_offset': 'OFF offset (°C)', + 'editor.test_tool': 'Testovací nástroj', + 'editor.test_select_entity': 'Vybrat entitu', + 'editor.test_empty': 'Vyberte entitu pro zobrazení dostupných příkazů…', + 'editor.test_export': 'Exportovat .md', + 'editor.test_no_fields': 'Žádná pole', + 'editor.test_select_placeholder': '— Vybrat —', + 'editor.error_prefix': 'Chyba: ', + 'editor.open_in_hacs': 'Otevřít v HACS', + 'editor.tabs.colors': 'Barvy', + 'editor.tabs.owd': 'Open Window Detection', + 'owd.enable': 'Open Window Detection', + 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.', + 'owd.sensors': 'Window/Door sensors', + 'owd.delays.title': 'Delays', + 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).', + 'owd.open_delay': 'After open (min)', + 'owd.close_delay': 'After closed (min)', + 'editor.tabs.away': 'Mimo domov', + 'editor.tabs.reset': 'Obnovit', + 'editor.tabs.sync': 'Sync engine', + 'sync.running': 'Probíhá synchronizace…', + 'sync.completed': 'Synchronizace dokončena', + 'editor.tabs.backup': 'Záloha', + 'backup.title': 'Záloha', + 'backup.desc': 'Zkopíruje obsah vašeho sync senzoru do záložního senzoru a v případě potřeby obnoví.', + 'backup.now': 'Zálohovat nyní', + 'backup.restore': 'Obnovit', + 'backup.delete': 'Smazat zálohu', + 'backup.delete.confirm': 'Smazat vybranou zálohu?', + 'backup.new': 'Nová záloha', + 'backup.auto': 'Automatická záloha', + 'backup.auto.desc': 'Provádět automatické zálohy v intervalech.', + 'backup.interval': 'Interval (dny)', + 'backup.interval.desc': 'Počet dnů mezi automatickými zálohami (min. 1).', + 'backup.sensor': 'Záložní senzor', + 'backup.last': 'Poslední záloha', + 'backup.slot.title': 'Vybrat zálohu', + 'backup.slot.desc': 'Vyberte, kterou zálohu obnovit.', + 'backup.slot.empty': 'Zatím žádné zálohy', + + // Away + 'away.enable': 'Povolit režim mimo domov', + 'away.desc': 'Když žádní z vybraných lidí nejsou doma, automaticky snižuje teploty na nastavenou hodnotu.', + 'away.temp': 'Teplota mimo domov', + 'away.persons': 'Osoby', + 'away.add_person': 'Přidat osobu', + + // Barvy tepla + 'editor.heat_colors.title': 'Barvy tepelných bloků', + 'editor.heat_colors.add': 'Přidat rozsah barev', + 'editor.colors.col_from': 'Od °C', + 'editor.colors.col_to': 'Do °C', + 'editor.colors.col_color': 'Barva', + 'editor.colors.empty': 'Zatím nebyly přidány žádné místnosti. Přidejte místnost pro definici barev.', + 'editor.colors.go_add': 'Přidat místnost', + 'editor.select_entities': 'Místnosti', + 'editor.colors.refresh': 'Aktualizovat rozvrh', + 'editor.colors.clear_local': 'Smazat lokální barevná data', + 'editor.colors.clear_local_confirm': 'Tímto smažete barevné rozsahy uložené ve vašem prohlížeči pro tuto kartu. Pokračovat?', + 'editor.colors.clear_shared': 'Smazat sdílené barevné úložiště', + 'editor.colors.clear_shared_confirm': 'Tímto smažete barevné rozsahy ve vybraném senzoru úložiště. Pokračovat?', + 'editor.colors.clear_all': 'Smazat všechna barevná data', + 'editor.colors.clear_all_confirm': 'Tímto smažete všechny barevné rozsahy jak lokálně, tak ve vybraném senzoru úložiště. Pokračovat?', + 'editor.colors.mode': 'Režim barev', + 'editor.colors.mode.desc': 'Zvolte barvy pro každou místnost nebo jeden set pro všechny.', + 'editor.colors.mode_per_room': 'Podle místnosti', + 'editor.colors.mode_global': 'Všechny místnosti', + 'editor.colors.global_title': 'Všechny místnosti', + 'editor.reload_editor': 'Znovu načíst editor', + 'editor.reload_dashboard': 'Znovu načíst dashboard', + + // Onboarding + 'onboard.title': 'Vítejte v Časové ose termostatu', + 'onboard.next': 'Další', + 'onboard.prev': 'Zpět', + 'onboard.skip': 'Přeskočit', + 'onboard.done': 'Hotovo', + 'onboard.dont_show': 'Nezobrazovat znovu', + 'onboard.step_main': 'Toto je hlavní časová osa. Zobrazuje požadované teploty během 24 hodin pro každou místnost.', + 'onboard.step_add': 'Přidejte tepelný blok přes + Přidat blok nebo dvojitým kliknutím na časovou osu.', + 'onboard.step_edit': 'Upravte nebo smažte blok dvojitým kliknutím. Použijte Dny v týdnu pro různé dny.', + 'onboard.reset': 'Zobrazit průvodce znovu', + 'onboard.reset_ok': 'Vítání se zobrazí příště.', + + // Pause + 'pause.menu_title': 'Pozastavit auto‑apply', + 'pause.off_1h': 'Pozastavit na 1 hodinu', + 'pause.off_5h': 'Pozastavit na 5 hodin', + 'pause.off_custom': 'Vlastní délka…', + 'pause.off_indef': 'Pozastavit do obnovení', + 'pause.resume': 'Obnovit nyní', + 'pause.close': 'Zavřít', + 'pause.active_until': 'Pozastaveno do {time}', + 'pause.active_indef': 'Pozastaveno', + 'editor.pausebtn.title': 'Tlačítko pozastavení', + 'editor.pausebtn.desc': 'Zobrazí globální tlačítko pozastavení v záhlaví karty. Během pauzy jsou všechny příkazy set_temperature potlačeny.', + 'editor.integration.connected': 'Integrace připravena', + 'editor.integration.disconnected': 'Integrace není k dispozici', + + // Profily + 'profiles.enable': 'Povolit profily (přepis)', + 'profiles.enable.desc': 'Povolit pojmenované denní rozvrhy, které můžete aktivovat pro přepsání hlavního plánu.', + 'profiles.button': 'Manuální plán', + 'profiles.editor.title': 'Profily', + 'profiles.add': 'Přidat profil', + 'profiles.rename': 'Přejmenovat', + 'profiles.delete': 'Smazat', + 'profiles.activate': 'Aktivovat', + 'profiles.deactivate': 'Deaktivovat', + 'profiles.active': 'Aktivní', + 'profiles.none': 'Žádný', + 'profiles.name_placeholder': 'Název profilu', + 'profiles.edit': 'Upravit', + 'profiles.preview_note': 'Pouze náhled. Rozvrhy se stanou aktivními po aktivaci profilu.', + 'profiles.active_label': 'Manuální profil aktivní:', + 'profiles.save_new': 'Uložit nový profil', + 'profiles.save_existing': 'Uložit změny do vybraného profilu', + + // Svátky + 'editor.tabs.holidays': 'Svátky', + 'holidays.enable': 'Povolit svátky', + 'holidays.desc': 'Použít speciální denní rozvrh o svátcích.', + 'holidays.source': 'Zdroj svátků', + 'holidays.source.calendar': 'Kalendář', + 'holidays.source.manual': 'Manuální data', + 'holidays.entity': 'Entita svátků', + 'holidays.entity_desc': 'Vyberte kalendář, který hlásí pouze on/off. Má být on ve svátky a off v ostatní dny.', + 'holidays.dates': 'Data svátků', + 'holidays.add_date': 'Přidat datum', + 'holidays.editor.title': 'Rozvrh o svátcích', + 'holidays.edit': 'Upravit rozvrh o svátcích', + 'holidays.button': 'Svátky', + 'editor.perroom.title': 'Výchozí °C pro místnost', + 'editor.perroom.desc': 'Zobrazit pole výchozí °C v každém řádku. Při vypnutí se použije globální výchozí °C pro všechny místnosti.', + 'away.resume_schedule': 'Obnovit plán', + 'away.resume_away': 'Obnovit teplotu mimo domov', + 'presence.button': 'Plány přítomnosti', + 'presence.editor.title': 'Plány přítomnosti', + 'presence.enable_advanced': 'Povolit pokročilé rozvrhy přítomnosti', + 'presence.enable_advanced.desc': 'Použijte kombinace a rozvrhy podle toho, kdo je doma/pryč.', + 'away.delay_enable': 'Zpozdit akce Nepřítomnosti', + 'away.delay_desc': 'Před použitím změn nepřítomnosti vyčkejte.', + 'away.delay_value': 'Zpoždění', + 'away.delay_unit': 'Jednotka', + 'away.delay_unit.seconds': 'Sekundy', + 'away.delay_unit.minutes': 'Minuty', + 'presence.combos': 'Kombinace', + 'presence.enable_combo': 'Povolit tuto kombinaci', + 'presence.all_home': 'Všichni doma', + 'presence.none_home': 'Nikdo doma', + 'presence.home': 'Doma: {names}', + 'presence.away': 'Mimo domov: {names}', + 'presence.manage': 'Spravovat kombinace', + 'presence.who_home': 'Kdo je doma', + 'presence.who_away': 'Kdo není doma' + , 'backup.select': 'Vyberte, co zálohovat' + , 'backup.select.desc': 'Zaškrtnuté položky jsou zahrnuty do zálohy/obnovy' + , 'backup.opt.all': 'Vše' + , 'backup.opt.main': 'Hlavní rozvrhy' + , 'backup.opt.weekday': 'Rozvrhy podle dní' + , 'backup.opt.presence': 'Plány přítomnosti' + , 'backup.opt.settings': 'Nastavení editoru' + , 'backup.opt.holiday': 'Sváteční rozvrhy' + , 'backup.opt.colors': 'Barevné bloky' + , 'editor.tabs.weekdays': 'Dny v týdnu' + , 'presence.live_header': 'Zobrazit živou přítomnost v záhlaví' + , 'presence.live_header.desc': 'Štítky jsou zobrazeny v pravém rohu.' + , 'week.view.title': 'Pohled na týden' + , 'week.view.rooms_one_day': 'Všechny místnosti • jeden den' + , 'week.view.days_one_room': 'Všechny dny • jedna místnost' + , 'week.view_switch.title': 'Přepnout zobrazení z časové osy' + , 'week.view_switch.desc': 'Zobrazí přepínač v záhlaví časové osy pro přepínání mezi dvěma režimy zobrazení dnů v týdnu.' + , 'week.title': 'Dny v týdnu' + , 'week.select_room': 'Vyberte místnost' + , 'err.config_entities': "Konfigurace musí obsahovat 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Video nebylo nalezeno' + , 'onboard.image_not_found': 'Obrázek nebyl nalezen' + , 'pause.prompt_minutes': 'Zadejte dobu trvání (minuty):' + , 'editor.test_tool': 'Testovací nástroj' + , 'editor.test_select_entity': 'Vyberte entitu' + , 'editor.test_empty': 'Nic k exportu' + , 'editor.test_export': 'Exportovat' + , 'editor.test_no_fields': 'Žádná pole k zobrazení' + , 'editor.test_select_placeholder': 'Vybrat...' + , 'editor.error_prefix': 'Chyba: ' + , 'backup.restore.confirm_overwrite': 'Obnovit ze zálohy? Tím se přepíší aktuální plány a nastavení.' + , 'backup.import.err_no_hass': 'Home Assistant není připraven' + , 'backup.import.err_storage_off': 'Sdílené úložiště je VYPNUTÉ. Nejprve jej zapněte.' + , 'backup.import.err_no_integration': 'Integrace thermostat_timeline není dostupná' + , 'backup.import.err_json': 'Neplatný JSON' + , 'profiles.name_exists': 'Název již existuje' + }, + sl: { + 'card.title_default': 'Časovnica termostata', + 'ui.edit_block': 'Uredi blok', + 'ui.add_block': 'Dodaj blok', + 'ui.from': 'Od', + 'ui.to': 'Do', + 'ui.temperature': 'Temperatura', + 'ui.delete': 'Izbriši', + 'ui.cancel': 'Prekliči', + 'ui.close': 'Zapri', + 'ui.unsaved_confirm': 'Imate neshranjene spremembe. Želite vseeno zapreti?', + 'ui.discard_changes': 'Zavrzi spremembe', + 'ui.keep_editing': 'Nadaljuj z urejanjem', + 'ui.save': 'Shrani', + 'ui.error_prefix': 'Napaka: ', + 'ui.overlap_msg': 'Izbrali ste prekrivajoče se čase — prekrivanje med {start} in {end}.', + 'ui.auto_fix': 'Samodejno popravi', + 'ui.fix_start_to': 'Nastavi začetek na {time}', + 'ui.fix_end_to': 'Nastavi konec na {time}', + // Inline meta + 'ui.current_temp': 'Trenutna temperatura:', + + 'editor.title_label': 'Naslov', + 'editor.add_entity': 'Dodaj prostor', + 'editor.entity_placeholder': 'Izberite entiteto', + 'editor.room_mode.title': 'Vrsta entitete prostora', + 'editor.room_mode.desc': 'Ko je omogočeno, izberite input_number (ciljna temperatura). Ko je onemogočeno, izberite climate entiteto.', + + 'editor.room_temp_sensor_override.title': 'Uporabi tipalo temperature prostora', + 'editor.room_temp_sensor_override.desc': 'Ko je omogočeno, prikazana temperatura prostora (in logika kotla) uporablja izbrano tipalo temperature namesto climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Tipalo temperature', + 'editor.drag_reorder': 'Povleci za spremembo vrstnega reda', + 'editor.remove': 'Odstrani', + 'editor.default_c': 'Privzeta °C', + 'editor.perroom.title': 'Privzeta °C po prostoru', + 'editor.perroom.desc': 'Prikaže polje Privzeta °C v vsaki vrstici. Ko je izklopljeno, se globalna Privzeta °C uporabi za vse prostore.', + 'editor.row_height_px': 'Višina vrstice (px)', + 'editor.max_c': 'Največ °C', + 'editor.min_c': 'Najmanj °C', + 'editor.auto_apply.title': 'Samodejna uporaba', + 'editor.auto_apply.desc': 'Ko je omogočeno, kartica poskuša nastaviti temperaturo na izbranih climate entitetah tako, da ustreza načrtovani vrednosti za "trenutno".', + 'editor.apply_edit.title': 'Ob urejanju', + 'editor.apply_edit.desc': 'Ko spremenite bloke na časovnici in sprememba vpliva na trenutni čas, se nova temperatura uporabi takoj.', + 'editor.apply_default.title': 'Ob spremembi privzete °C', + 'editor.apply_default.desc': 'Ko se "Privzeta °C" spremeni in to vpliva na trenutno obdobje, se nova temperatura uporabi takoj.', + + 'editor.show_room_temp.title': 'Prikaži temperaturo prostora', + 'editor.show_room_temp.desc': 'Prikaže mehurček s trenutno temperaturo prostora na vsaki vrstici v glavi časovnice.', + 'ui.copy_plan': 'Kopiraj načrt', + 'ui.paste_plan': 'Prilepi načrt', + 'ui.copy_day': 'Kopiraj dan', + 'ui.paste_day': 'Prilepi dan', + 'editor.merge_label': 'Združi z (dodaj dodatni termostat)', + 'editor.display_name_optional': 'Ime prostora (neobvezno)', + 'editor.merged_with': 'Združeno z', + 'editor.store_enable.title': 'Deljena shramba + ozadni nadzor', + 'editor.store_enable.short': 'Shrani urnike v izbrani senzor in omogoči nadzor v ozadju.', + 'editor.store_enable.desc': 'Shrani urnike v izbrani senzor in omogoči ozadnje ukaze set_temperature prek integracije Thermostat Timeline. Termostati se posodobijo tudi, ko kartica ni odprta. Prav tako deli urnike (toplotne bloke) in nastavitve kartice med uporabniki/napravami. Ko je izklopljeno, se ukazi pošiljajo le, ko je kartica vidna na napravi (tablica/PC/mobilnik) in mora ostati odprta.', + 'editor.store_missing': 'Integracija "Thermostat Pro Timeline Sync" ni nameščena. Deljena shramba ni na voljo.', + 'editor.clear_store': 'Počisti shrambo', + 'editor.clear_store_confirm': 'To bo izbrisalo vse shranjene urnike. Nadaljujem?', + 'editor.migrate_to_store': 'Prenesi podatke v shrambo', + 'editor.migrate_confirm': 'Prenesem urnike, shranjene v brskalniku, v izbrani senzor? To bo prepisalo obstoječe urnike v shrambi.', + 'editor.migrate_pull_confirm': 'Uvozim urnike iz shrambe in prepišem lokalne podatke v brskalniku?', + 'editor.select_storage_first': 'Najprej izberite senzor shrambe.', + 'editor.no_local_data': 'Lokalnih podatkov v brskalniku ni.', + 'editor.clear_all': 'Počisti vse podatke', + 'editor.clear_all_confirm': 'To bo izbrisalo urnike iz shrambe in lokalnega brskalnika. Nadaljujem?', + 'editor.clear_storage_only': 'Počisti samo shrambo', + 'editor.clear_storage_only_confirm': 'To bo izbrisalo urnike iz izbranega senzora shrambe. Nadaljujem?', + 'editor.clear_local_only': 'Počisti samo lokalne podatke', + 'editor.clear_local_only_confirm': 'To bo izbrisalo le urnike, shranjene v vašem brskalniku. Nadaljujem?', + 'editor.reset.desc': 'Kaj to naredi:\n\n• Počisti vse podatke: Ponastavi backend shrambo (.storage) za Thermostat Timeline in izbriše VSE lokalne predpomnilnike/osnutke v tem brskalniku.\n• Počisti samo lokalne podatke: Počisti samo podatke, shranjene v tem brskalniku (localStorage).\n\nNamig: Po popolni ponastavitvi osveži stran, da se stari osnutki ne pojavijo znova.', + 'editor.temp_sensor': 'Senzor temperature', + 'editor.test_tool': 'Testno orodje', + 'editor.test_select_entity': 'Izberite entiteto', + 'editor.test_empty': 'Izberite entiteto za prikaz razpoložljivih ukazov…', + 'editor.test_export': 'Izvozi .md', + 'editor.test_no_fields': 'Ni polj', + 'editor.test_select_placeholder': '— Izberi —', + 'editor.error_prefix': 'Napaka: ', + 'editor.open_in_hacs': 'Odpri v HACS' + , 'week.enable': 'Omogoči dneve v tednu' + , 'week.mode': 'Način tedna' + , 'week.mode.weekday_weekend': 'Delavnik + Vikend' + , 'week.mode.weekday_sat_sun': 'Delavnik + Sobota + Nedelja' + , 'week.mode.all_7': 'Vsi dnevi' + , 'week.button': 'Dnevi v tednu' + , 'week.group.weekdays': 'Delavniki' + , 'week.group.weekend': 'Vikend' + , 'week.days_full.mon': 'Ponedeljek' + , 'week.days_full.tue': 'Torek' + , 'week.days_full.wed': 'Sreda' + , 'week.days_full.thu': 'Četrtek' + , 'week.days_full.fri': 'Petek' + , 'week.days_full.sat': 'Sobota' + , 'week.days_full.sun': 'Nedelja' + , 'week.days.mon': 'Pon' + , 'week.days.tue': 'Tor' + , 'week.days.wed': 'Sre' + , 'week.days.thu': 'Čet' + , 'week.days.fri': 'Pet' + , 'week.days.sat': 'Sob' + , 'week.days.sun': 'Ned' + , 'week.editor.title': 'Urnik po dnevih' + , 'week.editor.preview_note': 'Samo predogled. Spremembe se uporabijo, ko pritisnete Shrani.' + , 'week.open_editor.desc': 'Odpri urejevalnik urnika po dnevih.' + , 'week.open_editor.btn': 'Odpri' + , 'week.view.title': 'Pogled tedna' + , 'week.view.rooms_one_day': 'Vsi prostori • en dan' + , 'week.view.days_one_room': 'Vsi dnevi • en prostor' + , 'week.view_switch.title': 'Preklopi pogled na časovnici' + , 'week.view_switch.desc': 'Prikaže preklopnik v glavi časovnice za preklapljanje med obema načinoma pogleda za dneve v tednu.' + , 'week.title': 'Dnevi v tednu' + , 'week.select_room': 'Izberi prostor' + , 'week.copy_to_rooms': 'Kopiraj teden v prostore' + , 'week.copy_to_rooms.title': 'Kopiraj teden v prostore' + , 'week.copy_to_rooms.select': 'Izberi prostore' + , 'week.copy_to_rooms.apply': 'Vstavi v prostore' + , 'week.enable.desc': 'Omogoči načrtovanje po posameznem dnevu. Ko je izklopljeno, se vsak dan uporabi isti načrt.' + , 'editor.timefmt.title': 'Oblika časa' + , 'editor.timefmt.desc': 'Izberite 24-urni ali 12-urni (AM/PM) prikaz časov na kartici.' + , 'editor.tempfmt.title': 'Enota temperature' + , 'editor.tempfmt.desc': 'Izberite Celsius (°C) ali Fahrenheit (°F) za prikaz na kartici in v urejevalnikih.' + , 'editor.timesrc.title': 'Vir časa' + , 'editor.timesrc.desc': 'Izberite, ali kartica za "zdaj" uporablja čas brskalnika ali časovni pas Home Assistanta.' + , 'editor.timesrc.browser': 'Brskalnik' + , 'editor.timesrc.ha': 'Home Assistant' + , 'editor.instance.title': 'ID konfiguracije' + , 'editor.instance.desc': 'Ohranite več neodvisnih urnikov/nastavitev pod različnimi ID-ji.' + , 'editor.instance.new_id': 'Nov ID' + , 'editor.tabs.settings': 'Nastavitve' + , 'editor.tabs.pause': 'Premor' + , 'editor.tabs.weekdays': 'Dnevi v tednu' + , 'editor.tabs.rooms': 'Prostori' + , 'editor.tabs.colors': 'Barve' + , 'editor.tabs.presence_sensor': 'Senzor prisotnosti' + , 'editor.presence_sensor.enable.title': 'Senzor prisotnosti' + , 'editor.presence_sensor.enable.desc': 'Omogoči nastavitev senzorja prisotnosti po posameznih prostorih.' + , 'editor.presence_sensor.entity.title': 'Entiteta prisotnosti' + , 'editor.presence_sensor.entity.desc': 'Izberi binary_sensor, ki označuje prisotnost v tem prostoru.' + , 'editor.presence_sensor.on_temp.title': 'Temperatura, ko je prisotnost VKLOPLJENA' + , 'editor.presence_sensor.on_temp.desc': 'Če je senzor VKLOPLJEN, ta soba uporablja to temperaturo namesto urnika.' + , 'editor.presence_sensor.on_delay.title': 'Zamik pred uporabo prisotnosti' + , 'editor.presence_sensor.on_delay.desc': 'Kako dolgo mora biti senzor VKLOPLJEN, preden se uporabi temperatura prisotnosti. Vrednost uporablja izbrano enoto.' + , 'editor.presence_sensor.off_delay.title': 'Zamik pred nadaljevanjem urnika' + , 'editor.presence_sensor.off_delay.desc': 'Kako dolgo mora biti senzor IZKLOPLJEN, preden se urnik nadaljuje. Vrednost uporablja izbrano enoto.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Odsotnost' + , 'editor.tabs.reset': 'Ponastavi' + , 'editor.tabs.boiler': 'Krmiljenje kotla' + , 'boiler.switch': 'Stikalo kotla' + , 'boiler.switch_type': 'Vrsta entitete' + , 'boiler.switch_type.switch': 'Stikalo' + , 'boiler.switch_type.input_boolean': 'Vhodni boolean' + , 'boiler.rooms': 'Vključeni prostori' + , 'boiler.rooms.desc': 'Izberite, kateri prostori naj bodo vključeni v krmiljenje kotla.' + , 'boiler.enable': 'Omogoči krmiljenje kotla' + , 'boiler.enable.desc': 'Prikaži nastavitve kotla v urejevalniku te kartice.' + , 'boiler.temp_sensor': 'Temperaturni senzor kotla' + , 'boiler.temp_sensor.desc': 'Temperaturni senzor za krmiljenje stikala kotla.' + , 'boiler.min_temp': 'Min. temperatura kotla (°C)' + , 'boiler.max_temp': 'Maks. temperatura kotla (°C)' + , 'boiler.offsets': 'Odmiki' + , 'boiler.offsets.desc': 'ON odmik: kako daleč pod načrtovanim ciljem, preden se vklopi. OFF odmik: kako daleč nad načrtovanim ciljem, preden se izklopi.' + , 'boiler.on_offset': 'ON odmik (°C)' + , 'boiler.off_offset': 'OFF odmik (°C)' + , 'editor.tabs.sync': 'Sinhronizacijski pogon' + , 'sync.running': 'Sinhronizacija poteka…' + , 'sync.completed': 'Sinhronizacija končana' + , 'away.enable': 'Omogoči način odsotnosti' + , 'away.desc': 'Ko nihče iz izbranih oseb ni doma, termostati samodejno spustijo na nastavljeno temperaturo.' + , 'away.temp': 'Temperatura odsotnosti' + , 'away.persons': 'Osebe' + , 'away.add_person': 'Dodaj osebo' + , 'away.resume_schedule': 'Nadaljuj urnik' + , 'away.resume_away': 'Nadaljuj temperaturo odsotnosti' + , 'editor.heat_colors.title': 'Barve toplotnih blokov' + , 'editor.heat_colors.add': 'Dodaj barvno območje' + , 'editor.colors.col_from': 'Od °C' + , 'editor.colors.col_to': 'Do °C' + , 'editor.colors.col_color': 'Barva' + , 'editor.colors.empty': 'Ni še dodanih prostorov. Dodajte prostor za določitev barv.' + , 'editor.colors.go_add': 'Dodaj prostor' + , 'editor.select_entities': 'Prostori' + , 'editor.colors.refresh': 'Osveži urnik' + , 'editor.colors.clear_local': 'Počisti lokalne barvne podatke' + , 'editor.colors.clear_local_confirm': 'To bo izbrisalo barvna območja, shranjena v vašem brskalniku za to kartico. Nadaljujem?' + , 'editor.colors.clear_shared': 'Počisti deljeno barvno shrambo' + , 'editor.colors.clear_shared_confirm': 'To bo izbrisalo barvna območja v izbranem senzoru shrambe. Nadaljujem?' + , 'editor.colors.clear_all': 'Počisti vse barvne podatke' + , 'editor.colors.clear_all_confirm': 'To bo izbrisalo vsa barvna območja iz lokalne in deljene shrambe. Nadaljujem?' + , 'editor.colors.mode': 'Način barv' + , 'editor.colors.mode.desc': 'Izberite barve po prostorih ali en nabor za vse prostore.' + , 'editor.colors.mode_per_room': 'Po prostorih' + , 'editor.colors.mode_global': 'Vsi prostori' + , 'editor.colors.global_title': 'Vsi prostori' + , 'editor.reload_editor': 'Ponovno naloži urejevalnik' + , 'editor.reload_dashboard': 'Ponovno naloži nadzorno ploščo' + , 'onboard.title': 'Dobrodošli v Thermostat Timeline' + , 'onboard.next': 'Naprej' + , 'onboard.prev': 'Nazaj' + , 'onboard.skip': 'Preskoči' + , 'onboard.done': 'Končano' + , 'onboard.dont_show': 'Ne pokaži več' + , 'onboard.step_main': 'To je glavna časovnica. Prikazuje želene temperature za 24 ur v vsakem prostoru.' + , 'onboard.step_add': 'Toplotni blok dodate z + Dodaj blok ali z dvojnim klikom na časovnico.' + , 'onboard.step_edit': 'Blok uredite ali izbrišete z dvojnim klikom. Uporabite Dneve v tednu za načrtovanje različnih dni.' + , 'onboard.reset': 'Prikaži vodič znova' + , 'onboard.reset_ok': 'Pozdrav se bo prikazal naslednjič.' + , 'pause.menu_title': 'Premor samodejne uporabe' + , 'pause.off_1h': 'Premor 1 ura' + , 'pause.off_5h': 'Premor 5 ur' + , 'pause.off_custom': 'Poljubno trajanje…' + , 'pause.off_indef': 'Premor do nadaljnjega' + , 'pause.resume': 'Nadaljuj zdaj' + , 'pause.close': 'Zapri' + , 'pause.active_until': 'V premoru do {time}' + , 'pause.active_indef': 'V premoru' + , 'editor.pausebtn.title': 'Gumb premora' + , 'editor.pausebtn.desc': 'V glavi kartice prikaži globalni gumb premora. Med premorom so vsi ukazi set_temperature blokirani.' + , 'editor.integration.connected': 'Integracija pripravljena' + , 'editor.integration.disconnected': 'Integracija ni na voljo' + , 'profiles.enable': 'Omogoči profile (prepis)' + , 'profiles.enable.desc': 'Dovoli poimenovane dnevne urnike, ki lahko preglasijo glavni načrt.' + , 'profiles.button': 'Ročni urnik' + , 'profiles.editor.title': 'Profili' + , 'profiles.add': 'Dodaj profil' + , 'profiles.rename': 'Preimenuj' + , 'profiles.delete': 'Izbriši' + , 'profiles.activate': 'Aktiviraj' + , 'profiles.deactivate': 'Deaktiviraj' + , 'profiles.active': 'Aktiven' + , 'profiles.none': 'Noben' + , 'profiles.name_placeholder': 'Ime profila' + , 'profiles.edit': 'Uredi' + , 'profiles.preview_note': 'Samo predogled. Urniki postanejo aktivni, ko profil aktivirate.' + , 'profiles.active_label': 'Ročni profil aktiven:' + , 'profiles.save_new': 'Shrani nov profil' + , 'profiles.save_existing': 'Shrani spremembe v izbran profil' + , 'editor.tabs.holidays': 'Prazniki' + , 'editor.tabs.backup': 'Varnostna kopija' + , 'backup.title': 'Varnostna kopija' + , 'backup.desc': 'Kopiraj urnike in nastavitve v varnostno kopijo ter jih po potrebi obnovi.' + , 'backup.now': 'Ustvari kopijo zdaj' + , 'backup.restore': 'Obnovi' + , 'backup.delete': 'Izbriši varnostno kopijo' + , 'backup.delete.confirm': 'Izbrišem izbrano varnostno kopijo?' + , 'backup.new': 'Nova varnostna kopija' + , 'backup.auto': 'Samodejna kopija' + , 'backup.auto.desc': 'Samodejno ustvarjaj varnostne kopije po urniku.' + , 'backup.interval': 'Interval (dni)' + , 'backup.interval.desc': 'Število dni med samodejnimi kopijami (min. 1).' + , 'backup.sensor': 'Senzor varnostne kopije' + , 'backup.last': 'Zadnja kopija' + , 'backup.slot.title': 'Izberi varnostno kopijo' + , 'backup.slot.desc': 'Izberite, kateri slot varnostne kopije želite obnoviti.' + , 'backup.slot.empty': 'Še ni varnostnih kopij' + , 'backup.select': 'Izberi, kaj kopirati' + , 'backup.select.desc': 'Označene postavke so vključene v kopiranje/obnovo' + , 'backup.opt.all': 'Vse' + , 'backup.opt.main': 'Glavni urniki' + , 'backup.opt.weekday': 'Urniki po dnevih' + , 'backup.opt.presence': 'Urniki prisotnosti' + , 'backup.opt.settings': 'Nastavitve urejevalnika' + , 'backup.opt.holiday': 'Praznični urniki' + , 'backup.opt.colors': 'Barvni bloki' + , 'holidays.enable': 'Omogoči praznike' + , 'holidays.desc': 'Uporabi poseben dnevni urnik ob praznikih.' + , 'holidays.source': 'Vir praznikov' + , 'holidays.source.calendar': 'Koledar' + , 'holidays.source.manual': 'Ročni datumi' + , 'holidays.entity': 'Entiteta za praznike' + , 'holidays.entity_desc': 'Izberite koledar, ki poroča le vklop/izklop. Na praznike mora biti vklopljen, sicer izklopljen.' + , 'holidays.dates': 'Datumi praznikov' + , 'holidays.add_date': 'Dodaj datum' + , 'holidays.editor.title': 'Praznični urnik' + , 'holidays.edit': 'Uredi praznični urnik' + , 'holidays.button': 'Prazniki' + , 'presence.button': 'Urniki prisotnosti' + , 'presence.editor.title': 'Urniki prisotnosti' + , 'presence.enable_advanced': 'Omogoči napredne urnike prisotnosti' + , 'presence.enable_advanced.desc': 'Uporabi kombinacije in urnike glede na to, kdo je doma/odsoten.' + , 'away.delay_enable': 'Zakasni dejanja odsotnosti' + , 'away.delay_desc': 'Počakaj preden uporabiš spremembe odsotnosti.' + , 'away.delay_value': 'Zamik' + , 'away.delay_unit': 'Enota' + , 'away.delay_unit.seconds': 'Sekunde' + , 'away.delay_unit.minutes': 'Minute' + , 'presence.live_header': 'Prikaži živo prisotnost v glavi' + , 'presence.live_header.desc': 'Oznake so prikazane v desnem kotu.' + , 'presence.combos': 'Kombinacije' + , 'presence.enable_combo': 'Omogoči to kombinacijo' + , 'presence.all_home': 'Vsi doma' + , 'presence.none_home': 'Nihče doma' + , 'presence.home': 'Doma: {names}' + , 'presence.away': 'Odsotni: {names}' + , 'presence.manage': 'Upravljaj kombinacije' + , 'presence.who_home': 'Kdo je doma' + , 'presence.who_away': 'Kdo ni doma' + , 'err.config_entities': "Konfiguracija mora imeti 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Videoposnetka ni bilo mogoče najti' + , 'onboard.image_not_found': 'Slike ni bilo mogoče najti' + , 'pause.prompt_minutes': 'Vnesite trajanje (minute):' + , 'editor.test_tool': 'Orodje za testiranje' + , 'editor.test_select_entity': 'Izberite entiteto' + , 'editor.test_empty': 'Ni ničesar za izvoz' + , 'editor.test_export': 'Izvozi' + , 'editor.test_no_fields': 'Ni polj za prikaz' + , 'editor.test_select_placeholder': 'Izberite...' + , 'editor.error_prefix': 'Napaka: ' + , 'backup.restore.confirm_overwrite': 'Obnovi iz varnostne kopije? To bo prepisalo trenutne urnike in nastavitve.' + , 'backup.import.err_no_hass': 'Home Assistant ni pripravljen' + , 'backup.import.err_storage_off': 'Skupna shramba je IZKLOPLJENA. Najprej jo vklopite.' + , 'backup.import.err_no_integration': 'Integracija thermostat_timeline ni na voljo' + , 'backup.import.err_json': 'Neveljaven JSON' + , 'profiles.name_exists': 'Ime že obstaja' + }, +}; +const TT_LANG_ALIAS = { no: 'nb', cz: 'cs', dk: 'da' }; +function ttGetLangFromHass(hass) { + // Home Assistant can report locales like "da_DK"; normalize to BCP-47 ("da-DK"). + const raw = + (typeof hass === 'string' ? hass : (hass?.locale?.language || hass?.language)) || + navigator.language || + 'en'; + return String(raw).replace(/_/g, '-').toLowerCase(); +} +function ttLocalize(key, langOrHass) { + const raw = typeof langOrHass === 'string' ? langOrHass : ttGetLangFromHass(langOrHass); + const lang = String(raw).replace(/_/g, '-').toLowerCase(); + const parts = lang.split('-'); + const candidates = [ + lang, + parts[0], + TT_LANG_ALIAS[lang], + TT_LANG_ALIAS[parts[0]], + 'en', + ].filter(Boolean); + for (const c of candidates) { + const dict = TT_I18N[c]; + if (dict && dict[key]) return dict[key]; + } + // Default/fallback language is English. + return TT_I18N.en[key] || key; +} +// ---- end i18n ---- + +/* eslint-disable no-undef */ + +// Simple runtime version to help with cache-busting diagnostics in HA. +// Update this when shipping changes so the version appears in the +// "Custom cards" panel and in logs. +const TT_CARD_VERSION = "2026.01.18-boiler-domain"; + +class ThermostatTimelineCard extends HTMLElement { + static get version() { return TT_CARD_VERSION; } + static getConfigElement() { return document.createElement("thermostat-timeline-card-editor"); } + static getStubConfig() { + return { + // Localize default title based on browser language; editor will further adapt to HA language + title: ttLocalize('card.title_default', navigator.language || 'en'), + entities: [], + time_12h: null, + time_source: 'browser', // 'browser' | 'ha' + color_global: false, // when true, use one global set of colors (key "*") for all rooms + now_update_ms: 60000, // kun til UI 'nu'-stregen + storage_enabled: true, // default: on -> uses file storage via API + // Optional per-card instance namespace (off by default for legacy compatibility) + instance_enabled: false, + // Pre-generate a unique ID so new cards don't inherit another card's instance. + // Safe chars only; will be normalized if user later edits it. + instance_id: `thermostat_pro_timeline_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,6)}`, + // Internal per-card uid to make local persistence stable even when multiple + // cards have identical entities/settings (and when Lovelace uses Shadow DOM). + instance_uid: `ttuid_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,10)}`, + storage_sync_min: 5, // minutes when mode = 'delay' (legacy) + backup_auto_enabled: false, + backup_interval_days: 1, + show_top_now: false, + now_extend_px: 76, + show_pause_button: true, + show_room_temp: true, + pause_sensor_enabled: false, + pause_sensor_entity: '', + labels: {}, + merges: {}, // { [primary_eid]: [linked_eid, ...] } + temp_sensors: {}, // { [primary_eid]: sensor.temperature_entity_id } + turn_on: {}, // { [primary_eid]: { enabled: bool, order: 'before'|'after' } } + boiler_enabled: false, // enable boiler controls in editor and runtime + boiler_switch: '', // optional switch.* entity used for boiler control + boiler_switch_domain: 'switch', // switch | input_boolean + boiler_rooms: null, // null = all rooms; array = selected climate rooms + boiler_on_offset: 0, // °C relative to target: ON when current < target - on_offset + boiler_off_offset: 0, // °C relative to target: OFF when current >= target + off_offset + boiler_temp_sensor: '', // optional sensor.* entity used for boiler temperature control + boiler_min_temp: 20, // boiler min temp (stored internally as °C) + boiler_max_temp: 25, // boiler max temp (stored internally as °C) + weekdays_enabled: false, + weekdays_mode: 'weekday_weekend', // 'weekday_weekend' | 'weekday_sat_sun' | 'all_7' + weekdays_view: 'all_rooms_one_day', // 'all_rooms_one_day' | 'one_room_all_days' + weekdays_view_switch_in_timeline: false, + weekdays_selected_room: '', + profiles_enabled: false, + away: { enabled: false, persons: [], target_c: 17, advanced_enabled: false, combos: {} }, + // Open Window Detection (editor + shared settings) + open_window: { enabled: false, sensors: {}, open_delay_min: 2, close_delay_min: 5 }, + holidays_enabled: false, + holidays_source: 'calendar', // 'calendar' | 'manual' + holidays_entity: '', // calendar.* or binary_sensor.* that is on/off for holiday + holidays_dates: [], // [ 'YYYY-MM-DD', ... ] + holidays_groups: [], // UI groups for manual ranges: [{id, from, to, dates:[...] }] + presence_live_header: true // Show live presence chips in header + , room_use_input_number: [] // per-room toggle: show/select input_number instead of climate (matches entities[] index) + , room_use_temp_sensor: [] // per-room toggle: use temp_sensors[eid] as room temperature instead of climate.current_temperature + }; + } + + connectedCallback(){ + // Attach minimal listeners to avoid re-render while typing in row inputs + try { + this.shadowRoot?.addEventListener('focusin', (e)=>{ + try { + const t = e.target; + if (!t) return; + // Suspend while editing any number input in row header + if ((t.tagName === 'INPUT' || t.tagName === 'HA-TEXTFIELD') && t.closest && t.closest('.row-head')) { + this._suspendRender = true; + } + } catch {} + }); + this.shadowRoot?.addEventListener('focusout', ()=>{ setTimeout(()=>{ this._suspendRender = false; }, 150); }); + } catch {} + } + + // ---------- Home Assistant hook ---------- + set hass(hass) { + this._hass = hass; this._lang = ttGetLangFromHass(hass); + // Storage is now file-based via API, no sensor auto-detection needed + + // (Select-based global profile removed) + + + // Detect Away mode changes and apply immediately + try { + const curAway = this._isAwayActive(); + if (this._lastAwayActive === null) { + this._lastAwayActive = curAway; + } else if (this._initialized && curAway !== this._lastAwayActive) { + this._lastAwayActive = curAway; + // Reset manual away bypass on any away enter/exit + this._awayBypass = false; + if (this._config?.auto_apply) { + // Apply immediately on Away enter/exit + this._applyCurrentSetpoints(true); + // When Sync is ON, nudge backend to apply right away as well + try { if (this._config?.storage_enabled) this._nudgeBackgroundApplyNow(); } catch {} + this._scheduleNextApply(); + } + } + } catch {} + + // Hent fra sensor hvis version ændret + // Detect Presence combo changes and apply immediately + try { + const curKey = this._activePresenceComboKey ? this._activePresenceComboKey() : null; + if (this._lastPresenceKey === null) { + this._lastPresenceKey = curKey; + } else if (this._initialized && curKey !== this._lastPresenceKey) { + this._lastPresenceKey = curKey; + if (this._config?.auto_apply) { + this._applyCurrentSetpoints(true); + try { if (this._config?.storage_enabled) this._nudgeBackgroundApplyNow(); } catch {} + this._scheduleNextApply(); + } + } + } catch {} + + // Detect per-room presence sensor transitions and apply immediately + // (also schedules timers so delayed ON/OFF can take effect without new HA events) + try { + const curSnap = this._presenceSensorActiveSnapshot ? this._presenceSensorActiveSnapshot() : {}; + if (this._lastPresenceSensorSnap == null) { + this._lastPresenceSensorSnap = curSnap; + } else if (this._initialized && this._presenceSensorSnapChanged && this._presenceSensorSnapChanged(this._lastPresenceSensorSnap, curSnap)) { + this._lastPresenceSensorSnap = curSnap; + if (this._config?.auto_apply) { + if (this._config?.storage_enabled) this._nudgeBackgroundApplyNow(); + else this._applyCurrentSetpoints(true, true); + this._scheduleNextApply(); + } + } else { + this._lastPresenceSensorSnap = curSnap; + } + try { this._schedulePresenceDelayTimer && this._schedulePresenceDelayTimer(); } catch {} + } catch {} + + // Detect pause sensor transitions (UI responsiveness + frontend-only behavior when Sync is OFF) + try { + const cur = this._isPausedBySensor ? !!this._isPausedBySensor() : false; + if (this._lastPauseSensorOn === null || this._lastPauseSensorOn === undefined) { + this._lastPauseSensorOn = cur; + } else if (this._initialized && cur !== this._lastPauseSensorOn) { + this._lastPauseSensorOn = cur; + try { if (!this._inlineEditing && !this._editing) this._render(); } catch {} + if (this._config?.auto_apply) { + if (cur) { + // Pause: cancel any pending boundary timer + try { this._scheduleNextApply(); } catch {} + } else { + // Resume: reconcile once + schedule next + this._applyCurrentSetpoints(true, true); + try { if (this._config?.storage_enabled) this._nudgeBackgroundApplyNow(); } catch {} + this._scheduleNextApply(); + } + } + } + } catch {} + + // Version checking is now done via API polling in _startStoreWatchTimer + + // When Sync is OFF, do not override local editor toggles (profiles_enabled) with sensor values. + // Visibility of the Manual schedule button is driven by local config and localStorage only. + + try { + if (this._config?.storage_enabled) this._startStoreWatchTimer(); + else if (this._storeWatchTimer) { clearInterval(this._storeWatchTimer); this._storeWatchTimer = null; } + } catch {} + + // Boiler control (frontend only when Sync is OFF) + try { this._setupBoilerControl(); } catch {} + + // (Removed select/state subscription; card updates via version change as before) + + if (!this._initialized) { + this._initialized = true; + this._init(); + this._loadStore(true).then(async () => { + this._ensureSchedules(); + // Ensure labels/merges/temp_sensors from backend are reflected in config before first render + try { await this._hydrateUiNamesFromBackend(); } catch {} + // Restore delayed sync countdown if previously scheduled + try { this._restoreSyncDue(); } catch {} + this._render(); + // Open deferred Holiday editor right after first render if flagged by editor + try { this._maybeOpenDeferredHoliday && this._maybeOpenDeferredHoliday(); } catch {} + // Ved load sætter vi korrekt setpoint én gang + this._applyCurrentSetpoints(true); + this._scheduleNextApply(); + // Push any YAML-provided settings to storage/local after first load + // but avoid resetting a pending delayed sync countdown on hard refresh + try { + const hasPending = Number(this._storeDelayDue||0) > Date.now() || !!localStorage.getItem(this._syncDueKey()); + if (!(this._config?.storage_enabled && this._config?.storage_sync_mode === 'delay' && hasPending)) { + this._debouncedSaveStore(); + } + } catch {} + // (Removed polling safety net) + }); + } else { + if (!this._inlineEditing && !this._editing) this._render(); + } + } + + // Ensure UI-friendly names (labels) are hydrated from backend if missing locally + async _hydrateUiNamesFromBackend(){ + try { + if (!this._config) return; + const hasAnyLabel = this._config.labels && typeof this._config.labels==='object' && Object.keys(this._config.labels).length>0; + const hasAnyMerge = this._config.merges && typeof this._config.merges==='object' && Object.keys(this._config.merges).length>0; + const hasAnySensors = this._config.temp_sensors && typeof this._config.temp_sensors==='object' && Object.keys(this._config.temp_sensors).length>0; + const hasAnyTurnOn = this._config.turn_on && typeof this._config.turn_on==='object' && Object.keys(this._config.turn_on).length>0; + if (hasAnyLabel && hasAnyMerge && hasAnySensors && hasAnyTurnOn) return; + const api = await this._apiFetchState(); + const s = api?.settings || {}; + let changed = false; + if (!hasAnyLabel && s.labels && typeof s.labels==='object' && Object.keys(s.labels).length){ this._config.labels = { ...s.labels }; changed = true; } + if (!hasAnyMerge && s.merges && typeof s.merges==='object' && Object.keys(s.merges).length){ this._config.merges = { ...s.merges }; changed = true; } + if (!hasAnySensors && s.temp_sensors && typeof s.temp_sensors==='object' && Object.keys(s.temp_sensors).length){ this._config.temp_sensors = { ...s.temp_sensors }; changed = true; } + if (!hasAnyTurnOn && s.turn_on && typeof s.turn_on==='object' && Object.keys(s.turn_on).length){ this._config.turn_on = { ...s.turn_on }; changed = true; } + if (changed){ + try { const payload = this._makeStoragePayload(true); localStorage.setItem(this._localStoreKey(), JSON.stringify(payload)); } catch {} + } + } catch {} + } + + // ---------- Config ---------- + setConfig(config) { + if (!config || !Array.isArray(config.entities)) throw new Error(ttLocalize('err.config_entities', navigator.language || 'en')); + const prevDefault = Number(this._config?.default_temp); + const yamlSetsDefault = Object.prototype.hasOwnProperty.call(config, 'default_temp'); + let rowh = Number(config.row_height ?? 64); rowh = isNaN(rowh) ? 64 : Math.max(40, Math.min(120, Math.round(rowh))); + let deft = Number(config.default_temp ?? 20); deft = isNaN(deft) ? 20 : Math.max(5, Math.min(35, deft)); + let nowms = Number(config.now_update_ms ?? 60000); nowms = isNaN(nowms) ? 60000 : Math.max(200, nowms); + let maxt = Number(config.max_temp ?? 25); maxt = isNaN(maxt) ? 25 : Math.max(5, Math.min(120, maxt)); + let mint = Number(config.min_temp ?? 5); mint = isNaN(mint) ? 5 : Math.max(0, Math.min(45, mint)); + // Boiler limits default to 20/25°C. Keep previous values when not provided. + const prevBMin = (this._config && Object.prototype.hasOwnProperty.call(this._config, 'boiler_min_temp')) ? this._config.boiler_min_temp : null; + const prevBMax = (this._config && Object.prototype.hasOwnProperty.call(this._config, 'boiler_max_temp')) ? this._config.boiler_max_temp : null; + const clampBoiler = (n)=>{ const v = Number(n); return Number.isFinite(v) ? Math.max(0, Math.min(120, v)) : null; }; + let boilerMin = (config.boiler_min_temp === undefined) + ? (Number.isFinite(Number(prevBMin)) ? Number(prevBMin) : 20) + : ((config.boiler_min_temp === null || config.boiler_min_temp === '') ? 20 : clampBoiler(config.boiler_min_temp)); + let boilerMax = (config.boiler_max_temp === undefined) + ? (Number.isFinite(Number(prevBMax)) ? Number(prevBMax) : 25) + : ((config.boiler_max_temp === null || config.boiler_max_temp === '') ? 25 : clampBoiler(config.boiler_max_temp)); + const showTop = Boolean(config.show_top_now ?? false); + let extendPx = Number(config.now_extend_px ?? 76); extendPx = isNaN(extendPx) ? 76 : Math.max(48, Math.min(140, Math.round(extendPx))); + + // Mark which keys were explicitly provided in YAML so storage won't + // override them on first hass() load + this._yamlProvided = { + labels: Object.prototype.hasOwnProperty.call(config, 'labels'), + merges: Object.prototype.hasOwnProperty.call(config, 'merges'), + color_ranges: Object.prototype.hasOwnProperty.call(config, 'color_ranges'), + default_temp: Object.prototype.hasOwnProperty.call(config, 'default_temp'), + min_temp: Object.prototype.hasOwnProperty.call(config, 'min_temp'), + max_temp: Object.prototype.hasOwnProperty.call(config, 'max_temp'), + temp_sensors: Object.prototype.hasOwnProperty.call(config, 'temp_sensors'), + turn_on: Object.prototype.hasOwnProperty.call(config, 'turn_on'), + boiler_enabled: Object.prototype.hasOwnProperty.call(config, 'boiler_enabled'), + boiler_switch: Object.prototype.hasOwnProperty.call(config, 'boiler_switch'), + boiler_switch_domain: Object.prototype.hasOwnProperty.call(config, 'boiler_switch_domain'), + boiler_rooms: Object.prototype.hasOwnProperty.call(config, 'boiler_rooms'), + boiler_on_offset: Object.prototype.hasOwnProperty.call(config, 'boiler_on_offset'), + boiler_off_offset: Object.prototype.hasOwnProperty.call(config, 'boiler_off_offset'), + boiler_temp_sensor: Object.prototype.hasOwnProperty.call(config, 'boiler_temp_sensor'), + boiler_min_temp: Object.prototype.hasOwnProperty.call(config, 'boiler_min_temp'), + boiler_max_temp: Object.prototype.hasOwnProperty.call(config, 'boiler_max_temp'), + show_pause_button: Object.prototype.hasOwnProperty.call(config, 'show_pause_button'), + show_room_temp: Object.prototype.hasOwnProperty.call(config, 'show_room_temp'), + pause_sensor_enabled: Object.prototype.hasOwnProperty.call(config, 'pause_sensor_enabled'), + pause_sensor_entity: Object.prototype.hasOwnProperty.call(config, 'pause_sensor_entity'), + away: Object.prototype.hasOwnProperty.call(config, 'away'), + time_12h: Object.prototype.hasOwnProperty.call(config, 'time_12h'), + time_source: Object.prototype.hasOwnProperty.call(config, 'time_source'), + temp_unit: Object.prototype.hasOwnProperty.call(config, 'temp_unit'), + holidays_enabled: Object.prototype.hasOwnProperty.call(config, 'holidays_enabled'), + holidays_source: Object.prototype.hasOwnProperty.call(config, 'holidays_source'), + holidays_entity: Object.prototype.hasOwnProperty.call(config, 'holidays_entity'), + holidays_dates: Object.prototype.hasOwnProperty.call(config, 'holidays_dates'), + presence_sensor_enabled: Object.prototype.hasOwnProperty.call(config, 'presence_sensor_enabled'), + presence_sensors: Object.prototype.hasOwnProperty.call(config, 'presence_sensors'), + presence_sensor_temps: Object.prototype.hasOwnProperty.call(config, 'presence_sensor_temps'), + presence_sensor_delays: Object.prototype.hasOwnProperty.call(config, 'presence_sensor_delays'), + presence_sensor_delay_units: Object.prototype.hasOwnProperty.call(config, 'presence_sensor_delay_units'), + }; + + this._config = { + // If no explicit title, localize a default based on HA/browser language + title: (config.title !== undefined && config.title !== null) + ? String(config.title) + : ttLocalize('card.title_default', this._hass || this._lang || navigator.language || 'en'), + entities: config.entities, + row_height: rowh, + default_temp: deft, + max_temp: maxt, + min_temp: mint, + temp_unit: (config.temp_unit === undefined || config.temp_unit === null || config.temp_unit === 'auto') + ? (this._detectPreferF() ? 'F' : 'C') + : (String(config.temp_unit).toUpperCase()==='F'?'F':'C'), + time_12h: (config.time_12h === undefined || config.time_12h === null || config.time_12h === 'auto') + ? this._detectPrefer12h() + : !!config.time_12h, + time_source: (config.time_source === 'ha' ? 'ha' : 'browser'), + now_update_ms: nowms, + // File-based storage is always used - no sensor entity configuration needed + storage_enabled: !!(config.storage_enabled ?? true), + instance_enabled: !!(config.instance_enabled ?? false), + instance_id: String(config.instance_id ?? ''), + storage_sync_mode: (config.storage_sync_mode === 'delay' ? 'delay' : 'instant'), + storage_sync_min: Number.isFinite(config.storage_sync_min) ? Math.max(0, Math.min(1440, Number(config.storage_sync_min))) : (this._config?.storage_sync_min ?? 5), + storage_sync_sec: Number.isFinite(config.storage_sync_sec) + ? Math.max(5, Math.min(86400, Math.round(config.storage_sync_sec))) + : (Number.isFinite(config.storage_sync_min) ? Math.max(5, Math.min(86400, Math.round(Number(config.storage_sync_min)*60))) : (this._config?.storage_sync_sec ?? 15)), + backup_auto_enabled: !!(config.backup_auto_enabled ?? this._config?.backup_auto_enabled ?? false), + backup_interval_days: Number.isFinite(config.backup_interval_days) ? Math.max(1, Math.min(365, Math.round(config.backup_interval_days))) : (this._config?.backup_interval_days ?? 1), + show_top_now: showTop, + now_extend_px: extendPx, + show_pause_button: !!(config.show_pause_button ?? true), + show_room_temp: !!(config.show_room_temp ?? true), + pause_sensor_enabled: !!(config.pause_sensor_enabled ?? this._config?.pause_sensor_enabled ?? false), + pause_sensor_entity: String(config.pause_sensor_entity ?? this._config?.pause_sensor_entity ?? ''), + presence_sensor_enabled: !!(config.presence_sensor_enabled ?? this._config?.presence_sensor_enabled ?? false), + presence_sensors: { ...(config.presence_sensors || {}) }, + presence_sensor_temps: (()=>{ + try { + const src = (config.presence_sensor_temps && typeof config.presence_sensor_temps === 'object') ? config.presence_sensor_temps : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const v = Number(src[k]); + if (Number.isFinite(v)) out[String(k)] = v; + } + return out; + } catch { return {}; } + })(), + presence_sensor_delays: (()=>{ + try { + const src = (config.presence_sensor_delays && typeof config.presence_sensor_delays === 'object') ? config.presence_sensor_delays : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const v = src?.[k]; + if (!v || typeof v !== 'object') continue; + const onS = Number(v.on_s); + const offS = Number(v.off_s); + const obj = {}; + if (Number.isFinite(onS) && onS >= 0) obj.on_s = onS; + if (Number.isFinite(offS) && offS >= 0) obj.off_s = offS; + if (Object.keys(obj).length) out[String(k)] = obj; + } + return out; + } catch { return {}; } + })(), + presence_sensor_delay_units: (()=>{ + try { + const src = (config.presence_sensor_delay_units && typeof config.presence_sensor_delay_units === 'object') ? config.presence_sensor_delay_units : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const u = String(src[k] || '').toLowerCase().trim(); + if (u === 'seconds' || u === 'minutes') out[String(k)] = u; + } + return out; + } catch { return {}; } + })(), + auto_apply: config.auto_apply ?? true, + apply_on_edit: config.apply_on_edit ?? true, + apply_on_default_change: config.apply_on_default_change ?? true, + labels: { ...(config.labels || {}) }, + merges: { ...(config.merges || {}) }, + temp_sensors: { ...(config.temp_sensors || {}) }, + turn_on: { ...(config.turn_on || {}) }, + boiler_enabled: !!(config.boiler_enabled ?? this._config?.boiler_enabled ?? false), + boiler_switch: String(config.boiler_switch ?? this._config?.boiler_switch ?? ''), + boiler_switch_domain: (()=>{ + try { + const sw = String(config.boiler_switch ?? this._config?.boiler_switch ?? ''); + const fromEid = (()=>{ + const dom = String(sw||'').split('.')[0]; + return (dom === 'input_boolean') ? 'input_boolean' : 'switch'; + })(); + const raw = String((config.boiler_switch_domain ?? this._config?.boiler_switch_domain ?? fromEid) || '').toLowerCase().trim(); + return (raw === 'input_boolean') ? 'input_boolean' : 'switch'; + } catch { return 'switch'; } + })(), + boiler_rooms: (()=>{ + try { + const ents = Array.isArray(config.entities) ? config.entities.filter(Boolean).map(String) : []; + const climateRooms = ents.filter(e=>String(e).startsWith('climate.')); + const raw = Object.prototype.hasOwnProperty.call(config, 'boiler_rooms') + ? config.boiler_rooms + : (this._config?.boiler_rooms ?? null); + if (Array.isArray(raw)) { + const want = new Set(raw.filter(Boolean).map(String)); + return climateRooms.filter(eid => want.has(eid)); + } + return null; + } catch { return null; } + })(), + boiler_on_offset: (()=>{ + try { + const prev = Number(this._config?.boiler_on_offset); + const raw = (config.boiler_on_offset === undefined) ? prev : Number(config.boiler_on_offset); + const v = Number.isFinite(raw) ? raw : (Number.isFinite(prev) ? prev : 0); + return Math.max(-10, Math.min(10, v)); + } catch { return 0; } + })(), + boiler_off_offset: (()=>{ + try { + const prev = Number(this._config?.boiler_off_offset); + const raw = (config.boiler_off_offset === undefined) ? prev : Number(config.boiler_off_offset); + const v = Number.isFinite(raw) ? raw : (Number.isFinite(prev) ? prev : 0); + return Math.max(-10, Math.min(10, v)); + } catch { return 0; } + })(), + boiler_temp_sensor: String(config.boiler_temp_sensor ?? this._config?.boiler_temp_sensor ?? ''), + boiler_min_temp: boilerMin, + boiler_max_temp: boilerMax, + color_ranges: { ...(config.color_ranges || {}) }, + color_global: !!(config.color_global ?? this._config?.color_global ?? false), + per_room_defaults: !!(config.per_room_defaults ?? this._config?.per_room_defaults ?? false), + presence_live_header: !!(config.presence_live_header ?? this._config?.presence_live_header ?? true), + weekdays_enabled: !!(config.weekdays_enabled ?? false), + weekdays_mode: (config.weekdays_mode || 'weekday_weekend'), + weekdays_view: (config.weekdays_view || this._config?.weekdays_view || 'all_rooms_one_day'), + weekdays_view_switch_in_timeline: !!(config.weekdays_view_switch_in_timeline ?? this._config?.weekdays_view_switch_in_timeline ?? false), + weekdays_selected_room: String(config.weekdays_selected_room || this._config?.weekdays_selected_room || ''), + profiles_enabled: !!(config.profiles_enabled ?? false), + away: { + enabled: !!(config.away?.enabled ?? false), + persons: [...(config.away?.persons || [])].filter(Boolean).map(String), + target_c: Number.isFinite(config.away?.target_c) ? Number(config.away.target_c) : 17, + advanced_enabled: !!(config.away?.advanced_enabled ?? this._config?.away?.advanced_enabled ?? false), + combos: { ...(config.away?.combos || this._config?.away?.combos || {}) }, + delay_enabled: !!(config.away?.delay_enabled ?? this._config?.away?.delay_enabled ?? false), + delay_value: Number.isFinite(config.away?.delay_value) ? Number(config.away.delay_value) : (this._config?.away?.delay_value ?? 0), + delay_unit: String(config.away?.delay_unit || this._config?.away?.delay_unit || 'minutes') + } + , holidays_enabled: !!(config.holidays_enabled ?? this._config?.holidays_enabled ?? false) + , holidays_source: (config.holidays_source || this._config?.holidays_source || 'calendar') + , holidays_entity: String(config.holidays_entity || this._config?.holidays_entity || '') + , holidays_dates: Array.isArray(config.holidays_dates) ? [...config.holidays_dates] : ([...(this._config?.holidays_dates || [])]) + , holidays_groups: Array.isArray(config.holidays_groups) ? [...config.holidays_groups] : ([...(this._config?.holidays_groups || [])]) + }; + + // Guard: Away/Presence should not be active until at least one person is selected. + try { + const ap = Array.isArray(this._config?.away?.persons) ? this._config.away.persons.filter(Boolean).map(String) : []; + this._config.away.persons = ap; + if (!ap.length) { + this._config.away.enabled = false; + this._config.away.advanced_enabled = false; + } + } catch {} + + // If YAML explicitly changed default_temp, update rows that still use the old + // default value (do not overwrite rooms with a user-specific override) + try { + if (yamlSetsDefault && Number.isFinite(prevDefault)) { + for (const eid of (this._config.entities || [])) { + const r = this._schedules[eid]; + if (!r) continue; + if (!Number.isFinite(r.defaultTemp) || Math.abs(Number(r.defaultTemp) - prevDefault) < 0.001) { + r.defaultTemp = this._config.default_temp; + } + } + } + } catch {} + + if (this._initialized) { + this._ensureSchedules(); + // Extra safety: if YAML changed default_temp, propagate to rows that + // still used the previous global default + try { + if (yamlSetsDefault && Number.isFinite(prevDefault)) { + for (const eid of (this._config.entities || [])) { + const r = this._schedules[eid]; + if (!r) continue; + if (!Number.isFinite(r.defaultTemp) || Math.abs(Number(r.defaultTemp) - prevDefault) < 0.001) { + r.defaultTemp = this._config.default_temp; + } + } + } + } catch {} + this._startUiTimer(); + if (!this._inlineEditing && !this._editing) this._render(); + this._scheduleNextApply(); + // Persist shared settings to storage so other users/dashboards pick them up + this._debouncedSaveStore(); + + // Boiler control (frontend only when Sync is OFF) + try { this._setupBoilerControl(); } catch {} + } + // Mark config as ready + this._hasConfig = true; + + // Boiler control (frontend only when Sync is OFF) + try { this._setupBoilerControl(); } catch {} + } + + getCardSize() { return 3 + (this._config?.entities?.length || 0) * 2; } + + // ---------- Lifecycle ---------- + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this._initialized = false; + this._config = ThermostatTimelineCard.getStubConfig(); + // Guard: wait for setConfig before any apply logic + this._hasConfig = false; + // Track which YAML keys were explicitly provided so storage doesn't + // override them on first load + this._yamlProvided = {}; + + // Pop-up overlays (block editor, weekday editor, copy-to-rooms, onboarding) + // Enable by default so Weekday and Add buttons are visible + this._disablePopups = false; + // Disable the onboarding guide popup explicitly + this._disableOnboard = true; + + // In-card settings popup state (draft; only saved on Save button) + this._settingsPopupTab = 'settings'; + this._settingsDraft = null; + + // Data + this._schedules = {}; // { [entity_id]: { defaultTemp:number, blocks:[{id,startMin,endMin,temp}] } } + + // UI state + this._uiTimer = null; // kun til at flytte "nu"-stregen + this._transitionTimer = null; // timer til næste setpoint-ændring + this._active = null; // { entity, id } + this._lastTap = { t: 0, target: null }; + this._editing = null; // { entity, blockId } + // When a weekly block is opened from main timeline, auto-save the weekly draft on block save + this._autoSaveWeeklyFromTimeline = false; + // When a profile block is opened from main timeline, auto-save the profile draft on block save + this._autoSaveProfileFromTimeline = false; + + // Inline edit hard-lock + this._inlineEditing = false; // er et inline input aktivt? + this._inlineEntity = null; // hvilken entitys Standard-felt? + this._inlineTempDraft = null; // den værdi man er i gang med at skrive + + // Sync/debounce + this._saving = false; + this._saveTimer = null; + this._saveDebounceMs = 300; + // Delayed sync scheduler + this._storeDelayTimer = null; + this._storeDelayDue = 0; + // Defer writes while an editor is open to avoid rapid file churn + this._pendingSaveAfterEdit = false; + this._markPendingSave = ()=>{ this._pendingSaveAfterEdit = true; try { this._persistDraftNow(); } catch {} try { this._render(); } catch {} }; + this._lastVersion = 0; + this._lastProfileVersion = 0; + // Polling watcher to pick up sensor updates in case frontend misses events + this._storeWatchTimer = null; + this._storeWatchMs = 10000; // ms + this._storeWatchBusy = false; // avoid overlapping polls + + // Cross-card sync (same dashboard + other tabs). Needed when storage is OFF, + // because multiple cards otherwise won't notice localStorage changes. + this._broadcastId = `ttbc_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,10)}`; + this._bc = null; + this._onStoreUpdated = (ev)=>{ + try { + const d = ev?.detail || {}; + if (!d || d.origin === this._broadcastId) return; + if (this._inlineEditing || this._editing) return; + const key = String(d.key || ''); + const myKey = this._localStoreKey(); + if (!key || !myKey || key !== myKey) return; + this._loadStore(false).then(()=>{ + try { this._ensureSchedules(); } catch {} + try { if (!this._inlineEditing && !this._editing) this._render(); } catch {} + try { this._scheduleNextApply(); } catch {} + }); + } catch {} + }; + try { window.addEventListener('tt-store-updated', this._onStoreUpdated); } catch {} + try { + if (typeof BroadcastChannel !== 'undefined') { + this._bc = new BroadcastChannel('thermostat_timeline_store_updated'); + this._bc.onmessage = (m)=>{ try { this._onStoreUpdated({ detail: m?.data }); } catch {} }; + } + } catch { this._bc = null; } + + // Boiler hysteresis control tick (frontend only when Sync is OFF) + this._boilerTimer = null; + + // Auto apply guard + this._lastApplied = {}; // { [entity_id]: { min:number, temp:number } } + this._lastAwayActive = null; // Track last away state to trigger immediate apply on change + this._lastPresenceKey = null; // Track last active presence combo key + // Track per-row tools open state + this._toolsOpen = {}; // { [entity_id]: boolean } + + // Copy/paste schedule state + this._copyFrom = null; // entity id of source room + this._copyPayload = null; // deep-cloned schedule from source + + // Weekday modal state + this._weeklyOpen = false; + this._weeklyEntity = null; + this._weeklyDraft = null; // { mode, days: { mon:[], ... } } + this._weeklyDayKey = null; + this._weeklyOverlayHiddenForEditor = false; + // Weekdays timeline view state (browse day when showing one day across rooms) + this._weekdaysViewDayKey = null; + // Weekly copy/paste by group (weekdays/weekend/sat/sun/mon..sun) + this._weeklyCopyFromGroup = null; + this._weeklyCopyPayload = null; + // Pending copy-to-rooms targets (applied on Save) + this._weeklyCopyTargets = []; + + // Profiles modal state (per-room named daily schedules) + this._profilesOpen = false; + this._profilesEntity = null; // entity id for which profiles are managed + this._profilesSelected = null; // currently selected profile name + this._profilesOverlayHiddenForEditor = false; // track if hidden while block editor is open + this._profilesDraft = null; // { name: string, blocks: [...], defaultTemp:number } + this._profilesDirty = false; + this._profilesSaveLock = false; // debounce guard to prevent double-prompt on Enter + this._profilesRoom = null; // current room eid being edited in profiles modal + + // Global profile (shared via integration settings) + this._globalProfile = null; + // Track settings version even when storage is off + this._lastSettingsVersion = 0; + this._lastColorsVersion = 0; + this._lastWeekdayVersion = 0; + // HA event subscription (state_changed for storage sensor) + this._haEvtUnsub = null; + this._subStorageEid = null; + + // Holidays modal state + this._holidayOpen = false; + this._holidayEntity = null; // deprecated (use _holidayRoom) + this._holidayRoom = null; // current room eid being edited in holiday modal + this._holidayDraft = null; // { rooms: { [eid]: [blocks] } } + this._holidayOverlayHiddenForEditor = false; + // Holiday copy popup + this._holidayCopyOpen = false; + this._holidayCopySel = null; // value string like 'main::main', 'weekday::mon', 'profile::Name', 'presence::key' + + // External refresh hook (legacy). Reload store so other cards update immediately. + this._onExternalRefresh = () => { + try { + if (this._inlineEditing || this._editing) return; + this._loadStore(false).then(()=>{ + try { this._ensureSchedules(); } catch {} + try { this._render(); } catch {} + try { this._scheduleNextApply(); } catch {} + }); + } catch {} + }; + try { window.addEventListener('thermostat-timeline-refresh', this._onExternalRefresh); } catch {} + // External request to open holiday editor for a specific room + this._onOpenHoliday = (ev)=>{ try { const eid = ev?.detail?.eid || (this._config?.entities||[])[0] || null; if (eid) { this._openHolidayEditor(eid); } } catch {} }; + try { window.addEventListener('tt-open-holiday', this._onOpenHoliday); } catch {} + // External request to open presence editor + this._onOpenPresence = (ev)=>{ try { const eid = ev?.detail?.eid || (this._config?.entities||[])[0] || null; if (eid) this._openPresenceEditor(eid); } catch {} }; + try { window.addEventListener('tt-open-presence', this._onOpenPresence); } catch {} + + // Onboarding state + this._onboardOpen = false; + this._onboardStep = 0; + this._onboardLastUrl = null; + this._onboardSlides = [ + // Supports video (.mp4/.webm) or images (.webp/.png/.gif) + { key: 'onboard.step_main', img: this._assetUrl('media/thermostat-guide/step1.mp4'), fallback: '/local/media/thermostat-guide/step1.mp4' }, + { key: 'onboard.step_add', img: this._assetUrl('media/thermostat-guide/step2.mp4'), fallback: '/local/media/thermostat-guide/step2.mp4' }, + { key: 'onboard.step_edit', img: this._assetUrl('media/thermostat-guide/step3.mp4'), fallback: '/local/media/thermostat-guide/step3.mp4' }, + ]; + + // Global pause state (temporary suppression of set_temperature) + this._pauseIndef = false; // boolean + this._pauseUntilMs = 0; // epoch ms; > now => paused until time + this._resumeTimer = null; // timer to auto-resume + this._countdownTimer = null; // 1s updater for header countdown + // Sync countdown UI (for delayed sync) + this._syncTimer = null; + this._syncJustUntil = 0; // ms timestamp to show "done" message briefly + // Away bypass (manual override while nobody is home) + this._awayBypass = false; // when true and away is active, do not clamp to away.target_c + // Header tools open/close state + this._headToolsOpen = false; + + // Presence (advanced away) modal state + this._presenceOpen = false; + this._presenceCombo = null; // active combo key selected for editing + this._presenceRoom = null; // current room eid being edited in presence modal + this._presenceDraft = null; // { combo: string, rooms: { [eid]: [blocks] } } + } + + disconnectedCallback() { + if (this._uiTimer) clearInterval(this._uiTimer); + if (this._transitionTimer) clearTimeout(this._transitionTimer); + if (this._saveTimer) clearTimeout(this._saveTimer); + if (this._storeWatchTimer) clearInterval(this._storeWatchTimer); + try { if (this._onExternalRefresh) window.removeEventListener('thermostat-timeline-refresh', this._onExternalRefresh); } catch {} + try { if (this._onStoreUpdated) window.removeEventListener('tt-store-updated', this._onStoreUpdated); } catch {} + try { if (this._bc && this._bc.close) this._bc.close(); } catch {} + this._bc = null; + try { if (this._onOpenHoliday) window.removeEventListener('tt-open-holiday', this._onOpenHoliday); } catch {} + try { if (this._onOpenPresence) window.removeEventListener('tt-open-presence', this._onOpenPresence); } catch {} + try { if (this._haEvtUnsub) { this._haEvtUnsub(); this._haEvtUnsub = null; } } catch {} + } + + _broadcastStoreUpdated(key){ + try { + const payload = { key: String(key || ''), origin: this._broadcastId, ts: Date.now() }; + try { window.dispatchEvent(new CustomEvent('tt-store-updated', { detail: payload })); } catch {} + try { if (this._bc && this._bc.postMessage) this._bc.postMessage(payload); } catch {} + } catch {} + } + + // Resolve a static asset URL so it works in both dev (/local) and HACS (/hacsfiles/) installs. + // Place assets next to the JS file in your HACS repo (e.g. ./media/...), + // or in Home Assistant's config/www (refer using media/...). + _assetUrl(p){ + try { + const rel = String(p||'').replace(/^[\/\\]+/, ''); + // Resolve relative to the current JS module location (works under /local and /hacsfiles) + return new URL(rel, import.meta.url).href; + } catch (e) { + try { return `/local/${String(p||'').replace(/^[\/\\]+/, '')}`; } catch { return String(p||''); } + } + } + + // ---------- Storage (file-based via API) ---------- + // Stub function for backwards compatibility - always returns empty string (no sensor entities) + _storageEntity(kind = 'schedules') { return ''; } + + _normInstanceId(raw){ + try { + const s = String(raw ?? '').trim(); + if (!s) return 'default'; + const out = []; + for (const ch of s) { + const ok = (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch === '_' || ch === '-' || ch === '.'; + out.push(ok ? ch : '_'); + } + const s2 = out.join('').slice(0, 64); + return s2 || 'default'; + } catch { return 'default'; } + } + + _instanceId(){ + try { + if (!this._config?.instance_enabled) return 'default'; + return this._normInstanceId(this._config?.instance_id || 'default'); + } catch { return 'default'; } + } + + _localStoreKey(){ + try { + const iid = this._instanceId(); + return (this._config?.instance_enabled) ? `thermostat_timeline_store:${iid}` : 'thermostat_timeline_store'; + } catch { return 'thermostat_timeline_store'; } + } + + _hardResetKey(){ + // Marker to suppress importing legacy/local drafts after a full factory reset. + return 'tt_hard_reset_ts'; + } + _hardResetActive(maxAgeMs = 10 * 60 * 1000){ + try { + const raw = localStorage.getItem(this._hardResetKey()) || ''; + const ts = Number(raw); + if (!Number.isFinite(ts) || ts <= 0) return false; + return (Date.now() - ts) >= 0 && (Date.now() - ts) <= maxAgeMs; + } catch { return false; } + } + + _hashStr(s){ + try { + let h = 5381; + const str = String(s ?? ''); + for (let i = 0; i < str.length; i++) { + h = ((h << 5) + h) ^ str.charCodeAt(i); + } + return (h >>> 0).toString(36); + } catch { return '0'; } + } + + _instanceCfgLocalKeyLegacy(){ + // Legacy v1 key (shared across cards with the same entities) + try { + const ents = Array.isArray(this._config?.entities) ? this._config.entities.filter(Boolean).map(String) : []; + const base = `v1|${ents.slice().sort().join(',')}`; + return `tt_instance_cfg:${this._hashStr(base)}`; + } catch { return 'tt_instance_cfg:0'; } + } + + _instanceCfgGroupHash(){ + // Used to group similar cards; keep aligned with legacy v1 base. + try { + const ents = Array.isArray(this._config?.entities) ? this._config.entities.filter(Boolean).map(String) : []; + const base = `v1|${ents.slice().sort().join(',')}`; + return this._hashStr(base); + } catch { return '0'; } + } + + _pageHash(){ + try { + const loc = (window?.location?.pathname || '') + (window?.location?.search || '') + (window?.location?.hash || ''); + return this._hashStr(loc); + } catch { return '0'; } + } + + _cardOrdinalInGroup(){ + try { + const grp = this._instanceCfgGroupHash(); + const root = (typeof this.getRootNode === 'function' ? this.getRootNode() : null); + const scope = (root && typeof root.querySelectorAll === 'function') ? root : document; + const all = Array.from(scope.querySelectorAll('thermostat-timeline-card') || []); + const same = []; + for (const el of all) { + try { + const g = (typeof el?._instanceCfgGroupHash === 'function') ? el._instanceCfgGroupHash() : null; + if (g === grp) same.push(el); + } catch {} + } + const idx = same.indexOf(this); + return idx >= 0 ? idx : 0; + } catch { return 0; } + } + + _instanceCfgLocalKey(){ + // v3: stable per-card key + try { + const uid = this._normInstanceId(this._config?.instance_uid || ''); + if (uid && uid !== 'default') return `tt_instance_cfg:v3:${uid}`; + // Fallback (should rarely happen) + const page = this._pageHash(); + const grp = this._instanceCfgGroupHash(); + const ord = this._cardOrdinalInGroup(); + return `tt_instance_cfg:v2:${page}:${grp}:${ord}`; + } catch { return 'tt_instance_cfg:v3:0'; } + } + + _instanceCfgLocalKeyV2(){ + try { + const page = this._pageHash(); + const grp = this._instanceCfgGroupHash(); + const ord = this._cardOrdinalInGroup(); + return `tt_instance_cfg:v2:${page}:${grp}:${ord}`; + } catch { return 'tt_instance_cfg:v2:0:0:0'; } + } + + _genInstanceId(){ + try { + const suffix = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2,6)}`; + return this._normInstanceId(`thermostat_pro_timeline_${suffix}`); + } catch { + return this._normInstanceId(`thermostat_pro_timeline_${Math.random().toString(36).slice(2,10)}`); + } + } + + _loadPersistedInstanceCfg(){ + try { + const key = this._instanceCfgLocalKey(); + const raw = localStorage.getItem(key) || ''; + if (raw) { + const obj = JSON.parse(raw); + if (!obj || typeof obj !== 'object') return null; + const enabled = !!obj.enabled; + const id = this._normInstanceId(obj.id || ''); + return { enabled, id }; + } + + // v2 fallback (older per-card key) + try { + const k2 = this._instanceCfgLocalKeyV2(); + const r2 = localStorage.getItem(k2) || ''; + if (r2) { + const o2 = JSON.parse(r2); + const enabled2 = !!o2?.enabled; + const id2 = this._normInstanceId(o2?.id || ''); + if (enabled2 && id2) { + try { this._persistInstanceCfg(true, id2); } catch {} + return { enabled: true, id: id2 }; + } + } + } catch {} + + // Migration from legacy shared key (prevents two identical cards getting the same ID) + const legacyKey = this._instanceCfgLocalKeyLegacy(); + const legacyRaw = localStorage.getItem(legacyKey) || ''; + if (!legacyRaw) return null; + const objL = JSON.parse(legacyRaw); + if (!objL || typeof objL !== 'object') return null; + const enabledL = !!objL.enabled; + const idL = this._normInstanceId(objL.id || ''); + if (!enabledL || !idL) return null; + + const ord = this._cardOrdinalInGroup(); + if (ord === 0) { + try { this._persistInstanceCfg(true, idL); } catch {} + return { enabled: true, id: idL }; + } + + // For subsequent cards with identical config, auto-generate a new id. + const newId = this._genInstanceId(); + try { this._persistInstanceCfg(true, newId); } catch {} + try { this._copyInstanceData(idL, newId); } catch {} + return { enabled: true, id: newId }; + } catch { return null; } + } + + _persistInstanceCfg(enabled, id){ + try { + const obj = { enabled: !!enabled, id: this._normInstanceId(id || '') }; + localStorage.setItem(this._instanceCfgLocalKey(), JSON.stringify(obj)); + } catch {} + } + + async _copyInstanceData(oldId, newId){ + try { + const oldI = this._normInstanceId(oldId); + const newI = this._normInstanceId(newId); + if (!oldI || !newI || oldI === newI) return; + + // Backend store copy (keeps old instance intact) + if (this._config?.storage_enabled && this._hass?.callApi && this._hass?.callService) { + try { + const api = await this._hass.callApi('GET', `thermostat_timeline/state?instance_id=${encodeURIComponent(oldI)}`); + const payload = { force: true, instance_id: newI, activate: false }; + if (api?.schedules) payload.schedules = api.schedules; + if (api?.settings) payload.settings = api.settings; + if (api?.colors) payload.colors = api.colors; + if (api?.weekdays) payload.weekdays = api.weekdays; + if (api?.profiles) payload.profiles = api.profiles; + await this._hass.callService('thermostat_timeline', 'set_store', payload); + return; + } catch {} + } + + // Local-only copy + try { this._copyInstanceLocalKeys(oldI, newI); } catch {} + } catch {} + } + + async _renameInstanceData(oldId, newId){ + try { + const oldI = this._normInstanceId(oldId); + const newI = this._normInstanceId(newId); + if (!oldI || !newI || oldI === newI) return false; + + // Backend rename/move (does not create a second instance) + if (this._config?.storage_enabled && this._hass?.callService) { + try { + await this._hass.callService('thermostat_timeline', 'rename_instance', { + old_instance_id: oldI, + new_instance_id: newI, + }); + return true; + } catch {} + } + + // Local-only rename/move + try { return !!this._moveInstanceLocalKeys(oldI, newI); } catch {} + } catch {} + return false; + } + + _hydrateInstanceCfgFromLocal(){ + try { + const curEnabled = !!this._config?.instance_enabled; + const curId = String(this._config?.instance_id || '').trim(); + if (curEnabled && curId) { + this._persistInstanceCfg(true, curId); + return; + } + const persisted = this._loadPersistedInstanceCfg(); + if (persisted && persisted.enabled && persisted.id) { + this._config.instance_enabled = true; + this._config.instance_id = persisted.id; + } + } catch {} + } + + _apiSupported(){ try { return !!(this._hass?.callApi) || !!(this._hass?.auth?.data?.access_token); } catch { return false; } } + async _apiGet(path){ + const url = path.startsWith('/') ? path : `/api/${path}`; + // 1) Prefer Home Assistant's callApi when available + if (this._hass?.callApi) { + try { + return await this._hass.callApi('GET', path); + } catch (e) {} + } + // 2) Fallback: direct fetch with bearer token (mobile/webview safety) + try { + const token = this._hass?.auth?.data?.access_token; + const resp = await fetch(url, { + method: 'GET', + headers: token ? { Authorization: `Bearer ${token}` } : {}, + credentials: 'same-origin' + }); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + return await resp.json(); + } catch (e2) { return null; } + } + async _apiFetchState(){ + try { + const iid = this._instanceId(); + const q = (this._config?.instance_enabled && iid) ? `?instance_id=${encodeURIComponent(iid)}` : ''; + return await this._apiGet(`thermostat_timeline/state${q}`); + } catch { + return await this._apiGet('thermostat_timeline/state'); + } + } + async _apiFetchVersion(){ return await this._apiGet('thermostat_timeline/version'); } + + async _loadStore(preferSensor = true) { + const hardReset = this._hardResetActive(); + let stMain = null, stSet = null, stCol = null, stWeek = null, stProf = null; + // Load from integration API (file-based storage) + if (this._config?.storage_enabled) { + const api = await this._apiFetchState(); + if (api && typeof api === 'object') { + stMain = { state: Number(api.version || 0), attributes: { schedules: api.schedules, weekdays: api.weekdays, profiles: api.profiles, settings: api.settings, colors: api.colors } }; + stSet = { state: Number(api.settings_version || api.version || 0), attributes: { settings: api.settings, colors: api.colors } }; + stCol = { state: Number(api.colors_version || api.version || 0), attributes: { colors: api.colors } }; + stWeek = { state: Number(api.weekday_version || api.version || 0), attributes: { weekdays: api.weekdays } }; + stProf = { state: Number(api.profile_version || api.version || 0), attributes: { profiles: api.profiles, settings: api.settings } }; + } + } + + if (stMain || stSet || stCol || stWeek || stProf) { + try { + const attrsMain = stMain?.attributes || {}; + const attrsSet = stSet?.attributes || {}; + const attrsCol = stCol?.attributes || {}; + const attrsWeek = stWeek?.attributes || {}; + const attrsProf = stProf?.attributes || {}; + let storUnit = 'C'; + const schedSource = (attrsMain?.schedules && typeof attrsMain.schedules === "object") ? attrsMain : ((attrsSet?.schedules && typeof attrsSet.schedules === 'object') ? attrsSet : null); + if (schedSource) { + // Unwrap if an older/incorrect payload put the entire object under 'schedules' + let sch = JSON.parse(JSON.stringify(schedSource.schedules)); + if (sch && sch.schedules && typeof sch.schedules === 'object') sch = sch.schedules; + // Canonical storage is °C. For backwards compatibility, detect legacy stores that saved °F values. + try { + const su = String((schedSource?.settings?.storage_temp_unit || '')).toUpperCase(); + const legacyDisplayUnit = String((schedSource?.settings?.temp_unit || 'C')).toUpperCase(); + const maxT = this._maxTempInSchedules(sch); + const looksLikeF = Number.isFinite(maxT) && maxT > 45; + const legacyWasF = (legacyDisplayUnit === 'F'); + const storeWasF = (su === 'F') || (!su && (legacyWasF || looksLikeF)); + if (storeWasF) sch = this._convertSchedulesTemps(sch, 'C'); + } catch {} + this._schedules = sch; + // If there is a pending delayed sync (due stored locally), prefer draft schedules + try { + if (hardReset) throw new Error('hard reset active'); + const dueRaw = localStorage.getItem(this._syncDueKey()) || ''; + const due = Number(dueRaw); + if (Number.isFinite(due) && due > Date.now()) { + const dr = localStorage.getItem(this._draftKey()) || ''; + if (dr) { + const parsed = JSON.parse(dr); + const dsch = parsed?.schedules && parsed.schedules.schedules ? parsed.schedules.schedules : (parsed?.schedules || parsed || {}); + if (dsch && typeof dsch === 'object') { + this._schedules = dsch; + } + } + } + } catch {} + } + // Merge weekday-only payload into schedules (weekly + weekly_modes) + try { + const weekSrc = (attrsWeek?.weekdays && typeof attrsWeek.weekdays === 'object') ? attrsWeek.weekdays + : ((attrsMain?.weekdays && typeof attrsMain.weekdays === 'object') ? attrsMain.weekdays : null); + if (weekSrc && typeof weekSrc === 'object') { + for (const eid of Object.keys(weekSrc)) { + const wk = weekSrc[eid]; + if (!wk || typeof wk !== 'object') continue; + const row = this._schedules[eid] || {}; + if (wk.weekly) row.weekly = wk.weekly; + if (wk.weekly_modes) row.weekly_modes = wk.weekly_modes; + this._schedules[eid] = row; + } + } + } catch {} + // Merge profile-only payload into schedules (profiles + activeProfile) + try { + let profSrc = null; + if (attrsProf?.profiles && typeof attrsProf.profiles === 'object') profSrc = attrsProf.profiles; + else if (attrsMain?.profiles && typeof attrsMain.profiles === 'object') profSrc = attrsMain.profiles; + if (profSrc && typeof profSrc === 'object') { + // Canonical storage is °C. For backwards compatibility, detect legacy °F profile payloads. + try { + const su = String((attrsProf?.settings?.storage_temp_unit || attrsMain?.settings?.storage_temp_unit || '')).toUpperCase(); + const legacyDisplayUnit = String((attrsProf?.settings?.temp_unit || attrsMain?.settings?.temp_unit || 'C')).toUpperCase(); + const maxT = this._maxTempInSchedules(profSrc); + const looksLikeF = Number.isFinite(maxT) && maxT > 45; + const legacyWasF = (legacyDisplayUnit === 'F'); + const storeWasF = (su === 'F') || (!su && (legacyWasF || looksLikeF)); + if (storeWasF) profSrc = this._convertSchedulesTemps(profSrc, 'C'); + } catch {} + for (const eid of Object.keys(profSrc)) { + const src = profSrc[eid]; + if (!src || typeof src !== 'object') continue; + const row = this._schedules[eid] || {}; + if (src.profiles && typeof src.profiles === 'object') row.profiles = src.profiles; + if (typeof src.activeProfile === 'string' || src.activeProfile === null) row.activeProfile = src.activeProfile; + this._schedules[eid] = row; + } + } + } catch {} + // Optional shared settings (prefer dedicated settings sensor when provided) + let settingsSource = null; + if (attrsSet.settings && typeof attrsSet.settings === 'object') settingsSource = attrsSet.settings; + else if (attrsMain.settings && typeof attrsMain.settings === 'object') settingsSource = attrsMain.settings; + try { if (storUnit === 'C' && settingsSource?.temp_unit) storUnit = String(settingsSource.temp_unit).toUpperCase(); } catch {} + let colorsSource = null; + if (attrsCol.colors && typeof attrsCol.colors === 'object') colorsSource = attrsCol.colors; + else if (attrsSet.colors && typeof attrsSet.colors === 'object') colorsSource = attrsSet.colors; + else if (settingsSource && (settingsSource.color_ranges || settingsSource.color_global)) { + colorsSource = { color_ranges: settingsSource.color_ranges, color_global: settingsSource.color_global }; + } + if (settingsSource) { + try { + const s0 = settingsSource; + // Canonical storage is °C. For backwards compatibility, detect legacy stores that saved numeric settings in °F. + let s = s0; + try { + const su = String(s0?.storage_temp_unit || '').toUpperCase(); + const legacyDisplayUnit = String(s0?.temp_unit||'C').toUpperCase(); + const nums = [s0?.default_temp, s0?.min_temp, s0?.max_temp, s0?.boiler_min_temp, s0?.boiler_max_temp, s0?.away?.target_c].map(v=>Number(v)).filter(v=>Number.isFinite(v)); + const maxN = nums.length ? Math.max(...nums) : -Infinity; + const looksLikeF = Number.isFinite(maxN) && maxN > 45; + const legacyWasF = (legacyDisplayUnit === 'F'); + const storeWasF = (su === 'F') || (!su && (legacyWasF || looksLikeF)); + if (storeWasF) { + const conv = (n)=>{ const v=Number(n); return Number.isFinite(v)? this._fToC(v): n; }; + const so = { ...s0 }; + if (so.default_temp !== undefined) so.default_temp = conv(so.default_temp); + if (so.min_temp !== undefined) so.min_temp = conv(so.min_temp); + if (so.max_temp !== undefined) so.max_temp = conv(so.max_temp); + if (so.boiler_min_temp !== undefined) so.boiler_min_temp = conv(so.boiler_min_temp); + if (so.boiler_max_temp !== undefined) so.boiler_max_temp = conv(so.boiler_max_temp); + if (so.away && typeof so.away==='object') { + so.away = { ...so.away }; + if (so.away.target_c !== undefined) so.away.target_c = conv(so.away.target_c); + } + try { + if (so.presence_sensor_temps && typeof so.presence_sensor_temps === 'object') { + const out = {}; + for (const k of Object.keys(so.presence_sensor_temps||{})) out[String(k)] = conv(so.presence_sensor_temps[k]); + so.presence_sensor_temps = out; + } + } catch {} + s = so; + } + } catch {} + // Pick up format/unit from shared storage when not YAML-provided + if (typeof s.time_12h === 'boolean' && !this._yamlProvided?.time_12h) this._config.time_12h = !!s.time_12h; + if (typeof s.temp_unit === 'string' && !this._yamlProvided?.temp_unit) this._config.temp_unit = (String(s.temp_unit).toUpperCase()==='F' ? 'F' : 'C'); + // Pick up time_source from shared storage when not YAML-provided + if (typeof s.time_source === 'string' && !this._yamlProvided?.time_source) this._config.time_source = (s.time_source === 'ha' ? 'ha' : 'browser'); + if (s.color_ranges && typeof s.color_ranges==='object' && !this._yamlProvided?.color_ranges) this._config.color_ranges = { ...s.color_ranges }; + if (typeof s.color_global === 'boolean') this._config.color_global = !!s.color_global; + if (Number.isFinite(s.default_temp)) this._config.default_temp = Number(s.default_temp); + if (Number.isFinite(s.min_temp)) this._config.min_temp = Number(s.min_temp); + if (Number.isFinite(s.max_temp)) this._config.max_temp = Number(s.max_temp); + if (Number.isFinite(s.row_height)) this._config.row_height = Math.max(30, Math.min(200, Math.round(Number(s.row_height)))); + + // Rooms from shared storage (used by Settings popup) + try { + if (Array.isArray(s.entities)) { + const ents = s.entities.filter(Boolean).map(String); + this._config.entities = ents; + const rawModes = Array.isArray(s.room_use_input_number) ? s.room_use_input_number.map(v=>!!v) : []; + while (rawModes.length < ents.length) rawModes.push(false); + this._config.room_use_input_number = ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return true; + if (String(eid).startsWith('climate.')) return false; + return !!rawModes[idx]; + }); + + const rawTemp = Array.isArray(s.room_use_temp_sensor) ? s.room_use_temp_sensor.map(v=>!!v) : []; + while (rawTemp.length < ents.length) rawTemp.push(false); + this._config.room_use_temp_sensor = ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return false; + if (!String(eid).startsWith('climate.')) return false; + return !!rawTemp[idx]; + }); + } + } catch {} + + if (s.merges && typeof s.merges === 'object' && !this._yamlProvided?.merges) this._config.merges = { ...s.merges }; + if (s.labels && typeof s.labels === 'object' && !this._yamlProvided?.labels) this._config.labels = { ...s.labels }; + if (s.temp_sensors && typeof s.temp_sensors === 'object' && !this._yamlProvided?.temp_sensors) this._config.temp_sensors = { ...s.temp_sensors }; + if (s.turn_on && typeof s.turn_on === 'object' && !this._yamlProvided?.turn_on) this._config.turn_on = { ...s.turn_on }; + if (s.presence_sensors && typeof s.presence_sensors === 'object' && !this._yamlProvided?.presence_sensors) this._config.presence_sensors = { ...s.presence_sensors }; + try { + if (s.presence_sensor_temps && typeof s.presence_sensor_temps === 'object' && !this._yamlProvided?.presence_sensor_temps) { + const out = {}; + for (const k of Object.keys(s.presence_sensor_temps||{})) { + const v = Number(s.presence_sensor_temps[k]); + if (Number.isFinite(v)) out[String(k)] = v; + } + this._config.presence_sensor_temps = out; + } + } catch {} + try { + if (s.presence_sensor_delays && typeof s.presence_sensor_delays === 'object' && !this._yamlProvided?.presence_sensor_delays) { + const out = {}; + for (const k of Object.keys(s.presence_sensor_delays||{})) { + const v = s.presence_sensor_delays?.[k]; + if (!v || typeof v !== 'object') continue; + const onS = Number(v.on_s); + const offS = Number(v.off_s); + const obj = {}; + if (Number.isFinite(onS) && onS >= 0) obj.on_s = onS; + if (Number.isFinite(offS) && offS >= 0) obj.off_s = offS; + if (Object.keys(obj).length) out[String(k)] = obj; + } + this._config.presence_sensor_delays = out; + } + } catch {} + + try { + if (s.presence_sensor_delay_units && typeof s.presence_sensor_delay_units === 'object' && !this._yamlProvided?.presence_sensor_delay_units) { + const out = {}; + for (const k of Object.keys(s.presence_sensor_delay_units||{})) { + const u = String(s.presence_sensor_delay_units?.[k] || '').toLowerCase().trim(); + out[String(k)] = (u === 'seconds') ? 'seconds' : 'minutes'; + } + this._config.presence_sensor_delay_units = out; + } + } catch {} + + // Fallbacks for Rooms tab settings if backend didn't persist them: + // Merge from local browser copy to avoid losing user edits (e.g., custom room names) + try { + if (hardReset) throw new Error('hard reset active'); + const rawLocal = localStorage.getItem(this._localStoreKey()) || ''; + if (rawLocal) { + const parsed = JSON.parse(rawLocal); + const ls = parsed?.settings || {}; + // Only merge when YAML did not provide the key and backend didn't send it + const isEmptyObj = (o)=>{ try { return o && typeof o==='object' && Object.keys(o).length===0; } catch { return false; } }; + if (!this._yamlProvided?.labels && (!s.labels || typeof s.labels !== 'object' || isEmptyObj(s.labels))) { + if (ls.labels && typeof ls.labels === 'object') { + this._config.labels = { ...(this._config.labels || {}), ...ls.labels }; + } + } + if (!this._yamlProvided?.merges && (!s.merges || typeof s.merges !== 'object' || isEmptyObj(s.merges))) { + if (ls.merges && typeof ls.merges === 'object') { + this._config.merges = { ...(this._config.merges || {}), ...ls.merges }; + } + } + if (!this._yamlProvided?.temp_sensors && (!s.temp_sensors || typeof s.temp_sensors !== 'object' || isEmptyObj(s.temp_sensors))) { + if (ls.temp_sensors && typeof ls.temp_sensors === 'object') { + this._config.temp_sensors = { ...(this._config.temp_sensors || {}), ...ls.temp_sensors }; + } + } + if (!this._yamlProvided?.turn_on && (!s.turn_on || typeof s.turn_on !== 'object' || isEmptyObj(s.turn_on))) { + if (ls.turn_on && typeof ls.turn_on === 'object') { + this._config.turn_on = { ...(this._config.turn_on || {}), ...ls.turn_on }; + } + } + if (!this._yamlProvided?.presence_sensors && (!s.presence_sensors || typeof s.presence_sensors !== 'object' || isEmptyObj(s.presence_sensors))) { + if (ls.presence_sensors && typeof ls.presence_sensors === 'object') { + this._config.presence_sensors = { ...(this._config.presence_sensors || {}), ...ls.presence_sensors }; + } + } + if (!this._yamlProvided?.presence_sensor_temps && (!s.presence_sensor_temps || typeof s.presence_sensor_temps !== 'object' || isEmptyObj(s.presence_sensor_temps))) { + if (ls.presence_sensor_temps && typeof ls.presence_sensor_temps === 'object') { + this._config.presence_sensor_temps = { ...(this._config.presence_sensor_temps || {}), ...ls.presence_sensor_temps }; + } + } + if (!this._yamlProvided?.presence_sensor_delays && (!s.presence_sensor_delays || typeof s.presence_sensor_delays !== 'object' || isEmptyObj(s.presence_sensor_delays))) { + if (ls.presence_sensor_delays && typeof ls.presence_sensor_delays === 'object') { + this._config.presence_sensor_delays = { ...(this._config.presence_sensor_delays || {}), ...ls.presence_sensor_delays }; + } + } + if (!this._yamlProvided?.presence_sensor_delay_units && (!s.presence_sensor_delay_units || typeof s.presence_sensor_delay_units !== 'object' || isEmptyObj(s.presence_sensor_delay_units))) { + if (ls.presence_sensor_delay_units && typeof ls.presence_sensor_delay_units === 'object') { + this._config.presence_sensor_delay_units = { ...(this._config.presence_sensor_delay_units || {}), ...ls.presence_sensor_delay_units }; + } + } + if (Array.isArray(this._config.entities) && (!Array.isArray(s.entities) || !s.entities.length)) { + if (Array.isArray(ls.entities) && ls.entities.length) { + this._config.entities = ls.entities.filter(Boolean).map(String); + // room_use_input_number alignment + const rawModes = Array.isArray(ls.room_use_input_number) ? ls.room_use_input_number.map(v=>!!v) : []; + while (rawModes.length < this._config.entities.length) rawModes.push(false); + this._config.room_use_input_number = this._config.entities.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return true; + if (String(eid).startsWith('climate.')) return false; + return !!rawModes[idx]; + }); + + const rawTemp = Array.isArray(ls.room_use_temp_sensor) ? ls.room_use_temp_sensor.map(v=>!!v) : []; + while (rawTemp.length < this._config.entities.length) rawTemp.push(false); + this._config.room_use_temp_sensor = this._config.entities.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return false; + if (!String(eid).startsWith('climate.')) return false; + return !!rawTemp[idx]; + }); + } + } + } + } catch {} + if ((typeof s.boiler_enabled === 'boolean' || typeof s.boiler_enabled === 'number') && !this._yamlProvided?.boiler_enabled) this._config.boiler_enabled = !!s.boiler_enabled; + if (typeof s.boiler_switch === 'string' && !this._yamlProvided?.boiler_switch) this._config.boiler_switch = s.boiler_switch || ''; + if (!this._yamlProvided?.boiler_rooms) { + if (Array.isArray(s.boiler_rooms)) this._config.boiler_rooms = s.boiler_rooms.filter(Boolean).map(String); + else if (s.boiler_rooms === null) this._config.boiler_rooms = null; + } + if (!this._yamlProvided?.boiler_on_offset) { + if (s.boiler_on_offset === null) this._config.boiler_on_offset = 0; + else { const v = Number(s.boiler_on_offset); if (Number.isFinite(v)) this._config.boiler_on_offset = Math.max(-10, Math.min(10, v)); } + } + if (!this._yamlProvided?.boiler_off_offset) { + if (s.boiler_off_offset === null) this._config.boiler_off_offset = 0; + else { const v = Number(s.boiler_off_offset); if (Number.isFinite(v)) this._config.boiler_off_offset = Math.max(-10, Math.min(10, v)); } + } + if (typeof s.boiler_temp_sensor === 'string' && !this._yamlProvided?.boiler_temp_sensor) this._config.boiler_temp_sensor = s.boiler_temp_sensor || ''; + if (!this._yamlProvided?.boiler_min_temp) { + if (s.boiler_min_temp === null) this._config.boiler_min_temp = 20; + else { const v = Number(s.boiler_min_temp); if (Number.isFinite(v)) this._config.boiler_min_temp = v; } + } + if (!this._yamlProvided?.boiler_max_temp) { + if (s.boiler_max_temp === null) this._config.boiler_max_temp = 25; + else { const v = Number(s.boiler_max_temp); if (Number.isFinite(v)) this._config.boiler_max_temp = v; } + } + try { this._awayBypass = !!s.away_bypass; } catch {} + if (typeof s.per_room_defaults === 'boolean') this._config.per_room_defaults = !!s.per_room_defaults; + if (typeof s.show_pause_button === 'boolean') this._config.show_pause_button = !!s.show_pause_button; + if (typeof s.show_room_temp === 'boolean' && !this._yamlProvided?.show_room_temp) this._config.show_room_temp = !!s.show_room_temp; + if (typeof s.pause_sensor_enabled === 'boolean' && !this._yamlProvided?.pause_sensor_enabled) this._config.pause_sensor_enabled = !!s.pause_sensor_enabled; + if (typeof s.pause_sensor_entity === 'string' && !this._yamlProvided?.pause_sensor_entity) this._config.pause_sensor_entity = String(s.pause_sensor_entity || ''); + if ((typeof s.presence_sensor_enabled === 'boolean' || typeof s.presence_sensor_enabled === 'number') && !this._yamlProvided?.presence_sensor_enabled) this._config.presence_sensor_enabled = !!s.presence_sensor_enabled; + if (typeof s.profiles_enabled === 'boolean' && !this._yamlProvided?.profiles_enabled) this._config.profiles_enabled = !!s.profiles_enabled; + if (typeof s.auto_apply_enabled === 'boolean') this._config.auto_apply = !!s.auto_apply_enabled; + if (typeof s.apply_on_edit === 'boolean') this._config.apply_on_edit = !!s.apply_on_edit; + if (typeof s.apply_on_default_change === 'boolean') this._config.apply_on_default_change = !!s.apply_on_default_change; + // Open Window Detection from shared storage + try { if (s.open_window && typeof s.open_window === 'object') this._config.open_window = { ...s.open_window }; } catch {} + // Holidays shared settings + try { + if (typeof s.holidays_enabled === 'boolean') this._config.holidays_enabled = !!s.holidays_enabled; + if (typeof s.holidays_source === 'string' && !this._yamlProvided?.holidays_source) this._config.holidays_source = s.holidays_source === 'manual' ? 'manual' : 'calendar'; + if (typeof s.holidays_entity === 'string' && !this._yamlProvided?.holidays_entity) this._config.holidays_entity = s.holidays_entity || ''; + if (Array.isArray(s.holidays_dates) && !this._yamlProvided?.holidays_dates) this._config.holidays_dates = [...s.holidays_dates]; + if (Array.isArray(s.holidays_groups)) this._config.holidays_groups = [...s.holidays_groups]; + } catch {} + // Weekdays settings (shared) + if (this._config?.respect_storage_weekdays !== false) { + try { if (typeof s.weekdays_enabled === 'boolean') this._config.weekdays_enabled = !!s.weekdays_enabled; } catch {} + try { if (typeof s.weekdays_mode === 'string') this._config.weekdays_mode = s.weekdays_mode; } catch {} + try { if (typeof s.weekdays_view === 'string') this._config.weekdays_view = s.weekdays_view; } catch {} + try { if (typeof s.weekdays_view_switch_in_timeline === 'boolean') this._config.weekdays_view_switch_in_timeline = !!s.weekdays_view_switch_in_timeline; } catch {} + try { if (typeof s.weekdays_selected_room === 'string') this._config.weekdays_selected_room = s.weekdays_selected_room; } catch {} + } + // Pick up a global profile selection from shared storage + try { this._globalProfile = (typeof s.global_profile === 'string' && s.global_profile) ? String(s.global_profile) : null; } catch {} + // Backup settings + try { + if (typeof s.backup_auto_enabled === 'boolean') this._config.backup_auto_enabled = !!s.backup_auto_enabled; + if (Number.isFinite(s.backup_interval_days)) this._config.backup_interval_days = Math.max(1, Math.min(365, Math.round(Number(s.backup_interval_days)))); + else if (Number.isFinite(s.backup_interval_min)) this._config.backup_interval_days = Math.max(1, Math.min(365, Math.ceil(Number(s.backup_interval_min)/1440))); + } catch {} + if (s.away && typeof s.away === 'object') { + try { + const a = s.away; + this._config.away = { + enabled: !!a.enabled, + persons: Array.isArray(a.persons) ? [...a.persons].filter(Boolean).map(String) : [], + target_c: Number.isFinite(a.target_c) ? Number(a.target_c) : (this._config.away?.target_c ?? 17), + advanced_enabled: !!(a.advanced_enabled ?? this._config?.away?.advanced_enabled ?? false), + combos: (a.combos && typeof a.combos === 'object') ? { ...a.combos } : (this._config?.away?.combos || {}), + delay_enabled: !!(a.delay_enabled ?? this._config?.away?.delay_enabled ?? false), + delay_value: Number.isFinite(a.delay_value) ? Number(a.delay_value) : (this._config?.away?.delay_value ?? 0), + delay_unit: String(a.delay_unit || this._config?.away?.delay_unit || 'minutes') + }; + } catch {} + } + // Guard: Away/Presence should not be active until at least one person is selected. + try { + const ap = Array.isArray(this._config?.away?.persons) ? this._config.away.persons.filter(Boolean).map(String) : []; + this._config.away.persons = ap; + if (!ap.length) { this._config.away.enabled = false; this._config.away.advanced_enabled = false; } + } catch {} + // Pause flags from shared storage + try { + this._pauseIndef = !!s.pause_indef; + const pu = Number(s.pause_until_ms); + this._pauseUntilMs = Number.isFinite(pu) ? pu : 0; + } catch {} + // Delayed sync due time from shared storage (persist across reloads) + try { + const due = Number(s.sync_pending_until_ms); + if (Number.isFinite(due) && due > Date.now()) { + this._storeDelayDue = due; + try { localStorage.setItem(this._syncDueKey(), String(due)); } catch {} + // While a delayed sync is pending, prefer local browser draft for settings that affect UI immediately + try { + if (hardReset) throw new Error('hard reset active'); + const rawLocal = localStorage.getItem(this._localStoreKey()) || ''; + if (rawLocal) { + const parsed = JSON.parse(rawLocal); + const ls = parsed?.settings || {}; + if (typeof ls.holidays_enabled === 'boolean') this._config.holidays_enabled = !!ls.holidays_enabled; + if (typeof ls.holidays_source === 'string') this._config.holidays_source = (ls.holidays_source === 'manual') ? 'manual' : 'calendar'; + if (typeof ls.holidays_entity === 'string') this._config.holidays_entity = String(ls.holidays_entity||''); + if (Array.isArray(ls.holidays_dates)) this._config.holidays_dates = [...ls.holidays_dates]; + if (Array.isArray(ls.holidays_groups)) this._config.holidays_groups = [...ls.holidays_groups]; + } + } catch {} + } + } catch {} + } catch {} + } + if (colorsSource) { + try { + if (colorsSource.color_ranges && typeof colorsSource.color_ranges==='object' && !this._yamlProvided?.color_ranges) this._config.color_ranges = { ...colorsSource.color_ranges }; + if (typeof colorsSource.color_global === 'boolean') this._config.color_global = !!colorsSource.color_global; + } catch {} + } + this._lastVersion = Number(stMain?.state || stSet?.state || 0) || 0; + this._lastSettingsVersion = Number(stSet?.state || stMain?.state || 0) || this._lastVersion; + this._lastColorsVersion = Number(stCol?.state || this._lastSettingsVersion || this._lastVersion || 0) || 0; + this._lastWeekdayVersion = Number(stWeek?.state || this._lastVersion || 0) || 0; + this._lastProfileVersion = Number(stProf?.state || this._lastVersion || 0) || 0; + try { const payload = this._makeStoragePayload(true); localStorage.setItem(this._localStoreKey(), JSON.stringify(payload)); } catch {} + return; + } catch (e) { /* fallback */ } + } + try { + // During a hard reset window: do not import any local browser state. + if (hardReset) { this._schedules = {}; return; } + + let raw = localStorage.getItem(this._localStoreKey()) || ""; + // Migration fallback: if instances are enabled and no per-instance cache exists yet, reuse legacy cache. + if (!raw && this._config?.instance_enabled) { + const legacy = localStorage.getItem('thermostat_timeline_store') || ''; + if (legacy) { + raw = legacy; + try { localStorage.setItem(this._localStoreKey(), legacy); } catch {} + } + } + if (!raw) { this._schedules = {}; return; } + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object' && parsed.schedules) { + // Unwrap legacy nested structure if present + const sch = parsed.schedules && parsed.schedules.schedules ? parsed.schedules.schedules : parsed.schedules; + // If local copy was stored as °F, convert to °C for internal use + try { + const unit = String(parsed?.settings?.temp_unit||'C').toUpperCase(); + this._schedules = (unit==='F') ? this._convertSchedulesTemps(sch||{}, 'C') : (sch||{}); + } catch { this._schedules = sch || {}; } + try { + const wk = parsed.weekdays; + if (wk && typeof wk === 'object') { + for (const eid of Object.keys(wk)) { + const row = this._schedules[eid] || {}; + const src = wk[eid]; + if (src && typeof src === 'object') { + if (src.weekly) row.weekly = src.weekly; + if (src.weekly_modes) row.weekly_modes = src.weekly_modes; + this._schedules[eid] = row; + } + } + } + } catch {} + try { + let prof = parsed.profiles; + const unit = String(parsed?.settings?.temp_unit||'C').toUpperCase(); + if (prof && typeof prof === 'object' && unit === 'F') { + prof = this._convertSchedulesTemps(prof, 'C'); + } + if (prof && typeof prof === 'object') { + for (const eid of Object.keys(prof)) { + const src = prof[eid]; + if (!src || typeof src !== 'object') continue; + const row = this._schedules[eid] || {}; + if (src.profiles && typeof src.profiles === 'object') row.profiles = src.profiles; + if (Object.prototype.hasOwnProperty.call(src, 'activeProfile')) row.activeProfile = src.activeProfile; + this._schedules[eid] = row; + } + } + } catch {} + const s0 = parsed.settings || {}; + // Convert numeric settings from °F to °C for internal use + let s = s0; + try { + const unit = String(s0?.temp_unit||'C').toUpperCase(); + if (unit==='F') { + const conv = (n)=>{ const v=Number(n); return Number.isFinite(v)? this._fToC(v): n; }; + const so = { ...s0 }; + if (so.default_temp !== undefined) so.default_temp = conv(so.default_temp); + if (so.min_temp !== undefined) so.min_temp = conv(so.min_temp); + if (so.max_temp !== undefined) so.max_temp = conv(so.max_temp); + if (so.boiler_min_temp !== undefined) so.boiler_min_temp = conv(so.boiler_min_temp); + if (so.boiler_max_temp !== undefined) so.boiler_max_temp = conv(so.boiler_max_temp); + if (so.away && typeof so.away==='object') { so.away = { ...so.away }; if (so.away.target_c !== undefined) so.away.target_c = conv(so.away.target_c); } + try { + if (so.presence_sensor_temps && typeof so.presence_sensor_temps === 'object') { + const out = {}; + for (const k of Object.keys(so.presence_sensor_temps||{})) out[String(k)] = conv(so.presence_sensor_temps[k]); + so.presence_sensor_temps = out; + } + } catch {} + s = so; + } + } catch {} + // Pick up format/unit from local browser storage when not YAML-provided + if (typeof s.time_12h === 'boolean' && !this._yamlProvided?.time_12h) this._config.time_12h = !!s.time_12h; + if (typeof s.temp_unit === 'string' && !this._yamlProvided?.temp_unit) this._config.temp_unit = (String(s.temp_unit).toUpperCase()==='F' ? 'F' : 'C'); + if (s.color_ranges && typeof s.color_ranges==='object' && !this._yamlProvided?.color_ranges) this._config.color_ranges = { ...s.color_ranges }; + if (typeof s.color_global === 'boolean') this._config.color_global = !!s.color_global; + if (Number.isFinite(s.default_temp) && !this._yamlProvided?.default_temp) this._config.default_temp = Number(s.default_temp); + if (Number.isFinite(s.min_temp) && !this._yamlProvided?.min_temp) this._config.min_temp = Number(s.min_temp); + if (Number.isFinite(s.max_temp) && !this._yamlProvided?.max_temp) this._config.max_temp = Number(s.max_temp); + + // Rooms from local browser storage (used by Settings popup) + try { + if (Array.isArray(s.entities)) { + const ents = s.entities.filter(Boolean).map(String); + this._config.entities = ents; + const rawModes = Array.isArray(s.room_use_input_number) ? s.room_use_input_number.map(v=>!!v) : []; + while (rawModes.length < ents.length) rawModes.push(false); + this._config.room_use_input_number = ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return true; + if (String(eid).startsWith('climate.')) return false; + return !!rawModes[idx]; + }); + + const rawTemp = Array.isArray(s.room_use_temp_sensor) ? s.room_use_temp_sensor.map(v=>!!v) : []; + while (rawTemp.length < ents.length) rawTemp.push(false); + this._config.room_use_temp_sensor = ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return false; + if (!String(eid).startsWith('climate.')) return false; + return !!rawTemp[idx]; + }); + } + } catch {} + + if (s.merges && typeof s.merges==='object' && !this._yamlProvided?.merges) this._config.merges = { ...s.merges }; + if (s.labels && typeof s.labels==='object' && !this._yamlProvided?.labels) this._config.labels = { ...s.labels }; + if (s.temp_sensors && typeof s.temp_sensors==='object' && !this._yamlProvided?.temp_sensors) this._config.temp_sensors = { ...s.temp_sensors }; + if (s.presence_sensors && typeof s.presence_sensors==='object' && !this._yamlProvided?.presence_sensors) this._config.presence_sensors = { ...s.presence_sensors }; + try { + if (s.presence_sensor_temps && typeof s.presence_sensor_temps === 'object' && !this._yamlProvided?.presence_sensor_temps) { + const out = {}; + for (const k of Object.keys(s.presence_sensor_temps||{})) { + const v = Number(s.presence_sensor_temps[k]); + if (Number.isFinite(v)) out[String(k)] = v; + } + this._config.presence_sensor_temps = out; + } + } catch {} + try { + if (s.presence_sensor_delays && typeof s.presence_sensor_delays === 'object' && !this._yamlProvided?.presence_sensor_delays) { + const out = {}; + for (const k of Object.keys(s.presence_sensor_delays||{})) { + const v = s.presence_sensor_delays?.[k]; + if (!v || typeof v !== 'object') continue; + const onS = Number(v.on_s); + const offS = Number(v.off_s); + const obj = {}; + if (Number.isFinite(onS) && onS >= 0) obj.on_s = onS; + if (Number.isFinite(offS) && offS >= 0) obj.off_s = offS; + if (Object.keys(obj).length) out[String(k)] = obj; + } + this._config.presence_sensor_delays = out; + } + } catch {} + try { + if (s.presence_sensor_delay_units && typeof s.presence_sensor_delay_units === 'object' && !this._yamlProvided?.presence_sensor_delay_units) { + const out = {}; + for (const k of Object.keys(s.presence_sensor_delay_units||{})) { + const u = String(s.presence_sensor_delay_units?.[k] || '').toLowerCase().trim(); + out[String(k)] = (u === 'seconds') ? 'seconds' : 'minutes'; + } + this._config.presence_sensor_delay_units = out; + } + } catch {} + if ((typeof s.boiler_enabled === 'boolean' || typeof s.boiler_enabled === 'number') && !this._yamlProvided?.boiler_enabled) this._config.boiler_enabled = !!s.boiler_enabled; + if (typeof s.boiler_switch === 'string' && !this._yamlProvided?.boiler_switch) this._config.boiler_switch = s.boiler_switch || ''; + if (!this._yamlProvided?.boiler_rooms) { + if (Array.isArray(s.boiler_rooms)) this._config.boiler_rooms = s.boiler_rooms.filter(Boolean).map(String); + else if (s.boiler_rooms === null) this._config.boiler_rooms = null; + } + if (!this._yamlProvided?.boiler_on_offset) { + if (s.boiler_on_offset === null) this._config.boiler_on_offset = 0; + else { const v = Number(s.boiler_on_offset); if (Number.isFinite(v)) this._config.boiler_on_offset = Math.max(-10, Math.min(10, v)); } + } + if (!this._yamlProvided?.boiler_off_offset) { + if (s.boiler_off_offset === null) this._config.boiler_off_offset = 0; + else { const v = Number(s.boiler_off_offset); if (Number.isFinite(v)) this._config.boiler_off_offset = Math.max(-10, Math.min(10, v)); } + } + if (typeof s.boiler_temp_sensor === 'string' && !this._yamlProvided?.boiler_temp_sensor) this._config.boiler_temp_sensor = s.boiler_temp_sensor || ''; + if (!this._yamlProvided?.boiler_min_temp) { + if (s.boiler_min_temp === null) this._config.boiler_min_temp = 20; + else { const v = Number(s.boiler_min_temp); if (Number.isFinite(v)) this._config.boiler_min_temp = v; } + } + if (!this._yamlProvided?.boiler_max_temp) { + if (s.boiler_max_temp === null) this._config.boiler_max_temp = 25; + else { const v = Number(s.boiler_max_temp); if (Number.isFinite(v)) this._config.boiler_max_temp = v; } + } + try { this._awayBypass = !!s.away_bypass; } catch {} + // Open Window Detection from local storage copy + try { if (s.open_window && typeof s.open_window==='object') this._config.open_window = { ...s.open_window }; } catch {} + // Weekdays flags and view (persist across refresh) — skip if editor preview forbids overrides + if (this._config?.respect_storage_weekdays !== false) { + try { if (typeof s.weekdays_enabled === 'boolean') this._config.weekdays_enabled = !!s.weekdays_enabled; } catch {} + try { if (typeof s.weekdays_mode === 'string') this._config.weekdays_mode = s.weekdays_mode; } catch {} + try { if (typeof s.weekdays_view === 'string') this._config.weekdays_view = s.weekdays_view; } catch {} + try { if (typeof s.weekdays_view_switch_in_timeline === 'boolean') this._config.weekdays_view_switch_in_timeline = !!s.weekdays_view_switch_in_timeline; } catch {} + try { if (typeof s.weekdays_selected_room === 'string') this._config.weekdays_selected_room = s.weekdays_selected_room; } catch {} + } + if (typeof s.per_room_defaults === 'boolean') this._config.per_room_defaults = !!s.per_room_defaults; + if (typeof s.show_pause_button === 'boolean' && !this._yamlProvided?.show_pause_button) this._config.show_pause_button = !!s.show_pause_button; + if (typeof s.show_room_temp === 'boolean' && !this._yamlProvided?.show_room_temp) this._config.show_room_temp = !!s.show_room_temp; + if (typeof s.pause_sensor_enabled === 'boolean' && !this._yamlProvided?.pause_sensor_enabled) this._config.pause_sensor_enabled = !!s.pause_sensor_enabled; + if (typeof s.pause_sensor_entity === 'string' && !this._yamlProvided?.pause_sensor_entity) this._config.pause_sensor_entity = String(s.pause_sensor_entity || ''); + if (s.away && typeof s.away === 'object') { + try { + const a = s.away; + this._config.away = { + enabled: !!a.enabled, + persons: Array.isArray(a.persons) ? [...a.persons].filter(Boolean).map(String) : [], + target_c: Number.isFinite(a.target_c) ? Number(a.target_c) : (this._config.away?.target_c ?? 17), + advanced_enabled: !!(a.advanced_enabled ?? this._config?.away?.advanced_enabled ?? false), + combos: (a.combos && typeof a.combos === 'object') ? { ...a.combos } : (this._config?.away?.combos || {}), + delay_enabled: !!(a.delay_enabled ?? this._config?.away?.delay_enabled ?? false), + delay_value: Number.isFinite(a.delay_value) ? Number(a.delay_value) : (this._config?.away?.delay_value ?? 0), + delay_unit: String(a.delay_unit || this._config?.away?.delay_unit || 'minutes') + }; + } catch {} + } + // Guard: Away/Presence should not be active until at least one person is selected. + try { + const ap = Array.isArray(this._config?.away?.persons) ? this._config.away.persons.filter(Boolean).map(String) : []; + this._config.away.persons = ap; + if (!ap.length) { this._config.away.enabled = false; this._config.away.advanced_enabled = false; } + } catch {} + // Load flags from local storage copy + try { this._pauseIndef = !!s.pause_indef; const pu = Number(s.pause_until_ms); this._pauseUntilMs = Number.isFinite(pu) ? pu : 0; } catch {} + try { if (typeof s.profiles_enabled === 'boolean' && !this._yamlProvided?.profiles_enabled) this._config.profiles_enabled = !!s.profiles_enabled; } catch {} + try { if ((typeof s.presence_sensor_enabled === 'boolean' || typeof s.presence_sensor_enabled === 'number') && !this._yamlProvided?.presence_sensor_enabled) this._config.presence_sensor_enabled = !!s.presence_sensor_enabled; } catch {} + try { if (typeof s.apply_on_edit === 'boolean') this._config.apply_on_edit = !!s.apply_on_edit; } catch {} + try { this._globalProfile = (typeof s.global_profile === 'string' && s.global_profile) ? String(s.global_profile) : null; } catch {} + try { if (typeof s.backup_auto_enabled === 'boolean') this._config.backup_auto_enabled = !!s.backup_auto_enabled; if (Number.isFinite(s.backup_interval_days)) this._config.backup_interval_days = Math.max(1, Math.min(365, Math.round(Number(s.backup_interval_days)))); else if (Number.isFinite(s.backup_interval_min)) this._config.backup_interval_days = Math.max(1, Math.min(365, Math.ceil(Number(s.backup_interval_min)/1440))); } catch {} + // Sync policy from storage + try { + if (typeof s.sync_mode === 'string') this._config.storage_sync_mode = (s.sync_mode === 'delay') ? 'delay' : 'instant'; + if (Number.isFinite(s.sync_delay_min)) this._config.storage_sync_min = Math.max(0, Math.min(1440, Number(s.sync_delay_min))); + if (Number.isFinite(s.sync_delay_sec)) this._config.storage_sync_sec = Math.max(5, Math.min(86400, Math.round(Number(s.sync_delay_sec)))); + else if (Number.isFinite(s.sync_delay_min)) this._config.storage_sync_sec = Math.max(5, Math.min(86400, Math.round(Number(s.sync_delay_min)*60))); + } catch {} + // Holidays from local storage + try { + if (typeof s.holidays_enabled === 'boolean') this._config.holidays_enabled = !!s.holidays_enabled; + if (typeof s.holidays_source === 'string' && !this._yamlProvided?.holidays_source) this._config.holidays_source = s.holidays_source === 'manual' ? 'manual' : 'calendar'; + if (typeof s.holidays_entity === 'string' && !this._yamlProvided?.holidays_entity) this._config.holidays_entity = s.holidays_entity || ''; + if (Array.isArray(s.holidays_dates) && !this._yamlProvided?.holidays_dates) this._config.holidays_dates = [...s.holidays_dates]; + } catch {} + } else { + // backward compat: schedules stored directly + this._schedules = parsed || {}; + } + } + catch { this._schedules = {}; } + } + + _editSessionActive(){ return !!(this._editing || this._inlineEditing || this._weeklyOpen || this._profilesOpen || this._holidayOpen || this._presenceOpen); } + _ensureEditModeOn(){ this._pendingSaveAfterEdit = true; } + _flushPendingSave(){ + try { + if (!this._pendingSaveAfterEdit) return; + if (this._editSessionActive()) return; + this._pendingSaveAfterEdit = false; + this._saveStore(true); + } catch {} + } + + async _saveStore(force = false) { + if (!force && this._editSessionActive()) { + this._markPendingSave(); + return; + } + if (!force && this._pendingSaveAfterEdit) return; + this._pendingSaveAfterEdit = false; + try { + const payload = this._makeStoragePayload(true); + localStorage.setItem(this._localStoreKey(), JSON.stringify(payload)); + } catch {} + try { this._broadcastStoreUpdated(this._localStoreKey()); } catch {} + // Always persist local browser copy immediately + const writeRemote = async () => { + if (!this._config?.storage_enabled) { + // When shared storage is turned off, still push a minimal settings payload to disable backend apply + try { + const basePayload = { + settings: { + auto_apply_enabled: false, + apply_on_edit: !!this._config.apply_on_edit, + profiles_enabled: !!this._config.profiles_enabled, + global_profile: this._globalProfile || null, + boiler_enabled: !!this._config.boiler_enabled, + boiler_switch: String(this._config.boiler_switch || ''), + boiler_rooms: Array.isArray(this._config.boiler_rooms) ? this._config.boiler_rooms.filter(Boolean).map(String) : null, + boiler_on_offset: (this._config.boiler_on_offset === null || this._config.boiler_on_offset === undefined) ? null : Number(this._config.boiler_on_offset), + boiler_off_offset: (this._config.boiler_off_offset === null || this._config.boiler_off_offset === undefined) ? null : Number(this._config.boiler_off_offset), + boiler_temp_sensor: String(this._config.boiler_temp_sensor || ''), + boiler_min_temp: (this._config.boiler_min_temp === null || this._config.boiler_min_temp === undefined) ? null : Number(this._config.boiler_min_temp), + boiler_max_temp: (this._config.boiler_max_temp === null || this._config.boiler_max_temp === undefined) ? null : Number(this._config.boiler_max_temp), + show_pause_button: !!(this._config.show_pause_button ?? true), + show_room_temp: !!(this._config.show_room_temp ?? true), + pause_sensor_enabled: !!(this._config.pause_sensor_enabled ?? false), + pause_sensor_entity: String(this._config.pause_sensor_entity || ''), + pause_indef: !!this._pauseIndef, + pause_until_ms: Number(this._pauseUntilMs||0), + open_window: this._config.open_window || { enabled:false, sensors:{}, open_delay_min:2, close_delay_min:5 }, + holidays_enabled: !!this._config.holidays_enabled, + holidays_source: this._config.holidays_source || 'calendar', + holidays_entity: this._config.holidays_entity || '', + holidays_dates: Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : [] + }, + colors: { color_ranges: this._config.color_ranges, color_global: !!this._config.color_global } + }; + if (this._config?.instance_enabled) { + basePayload.instance_id = this._instanceId(); + basePayload.activate = false; + } + await this._hass.callService("thermostat_timeline", "set_store", basePayload); + } catch {} + return; + } + this._saving = true; + try { + let payload = this._makeStoragePayload(true); + // Merge with current backend settings to avoid wiping keys the editor/card doesn't currently include (e.g. labels) + try { + const state = await this._apiFetchState(); + const base = (state && typeof state === 'object' && state.settings && typeof state.settings === 'object') ? state.settings : {}; + // If no base available (early startup), avoid writing holidays_enabled unless user toggled + if ((!state || !state.settings) && !this._holidaysToggleTouched) { + try { if (payload?.settings && Object.prototype.hasOwnProperty.call(payload.settings,'holidays_enabled')) delete payload.settings.holidays_enabled; } catch {} + } + const incoming = payload.settings || {}; + const merged = { ...base, ...incoming }; + // If user didn't toggle holidays during this session, keep backend value explicitly + try { + if (!this._holidaysToggleTouched && typeof base.holidays_enabled === 'boolean') { + merged.holidays_enabled = base.holidays_enabled; + } + } catch {} + // Heuristic: avoid flipping holidays_enabled=true -> false on startup if nothing holiday-related changed. + try { + const bHE = typeof base.holidays_enabled === 'boolean' ? base.holidays_enabled : undefined; + const iHE = typeof incoming.holidays_enabled === 'boolean' ? incoming.holidays_enabled : undefined; + // If background push (no user toggle) tries to change the value, ignore and keep backend + if (this._holidaysToggleTouched !== true && bHE !== undefined && iHE !== undefined && bHE !== iHE) { + merged.holidays_enabled = bHE; + } else if (bHE === true && iHE === false) { + const normArr = (a)=>{ try { return Array.from(new Set((a||[]).map(String))).sort(); } catch { return []; } }; + const eqDates = JSON.stringify(normArr(incoming.holidays_dates)) === JSON.stringify(normArr(base.holidays_dates)); + const eqSource = String(incoming.holidays_source||'') === String(base.holidays_source||''); + const eqEntity = String(incoming.holidays_entity||'') === String(base.holidays_entity||''); + const eqGroups = JSON.stringify(incoming.holidays_groups||[]) === JSON.stringify(base.holidays_groups||[]); + if (eqDates && eqSource && eqEntity && eqGroups) { + merged.holidays_enabled = true; + } + } + } catch {} + const keepIfEmpty = (key)=>{ + try { + const inc = incoming[key]; + const hasInc = inc && typeof inc === 'object' && Object.keys(inc).length > 0; + const baseVal = base[key]; + const hasBase = baseVal && typeof baseVal === 'object' && Object.keys(baseVal).length > 0; + if (!hasInc && hasBase) merged[key] = baseVal; // preserve backend value when our copy is empty + } catch {} + }; + // If labels were explicitly edited in this session, allow an empty object to clear labels + if (!this._labelsTouched) keepIfEmpty('labels'); + keepIfEmpty('merges'); + keepIfEmpty('temp_sensors'); + payload = { ...payload, settings: merged }; + } catch {} + // Push all data to the integration via set_store service (writes to file) + if (this._config?.instance_enabled) { + payload = { ...payload, instance_id: this._instanceId(), activate: true }; + } + await this._hass.callService("thermostat_timeline", "set_store", payload); + try { this._broadcastStoreUpdated(this._localStoreKey()); } catch {} + } catch (e) {} + finally { + setTimeout(() => { + this._saving = false; + try { this._holidaysToggleTouched = false; } catch {} + try { this._labelsTouched = false; } catch {} + // Show "done" for a short moment and ensure UI keeps ticking + this._syncJustUntil = Date.now() + 2500; + // Clear previous due so countdown disappears until next change + this._storeDelayDue = 0; + this._startSyncTimer(); + this._render(); + }, 600); + } + }; + + // Decide sync policy + const mode = this._config?.storage_sync_mode || 'instant'; + const secsCfg = Number(this._config?.storage_sync_sec || 0); + const secs = Math.max(5, Math.min(86400, Number.isFinite(secsCfg) ? secsCfg : Math.max(0, Number(this._config?.storage_sync_min || 0))*60)); + if (this._config?.storage_enabled && mode === 'delay' && secs > 0) { + // Trailing save after N minutes; coalesce multiple changes + const delayMs = secs * 1000; + if (this._storeDelayTimer) try { clearTimeout(this._storeDelayTimer); } catch {} + this._storeDelayDue = Date.now() + delayMs; + try { localStorage.setItem(this._syncDueKey(), String(this._storeDelayDue)); } catch {} + // Save a draft of current schedules so they survive a hard refresh until delayed sync writes + try { localStorage.setItem(this._draftKey(), JSON.stringify({ schedules: this._schedules })); } catch {} + // Persist due time across reloads + // If we just finished a sync, drop the "done" flag so countdown shows immediately + this._syncJustUntil = 0; + this._storeDelayTimer = setTimeout(() => { this._storeDelayTimer = null; this._writeStoreNow(); }, delayMs); + try { this._startSyncTimer(); this._render(); } catch {} + } else { + // Instant (existing behavior with short debounce handled by callers) + await this._writeStoreNow(); + } + } + + _debouncedSaveStore() { + // Keep short debounce for bursts; final saving respects sync policy inside _saveStore + if (this._saveTimer) clearTimeout(this._saveTimer); + // Persist a local draft immediately so edits survive hard refresh before save runs + try { this._persistDraftNow(); } catch {} + this._saveTimer = setTimeout(() => this._saveStore(), this._saveDebounceMs); + } + + // ---------- Helpers ---------- + _sortBlocks(entity) { const r = this._schedules[entity]; r.blocks.sort((a,b)=>a.startMin - b.startMin || a.endMin - b.endMin); } + _neighbors(entity, id) { const r = this._schedules[entity]; const i = r.blocks.findIndex(b=>b.id===id); if (i === -1) return {left:null, right:null, index:-1}; return { left: r.blocks[i-1] || null, right: r.blocks[i+1] || null, index:i }; } + _applyNoOverlapResize(entity, b, edge, proposed) { this._sortBlocks(entity); const {left, right} = this._neighbors(entity, b.id); if (edge === "left") { let ns = this._clamp(Math.floor(proposed), 0, b.endMin - 5); if (left) ns = Math.max(ns, left.endMin); b.startMin = ns; } else if (edge === "right") { let ne = this._clamp(Math.ceil(proposed), b.startMin + 5, 1440); if (right) ne = Math.min(ne, right.startMin); b.endMin = ne; } this._sortBlocks(entity); } + _label(min) { + if (!Number.isFinite(min)) min = 0; + const m = ((Math.floor(min) % 1440) + 1440) % 1440; + const hh = Math.floor(m/60), mm = Math.round(m%60); + if (this._config?.time_12h) { + const am = hh < 12; + let h = hh % 12; if (h === 0) h = 12; + return `${String(h).padStart(2,'0')}:${String(mm).padStart(2,'0')} ${am ? 'AM' : 'PM'}`; + } + return `${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')}`; + } + _timeParts(min){ + // Returns { main: 'hh:mm', mer: 'AM'|'PM'|null } according to current format + if (!Number.isFinite(min)) min = 0; + const m = ((Math.floor(min) % 1440) + 1440) % 1440; + const hh = Math.floor(m/60), mm = Math.round(m%60); + if (this._config?.time_12h) { + const am = hh < 12; + let h = hh % 12; if (h === 0) h = 12; + return { main: `${String(h).padStart(2,'0')}:${String(mm).padStart(2,'0')}`, mer: am ? 'AM' : 'PM' }; + } + return { main: `${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')}`, mer: null }; + } + _hourLabel(h){ const min = (h%24)*60; return this._label(min); } + _useHaTime(){ try { return String(this._config?.time_source||'browser') === 'ha'; } catch { return false; } } + _getNowMin(){ + try { + if (this._useHaTime() && this._hass?.config?.time_zone) { + const tz = this._hass.config.time_zone; + const fmt = new Intl.DateTimeFormat('en-US', { timeZone: tz, hour: '2-digit', minute: '2-digit', hour12: false }); + const parts = fmt.formatToParts(new Date()); + const hh = Number(parts.find(p=>p.type==='hour')?.value||'0'); + const mm = Number(parts.find(p=>p.type==='minute')?.value||'0'); + return hh*60 + mm; + } + } catch {} + const d=new Date(); return d.getHours()*60 + d.getMinutes(); + } + _clamp(v,a,b){ if (isNaN(v)) return a; return Math.min(Math.max(v,a),b); } + _prettyName(eid){ const st=this._hass?.states?.[eid]; if (st?.attributes?.friendly_name) return st.attributes.friendly_name; const base=(eid||"").split(".")[1]||eid||""; return base.replace(/_/g," ").replace(/\b\w/g,(m)=>m.toUpperCase()); } + + _isCompactScale(){ + // Compact = show every other hour on small or touch devices (incl. mobile portrait) + try { + const isCoarse = window.matchMedia && window.matchMedia('(pointer:coarse)').matches; + // Prefer the card width when available, fallback to window width + const hostW = (this.shadowRoot && this.shadowRoot.host && this.shadowRoot.host.clientWidth) || 0; + const w = hostW || window.innerWidth || 0; + // Consider compact on touch devices (phones/tablets) or narrow widths + return isCoarse || w <= 720; // ~mobile/tablet portrait breakpoint + } catch (e) { return false; } + } + _detectPrefer12h(){ + try { + // Prefer Home Assistant locale if available + const lf = this._hass?.locale?.time_format || this._hass?.locale?.time_format_preference || ''; + if (typeof lf === 'string') { + const s = lf.toLowerCase(); + if (s.includes('12')) return true; + if (s.includes('24')) return false; + } + } catch {} + try { + // Fallback: browser preference + const fmt = new Intl.DateTimeFormat(navigator.language || undefined, { hour: 'numeric' }); + const ro = fmt.resolvedOptions?.() || {}; + if (typeof ro.hour12 === 'boolean') return ro.hour12; + } catch {} + // Final fallback: 24h + return false; + } + _detectPreferF(){ + try { + const u = this._hass?.config?.unit_system?.temperature || this._hass?.locale?.temperature || ''; + if (typeof u === 'string' && u.toUpperCase().includes('F')) return true; + } catch {} + try { + const f = new Intl.NumberFormat(navigator.language || undefined, { style:'unit', unit:'fahrenheit' }); + // Heuristic: if formatter exists, still default to C; most regions outside US use C, but HA already covers primary preference + } catch {} + return false; + } + _getLocalProfilesEnabled(){ + try { + const raw = localStorage.getItem(this._localStoreKey()) || ''; + if (!raw) return false; + const parsed = JSON.parse(raw); + const s = parsed?.settings || {}; + return !!s.profiles_enabled; + } catch { return false; } + } + _isF(){ return (this._config?.temp_unit||'C')==='F'; } + _unitSymbol(){ return this._isF()? '°F' : '°C'; } + _toDisplayTemp(c){ return this._isF()? Math.round((c*9/5+32)*10)/10 : c; } + _fromDisplayTemp(v){ return this._isF()? ( (v-32)*5/9 ) : v; } + // --- Temperature conversion helpers (internal storage is °C) --- + _cToF(c){ try { return (Number(c)*9/5)+32; } catch { return c; } } + _fToC(f){ try { return (Number(f)-32)*5/9; } catch { return f; } } + // Service calls must follow Home Assistant's unit system (not the card display unit) + _serviceTempFromC(c){ return this._haIsF() ? this._cToF(c) : c; } + _haIsF(){ try { const u = String(this._hass?.config?.unit_system?.temperature||''); return u.toUpperCase().includes('F'); } catch { return false; } } + + _readSensorTemperatureC(sensorEid){ + try { + const st = this._hass?.states?.[sensorEid]; + const raw = Number(String(st?.state ?? '').trim().replace(',', '.')); + if (!Number.isFinite(raw)) return NaN; + const u = String(st?.attributes?.unit_of_measurement || this._hass?.config?.unit_system?.temperature || '').toUpperCase(); + const isF = u.includes('F'); + return isF ? this._fToC(raw) : raw; + } catch { return NaN; } + } + + _readClimateCurrentTemperatureC(climateEid){ + try { + const st = this._hass?.states?.[climateEid]; + let raw = Number(String(st?.attributes?.current_temperature ?? st?.attributes?.current_temp ?? st?.attributes?.temperature ?? '').trim().replace(',', '.')); + if (!Number.isFinite(raw)) { + // Some custom climate entities expose the temperature as a numeric state. + raw = Number(String(st?.state ?? '').trim().replace(',', '.')); + } + if (!Number.isFinite(raw)) return NaN; + return this._haIsF() ? this._fToC(raw) : raw; + } catch { return NaN; } + } + + _roomCurrentTemperatureC(primaryEid){ + try { + if (!this._hass || !this._config) return NaN; + const ents = Array.isArray(this._config.entities) ? this._config.entities.map(String) : []; + const idx = ents.indexOf(String(primaryEid)); + const flags = Array.isArray(this._config.room_use_temp_sensor) ? this._config.room_use_temp_sensor : []; + const enabled = (idx >= 0) ? !!flags[idx] : false; + + if (enabled) { + const sensorEid = String(this._config?.temp_sensors?.[primaryEid] || '').trim(); + if (sensorEid) { + const v = this._readSensorTemperatureC(sensorEid); + if (Number.isFinite(v)) return v; + } + } + + // Fallback: climate current_temperature (average across merged group) + const merged = this._config?.merges?.[primaryEid]; + const extra = Array.isArray(merged) ? merged : []; + const group = [primaryEid, ...extra]; + const vals = []; + for (const g of group) { + const v = this._readClimateCurrentTemperatureC(g); + if (Number.isFinite(v)) vals.push(v); + } + if (!vals.length) return NaN; + return vals.reduce((a,b)=>a+b,0) / vals.length; + } catch { return NaN; } + } + async _applySetpointForEntity(eid, desiredC){ + try { + const dom = String(eid||'').split('.')[0] || ''; + if (dom === 'input_number') { + // Mirror climate behavior: send in configured display/service unit (°C or °F) + const val = this._serviceTempFromC(desiredC); + await this._hass.callService('input_number','set_value', { entity_id: eid, value: val }); + return; + } + if (dom !== 'climate') return; + const st = this._hass?.states?.[eid]; if (!st) return; + const attrs = st.attributes || {}; + let hvacMode = String(attrs.hvac_mode ?? st.state ?? '').toLowerCase(); + const hvacModes = Array.isArray(attrs.hvac_modes) ? attrs.hvac_modes.map(x=>String(x).toLowerCase()) : []; + const presetMode = String(attrs.preset_mode || '').toLowerCase(); + const presetModes = Array.isArray(attrs.preset_modes) ? attrs.preset_modes.map(x=>String(x).toLowerCase()) : []; + + // Optional: enable manual/hold preset to avoid schedule overrides + try { + const want = presetModes.includes('manual') ? 'manual' : (presetModes.includes('hold') ? 'hold' : null); + if (want && presetMode !== want){ + await this._hass.callService('climate','set_preset_mode',{ entity_id: eid, preset_mode: want }); + } + } catch {} + + // If mode ignores setpoint, try switching to heat/cool + try { + if (['off','dry','fan_only'].includes(hvacMode)){ + let newMode = null; + if (hvacModes.includes('heat')) newMode = 'heat'; else if (hvacModes.includes('cool')) newMode = 'cool'; + if (newMode){ await this._hass.callService('climate','set_hvac_mode',{ entity_id: eid, hvac_mode: newMode }); hvacMode = newMode; } + } + } catch {} + + // Decide between single setpoint vs range + const hasLow = Object.prototype.hasOwnProperty.call(attrs, 'target_temp_low'); + const hasHigh = Object.prototype.hasOwnProperty.call(attrs, 'target_temp_high'); + const modeSuggestsRange = (hvacMode === 'heat_cool') || (hvacMode === 'auto' && hasLow && hasHigh); + if (modeSuggestsRange && (hasLow || hasHigh)){ + let band = 1.0; // °C total band + try { const b = Number(this._config?.range_band_c); if (Number.isFinite(b) && b > 0.2 && b <= 10) band = b; } catch {} + const half = Math.max(0.2, band/2); + const lowC = desiredC - half; + const highC = desiredC + half; + const data = { entity_id: eid, target_temp_low: this._serviceTempFromC(lowC), target_temp_high: this._serviceTempFromC(highC) }; + await this._hass.callService('climate','set_temperature', data); + return; + } + await this._hass.callService('climate','set_temperature', { entity_id: eid, temperature: this._serviceTempFromC(desiredC) }); + } catch {} + } + // Convert a schedules object between units (returns deep clone) + _convertSchedulesTemps(schedules, toUnit){ + try { + const clone = JSON.parse(JSON.stringify(schedules||{})); + const toF = String(toUnit).toUpperCase()==='F'; + const conv = (x)=>{ const n = Number(x); if (!Number.isFinite(n)) return x; return toF ? this._cToF(n) : this._fToC(n); }; + const convertBlocks = (arr)=>{ if (!Array.isArray(arr)) return; for (const b of arr){ if (b && typeof b==='object' && 'temp' in b){ b.temp = conv(b.temp); } } }; + for (const eid of Object.keys(clone||{})){ + const row = clone[eid]; if (!row || typeof row!=='object') continue; + if ('defaultTemp' in row) row.defaultTemp = conv(row.defaultTemp); + convertBlocks(row.blocks); + // Weekly + try { const days = row?.weekly?.days||{}; for (const k of Object.keys(days)) convertBlocks(days[k]); } catch {} + // Profiles + try { const prof = row?.profiles||{}; for (const p of Object.keys(prof)){ convertBlocks(prof[p]?.blocks); } } catch {} + // Holiday + try { convertBlocks(row?.holiday?.blocks); } catch {} + // Presence + try { const pres = row?.presence||{}; for (const key of Object.keys(pres)){ convertBlocks(pres[key]?.blocks); } } catch {} + } + return clone; + } catch { return schedules; } + } + + // Heuristic for legacy stores: detect if schedule temps look like °F (typical °C schedules are < ~45) + _maxTempInSchedules(schedules){ + let maxV = -Infinity; + const scanBlocks = (arr)=>{ + if (!Array.isArray(arr)) return; + for (const b of arr){ + try { + if (!b || typeof b !== 'object') continue; + const v = Number(b.temp); + if (Number.isFinite(v)) maxV = Math.max(maxV, v); + } catch {} + } + }; + try { + const s = schedules || {}; + for (const eid of Object.keys(s)){ + const row = s[eid]; + if (!row || typeof row !== 'object') continue; + const d = Number(row.defaultTemp); + if (Number.isFinite(d)) maxV = Math.max(maxV, d); + scanBlocks(row.blocks); + try { const days = row?.weekly?.days||{}; for (const k of Object.keys(days)) scanBlocks(days[k]); } catch {} + try { const prof = row?.profiles||{}; for (const p of Object.keys(prof)) scanBlocks(prof[p]?.blocks); } catch {} + try { scanBlocks(row?.holiday?.blocks); } catch {} + try { const pres = row?.presence||{}; for (const key of Object.keys(pres)) scanBlocks(pres[key]?.blocks); } catch {} + } + } catch {} + return Number.isFinite(maxV) ? maxV : -Infinity; + } + + // Build payload for shared storage respecting selected unit for numbers + _makeStoragePayload(includeSchedules=true){ + try { + // Canonical shared storage is always °C to avoid drift when multiple clients use different display units. + // (Display unit is controlled by settings.temp_unit and is independent of stored numeric values.) + const wantF = false; + let schedules = undefined; + let weekdays = {}; + let profiles = {}; + const useSeparateProfiles = this._config?.storage_enabled && !!this._storageEntity('profiles'); + if (includeSchedules) { + const cloned = JSON.parse(JSON.stringify(this._schedules||{})); + // Split weekday data into separate payload + try { + for (const eid of Object.keys(cloned||{})) { + const row = cloned[eid]; + if (!row || typeof row !== 'object') continue; + const wk = {}; + if (row.weekly) { wk.weekly = row.weekly; delete row.weekly; } + if (row.weekly_modes) { wk.weekly_modes = row.weekly_modes; delete row.weekly_modes; } + if (Object.keys(wk).length) weekdays[eid] = wk; + const prof = {}; + if (row.profiles && typeof row.profiles === 'object') prof.profiles = row.profiles; + if (Object.prototype.hasOwnProperty.call(row, 'activeProfile')) prof.activeProfile = row.activeProfile; + if (Object.keys(prof).length) { + // Always keep profiles inside the schedules payload as a fallback, even when a dedicated profiles sensor is configured. + // This prevents losing data if the profiles sensor is missing or fails to update. + profiles[eid] = prof; + } + } + } catch {} + schedules = cloned; + } + // Convert numeric settings if storing as °F + const toStore = (n)=>{ const v = Number(n); if (!Number.isFinite(v)) return n; return v; }; + const awayCfg = this._config?.away || {}; + const awayOut = { ...awayCfg }; + // Guard: Away/Presence should not be active until at least one person is selected. + try { + awayOut.persons = Array.isArray(awayOut.persons) ? awayOut.persons.filter(Boolean).map(String) : []; + if (!awayOut.persons.length) { awayOut.enabled = false; awayOut.advanced_enabled = false; } + } catch {} + if (typeof awayOut.target_c !== 'undefined') awayOut.target_c = toStore(awayOut.target_c); + const settings = { + // Rooms (used by Settings popup) + entities: Array.isArray(this._config.entities) ? this._config.entities.filter(Boolean).map(String) : [], + room_use_input_number: (()=>{ + try { + const ents = Array.isArray(this._config.entities) ? this._config.entities.filter(Boolean).map(String) : []; + const raw = Array.isArray(this._config.room_use_input_number) ? this._config.room_use_input_number.slice() : []; + while (raw.length < ents.length) raw.push(false); + return ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return true; + if (String(eid).startsWith('climate.')) return false; + return !!raw[idx]; + }); + } catch { return []; } + })(), + + room_use_temp_sensor: (()=>{ + try { + const ents = Array.isArray(this._config.entities) ? this._config.entities.filter(Boolean).map(String) : []; + const raw = Array.isArray(this._config.room_use_temp_sensor) ? this._config.room_use_temp_sensor.slice() : []; + while (raw.length < ents.length) raw.push(false); + return ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return false; + if (!String(eid).startsWith('climate.')) return false; + return !!raw[idx]; + }); + } catch { return []; } + })(), + + time_12h: this._config.time_12h, + temp_unit: this._config.temp_unit, + storage_temp_unit: 'C', + time_source: this._config.time_source, + row_height: Number(this._config.row_height ?? 64), + default_temp: toStore(Number(this._config.default_temp||20)), + min_temp: toStore(Number(this._config.min_temp ?? 5)), + max_temp: toStore(Number(this._config.max_temp ?? 25)), + weekdays_enabled: !!this._config.weekdays_enabled, + weekdays_mode: this._config.weekdays_mode || 'weekday_weekend', + weekdays_view: this._config.weekdays_view || 'all_rooms_one_day', + weekdays_view_switch_in_timeline: !!(this._config.weekdays_view_switch_in_timeline ?? false), + weekdays_selected_room: this._config.weekdays_selected_room || '', + per_room_defaults: !!this._config.per_room_defaults, + away: awayOut, + away_bypass: !!this._awayBypass, + merges: this._config.merges, + labels: this._config.labels, + temp_sensors: this._config.temp_sensors, + turn_on: this._config.turn_on, + presence_sensors: this._config.presence_sensors, + presence_sensor_temps: (()=>{ + try { + const src = (this._config.presence_sensor_temps && typeof this._config.presence_sensor_temps === 'object') ? this._config.presence_sensor_temps : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const v = Number(src[k]); + if (Number.isFinite(v)) out[String(k)] = toStore(v); + } + return out; + } catch { return {}; } + })(), + presence_sensor_delays: (()=>{ + try { + const src = (this._config.presence_sensor_delays && typeof this._config.presence_sensor_delays === 'object') ? this._config.presence_sensor_delays : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const v = src?.[k]; + if (!v || typeof v !== 'object') continue; + const onS = Number(v.on_s); + const offS = Number(v.off_s); + const obj = {}; + if (Number.isFinite(onS) && onS >= 0) obj.on_s = onS; + if (Number.isFinite(offS) && offS >= 0) obj.off_s = offS; + if (Object.keys(obj).length) out[String(k)] = obj; + } + return out; + } catch { return {}; } + })(), + presence_sensor_delay_units: (()=>{ + try { + const src = (this._config.presence_sensor_delay_units && typeof this._config.presence_sensor_delay_units === 'object') ? this._config.presence_sensor_delay_units : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const u = String(src[k]||'').toLowerCase().trim(); + if (u === 'seconds' || u === 'minutes') out[String(k)] = u; + } + return out; + } catch { return {}; } + })(), + open_window: this._config.open_window, + boiler_enabled: !!this._config.boiler_enabled, + boiler_switch: String(this._config.boiler_switch || ''), + boiler_rooms: Array.isArray(this._config.boiler_rooms) ? this._config.boiler_rooms.filter(Boolean).map(String) : null, + boiler_on_offset: (this._config.boiler_on_offset === null || this._config.boiler_on_offset === undefined) ? null : toStore(this._config.boiler_on_offset), + boiler_off_offset: (this._config.boiler_off_offset === null || this._config.boiler_off_offset === undefined) ? null : toStore(this._config.boiler_off_offset), + boiler_temp_sensor: String(this._config.boiler_temp_sensor || ''), + boiler_min_temp: (this._config.boiler_min_temp === null || this._config.boiler_min_temp === undefined) ? null : toStore(this._config.boiler_min_temp), + boiler_max_temp: (this._config.boiler_max_temp === null || this._config.boiler_max_temp === undefined) ? null : toStore(this._config.boiler_max_temp), + show_pause_button: !!(this._config.show_pause_button ?? true), + show_room_temp: !!(this._config.show_room_temp ?? true), + pause_sensor_enabled: !!(this._config.pause_sensor_enabled ?? false), + pause_sensor_entity: String(this._config.pause_sensor_entity || ''), + presence_sensor_enabled: !!(this._config.presence_sensor_enabled ?? false), + presence_live_header: !!(this._config.presence_live_header ?? true), + auto_apply_enabled: !!this._config.auto_apply, + apply_on_edit: !!this._config.apply_on_edit, + apply_on_default_change: !!this._config.apply_on_default_change, + backup_auto_enabled: !!this._config.backup_auto_enabled, + backup_interval_days: Number(this._config.backup_interval_days||1), + profiles_enabled: !!this._config.profiles_enabled, + global_profile: this._globalProfile || null, + pause_indef: !!this._pauseIndef, + pause_until_ms: Number(this._pauseUntilMs||0), + holidays_enabled: !!this._config.holidays_enabled, + holidays_source: this._config.holidays_source || 'calendar', + holidays_entity: this._config.holidays_entity || '', + holidays_dates: Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : [], + holidays_groups: Array.isArray(this._config.holidays_groups) ? this._config.holidays_groups : [], + sync_mode: (this._config.storage_sync_mode||'instant'), + sync_delay_min: Number(Math.max(0, Math.round((this._config.storage_sync_sec||0)/60))), + sync_delay_sec: Number(this._config.storage_sync_sec||0) + }; + // Do not include holidays_enabled in generic payload unless user toggled it in this session + try { if (!this._holidaysToggleTouched) delete settings.holidays_enabled; } catch {} + const colors = { color_ranges: this._config.color_ranges, color_global: !!this._config.color_global }; + // When a dedicated colors sensor is configured, avoid duplicating colors inside settings payload + if (this._config?.storage_enabled && this._storageEntity('colors')) { + delete settings.color_ranges; + delete settings.color_global; + } else { + settings.color_ranges = colors.color_ranges; + settings.color_global = colors.color_global; + } + const out = { settings, colors }; + if (includeSchedules) { out.schedules = schedules; out.weekdays = weekdays; out.profiles = profiles; } + return out; + } catch { return { schedules: this._schedules, settings: { temp_unit: this._config.temp_unit } }; } + } + _maxDisplay(){ const c = Number(this._config?.max_temp ?? 25); return this._toDisplayTemp(c); } + _minDisplay(){ const c = Number(this._config?.min_temp ?? 5); return this._toDisplayTemp(c); } + _rowDefaultTemp(row){ + try { + if (this._config?.per_room_defaults) { + const v = Number(row?.defaultTemp); + return Number.isFinite(v) ? v : Number(this._config?.default_temp ?? 20); + } + return Number(this._config?.default_temp ?? 20); + } catch { return Number(this._config?.default_temp ?? 20); } + } + _todayFullName(){ + try { + const lang = ttGetLangFromHass(this._hass || this._lang || 'en'); + const opts = { weekday: 'long' }; + if (this._useHaTime() && this._hass?.config?.time_zone) Object.assign(opts, { timeZone: this._hass.config.time_zone }); + const s = new Intl.DateTimeFormat(lang, opts).format(new Date()); + return s.charAt(0).toUpperCase()+s.slice(1); + } catch { + const names = { mon:'Monday', tue:'Tuesday', wed:'Wednesday', thu:'Thursday', fri:'Friday', sat:'Saturday', sun:'Sunday' }; + return names[this._todayKey()] || 'Today'; + } + } + + _weekdayFullName(dayKey){ + try { + const k = String(dayKey || '').toLowerCase(); + const map = { + mon: 'week.days_full.mon', + tue: 'week.days_full.tue', + wed: 'week.days_full.wed', + thu: 'week.days_full.thu', + fri: 'week.days_full.fri', + sat: 'week.days_full.sat', + sun: 'week.days_full.sun' + }; + const t = map[k] ? this._t(map[k]) : ''; + if (t) return String(t); + } catch {} + try { + const names = { mon:'Monday', tue:'Tuesday', wed:'Wednesday', thu:'Thursday', fri:'Friday', sat:'Saturday', sun:'Sunday' }; + return names[String(dayKey||'').toLowerCase()] || this._todayFullName(); + } catch { return this._todayFullName(); } + } + + // Format ISO timestamp in a readable, localized way + _fmtDateTimeIso(iso){ + try { + if (!iso) return ''; + const d = new Date(iso); + const lang = ttGetLangFromHass(this._hass || this._lang || 'en'); + const opts = { dateStyle: 'medium', timeStyle: 'short' }; + if (this._useHaTime() && this._hass?.config?.time_zone) opts.timeZone = this._hass.config.time_zone; + return new Intl.DateTimeFormat(lang, opts).format(d); + } catch { return String(iso||''); } + } + + // --- Weekday helpers --- + _weekdayNames(){ return ['mon','tue','wed','thu','fri','sat','sun']; } + + _timelineDisplayDayKey(){ + try { + const enabled = !!this._config?.weekdays_enabled; + const view = String(this._config?.weekdays_view || 'all_rooms_one_day'); + if (enabled && view === 'all_rooms_one_day') { + const names = this._weekdayNames(); + const cur = String(this._weekdaysViewDayKey || '').toLowerCase(); + if (names.includes(cur)) return cur; + const today = this._todayKey(); + this._weekdaysViewDayKey = today; + return today; + } + } catch {} + return this._todayKey(); + } + + _setTimelineDisplayDayKey(dayKey){ + try { + const k = String(dayKey || '').toLowerCase(); + if (this._weekdayNames().includes(k)) this._weekdaysViewDayKey = k; + } catch {} + } + + _weekdaysStorageDayKeyForMode(displayDayKey, mode){ + const d = String(displayDayKey || 'mon').toLowerCase(); + const isWk = ['mon','tue','wed','thu','fri'].includes(d); + const m = String(mode || 'weekday_weekend'); + if (m === 'weekday_weekend') return isWk ? 'mon' : 'sat'; + if (m === 'weekday_sat_sun') return isWk ? 'mon' : d; // sat/sun + return d; // all_7 + } + _todayKey(){ + try { + if (this._useHaTime() && this._hass?.config?.time_zone) { + const tz = this._hass.config.time_zone; + const parts = new Intl.DateTimeFormat('en-US', { timeZone: tz, weekday: 'short' }).format(new Date()); + const map = { Mon:'mon', Tue:'tue', Wed:'wed', Thu:'thu', Fri:'fri', Sat:'sat', Sun:'sun' }; + return map[parts] || 'mon'; + } + } catch {} + const d=new Date(); const idx=((d.getDay()+6)%7); return this._weekdayNames()[idx]; + } + _ensureWeeklyStruct(row){ + if (!row.weekly) { row.weekly = { mode: this._config?.weekdays_mode || 'weekday_weekend', days: {} }; } + const names=this._weekdayNames(); + for (const k of names){ if (!Array.isArray(row.weekly.days[k])) row.weekly.days[k] = []; } + // Maintain separate saved schedules per mode + if (!row.weekly_modes) { + row.weekly_modes = { + weekday_weekend: { days: {} }, + weekday_sat_sun: { days: {} }, + all_7: { days: {} } + }; + // Seed current into active mode bucket + try { + const curMode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + row.weekly_modes[curMode] = { days: JSON.parse(JSON.stringify(row.weekly?.days||{})) }; + } catch {} + } + try { + const modes = ['weekday_weekend','weekday_sat_sun','all_7']; + for (const m of modes){ + const bucket = row.weekly_modes[m] = row.weekly_modes[m] || { days: {} }; + for (const k of names){ if (!Array.isArray(bucket.days[k])) bucket.days[k] = []; } + } + } catch {} + return row; + } + _getBlocksForDay(row, dayKey){ if (!this._config?.weekdays_enabled || !row?.weekly) return row?.blocks || []; const days = row.weekly.days || {}; return Array.isArray(days[dayKey]) ? days[dayKey] : (row.blocks||[]); } + _weeklyEditableKeys(mode){ const all = this._weekdayNames(); if (!mode) mode = 'weekday_weekend'; if (mode==='weekday_weekend') return ['weekdays','weekend']; if (mode==='weekday_sat_sun') return ['weekdays','sat','sun']; if (mode==='all_7') return all; return ['weekdays','weekend']; } + _effectiveDayKey(sel){ if (sel==='weekdays') return 'mon'; if (sel==='weekend') return 'sat'; return sel || 'mon'; } + + // --- Profiles (named daily schedules) helpers --- + _ensureProfilesStruct(row){ + try { + if (!row) return row; + if (!row.profiles || typeof row.profiles !== 'object') row.profiles = {}; + if (!('activeProfile' in row)) row.activeProfile = null; + } catch {} + return row; + } + + // --- Holidays helpers --- + _ensureHolidayStruct(row){ + try { if (!row) return row; if (!row.holiday || !Array.isArray(row.holiday.blocks)) row.holiday = { blocks: [] }; } catch {} return row; + } + + // --- Presence (advanced away) helpers --- + _ensurePresenceStruct(row){ try { if (!row) return row; if (!row.presence || typeof row.presence !== 'object') row.presence = {}; } catch {} return row; } + _presencePersons(){ try { return Array.isArray(this._config?.away?.persons) ? [...this._config.away.persons] : []; } catch { return []; } } + _presenceComboKey(homeList, awayList){ + try { + const h = [...(homeList||[])].map(String).sort(); + const a = [...(awayList||[])].map(String).sort(); + return `H:${h.join(',')}|A:${a.join(',')}`; + } catch { return 'H:|A:'; } + } + _presenceAllCombos(){ + try { + const persons = this._presencePersons().map(String); + const n = persons.length; const out = []; + for (let mask=0; mask < (1<>i)&1) ? home.push(persons[i]) : away.push(persons[i]); } + // Skip the "all home" combination — when everyone is home, the main/weekdays + // schedule should be used and advanced presence should not override. + if (home.length === persons.length) continue; + const key = this._presenceComboKey(home, away); + const homeNames = home.map(e=> this._hass?.states?.[e]?.attributes?.friendly_name || (e.split('.')[1]||e)).join(', '); + const awayNames = away.map(e=> this._hass?.states?.[e]?.attributes?.friendly_name || (e.split('.')[1]||e)).join(', '); + let label; + if (home.length === 0) label = this._t('presence.none_home'); + else { + const h = (this._t('presence.home') || 'Home: {names}').replace('{names}', homeNames); + const a = (this._t('presence.away') || 'Away: {names}').replace('{names}', awayNames); + label = `${h} • ${a}`; + } + out.push({ key, home:[...home], away:[...away], label }); + } + // Sort with "none home" first, then by length of home desc (all‑home is omitted) + out.sort((x,y)=>{ + if (x.home.length===0 && y.home.length!==0) return -1; + if (y.home.length===0 && x.home.length!==0) return 1; + return y.home.length - x.home.length; + }); + return out; + } catch { return []; } + } + _activePresenceComboKey(){ + try { + if (!this._config?.away?.advanced_enabled) return null; + const persons = this._presencePersons(); if (!persons.length) return null; + const home=[], away=[]; + for (const p of persons){ const st = this._hass?.states?.[p]; const s = String(st?.state||'').toLowerCase(); if (s==='home') home.push(p); else away.push(p); } + // Apply optional delay before switching into (or out of) presence mode + try { + const delayMs = this._awayDelayThresholdMs(); + if (delayMs > 0){ + const anyAway = (home.length !== persons.length); + const now = Date.now(); + if (this._awayWant !== anyAway) { this._awayWant = anyAway; this._awayWantSinceMs = now; } + const stable = (now - (this._awayWantSinceMs||0)) >= delayMs; + if (!stable) return this._lastPresenceKey || null; + } + } catch {} + // When everyone is home, do not use advanced presence at all — fall back to + // the main/weekdays schedule. No need for an "all home" presence plan. + if (home.length === persons.length) return null; + const key = this._presenceComboKey(home, away); + const combos = (this._config?.away?.combos) || {}; + const meta = combos[key]; + if (meta && meta.enabled) { this._lastPresenceKey = key; return key; } + return null; + } catch { return null; } + } + _todayISO(){ + try { + if (this._useHaTime() && this._hass?.config?.time_zone) { + const tz = this._hass.config.time_zone; + const parts = new Intl.DateTimeFormat('en-CA', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date()); + // en-CA gives YYYY-MM-DD + return String(parts); + } + } catch {} + try { const d=new Date(); const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`; } catch { return ''; } + } + _isHolidayActive(){ + try { + if (!this._config?.holidays_enabled) return false; + const src = this._config.holidays_source || 'calendar'; + if (src === 'manual') { + const list = Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : []; + const today = this._todayISO(); + return !!list.find(d=>String(d)===today); + } + // calendar/binary_sensor entity + const eid = this._config.holidays_entity || ''; + if (!eid) return false; + const st = this._hass?.states?.[eid]; + const s = String(st?.state||'').toLowerCase(); + return s === 'on'; + } catch { return false; } + } + + // --- Merge helpers --- + _groupPrimaryOf(eid){ + const prims = this._config?.entities || []; + if (prims.includes(eid)) return eid; + const merges = this._config?.merges || {}; + for (const p of prims){ const arr = merges[p]||[]; if (arr.includes(eid)) return p; } + return eid; + } + _allTargetEntities(){ + const out = new Set(); + const merges = this._config?.merges || {}; + for (const p of (this._config?.entities||[])){ + out.add(p); + for (const l of (merges[p]||[])) out.add(l); + } + return Array.from(out); + } + + _desiredTempFor(eid, nowMin){ const primary = this._groupPrimaryOf(eid); const row = this._schedules[primary]; if (!row) return null; this._ensureHolidayStruct(row); this._ensurePresenceStruct(row); let blocks = row.blocks||[]; let usedPresence = false; let usedHoliday = false; let presenceKeyActive = null; let presenceHasBlocksForRoom = false; + // Holidays override take highest precedence when enabled + try { + if (this._isHolidayActive() && row.holiday && Array.isArray(row.holiday.blocks) && row.holiday.blocks.length) { + blocks = row.holiday.blocks || []; + usedHoliday = true; + } else { + // fall through + } + } catch {} + // Advanced presence overrides (if enabled and a combo is active) — below holidays, above profiles/weekdays + try { + presenceKeyActive = this._activePresenceComboKey && this._activePresenceComboKey(); + if (presenceKeyActive) { + const roomBlocks = row.presence && row.presence[presenceKeyActive] && Array.isArray(row.presence[presenceKeyActive].blocks) + ? row.presence[presenceKeyActive].blocks : []; + if (roomBlocks.length) { + blocks = roomBlocks; + usedPresence = true; + presenceHasBlocksForRoom = true; + } else { + // Presence combo is active but this room has no presence schedule -> use a virtual all‑day Away block + const awayC = Number(this._config?.away?.target_c ?? 17); + const fallback = Number.isFinite(awayC) ? awayC : this._rowDefaultTemp(row); + blocks = [{ id: '__presence_away__', startMin: 0, endMin: 1440, temp: fallback }]; + usedPresence = true; // treat as presence so simple away clamp won't run later + presenceHasBlocksForRoom = false; + } + } + } catch {} + // Profiles/Weekdays fallback if presence not used + if (!usedPresence) { + try { + if (this._config?.profiles_enabled && !(this._isHolidayActive() && row?.holiday?.blocks?.length)) { + this._ensureProfilesStruct(row); + const ap = row.activeProfile; + if (ap && row.profiles && Array.isArray(row.profiles[ap]?.blocks) && (row.profiles[ap].blocks||[]).length) { + blocks = row.profiles[ap].blocks || []; + } else if (this._config?.weekdays_enabled && row.weekly) { + const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey); + } + } else if (this._config?.weekdays_enabled && row.weekly) { + const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey); + } + } catch { /* fall back to default blocks */ } + } + const hit = (blocks||[]).find(b => nowMin >= b.startMin && nowMin < b.endMin); + let want = Number(hit ? hit.temp : this._rowDefaultTemp(row)); + // Apply away override if active + // Rules: + // - If advanced presence is enabled, the simple Away clamp must NEVER apply + // (advanced replaces basic away entirely), regardless of whether a presence + // schedule is currently used. + // - If advanced presence is OFF, apply simple Away clamp only when we did not + // use a presence schedule for this room (usedPresence=false). + try { + const advOn = !!(this._config?.away?.advanced_enabled); + if (!advOn && !usedPresence && this._isAwayActive() && !this._awayBypass) { + const targetC = Number(this._config?.away?.target_c ?? 17); + if (Number.isFinite(targetC)) want = Math.min(want, targetC); + } + } catch {} + + // Per-room presence sensor override with optional on/off delays + try { + if (this._config?.presence_sensor_enabled) { + const sEid = this._presenceSensorEntityFor ? this._presenceSensorEntityFor(primary) : ''; + if (sEid) { + const active = this._presenceSensorActiveFor ? !!this._presenceSensorActiveFor(primary) : false; + if (active) { + const tmap = (this._config?.presence_sensor_temps && typeof this._config.presence_sensor_temps === 'object') ? this._config.presence_sensor_temps : {}; + const ov = Number(tmap?.[primary]); + if (Number.isFinite(ov)) want = ov; + } + } + } + } catch {} + return want; } + + // ---- Presence sensor (per-room) helpers ---- + _presenceSensorEntityFor(primary){ + try { + if (!this._config?.presence_sensor_enabled) return ''; + const map = (this._config?.presence_sensors && typeof this._config.presence_sensors === 'object') ? this._config.presence_sensors : {}; + const sEid = String(map?.[primary] || '').trim(); + if (sEid && sEid.startsWith('binary_sensor.')) return sEid; + } catch {} + return ''; + } + + _presenceSensorDelayMsFor(primary){ + try { + const defaultS = 180; // 3 minutes + const dmap = (this._config?.presence_sensor_delays && typeof this._config.presence_sensor_delays === 'object') ? this._config.presence_sensor_delays : {}; + const o = (dmap?.[primary] && typeof dmap?.[primary] === 'object') ? dmap[primary] : {}; + const onS0 = Number(o?.on_s); + const offS0 = Number(o?.off_s); + const onS = Number.isFinite(onS0) ? Math.max(0, onS0) : defaultS; + const offS = Number.isFinite(offS0) ? Math.max(0, offS0) : defaultS; + const onMs = onS * 1000; + const offMs = offS * 1000; + return { onMs, offMs }; + } catch {} + return { onMs: 0, offMs: 0 }; + } + + _presenceSensorActiveFor(primary){ + try { + if (!this._config?.presence_sensor_enabled) return false; + const sEid = this._presenceSensorEntityFor(primary); + if (!sEid) return false; + + const st = this._hass?.states?.[sEid]; + const s = String(st?.state || '').toLowerCase().trim(); + const now = Date.now(); + const changedMs = (()=>{ + try { + const raw = st?.last_changed || st?.last_updated || null; + const ms = raw ? Date.parse(String(raw)) : NaN; + return Number.isFinite(ms) ? ms : now; + } catch { return now; } + })(); + + const { onMs, offMs } = this._presenceSensorDelayMsFor(primary); + + try { if (!this._presenceSensorLatch) this._presenceSensorLatch = {}; } catch {} + const latch0 = this._presenceSensorLatch?.[primary]; + const latch = (latch0 && typeof latch0 === 'object') ? latch0 : { last: null, sinceMs: changedMs, active: false }; + + // Sync our "since" to HA's last_changed for accuracy + if (latch.last !== s || Number(latch.sinceMs||0) !== Number(changedMs||0)) { + latch.last = s; + latch.sinceMs = changedMs; + } + const dur = now - Number(latch.sinceMs||now); + + if (s === 'on') { + if (!latch.active) { + latch.active = (dur >= onMs); + } + } else if (s === 'off') { + if (latch.active) { + latch.active = !(dur >= offMs); + } + } else { + // Unknown/unavailable: fall back to schedule (disable override) + latch.active = false; + } + + this._presenceSensorLatch[primary] = latch; + return !!latch.active; + } catch {} + return false; + } + + _presenceSensorActiveSnapshot(){ + const out = {}; + try { + if (!this._config?.presence_sensor_enabled) return out; + const ents = Array.isArray(this._config?.entities) ? this._config.entities.filter(Boolean).map(String) : []; + for (const primary of ents) { + const sEid = this._presenceSensorEntityFor(primary); + if (!sEid) continue; + out[primary] = !!this._presenceSensorActiveFor(primary); + } + } catch {} + return out; + } + + _presenceSensorSnapChanged(a, b){ + try { + const aa = (a && typeof a === 'object') ? a : {}; + const bb = (b && typeof b === 'object') ? b : {}; + const keys = new Set([...(Object.keys(aa)), ...(Object.keys(bb))]); + for (const k of keys) { + if (!!aa[k] !== !!bb[k]) return true; + } + } catch {} + return false; + } + + _schedulePresenceDelayTimer(){ + try { if (this._presenceDelayTimer) { clearTimeout(this._presenceDelayTimer); this._presenceDelayTimer = null; } } catch {} + try { + if (!this._config?.auto_apply) return; + if (!this._config?.presence_sensor_enabled) return; + if (!this._hass) return; + if (!this._hasConfig) return; + if (this._isPaused && this._isPaused()) return; + + const ents = Array.isArray(this._config?.entities) ? this._config.entities.filter(Boolean).map(String) : []; + const now = Date.now(); + let bestDue = null; + + for (const primary of ents) { + const sEid = this._presenceSensorEntityFor(primary); + if (!sEid) continue; + const st = this._hass?.states?.[sEid]; + const s = String(st?.state || '').toLowerCase().trim(); + if (s !== 'on' && s !== 'off') continue; + + // Ensure latch is updated before computing due + const active = !!this._presenceSensorActiveFor(primary); + const { onMs, offMs } = this._presenceSensorDelayMsFor(primary); + + const changedMs = (()=>{ + try { + const raw = st?.last_changed || st?.last_updated || null; + const ms = raw ? Date.parse(String(raw)) : NaN; + return Number.isFinite(ms) ? ms : now; + } catch { return now; } + })(); + + let due = null; + if (s === 'on' && !active && onMs > 0) due = changedMs + onMs; + else if (s === 'off' && active && offMs > 0) due = changedMs + offMs; + + if (due != null && due > now) { + if (bestDue == null || due < bestDue) bestDue = due; + } + } + + if (bestDue == null) return; + const delay = Math.max(500, bestDue - Date.now()); + this._presenceDelayTimer = setTimeout(async ()=>{ + try { + if (!this._config?.auto_apply) return; + if (this._isPaused && this._isPaused()) return; + if (this._config?.storage_enabled) { + await this._nudgeBackgroundApplyNow(); + } else { + await this._applyCurrentSetpoints(true, true); + } + this._scheduleNextApply(); + } catch {} + try { this._schedulePresenceDelayTimer(); } catch {} + }, delay); + } catch {} + } + + _awayDelayThresholdMs(){ try { const a=this._config?.away||{}; if (!a.delay_enabled) return 0; const v = Number(a.delay_value||0); const u = String(a.delay_unit||'minutes'); const ms = Math.max(0, Math.round(v)) * (u==='seconds'?1000:60000); return ms; } catch { return 0; } } + _isAwayActive(){ + try { + const a = this._config?.away; if (!a || !a.enabled) return false; + const persons = Array.isArray(a.persons) ? a.persons : []; + if (!persons.length) return false; + let want = true; // assume away until we find someone home + for (const p of persons){ const st = this._hass?.states?.[p]; const s = (st?.state||'').toLowerCase(); if (s === 'home') { want = false; break; } } + const delayMs = this._awayDelayThresholdMs(); + if (!delayMs) { this._lastAwayActive = want; return want; } + const now = Date.now(); + if (this._awayWant !== want) { this._awayWant = want; this._awayWantSinceMs = now; } + const stable = (now - (this._awayWantSinceMs||0)) >= delayMs; + const prev = !!this._lastAwayActive; + const out = stable ? want : prev; + this._lastAwayActive = out; + return out; + } catch { return false; } + } + + // ---- Pause helpers ---- + _isPausedBySensor(){ + try { + if (!this._config?.pause_sensor_enabled) return false; + const eid = String(this._config?.pause_sensor_entity || '').trim(); + if (!eid || !eid.startsWith('binary_sensor.')) return false; + const st = this._hass?.states?.[eid]; + const s = String(st?.state || '').toLowerCase(); + return s === 'on'; + } catch { return false; } + } + _isPaused(){ + try { + if (this._isPausedBySensor()) return true; + if (this._pauseIndef) return true; + const until = Number(this._pauseUntilMs||0); + if (Number.isFinite(until) && until > Date.now()) return true; + } catch {} + return false; + } + _resumeAtMs(){ const u = Number(this._pauseUntilMs||0); return (Number.isFinite(u) && u>0) ? u : null; } + _scheduleResumeTimer(){ + try { + if (this._resumeTimer) clearTimeout(this._resumeTimer); + if (!this._isPaused()) return; + if (this._pauseIndef || this._isPausedBySensor()) return; // manual or sensor resume only + const when = this._resumeAtMs(); + if (!when) return; + const delay = Math.max(500, when - Date.now()); + this._resumeTimer = setTimeout(async ()=>{ + // Resume reached: re-render UI and apply immediately once + try { this._render(); } catch {} + await this._applyCurrentSetpoints(true, true); + this._scheduleNextApply(); + }, delay); + } catch {} + } + + _formatPauseCountdown(){ + try { + const until = Number(this._pauseUntilMs||0); + const now = Date.now(); + if (!until || until <= now) return ''; + let ms = until - now; + const totalSec = Math.floor(ms/1000); + const h = Math.floor(totalSec/3600); + const m = Math.floor((totalSec%3600)/60); + const s = totalSec%60; + const pad = (n)=>String(n).padStart(2,'0'); + if (h>0) return `${h}:${pad(m)}:${pad(s)}`; + return `${m}:${pad(s)}`; + } catch { return ''; } + } + _startCountdownTimer(){ + try { + if (this._countdownTimer) clearInterval(this._countdownTimer); + const wrap = this.shadowRoot && this.shadowRoot.querySelector('.pause-wrap'); + const etaEl = wrap && wrap.querySelector && wrap.querySelector('.pause-eta'); + if (!this._isPaused() || this._pauseIndef || this._isPausedBySensor() || !etaEl) return; + const tick = ()=>{ try { if (!this._isPaused() || this._pauseIndef) { clearInterval(this._countdownTimer); this._countdownTimer=null; etaEl.textContent=''; return; } etaEl.textContent = this._formatPauseCountdown(); } catch {} }; + tick(); + this._countdownTimer = setInterval(tick, 1000); + } catch {} + } + + _nextBoundaryDate(){ + // HA timezone mode: compute delay in HA tz and return a local Date for the timeout + if (this._useHaTime() && this._hass?.config?.time_zone) { + const nowMin = this._getNowMin(); let bestT = null; let bestDelta = Infinity; + for (const eid of (this._config.entities || [])){ + const row = this._schedules[eid]; if (!row) continue; const times = []; + // Prefer presence schedule when enabled and active; then profiles/weekdays + let blocks = row.blocks||[]; let usedPresence=false; + try { + const key = this._activePresenceComboKey && this._activePresenceComboKey(); + if (key && row.presence && Array.isArray(row.presence[key]?.blocks) && row.presence[key].blocks.length) { + blocks = row.presence[key].blocks || []; usedPresence=true; + } + } catch {} + if (!usedPresence) { + try { + if (this._config?.profiles_enabled) { + this._ensureProfilesStruct(row); + const ap = row.activeProfile; + if (ap && row.profiles && Array.isArray(row.profiles[ap]?.blocks) && (row.profiles[ap].blocks||[]).length) { + blocks = row.profiles[ap].blocks || []; + } else if (this._config?.weekdays_enabled && row.weekly) { + const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey) || []; + } + } else if (this._config?.weekdays_enabled && row.weekly) { + const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey) || []; + } + } catch {} + } + for (const b of blocks) { times.push(b.startMin, b.endMin); } + for (const t of times){ let delta = (t - nowMin + 1440) % 1440; if (delta === 0) delta = 1; if (delta < bestDelta){ bestDelta = delta; bestT = t; } } + } + // Always consider midnight rollover (00:00) + { + let deltaMid = (1440 - nowMin) % 1440; const tMid = 0; if (deltaMid === 0) deltaMid = 1; if (deltaMid < bestDelta) { bestDelta = deltaMid; bestT = tMid; } + } + // Use delta minutes (not the raw boundary minute) so a boundary that + // equals nowMin (e.g. 08:00) never schedules a rapid 500ms loop. + let delayMs; + if (!Number.isFinite(bestDelta) || bestDelta === Infinity) { + delayMs = Math.max(60000, ((1440 - nowMin) % 1440) * 60000); + } else { + delayMs = Math.max(60000, bestDelta * 60000); + } + return new Date(Date.now() + delayMs); + } + // Browser/local timezone mode (existing behavior) + const now = new Date(); const nowMin = this._getNowMin(); let bestT = null; let bestDelta = Infinity; for (const eid of (this._config.entities || [])){ + const row = this._schedules[eid]; if (!row) continue; const times = []; + // Prefer presence schedule when enabled and active; then profiles/weekdays + let blocks = row.blocks||[]; let usedPresence=false; + try { + const key = this._activePresenceComboKey && this._activePresenceComboKey(); + if (key && row.presence && Array.isArray(row.presence[key]?.blocks) && row.presence[key].blocks.length) { + blocks = row.presence[key].blocks || []; usedPresence=true; + } + } catch {} + if (!usedPresence) { + try { + if (this._config?.profiles_enabled) { + this._ensureProfilesStruct(row); + const ap = row.activeProfile; + if (ap && row.profiles && Array.isArray(row.profiles[ap]?.blocks) && (row.profiles[ap].blocks||[]).length) { + blocks = row.profiles[ap].blocks || []; + } else if (this._config?.weekdays_enabled && row.weekly) { + const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey) || []; + } + } else if (this._config?.weekdays_enabled && row.weekly) { + const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey) || []; + } + } catch {} + } + for (const b of blocks) { times.push(b.startMin, b.endMin); } + for (const t of times){ let delta = (t - nowMin + 1440) % 1440; if (delta === 0) delta = 1; if (delta < bestDelta){ bestDelta = delta; bestT = t; } } + } + // Always consider midnight rollover (00:00) as a boundary so we re-apply + // the next day's desired temperature even if today's schedule has no block + // ending/starting at 00:00. This makes day change bullet-proof. + { + let deltaMid = (1440 - nowMin) % 1440; // minutes until midnight + const tMid = 0; + if (deltaMid === 0) deltaMid = 1; // avoid scheduling "now" + if (deltaMid < bestDelta) { bestDelta = deltaMid; bestT = tMid; } + } + // Same safeguard as HA-time mode: schedule based on delta minutes. + if (!Number.isFinite(bestDelta) || bestDelta === Infinity) { + const ms = Math.max(60000, ((1440 - nowMin) % 1440) * 60000); + return new Date(Date.now() + ms); + } + return new Date(Date.now() + Math.max(60000, bestDelta * 60000)); } + + // (removed next-boundary-per-row helper) + + _scheduleNextApply(){ + if (this._transitionTimer) clearTimeout(this._transitionTimer); + // If auto-apply is off completely, do nothing + if (!this._config?.auto_apply) return; + // Don't schedule until config is available + if (!this._hasConfig) return; + // If paused by binary sensor, don't schedule boundary timer; resume when sensor changes + if (this._isPausedBySensor && this._isPausedBySensor()) { this._scheduleResumeTimer(); return; } + // If paused indefinitely, don't schedule boundary timer; only resume via user action + if (this._pauseIndef) { this._scheduleResumeTimer(); return; } + // Choose earlier of boundary and pause-resume moment if set + const boundary = this._nextBoundaryDate(); + let targetMs = boundary.getTime(); + const resumeMs = this._resumeAtMs(); + const isResume = !!(resumeMs && resumeMs < targetMs); + if (isResume) targetMs = resumeMs; + const delay = Math.max(500, targetMs - Date.now()); + this._transitionTimer = setTimeout(async () => { + await this._applyCurrentSetpoints(true, isResume); + this._scheduleNextApply(); + }, delay); + // Also make sure we have a standalone resume timer if paused + this._scheduleResumeTimer(); + } + + _setupBoilerControl(){ + try { + if (this._boilerTimer) { clearInterval(this._boilerTimer); this._boilerTimer = null; } + if (!this._hass || !this._config) return; + // Boiler control runs in frontend only when Sync is OFF. + if (this._config?.storage_enabled) return; + if (!this._config?.auto_apply) return; + if (!this._config?.boiler_enabled) return; + const sw = String(this._config.boiler_switch || ''); + if (!sw) return; + const dom = String(sw).split('.')[0]; + if (!(dom === 'switch' || dom === 'input_boolean')) return; + + // Run once immediately + then every minute. + try { this._boilerTick(); } catch {} + this._boilerTimer = setInterval(() => { try { this._boilerTick(); } catch {} }, 60000); + } catch {} + } + + _boilerTick(){ + try { + if (!this._hass || !this._config) return; + if (this._config?.storage_enabled) return; + if (!this._config?.auto_apply) return; + if (!this._config?.boiler_enabled) return; + if (this._isPaused && this._isPaused()) return; + + const sw = String(this._config.boiler_switch || ''); + if (!sw) return; + + // New logic: compare each selected climate entity's current_temperature to + // its scheduled target temperature for "now". + // - If ANY room is below its scheduled target -> switch ON + // - If ALL rooms are at/above their scheduled target -> switch OFF + // If some rooms have missing data, we only turn OFF when we can confirm + // ALL rooms are at/above target. + const nowMin = this._getNowMin(); + const allRooms = (Array.isArray(this._config.entities) ? this._config.entities : []) + .map(e=>String(e||'')) + .filter(e=>e.startsWith('climate.')); + const allSet = new Set(allRooms); + const cfgRooms = this._config?.boiler_rooms; + const roomEntities = Array.isArray(cfgRooms) + ? cfgRooms.map(e=>String(e||'')).filter(e=>allSet.has(e) && e.startsWith('climate.')) + : allRooms; + if (!roomEntities.length) return; + + const onOff = (()=>{ const v = Number(this._config?.boiler_on_offset ?? 0); return Number.isFinite(v) ? v : 0; })(); + const offOff = (()=>{ const v = Number(this._config?.boiler_off_offset ?? 0); return Number.isFinite(v) ? v : 0; })(); + let anyNeedHeat = false; + let anyUnknown = false; + let anySeen = false; + let allOkForOff = true; + + for (const eid of roomEntities) { + const desiredC = Number(this._desiredTempFor ? this._desiredTempFor(eid, nowMin) : null); + const curC = Number(this._roomCurrentTemperatureC ? this._roomCurrentTemperatureC(eid) : NaN); + + if (!Number.isFinite(desiredC) || !Number.isFinite(curC)) { + anyUnknown = true; + continue; + } + + anySeen = true; + if (curC < (desiredC - onOff)) { anyNeedHeat = true; break; } + if (curC < (desiredC + offOff)) allOkForOff = false; + } + + if (!anySeen) return; + + let want = null; + if (anyNeedHeat) want = 'on'; + else if (!anyUnknown && allOkForOff) want = 'off'; + else return; + + const stSw = this._hass.states?.[sw]; + const cur = String(stSw?.state || ''); + if (want === 'on' && cur === 'on') return; + if (want === 'off' && cur === 'off') return; + + const dom = String(sw).split('.')[0]; + if (!(dom === 'switch' || dom === 'input_boolean')) return; + if (want === 'on') this._hass.callService(dom, 'turn_on', { entity_id: sw }); + else this._hass.callService(dom, 'turn_off', { entity_id: sw }); + } catch {} + } + + async _applyCurrentSetpoints(force=false, reconcile=false){ if (!this._config?.auto_apply || !this._hass) return; if (!this._hasConfig) return; if (this._config?.storage_enabled) return; if (this._isPaused()) return; const nowMin = this._getNowMin(); const targets = this._allTargetEntities(); for (const eid of targets){ + // If this call is from boundary timer (force===true), only apply for entities that have boundary now. + // On resume from pause (reconcile=true), reconcile across all entities. + if (force && !reconcile) { + const primary = this._groupPrimaryOf(eid); + const row = this._schedules[primary]; + if (!row || !this._entityHasBoundaryNow(row, nowMin)) { continue; } + } + let desired = this._desiredTempFor(eid, nowMin); + const mx = this._config?.max_temp ?? 25; const mn = this._config?.min_temp ?? 5; if (Number.isFinite(mx)) desired = Math.min(desired, mx); if (Number.isFinite(mn)) desired = Math.max(desired, mn); + if (desired == null || Number.isNaN(desired)) continue; + const last = this._lastApplied[eid] || {}; + // Only send when desired setpoint changes for this entity. + // Prevents overriding manual changes in other rooms. + if (!reconcile && Number.isFinite(last.temp) && Math.abs(Number(last.temp) - desired) < 0.05) continue; + const st = this._hass.states?.[eid]; + const cur = Number(st?.attributes?.temperature ?? st?.attributes?.target_temperature ?? st?.attributes?.target_temp); + const willSendF = this._isF(); const haIsF = this._haIsF(); + let curU = cur; + if (Number.isFinite(curU)) { + if (willSendF && !haIsF) curU = this._cToF(curU); + else if (!willSendF && haIsF) curU = this._fToC(curU); + } + const sendTemp = this._serviceTempFromC(desired); + if (Number.isFinite(curU) && Math.abs(curU - sendTemp) < 0.05){ this._lastApplied[eid] = { min: nowMin, temp: desired }; continue; } + try { await this._applySetpointForEntity(eid, desired); this._lastApplied[eid] = { min: nowMin, temp: desired }; } + catch (e) {} + } } + + // ---------- "Apply on change" helper ---------- + _desiredNowSnapshot(){ const nowMin = this._getNowMin(); const snap = {}; for (const eid of this._allTargetEntities()) snap[eid] = this._desiredTempFor(eid, nowMin); return snap; } + async _applyIfDesiredChanged(beforeSnap){ if (!this._config?.auto_apply) return; if (this._isPaused()) return; const after = this._desiredNowSnapshot(); const nowMin = this._getNowMin(); + if (this._config?.storage_enabled) { return; } + for (const eid of Object.keys(after)){ let a = after[eid]; const b = beforeSnap[eid]; if (a == null) continue; const mx=this._config?.max_temp ?? 25; const mn=this._config?.min_temp ?? 5; if (Number.isFinite(mx)) a = Math.min(a, mx); if (Number.isFinite(mn)) a = Math.max(a, mn); if (b == null || Math.abs(a - b) > 0.049){ try { await this._applySetpointForEntity(eid, a); this._lastApplied[eid] = { min: nowMin, temp: a }; } catch(e){} } } + } + + // Ping backend to apply now without waiting for delayed sync + async _nudgeBackgroundApplyNow(){ + try { + if (!this._hass || !this._config?.storage_enabled) return; + const _awayCfg = this._config?.away || {}; + const _awayOut = { ..._awayCfg }; + // When bypassing or using advanced presence, disable simple Away in backend apply + if (this._awayBypass || _awayOut.advanced_enabled) _awayOut.enabled = false; + const payload = this._makeStoragePayload(false); + payload.settings = { ...payload.settings, away: _awayOut }; + const p = { settings: payload.settings, force: true }; + if (this._config?.instance_enabled) { p.instance_id = this._instanceId(); p.activate = true; } + await this._hass.callService('thermostat_timeline','set_store', p); + } catch {} + } + + // Apply a setpoint immediately for the given entity (and its merged entities) + // using the provided temperature (in °C). Honours away override and min/max. + async _applyImmediateForEntity(entity, tempC, nowMinOverride=null){ + try { + if (!this._config?.auto_apply || !this._config?.apply_on_edit || !this._hass) return; + // When shared storage is enabled, integration handles the apply; card should not send + if (this._config?.storage_enabled) return; + if (this._isPaused()) return; + let desired = Number(tempC); + // Away override (disabled when advanced presence is enabled) + try { const advOn = !!(this._config?.away?.advanced_enabled); if (!advOn && this._isAwayActive()) { const a = Number(this._config?.away?.target_c ?? 17); if (Number.isFinite(a)) desired = Math.min(desired, a); } } catch {} + // Clamp to min/max + const mx = this._config?.max_temp ?? 25; const mn = this._config?.min_temp ?? 5; + if (Number.isFinite(mx)) desired = Math.min(desired, mx); + if (Number.isFinite(mn)) desired = Math.max(desired, mn); + const nowMin = Number.isFinite(nowMinOverride) ? nowMinOverride : this._getNowMin(); + const dom = String(entity||'').split('.')[0] || ''; + const targets = (dom === 'climate') ? [entity, ...(this._config?.merges?.[entity] || [])] : [entity]; + for (const eid of targets){ + try { + const __eid = eid; + if (!(typeof __eid==='string' && __eid.includes('.') && this._hass?.states?.[__eid])) continue; + const dom2 = __eid.split('.')[0]; + // Avoid redundant command if already at desired + let curU = NaN; + try { + const st = this._hass.states[__eid]; + if (dom2 === 'climate') { + const cur = Number(st?.attributes?.temperature ?? st?.attributes?.target_temperature ?? st?.attributes?.target_temp); + const willSendF = this._isF(); const haIsF = this._haIsF(); + curU = cur; + if (Number.isFinite(curU)) { + if (willSendF && !haIsF) curU = this._cToF(curU); + else if (!willSendF && haIsF) curU = this._fToC(curU); + } + } else if (dom2 === 'input_number') { + const raw = Number(st?.state); + curU = raw; + } + } catch {} + const sendTemp = this._serviceTempFromC(desired); + if (Number.isFinite(curU) && Math.abs(curU - sendTemp) < 0.049) { this._lastApplied[__eid] = { min: nowMin, temp: desired }; continue; } + await this._applySetpointForEntity(__eid, desired); + this._lastApplied[__eid] = { min: nowMin, temp: desired }; + } catch {} + } + } catch {} + } + + _entityHasBoundaryNow(row, nowMin){ + try { + let blocks = row.blocks||[]; + // Prefer presence schedule when enabled and active + try { + const key = this._activePresenceComboKey && this._activePresenceComboKey(); + if (key && row.presence && Array.isArray(row.presence[key]?.blocks) && row.presence[key].blocks.length) { + blocks = row.presence[key].blocks || []; + } else if (this._config?.weekdays_enabled && row.weekly) { + const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey) || []; + } + } catch { if (this._config?.weekdays_enabled && row.weekly){ const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey) || []; } } + // Allow a 1-minute grace window because we intentionally schedule the + // timer a tiny bit after the exact boundary to avoid races. Otherwise we + // might miss the equality check and skip applying at the boundary. + const prev = ((nowMin - 1) % 1440 + 1440) % 1440; + for (const b of (blocks||[])){ + const s = Number(b.startMin), e = Number(b.endMin); + if (Number.isFinite(s) && (s === nowMin || s === prev)) return true; + if (Number.isFinite(e) && (e === nowMin || e === prev)) return true; + } + } catch {} + return false; + } + + // ---------- UI ---------- + _init() { + this.shadowRoot.innerHTML = ` + + + +
    +
    +
    + +
    + +
    +
    + +
    + + +
    + +
    + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    `; this._applyCardI18n(); + + // Apply hide-popups class if requested + try { const card = this.shadowRoot.querySelector('ha-card'); if (this._disablePopups && card) card.classList.add('hide-popups'); } catch {} + + + this._ensureSchedules(); + this._startUiTimer(); + // Show onboarding for first-time users (disabled while popups are turned off) + try { if (!this._disablePopups) this._maybeOpenOnboard(); } catch {} + + // Wire pause overlay actions + try { + const ov = this.shadowRoot.querySelector('.overlay-pause'); + const b1 = this.shadowRoot.querySelector('.pause-1h'); + const b5 = this.shadowRoot.querySelector('.pause-5h'); + const bc = this.shadowRoot.querySelector('.pause-custom'); + const bi = this.shadowRoot.querySelector('.pause-indef'); + const br = this.shadowRoot.querySelector('.pause-resume'); + const bx = this.shadowRoot.querySelector('.pause-close'); + b1?.addEventListener('click', ()=>{ this._setPauseForMinutes(60); }); + b5?.addEventListener('click', ()=>{ this._setPauseForMinutes(300); }); + bc?.addEventListener('click', ()=>{ + try { + const txt = prompt(this._t('pause.prompt_minutes') || 'Enter duration (minutes):', '60'); + const m = Number(txt); + if (Number.isFinite(m) && m > 0) this._setPauseForMinutes(m); + } catch {} + }); + bi?.addEventListener('click', ()=>{ this._setPauseIndef(); }); + br?.addEventListener('click', ()=>{ this._resumeNow(); }); + bx?.addEventListener('click', ()=>{ try { ov?.classList.remove('open'); } catch {} }); + } catch {} + } + + _renderPauseStatus(){ + try { + const st = this.shadowRoot && this.shadowRoot.querySelector('.pause-status'); + if (!st) return; + if (!this._isPaused()) { st.textContent = ''; return; } + if (this._pauseIndef) { st.textContent = this._t('pause.active_indef'); return; } + const until = new Date(Number(this._pauseUntilMs||0)); + // Format time according to card preference + const hh = until.getHours(), mm = until.getMinutes(); + let label; + if (this._config?.time_12h) { + const am = hh < 12; let h = hh % 12; if (h===0) h=12; + label = `${String(h).padStart(2,'0')}:${String(mm).padStart(2,'0')} ${am?'AM':'PM'}`; + } else { + label = `${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')}`; + } + const msgTmpl = this._t('pause.active_until'); + st.textContent = (msgTmpl || 'Paused until {time}').replace('{time}', label); + } catch {} + } + + // --- Sync countdown helpers (delayed sync UI) --- + _formatSyncCountdown(){ + try { + const leftMs = Math.max(0, Number(this._storeDelayDue||0) - Date.now()); + const s = Math.ceil(leftMs/1000); + const mm = Math.floor(s/60), ss = s%60; + return `${String(mm).padStart(2,'0')}:${String(ss).padStart(2,'0')}`; + } catch { return '00:00'; } + } + _startSyncTimer(){ + if (this._syncTimer) return; + try { this._syncTimer = setInterval(()=>{ try { if (!this._inlineEditing && !this._editing) this._render(); } catch {} }, 1000); } catch {} + } + _stopSyncTimer(){ try { if (this._syncTimer) clearInterval(this._syncTimer); } catch {} this._syncTimer = null; } + + // Persist/restore delayed sync due time across hard refresh + _syncDueKey(){ return `tt_sync_due:${this._instanceId()}`; } + _draftKey(){ return `tt_sync_draft:${this._instanceId()}`; } + + _localStoreKeyFor(iid){ + const id = this._normInstanceId(iid); + if (!id) return 'thermostat_timeline_store'; + return `thermostat_timeline_store:${id}`; + } + _syncDueKeyFor(iid){ return `tt_sync_due:${this._normInstanceId(iid)}`; } + _draftKeyFor(iid){ return `tt_sync_draft:${this._normInstanceId(iid)}`; } + + _copyInstanceLocalKeys(oldId, newId){ + try { + const oldI = this._normInstanceId(oldId); + const newI = this._normInstanceId(newId); + if (!oldI || !newI || oldI === newI) return false; + + const oldStoreKey = this._localStoreKeyFor(oldI); + const newStoreKey = this._localStoreKeyFor(newI); + const oldStore = localStorage.getItem(oldStoreKey) || ''; + const newStore = localStorage.getItem(newStoreKey) || ''; + if (oldStore && newStore && newStore.length > 2 && oldStore !== newStore) { + const ok = confirm('Det nye ID har allerede data. Overskriv?'); + if (!ok) return false; + } + if (oldStore) { + try { localStorage.setItem(newStoreKey, oldStore); } catch {} + } + + // Move pending sync/draft keys (best-effort) + try { + const okDue = this._syncDueKeyFor(oldI); + const nkDue = this._syncDueKeyFor(newI); + const due = localStorage.getItem(okDue); + if (due != null) { + try { localStorage.setItem(nkDue, String(due)); } catch {} + } + } catch {} + try { + const okDr = this._draftKeyFor(oldI); + const nkDr = this._draftKeyFor(newI); + const dr = localStorage.getItem(okDr); + if (dr != null) { + try { localStorage.setItem(nkDr, String(dr)); } catch {} + } + } catch {} + + return true; + } catch { return false; } + } + + _moveInstanceLocalKeys(oldId, newId){ + try { + const oldI = this._normInstanceId(oldId); + const newI = this._normInstanceId(newId); + if (!oldI || !newI || oldI === newI) return false; + + const ok = this._copyInstanceLocalKeys(oldI, newI); + if (!ok) return false; + + try { localStorage.removeItem(this._localStoreKeyFor(oldI)); } catch {} + try { localStorage.removeItem(this._syncDueKeyFor(oldI)); } catch {} + try { localStorage.removeItem(this._draftKeyFor(oldI)); } catch {} + return true; + } catch { return false; } + } + + _clearAllLocalScheduleData(){ + // Wipes ALL local/browser keys related to thermostat timeline across all instances. + try { + const toDel = new Set(); + try { toDel.add('thermostat_timeline_store'); } catch {} + try { toDel.add('thermostat_timeline_onboard_seen_v1'); } catch {} + try { toDel.add('tt_defer_holiday_open'); } catch {} + + const prefixes = [ + 'thermostat_timeline_store:', + 'tt_sync_due:', + 'tt_sync_draft:', + 'tt_instance_cfg:', + ]; + + // Broad safety net: these prefixes are reserved for this card/integration. + const broadPrefixes = [ + 'thermostat_timeline', + 'tt_', + ]; + + for (let i = 0; i < localStorage.length; i++) { + const k = localStorage.key(i); + if (!k) continue; + if (k === 'thermostat_timeline_store') { toDel.add(k); continue; } + for (const p of prefixes) { + if (k.startsWith(p)) { toDel.add(k); break; } + } + + // Also catch any remaining thermostat_timeline/tt_* keys (UI flags etc.) + for (const p of broadPrefixes) { + if (k.startsWith(p)) { toDel.add(k); break; } + } + } + + for (const k of toDel) { + try { localStorage.removeItem(k); } catch {} + } + } catch {} + } + + async _clearAllDataEverywhereConfirmed(){ + // Full wipe: local/browser + backend store (if available) + in-memory state. + // Confirmation must be handled by caller. + try { + // Stop any pending delayed-sync timers so cleared data can't be written back. + try { if (this._storeDelayTimer) clearTimeout(this._storeDelayTimer); } catch {} + this._storeDelayTimer = null; + try { this._storeDelayDue = 0; } catch {} + try { this._stopSyncTimer(); } catch {} + + // Wipe local browser copy (ALL instances + pending sync/draft) + try { this._clearAllLocalScheduleData(); } catch {} + + // Mark a hard reset so subsequent loads won't re-import legacy/local drafts. + try { localStorage.setItem(this._hardResetKey(), String(Date.now())); } catch {} + + // Also clear room list and related config (rooms are configuration, not schedules) + try { + this._config.entities = []; + this._config.room_use_input_number = []; + this._config.room_use_temp_sensor = []; + this._config.merges = {}; + this._config.labels = {}; + this._config.temp_sensors = {}; + + // Reset instance config so the card starts in non-instance mode. + this._config.instance_enabled = false; + this._config.instance_id = ''; + this._config.instance_uid = ''; + + // Reset title + this._config.title = ''; + } catch {} + + // Also wipe in-memory schedules so UI reflects deletion immediately + try { + this._schedules = {}; + this._config.color_ranges = {}; + this._profilesSelected = null; this._profilesDraft = null; this._profilesDirty = false; this._profilesEditingNew = false; + try { this._openRows = new Set(); } catch {} + } catch {} + + if (this._hass) { + // Hard reset (deletes .storage files and recreates them empty) + let ok = false; + try { await this._hass.callService('thermostat_timeline', 'factory_reset', {}); ok = true; } catch {} + + // Backwards compatible fallback + if (!ok) { + try { await this._hass.callService('thermostat_timeline', 'clear_store', {}); ok = true; } catch {} + if (!ok) { + try { await this._hass.callService('thermostat_timeline', 'clear', {}); } catch {} + } + try { await this._hass.callService('thermostat_timeline', 'delete_backup', {}); } catch {} + } + + if (!ok) { + try { alert('Kunne ikke rydde data i Home Assistant (service-kald fejlede). Tjek Home Assistant log.'); } catch {} + } + } + + // Repaint editor/live cards + try { this._emit(true); } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + + // If settings popup is open, refresh its draft so it doesn't show stale values + try { this._initSettingsPopupDraft(); } catch {} + try { this._renderSettingsPopupTabs(); } catch {} + try { this._renderSettingsPopupTab(); } catch {} + return true; + } catch { return false; } + } + + _clearAllLocalOnlyConfirmed(){ + // Wipes local/browser thermostat timeline data only. + try { + try { this._clearAllLocalScheduleData(); } catch {} + try { localStorage.setItem(this._hardResetKey(), String(Date.now())); } catch {} + try { this._schedules = {}; this._ensureSchedules(); } catch {} + try { this._emit(true); } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + try { this._initSettingsPopupDraft(); } catch {} + try { this._renderSettingsPopupTab(); } catch {} + return true; + } catch { return false; } + } + _persistDraftNow(){ try { localStorage.setItem(this._draftKey(), JSON.stringify({ ts: Date.now(), schedules: this._schedules })); } catch {} } + _tryRestoreDraftRecent(maxAgeMs=15*60*1000){ try { const raw = localStorage.getItem(this._draftKey()) || ''; if (!raw) return false; const parsed = JSON.parse(raw); const ts = Number(parsed?.ts); if (!Number.isFinite(ts) || (Date.now()-ts) > maxAgeMs) return false; const sch = parsed?.schedules && parsed.schedules.schedules ? parsed.schedules.schedules : (parsed?.schedules || parsed || {}); if (sch && typeof sch === 'object') { this._schedules = sch; this._ensureSchedules(); return true; } } catch {} return false; } + _restoreSyncDue(){ + try { + if (!this._config?.storage_enabled || this._config?.storage_sync_mode !== 'delay') return; + // Prefer value from storage (already read in _loadStore); fallback to localStorage + if (Number.isFinite(this._storeDelayDue) && this._storeDelayDue > Date.now()) { + // Restore any recent local draft so UI shows latest edits + try { this._tryRestoreDraftRecent(); } catch {} + const remaining = Math.max(500, this._storeDelayDue - Date.now()); + if (this._storeDelayTimer) { try { clearTimeout(this._storeDelayTimer); } catch {} } + this._storeDelayTimer = setTimeout(()=>{ this._storeDelayTimer = null; this._writeStoreNow(); }, remaining); + this._startSyncTimer(); + return; + } + const key = this._syncDueKey(); + const raw = localStorage.getItem(key) || ''; + const due = Number(raw); + if (!Number.isFinite(due)) return; + if (due <= Date.now()) { try { localStorage.removeItem(key); } catch {} return; } + // Restore recent draft too + try { this._tryRestoreDraftRecent(); } catch {} + this._storeDelayDue = due; + const remaining = Math.max(500, due - Date.now()); + if (this._storeDelayTimer) { try { clearTimeout(this._storeDelayTimer); } catch {} } + this._storeDelayTimer = setTimeout(()=>{ this._storeDelayTimer = null; this._writeStoreNow(); }, remaining); + this._startSyncTimer(); + } catch {} + } + + async _writeStoreNow(){ + if (!this._config?.storage_enabled) return; + this._saving = true; + const key = this._syncDueKey(); + try { + // Build away for backend: if advanced presence is enabled, disable basic away in backend + const _awayCfg = this._config?.away || {}; + const _awayOut = { ..._awayCfg }; + if (_awayOut.advanced_enabled) _awayOut.enabled = false; + let payload = this._makeStoragePayload(true); + payload.settings = { ...payload.settings, away: _awayOut }; + // Merge with current backend settings to avoid clearing omitted keys (e.g., labels) on delayed write + try { + const state = await this._apiFetchState(); + const base = (state && typeof state === 'object' && state.settings && typeof state.settings === 'object') ? state.settings : {}; + const incoming = payload.settings || {}; + const merged = { ...base, ...incoming }; + const keepIfEmpty = (key)=>{ + try { + const inc = incoming[key]; + const hasInc = inc && typeof inc === 'object' && Object.keys(inc).length > 0; + const baseVal = base[key]; + const hasBase = baseVal && typeof baseVal === 'object' && Object.keys(baseVal).length > 0; + if (!hasInc && hasBase) merged[key] = baseVal; + } catch {} + }; + // Respect explicit label edits: allow an empty object to clear labels when touched + if (!this._labelsTouched) keepIfEmpty('labels'); + keepIfEmpty('merges'); + keepIfEmpty('temp_sensors'); + payload = { ...payload, settings: merged }; + } catch {} + // Push all data to the integration via set_store service (writes to file) + if (this._config?.instance_enabled) { + payload = { ...payload, instance_id: this._instanceId(), activate: true }; + } + await this._hass.callService("thermostat_timeline", "set_store", payload); + try { this._broadcastStoreUpdated(this._localStoreKey()); } catch {} + } + catch (e) {} + finally { + setTimeout(() => { + this._saving = false; + this._syncJustUntil = Date.now() + 2500; + this._storeDelayDue = 0; + try { localStorage.removeItem(key); } catch {} + this._startSyncTimer(); + this._render(); + try { this._labelsTouched = false; } catch {} + }, 600); + } + } + + _setPauseForMinutes(mins){ + try { + const ms = Math.max(1, Math.round(Number(mins)||0)) * 60000; + this._pauseIndef = false; + this._pauseUntilMs = Date.now() + ms; + // Persist to storage/local + this._debouncedSaveStore(); + this._render(); + this._renderPauseStatus(); + // Reschedule apply/resume + this._scheduleNextApply(); + // Notify backend immediately when shared storage is on + try { if (this._config?.storage_enabled) this._saveStore(); } catch {} + // Close overlay + try { this.shadowRoot.querySelector('.overlay-pause')?.classList.remove('open'); } catch {} + } catch {} + } + + _setPauseIndef(){ + try { + this._pauseIndef = true; this._pauseUntilMs = 0; + this._debouncedSaveStore(); + this._render(); + this._renderPauseStatus(); + this._scheduleNextApply(); + try { if (this._config?.storage_enabled) this._saveStore(); } catch {} + try { this.shadowRoot.querySelector('.overlay-pause')?.classList.remove('open'); } catch {} + } catch {} + } + + _resumeNow(){ + try { + this._pauseIndef = false; this._pauseUntilMs = 0; + this._debouncedSaveStore(); + this._render(); + this._scheduleNextApply(); + // Apply immediately to match plan "now" + this._applyCurrentSetpoints(true, true); + try { if (this._config?.storage_enabled) this._saveStore(); } catch {} + try { this.shadowRoot.querySelector('.overlay-pause')?.classList.remove('open'); } catch {} + } catch {} + } + + // If the editor asked to open the Holiday modal after the user pressed Save, + // read the flag from localStorage and open once, then clear the flag. + _maybeOpenDeferredHoliday(){ + try { + const flag = localStorage.getItem('tt_defer_holiday_open'); + if (flag === '1') { + localStorage.removeItem('tt_defer_holiday_open'); + const first = (this._config?.entities||[])[0] || null; + this._openHolidayEditor(first); + } + } catch {} + } + + _ensureSchedules() { + for (const eid of this._config.entities) { + if (!this._schedules[eid]) this._schedules[eid] = { defaultTemp: this._config.default_temp, blocks: [] }; + // Always maintain structures so state is preserved if toggles are flipped later + this._ensureProfilesStruct(this._schedules[eid]); + this._ensureHolidayStruct(this._schedules[eid]); + this._ensurePresenceStruct(this._schedules[eid]); + if (this._config?.weekdays_enabled) this._ensureWeeklyStruct(this._schedules[eid]); + } + } + + _startUiTimer() { + if (this._uiTimer) clearInterval(this._uiTimer); + this._uiTimer = setInterval(() => { if (!this._inlineEditing && !this._editing) this._render(); }, this._config.now_update_ms); + } + + _startStoreWatchTimer(){ + try { if (this._storeWatchTimer) clearInterval(this._storeWatchTimer); } catch {} + if (!this._config?.storage_enabled) { return; } + const ms = Math.max(800, Number(this._storeWatchMs||0) || 10000); + this._storeWatchTimer = setInterval(()=>{ + if (this._storeWatchBusy) return; + this._storeWatchBusy = true; + (async () => { + try { + let verSched = NaN, verSet = NaN, verCol = NaN, verWeek = NaN, verProf = NaN; + let haveVersions = false; + // Always use API for version checking - file-based storage only + if (this._apiSupported()) { + const ver = await this._apiFetchVersion(); + if (ver && typeof ver === 'object') { + verSched = Number(ver.version || 0); + verSet = Number(ver.settings_version || verSched || 0); + verCol = Number(ver.colors_version || verSet || verSched || 0); + verWeek = Number(ver.weekday_version || verCol || verSet || verSched || 0); + verProf = Number(ver.profile_version || verWeek || verCol || verSet || verSched || 0); + // Check for global profile changes from API data + const s = ver.settings || {}; + const newGP = (typeof s.global_profile === 'string' && s.global_profile) ? String(s.global_profile) : null; + const newPE = typeof s.profiles_enabled === 'boolean' ? !!s.profiles_enabled : this._config?.profiles_enabled; + let changed = false; + if (newGP !== this._globalProfile) { this._globalProfile = newGP; changed = true; } + if (newPE !== this._config.profiles_enabled) { this._config.profiles_enabled = newPE; changed = true; } + if (changed) { + try { + for (const id of (this._config?.entities||[])){ + const r = this._schedules[id]; if (!r) continue; this._ensureProfilesStruct(r); + if (!this._globalProfile) { r.activeProfile = null; continue; } + r.activeProfile = (r.profiles && r.profiles[this._globalProfile]) ? this._globalProfile : null; + } + } catch {} + if (!this._inlineEditing && !this._editing) this._render(); + this._scheduleNextApply(); + } + haveVersions = true; + } + } + if (!haveVersions) return; + const schedChanged = this._config?.storage_enabled && !this._saving && !Number.isNaN(verSched) && verSched !== this._lastVersion; + const settingsChanged = this._config?.storage_enabled && !this._saving && !Number.isNaN(verSet) && verSet !== this._lastSettingsVersion; + const colorsChanged = this._config?.storage_enabled && !this._saving && !Number.isNaN(verCol) && verCol !== this._lastColorsVersion; + const weekdaysChanged = this._config?.storage_enabled && !this._saving && !Number.isNaN(verWeek) && verWeek !== this._lastWeekdayVersion; + const profilesChanged = this._config?.storage_enabled && !this._saving && !Number.isNaN(verProf) && verProf !== this._lastProfileVersion; + if (schedChanged || settingsChanged || colorsChanged || weekdaysChanged || profilesChanged){ + this._lastVersion = verSched; + this._lastSettingsVersion = verSet; + this._lastColorsVersion = verCol; + this._lastWeekdayVersion = verWeek; + this._lastProfileVersion = verProf; + this._loadStore(false).then(()=>{ this._ensureSchedules(); if (!this._inlineEditing && !this._editing) this._render(); this._scheduleNextApply(); }); + } + } catch {} finally { + this._storeWatchBusy = false; + } + })(); + }, ms); + } + + _render() { + // Do not re-render while the user is actively typing in inline inputs (to avoid caret/selection loss) + if (this._suspendRender) return; + const qs = (s) => this.shadowRoot.querySelector(s); + const titleEl = qs('.title'); + const dayFull = qs('.weekday-full'); + const rowsEl = qs('.rows'); + const scaleEl = qs('.scale-inner'); + const headerEl = qs('.header'); + if (!rowsEl || !scaleEl || !titleEl) return; + + // Helper: build "All rooms" / "All days" view switch + const makeWeekViewSeg = (extraClass='')=>{ + try { + const isWeekEnabled = !!this._config?.weekdays_enabled; + const allowInlineViewSwitch = !!(this._config?.weekdays_view_switch_in_timeline); + const isHoliday = !!(this._isHolidayActive && this._isHolidayActive()); + if (!isWeekEnabled || !allowInlineViewSwitch || isHoliday) return null; + + const fullRooms = this._t('week.view.rooms_one_day') || 'All rooms • one day'; + const fullDays = this._t('week.view.days_one_room') || 'All days • one room'; + const short = (txt)=>{ + try { + const s = String(txt || '').split('•')[0].trim(); + return s || String(txt || ''); + } catch { return String(txt || ''); } + }; + + const setView = (mode)=>{ + try { + if (!this._config?.weekdays_enabled) return; + const next = (mode === 'one_room_all_days') ? 'one_room_all_days' : 'all_rooms_one_day'; + const cur = String(this._config?.weekdays_view || 'all_rooms_one_day'); + if (cur === next) return; + this._config.weekdays_view = next; + // Keep selected room consistent + try { + const ents = Array.isArray(this._config?.entities) ? this._config.entities.filter(Boolean).map(String) : []; + if (next === 'one_room_all_days') { + const want = String(this._config?.weekdays_selected_room || ''); + this._config.weekdays_selected_room = (want && ents.includes(want)) ? want : (ents[0] || ''); + } else { + this._config.weekdays_selected_room = ''; + try { this._setTimelineDisplayDayKey(this._todayKey()); } catch {} + } + } catch {} + this._render(); + try { this._debouncedSaveStore(); } catch {} + } catch {} + }; + + const seg = document.createElement('div'); + seg.className = 'sp-week-view-inline' + (extraClass ? (' ' + extraClass) : ''); + seg.setAttribute('role','group'); + seg.setAttribute('aria-label', this._t('week.title') || 'Weekdays'); + + const bRooms = document.createElement('button'); + bRooms.type = 'button'; + bRooms.textContent = short(fullRooms); + bRooms.title = String(fullRooms || ''); + bRooms.classList.toggle('active', String(this._config?.weekdays_view || 'all_rooms_one_day') === 'all_rooms_one_day'); + bRooms.addEventListener('click', ()=>setView('all_rooms_one_day')); + + const bDays = document.createElement('button'); + bDays.type = 'button'; + bDays.textContent = short(fullDays); + bDays.title = String(fullDays || ''); + bDays.classList.toggle('active', String(this._config?.weekdays_view || 'all_rooms_one_day') === 'one_room_all_days'); + bDays.addEventListener('click', ()=>setView('one_room_all_days')); + + seg.append(bRooms, bDays); + return seg; + } catch { return null; } + }; + + // Determine first active profile (if any) for header pill + let _activeProfileName = null; + try { + if (this._config?.profiles_enabled) { + for (const eid of (this._config?.entities || [])) { + const row = this._schedules?.[eid]; + if (!row) continue; + this._ensureProfilesStruct(row); + const ap = row.activeProfile || null; + if (ap) { _activeProfileName = String(ap); break; } + } + } + } catch {} + + { + const isDef = (txt)=>{ + try { + const s = String(txt || '').trim(); + for (const k of Object.keys(TT_I18N || {})){ + const v = TT_I18N[k] && TT_I18N[k]['card.title_default']; + if (v && String(v).trim() === s) return true; + } + } catch {} + return false; + }; + const shown = (!this._config.title || isDef(this._config.title)) ? this._t('card.title_default') : this._config.title; + titleEl.innerHTML = ''; + const tspan = document.createElement('span'); tspan.className = 'title-text'; tspan.textContent = shown; titleEl.append(tspan); + // Sync countdown/dots (left side, next to title) + try { + // Render only when shared storage enabled + const useDelay = !!(this._config?.storage_enabled) && (this._config?.storage_sync_mode === 'delay'); + const due = Number(this._storeDelayDue||0); + const justDone = Number(this._syncJustUntil||0) > Date.now(); + const running = !!this._saving; + if (useDelay && (running || justDone || (due > Date.now()))) { + const pill = document.createElement('span'); + pill.className = 'profile-pill sync-pill'; + const icon = document.createElement('ha-icon'); + const txt = document.createElement('span'); + txt.style.marginLeft = '6px'; + if (running) { + icon.setAttribute('icon','mdi:sync'); + pill.classList.add('primary'); + txt.textContent = this._t('sync.running'); + } else if (justDone) { + icon.setAttribute('icon','mdi:check-circle-outline'); + pill.classList.add('success'); + txt.textContent = this._t('sync.completed'); + } else { + icon.setAttribute('icon','mdi:timer-outline'); + pill.classList.add('ghost'); + txt.textContent = this._formatSyncCountdown(); + this._startSyncTimer(); + } + pill.append(icon, txt); + titleEl.append(pill); + if (!running && !justDone && !(due> Date.now())) this._stopSyncTimer(); + } else { + this._stopSyncTimer(); + } + } catch {} + if (_activeProfileName) { + const p = document.createElement('span'); p.className = 'profile-pill success'; const prefix = this._t('profiles.active_label') || 'Manual profile active:'; p.textContent = `${prefix} ${_activeProfileName}`; titleEl.append(p); + } + + } + // (Profile pill moved next to title; weekday center shows only day name now) + // Center header: show current weekday name when weekday mode is enabled + try { + if (dayFull) { + let label = ''; + // Prefer a Holiday label if holidays are active + try { if (this._isHolidayActive && this._isHolidayActive()) { label = this._t('holidays.button') || 'Holidays'; } } catch {} + if (!label && this._config?.weekdays_enabled) { + // Localized full weekday, e.g., "Mandag" (or browsed day in weekdays view) + const viewDayKey = this._timelineDisplayDayKey(); + const dayName = this._weekdayFullName(viewDayKey) || this._todayFullName(); + // If viewing all days for a single room, append room name for clarity + try { + const perRoomAllDays = String(this._config?.weekdays_view||'all_rooms_one_day') === 'one_room_all_days'; + if (perRoomAllDays) { + const ents = (this._config?.entities||[]); + const roomEid = (this._config?.weekdays_selected_room && ents.includes(this._config.weekdays_selected_room)) + ? this._config.weekdays_selected_room + : (ents[0]||''); + const roomLabel = this._config?.labels?.[roomEid] || this._prettyName(roomEid); + label = `${roomLabel}`.trim(); + } else { + label = dayName; + } + } catch { label = dayName; } + } + if (label) { + dayFull.innerHTML = ''; + + const ents = (this._config?.entities || []); + const isWeekEnabled = !!this._config?.weekdays_enabled; + const viewMode = String(this._config?.weekdays_view || 'all_rooms_one_day'); + const mkArrow = (dir)=>{ + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'wd-arrow'; + btn.textContent = dir < 0 ? '◀' : '▶'; + btn.style.cssText = 'background:none; border:none; color:var(--primary-text-color); cursor:pointer; padding:0 4px; font-size:inherit;'; + return btn; + }; + + let leftNavBtn = null; + let rightNavBtn = null; + + // In "Alle rum • én dag" view: arrows browse days (Mon..Sun) + if (isWeekEnabled && viewMode === 'all_rooms_one_day') { + const prevBtn = mkArrow(-1); + prevBtn.title = 'Previous day'; + prevBtn.addEventListener('click', () => { + const names = this._weekdayNames(); + const cur = this._timelineDisplayDayKey(); + const idx = Math.max(0, names.indexOf(cur)); + const newIdx = (idx - 1 + names.length) % names.length; + this._setTimelineDisplayDayKey(names[newIdx]); + this._render(); + }); + leftNavBtn = prevBtn; + + const nextBtn = mkArrow(1); + nextBtn.title = 'Next day'; + nextBtn.addEventListener('click', () => { + const names = this._weekdayNames(); + const cur = this._timelineDisplayDayKey(); + const idx = Math.max(0, names.indexOf(cur)); + const newIdx = (idx + 1) % names.length; + this._setTimelineDisplayDayKey(names[newIdx]); + this._render(); + }); + rightNavBtn = nextBtn; + } + // In "Alle dage • ét rum" view: keep room navigation arrows (only if multiple rooms) + else if (isWeekEnabled && viewMode === 'one_room_all_days' && ents.length > 1) { + const roomEid = (this._config?.weekdays_selected_room && ents.includes(this._config.weekdays_selected_room)) + ? this._config.weekdays_selected_room + : (ents[0]||''); + const roomIdx = ents.indexOf(roomEid); + + const prevBtn = mkArrow(-1); + prevBtn.title = 'Previous room'; + prevBtn.addEventListener('click', () => { + const newIdx = roomIdx <= 0 ? ents.length - 1 : roomIdx - 1; + this._config.weekdays_selected_room = ents[newIdx]; + this._render(); + }); + leftNavBtn = prevBtn; + + const nextBtn = mkArrow(1); + nextBtn.title = 'Next room'; + nextBtn.addEventListener('click', () => { + const newIdx = roomIdx >= ents.length - 1 ? 0 : roomIdx + 1; + this._config.weekdays_selected_room = ents[newIdx]; + this._render(); + }); + rightNavBtn = nextBtn; + } + + const leftWrap = document.createElement('div'); + leftWrap.className = 'wd-left'; + const centerWrap = document.createElement('div'); + centerWrap.className = 'wd-center'; + const rightWrap = document.createElement('div'); + rightWrap.className = 'wd-right'; + + // Mobile-only: show view switch on the weekday row (left-aligned) + try { + const segMobile = makeWeekViewSeg('sp-week-view-mobile'); + if (segMobile) leftWrap.appendChild(segMobile); + } catch {} + + const labelSpan = document.createElement('span'); + labelSpan.textContent = label; + // Keep arrows tight to the label (centered as a group) + if (leftNavBtn) centerWrap.appendChild(leftNavBtn); + centerWrap.appendChild(labelSpan); + if (rightNavBtn) centerWrap.appendChild(rightNavBtn); + + dayFull.append(leftWrap, centerWrap, rightWrap); + + dayFull.style.display = ''; + } else { + dayFull.textContent = ''; + dayFull.style.display = 'none'; + } + } + } catch {} + + // Scale ticks + scaleEl.innerHTML = ''; + const compact = this._isCompactScale(); + for (let i = 0; i <= 24; i++) { + if (compact && i % 2 !== 0) continue; + const t = document.createElement('div'); + t.className = 'tick ' + (i % 2 === 0 ? 'full' : 'short'); + t.style.left = `${(i / 24) * 100}%`; + if (i % 2 === 0 || !compact) { + const l = document.createElement('label'); + if (this._config?.time_12h){ + const p = this._timeParts(i===24?0:i*60); + l.innerHTML = `${p.main}${p.mer||''}`; + } else { + l.textContent = (i === 24) ? this._label(0) : this._label(i*60); + } + t.append(l); + } + scaleEl.append(t); + } + + // Rows + rowsEl.innerHTML = ''; + // Right header tools: global pause toggle (absolute top-right) + try { + if (headerEl) { + const spacer = headerEl.querySelector('.spacer') || headerEl; + let wrap = headerEl.querySelector('.pause-wrap'); + if (!wrap) { wrap = document.createElement('div'); wrap.className = 'pause-wrap'; spacer.append(wrap); } + else if (wrap.parentNode !== spacer) { try { spacer.append(wrap); } catch {} } + wrap.innerHTML = ''; + + // Desktop: show view switch in top row (hidden on mobile) + try { + const segDesktop = makeWeekViewSeg('sp-week-view-desktop'); + if (segDesktop) wrap.append(segDesktop); + } catch {} + + // Settings button (top-right) + try { + if (!this._disablePopups) { + const sbtn = document.createElement('button'); + sbtn.className = 'settings-btn'; + sbtn.type = 'button'; + try { sbtn.dataset.role = 'settingsbtn'; } catch {} + const lab = this._t('editor.tabs.settings') || 'Settings'; + sbtn.setAttribute('title', lab); + sbtn.setAttribute('aria-label', lab); + sbtn.innerHTML = ''; + sbtn.addEventListener('click', ()=>{ try { this._openSettingsPopup(); } catch {} }); + wrap.append(sbtn); + } + } catch {} + + if (this._config?.show_pause_button ?? true) { + const btn = document.createElement('button'); + btn.className = 'pause-btn' + (this._isPaused() ? ' paused' : ''); + btn.setAttribute('title', this._isPaused() ? this._t('pause.active_indef') : this._t('pause.menu_title')); + btn.innerHTML = ''; + btn.addEventListener('click', ()=>{ + try { + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-pause'); + if (ov) ov.classList.add('open'); + this._renderPauseStatus(); + } catch {} + }); + // Build optional bubble to the left + const paused = this._isPaused(); + const until = Number(this._pauseUntilMs||0); + const showCountdown = paused && !this._pauseIndef && until > Date.now(); + if (paused) { + const eta = document.createElement('span'); eta.className = 'pause-eta'; + eta.textContent = showCountdown ? this._formatPauseCountdown() : (this._t('pause.off_indef')); + wrap.append(eta); + wrap.append(btn); + if (showCountdown) this._startCountdownTimer(); + else if (this._countdownTimer) { try { clearInterval(this._countdownTimer); } catch {} this._countdownTimer = null; } + } else { + wrap.append(btn); + if (this._countdownTimer) { try { clearInterval(this._countdownTimer); } catch {} this._countdownTimer = null; } + } + } + // Global Away bypass button (visible only when Away is active: nobody home) + try { + const advOn = !!(this._config?.away?.advanced_enabled); + const awayActive = !!this._isAwayActive(); + if (this._config?.away?.enabled && !advOn && awayActive) { + const ab = document.createElement('button'); + ab.className = 'profiles-btn away-bypass-btn'; + // Make it red like a warning action + try { + ab.style.borderColor = 'var(--error-color)'; + ab.style.background = 'var(--error-color)'; + ab.style.color = 'var(--text-primary-color, #fff)'; + } catch {} + ab.textContent = this._awayBypass ? (this._t('away.resume_away') || 'Resume Away temp') : (this._t('away.resume_schedule') || 'Resume schedule'); + ab.addEventListener('click', async ()=>{ + try { + this._awayBypass = !this._awayBypass; + try { await this._saveStore(); } catch {} + try { await this._applyCurrentSetpoints(true); } catch {} + try { if (this._config?.storage_enabled) this._nudgeBackgroundApplyNow(); } catch {} + this._scheduleNextApply(); + ab.textContent = this._awayBypass ? (this._t('away.resume_away') || 'Resume Away temp') : (this._t('away.resume_schedule') || 'Resume schedule'); + } catch {} + }); + const pauseBtn = wrap.querySelector('.pause-btn'); + if (pauseBtn && pauseBtn.parentNode === wrap) { + wrap.insertBefore(ab, pauseBtn); + } else { + wrap.append(ab); + } + } else { + // Remove if not applicable + const old = wrap.querySelector('.away-bypass-btn'); if (old) old.remove(); + } + } catch {} + // Presence live status for Away persons + try { + const persons = (this._presencePersons && typeof this._presencePersons === 'function') ? this._presencePersons().filter(Boolean) : []; + const awayOn = !!(this._config?.away?.enabled); + const advOn = !!(this._config?.away?.advanced_enabled); + const wantLive = (awayOn || advOn) && !!(this._config?.presence_live_header ?? true); + if (persons.length && wantLive) { + let live = wrap.querySelector('.presence-live'); + if (!live) { live = document.createElement('div'); live.className = 'presence-live'; + const pauseBtn = wrap.querySelector('.pause-btn'); + if (pauseBtn && pauseBtn.parentNode === wrap) wrap.insertBefore(live, pauseBtn); else wrap.append(live); + } + live.innerHTML = ''; + for (const p of persons) { + const st = this._hass?.states?.[p]; + const state = String(st?.state || 'unknown'); + const friendly = st?.attributes?.friendly_name || (String(p||'').split('.')[1] || p); + const chip = document.createElement('span'); + chip.className = 'presence-chip ' + (state.toLowerCase() === 'home' ? 'home' : 'away'); + chip.setAttribute('title', `${friendly}: ${state}`); + const dot = document.createElement('span'); dot.className = 'presence-dot'; + const name = document.createElement('span'); name.className = 'presence-name'; name.textContent = friendly; + chip.append(dot, name); + live.append(chip); + } + } else { + // Remove if disabled + const live = wrap.querySelector('.presence-live'); if (live) live.remove(); + } + } catch {} + // Collapsible header tools (Presence / Profiles / Weekdays) + try { + let htools = wrap.querySelector('.head-tools'); + if (!htools) { htools = document.createElement('div'); htools.className = 'head-tools toolbar' + (this._headToolsOpen ? ' open' : ''); wrap.append(htools); } + htools.innerHTML = ''; + const toggleBtn = document.createElement('button'); toggleBtn.className='tools-toggle'; toggleBtn.type='button'; toggleBtn.setAttribute('aria-label','Toggle header tools'); toggleBtn.textContent = this._headToolsOpen ? '✕' : '❮'; + const toolsWrap = document.createElement('div'); toolsWrap.className = 'tools-wrap'; + const hasPending = !!this._pendingSaveAfterEdit; + const recentlySaved = (this._syncJustUntil || 0) > Date.now(); + const showButton = hasPending || this._saving || recentlySaved; + // Explicit Save button that writes all pending changes to storage; always visible (outside fold), only when pending changes exist + let saveBtn = null; + try { + saveBtn = document.createElement('button'); + saveBtn.className = 'profiles-btn success'; + saveBtn.type = 'button'; + saveBtn.style.marginRight = '6px'; + const idleLabel = this._t('ui.save') || 'Save'; + const savingLabel = this._t('sync.running') || 'Saving…'; + const doneLabel = this._t('sync.completed') || 'Saved'; + const setState = (state)=>{ + if (state==='saving') { saveBtn.disabled = true; saveBtn.textContent = savingLabel; } + else if (state==='done') { saveBtn.disabled = false; saveBtn.textContent = doneLabel; } + else { saveBtn.disabled = false; saveBtn.textContent = idleLabel; } + }; + setState(this._saving ? 'saving' : (recentlySaved ? 'done' : 'idle')); + if (!showButton) saveBtn.style.display = 'none'; + saveBtn.addEventListener('click', async ()=>{ + try { + setState('saving'); + const savePromise = this._saveStore(true); + const delayPromise = new Promise(r => setTimeout(r, 800)); + await Promise.all([savePromise, delayPromise]); + setState('done'); + setTimeout(()=>{ setState('idle'); this._render(); }, 1000); + } catch { + setState('idle'); + } + }); + } catch {} + // Presence button + if (this._config?.away?.advanced_enabled) { + const prb = document.createElement('button'); prb.className='profiles-btn primary'; prb.type='button'; prb.textContent=this._t('presence.button'); prb.addEventListener('click', ()=>{ const first=(this._config?.entities||[])[0]||null; if (first) this._openPresenceEditor(first); }); toolsWrap.append(prb); + } + // Profiles button + const wantProfilesBtn = !!(this._config?.profiles_enabled || this._getLocalProfilesEnabled()); + if ((this._config?.entities||[]).length && wantProfilesBtn) { + const pbtn = document.createElement('button'); pbtn.className='profiles-btn primary'; pbtn.type='button'; pbtn.textContent=this._t('profiles.button'); pbtn.addEventListener('click', ()=>{ const first=(this._config?.entities||[])[0]||null; if (first) this._openProfilesEditor(first); }); toolsWrap.append(pbtn); + } + // Weekdays button (header) + if (this._config?.weekdays_enabled && (this._config?.entities||[]).length) { + const wbtn = document.createElement('button'); wbtn.className='profiles-btn primary'; wbtn.type='button'; wbtn.textContent = this._t('week.button'); wbtn.addEventListener('click', ()=>{ const first=(this._config?.entities||[])[0]; if (first) this._openWeeklyEditor(first); }); toolsWrap.append(wbtn); + // When viewing all days for one room, show a small room selector + try { + const perDayView = String(this._config?.weekdays_view||'all_rooms_one_day')==='one_room_all_days'; + if (perDayView) { + const sel = document.createElement('select'); + sel.className = 'weekroom-select'; + sel.style.height = '32px'; sel.style.border = '1px solid var(--divider-color)'; sel.style.borderRadius = '8px'; sel.style.background = 'var(--card-background-color)'; sel.style.color = 'var(--primary-text-color)'; sel.style.marginLeft = '6px'; + const ents = (this._config?.entities || []); + for (const eid of ents){ const opt=document.createElement('option'); const label = this._config?.labels?.[eid] || (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1]||eid)); opt.value=eid; opt.textContent=label; sel.append(opt); } + const want = this._config?.weekdays_selected_room || ents[0] || ''; + sel.value = want; + sel.addEventListener('change', (e)=>{ const v=String(e.target.value||''); this._config.weekdays_selected_room = v; this._render(); }); + toolsWrap.append(sel); + } + } catch {} + } + // Toggle behavior + toggleBtn.addEventListener('click', ()=>{ + this._headToolsOpen = !this._headToolsOpen; if (this._headToolsOpen) { htools.classList.add('open'); toggleBtn.textContent='✕'; requestAnimationFrame(()=>{ try { toolsWrap.style.maxWidth=(toolsWrap.scrollWidth+2)+'px'; } catch {} }); } else { try { toolsWrap.style.maxWidth=(toolsWrap.scrollWidth+2)+'px'; } catch {}; htools.classList.remove('open'); toggleBtn.textContent='❮'; requestAnimationFrame(()=>{ toolsWrap.style.maxWidth='0px'; }); } + }); + // Place Save (if any) before collapsible tools, then toggle on the far right + if (saveBtn) htools.append(saveBtn); + htools.append(toolsWrap, toggleBtn); + // Hide toggle button if toolsWrap is empty + if (toolsWrap.children.length === 0) { + toggleBtn.style.display = 'none'; + } + // Ensure initial width reflects current open state + try { + if (this._headToolsOpen) { + requestAnimationFrame(()=>{ try { toolsWrap.style.maxWidth = (toolsWrap.scrollWidth + 2) + 'px'; } catch {} }); + } else { + toolsWrap.style.maxWidth = '0px'; + } + } catch {} + } catch {} + // Ensure pause button is the rightmost item in the header + try { + const pauseBtn = wrap.querySelector('.pause-btn'); + if (pauseBtn) wrap.appendChild(pauseBtn); + const settingsBtn = wrap.querySelector('.settings-btn'); + if (settingsBtn) wrap.appendChild(settingsBtn); + } catch {} + } + } catch {} + const nowMin = this._getNowMin(); + const nowPct = (nowMin / 1440) * 100; + const showTop = this._config.show_top_now; + const extendPx = this._config.now_extend_px; + + // Optional: render all selected weekdays for one room + let __weeklyBuilt = false; + try { + const perRoomAllDays = !!(this._config?.weekdays_enabled) && String(this._config?.weekdays_view||'all_rooms_one_day')==='one_room_all_days'; + if (perRoomAllDays) { + const ents = (this._config?.entities||[]); + const roomEid = (this._config?.weekdays_selected_room && ents.includes(this._config.weekdays_selected_room)) ? this._config.weekdays_selected_room : (ents[0]||null); + if (roomEid) { + const row = this._schedules[roomEid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureWeeklyStruct(row); + const mode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + const groupKeys = mode==='weekday_weekend' ? ['weekdays','weekend'] : (mode==='weekday_sat_sun' ? ['weekdays','sat','sun'] : ['mon','tue','wed','thu','fri','sat','sun']); + const labelFor = (k)=>{ + const map = { weekdays:'week.group.weekdays', weekend:'week.group.weekend', mon:'week.days_full.mon', tue:'week.days_full.tue', wed:'week.days_full.wed', thu:'week.days_full.thu', fri:'week.days_full.fri', sat:'week.days_full.sat', sun:'week.days_full.sun' }; + return this._t(map[k]) || k; + }; + const effKey = (k)=> (k==='weekdays') ? 'mon' : (k==='weekend' ? 'sat' : k); + const getBlocks = (dk)=>{ + try { const src = row.weekly_modes?.[mode]?.days || {}; const arr = src?.[dk]; if (Array.isArray(arr)) return arr; } catch {} + try { const arr2 = row.weekly?.days?.[dk]; if (Array.isArray(arr2)) return arr2; } catch {} + return []; + }; + for (const gk of groupKeys){ + const rowEl = document.createElement('div'); rowEl.className='row'; + const head = document.createElement('div'); head.className='row-head'; + const meta = document.createElement('div'); meta.className='meta'; + const name = document.createElement('span'); name.textContent = labelFor(gk); meta.append(name); head.append(meta); + + // Add toolbar with Add and Copy buttons for weekday view + const toolbar = document.createElement('div'); + toolbar.className = 'toolbar' + (this._toolsOpen[roomEid + ':' + effKey(gk)] ? ' open' : ''); + toolbar.style.minHeight = '32px'; + const toolsWrap = document.createElement('div'); + toolsWrap.className = 'tools-wrap'; + + // Toggle button for collapsible toolbar + const toggleBtn = document.createElement('button'); + toggleBtn.className = 'tools-toggle'; + toggleBtn.type = 'button'; + toggleBtn.setAttribute('aria-label', 'Toggle actions'); + toggleBtn.textContent = this._toolsOpen[roomEid + ':' + effKey(gk)] ? '✕' : '❮'; + toggleBtn.addEventListener('click', () => { + const toolKey = roomEid + ':' + effKey(gk); + const willOpen = !this._toolsOpen[toolKey]; + this._toolsOpen[toolKey] = willOpen; + if (willOpen) { + try { toolsWrap.style.maxWidth = '0px'; } catch {} + toolbar.classList.add('open'); + toggleBtn.textContent = '✕'; + requestAnimationFrame(() => { + try { const w = toolsWrap.scrollWidth || 0; toolsWrap.style.maxWidth = (w + 2) + 'px'; } catch {} + }); + } else { + try { const w = toolsWrap.scrollWidth || 0; toolsWrap.style.maxWidth = (w + 2) + 'px'; } catch {} + toolbar.classList.remove('open'); + toggleBtn.textContent = '❮'; + requestAnimationFrame(() => { try { toolsWrap.style.maxWidth = '0px'; } catch {} }); + } + }); + + // Add block button + const addBtn = document.createElement('button'); + addBtn.className = 'btn primary'; + addBtn.textContent = '+ ' + this._t('ui.add_block'); + addBtn.addEventListener('click', () => { + this._weeklyEntity = roomEid; + // Keep group/day key so editor respects current weekday mode. + // E.g. weekday_weekend: editing on Sunday should edit the "weekend" group. + this._ensureWeeklyStruct(row); + const mode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + this._weeklyDayKey = gk; + // Build draft from active mode bucket (weekly_modes is the primary source). + try { + const src = row.weekly_modes?.[mode]?.days || row.weekly?.days || {}; + const names = this._weekdayNames(); + const draftDays = JSON.parse(JSON.stringify(src)); + for (const k of names){ if (!Array.isArray(draftDays[k])) draftDays[k] = []; } + this._weeklyDraft = { mode, days: draftDays }; + // Include modes so edit/delete can fully reconcile sources. + try { this._weeklyDraft._weeklyModes = JSON.parse(JSON.stringify(row.weekly_modes || {})); } catch {} + } catch { + this._weeklyDraft = JSON.parse(JSON.stringify(row.weekly)); + try { this._weeklyDraft.mode = this._weeklyDraft.mode || mode; } catch {} + try { this._weeklyDraft._weeklyModes = JSON.parse(JSON.stringify(row.weekly_modes || {})); } catch {} + } + this._autoSaveWeeklyFromTimeline = true; + this._openWeeklyBlockEditor(null, 0); + }); + toolsWrap.append(addBtn); + + // Copy/Paste button + const copyBtn = document.createElement('button'); + copyBtn.className = 'btn ghost'; + const updateCopyBtn = ()=>{ + if (!this._copyFrom) { copyBtn.textContent = this._t('ui.copy_plan'); copyBtn.classList.remove('success', 'danger'); copyBtn.classList.add('ghost'); } + else if (this._copyFrom === roomEid + ':' + effKey(gk)) { copyBtn.textContent = this._t('ui.cancel'); copyBtn.classList.remove('success', 'ghost'); copyBtn.classList.add('danger'); } + else { copyBtn.textContent = this._t('ui.paste_plan'); copyBtn.classList.remove('ghost', 'danger'); copyBtn.classList.add('success'); } + }; + updateCopyBtn(); + copyBtn.addEventListener('click', async () => { + const dayKey = effKey(gk); + const eid_day = roomEid + ':' + dayKey; + + if (this._copyFrom && this._copyFrom === eid_day) { + this._copyFrom = null; this._copyPayload = null; this._render(); return; + } + + if (this._copyFrom && this._copyPayload) { + try { + const src = this._copyPayload; + this._ensureWeeklyStruct(row); + // Generate new IDs for copied blocks and paste them + const pastedBlocks = src.map(b=>({ + ...b, + id: Math.random().toString(36).slice(2,9) + })); + + // Paste to the same location where blocks come from + // Check if blocks come from weekly_modes (first priority in getBlocks) + const srcModes = row.weekly_modes?.[mode]?.days || {}; + if (srcModes?.[dayKey]) { + // Blocks are in weekly_modes, paste there + if (!row.weekly_modes[mode]) row.weekly_modes[mode] = { days: {} }; + row.weekly_modes[mode].days[dayKey] = pastedBlocks; + } else { + // Blocks are in weekly or new, paste to weekly + row.weekly.days[dayKey] = pastedBlocks; + } + + await this._saveStore(); + this._render(); + } catch(e){} + return; + } + + try { + const dayBlocks = getBlocks(dayKey); + const payload = dayBlocks.map(b=>({ startMin:b.startMin, endMin:b.endMin, temp:b.temp })); + this._copyFrom = eid_day; + this._copyPayload = payload; + this._render(); + } catch(e){} + }); + toolsWrap.append(copyBtn); + + toolbar.append(toggleBtn); + toolbar.append(toolsWrap); + head.append(toolbar); + + rowEl.append(head); + const track = document.createElement('div'); track.className='track'; track.style.setProperty('--row-height', `${this._config.row_height}px`); + const dkey = effKey(gk); + const blocks = getBlocks(dkey); + for (const b of blocks){ + const pctStart = (b.startMin / 1440) * 100; + const pctWidth = ((b.endMin - b.startMin) / 1440) * 100; + const bl = document.createElement('div'); + bl.className='block'; + bl.style.left=`${pctStart}%`; + bl.style.width=`${pctWidth}%`; + try { + const clr = this._colorFor(roomEid, b.temp); + if (clr){ + bl.style.background = clr; + bl.style.borderColor = clr; + const txt=this._contrastTextColor(clr); + if (txt) bl.style.color = txt; + } + } catch {} + UiHelper.genBlockPill( + this, + bl, + `${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}` + ); + + // Add double-click to edit block + bl.addEventListener('dblclick', (e) => { + try { e.preventDefault(); e.stopPropagation(); } catch {} + this._weeklyEntity = roomEid; + // Keep group key (gk) so the editor knows whether it's weekdays/weekend vs specific day. + this._ensureWeeklyStruct(row); + const mode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + this._weeklyDayKey = gk; + try { + const src = row.weekly_modes?.[mode]?.days || row.weekly?.days || {}; + const names = this._weekdayNames(); + const draftDays = JSON.parse(JSON.stringify(src)); + for (const k of names){ if (!Array.isArray(draftDays[k])) draftDays[k] = []; } + this._weeklyDraft = { mode, days: draftDays }; + try { this._weeklyDraft._weeklyModes = JSON.parse(JSON.stringify(row.weekly_modes || {})); } catch {} + } catch { + this._weeklyDraft = JSON.parse(JSON.stringify(row.weekly)); + try { this._weeklyDraft.mode = this._weeklyDraft.mode || mode; } catch {} + try { this._weeklyDraft._weeklyModes = JSON.parse(JSON.stringify(row.weekly_modes || {})); } catch {} + } + this._autoSaveWeeklyFromTimeline = true; + this._openWeeklyBlockEditor(b.id); + }); + + // Touch/iPad: detect double-tap + bl.addEventListener('touchend', (e) => { + const now = Date.now(); + const last = this._lastWeeklyTap || { t: 0, target: null }; + if (last.target === bl && (now - last.t) < 350) { + try { e.preventDefault(); e.stopPropagation(); } catch {} + this._lastWeeklyTap = { t: 0, target: null }; + this._weeklyEntity = roomEid; + this._ensureWeeklyStruct(row); + const mode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + this._weeklyDayKey = gk; + try { + const src = row.weekly_modes?.[mode]?.days || row.weekly?.days || {}; + const names = this._weekdayNames(); + const draftDays = JSON.parse(JSON.stringify(src)); + for (const k of names){ if (!Array.isArray(draftDays[k])) draftDays[k] = []; } + this._weeklyDraft = { mode, days: draftDays }; + try { this._weeklyDraft._weeklyModes = JSON.parse(JSON.stringify(row.weekly_modes || {})); } catch {} + } catch { + this._weeklyDraft = JSON.parse(JSON.stringify(row.weekly)); + try { this._weeklyDraft.mode = this._weeklyDraft.mode || mode; } catch {} + try { this._weeklyDraft._weeklyModes = JSON.parse(JSON.stringify(row.weekly_modes || {})); } catch {} + } + this._autoSaveWeeklyFromTimeline = true; + this._openWeeklyBlockEditor(b.id); + } else { + this._lastWeeklyTap = { t: now, target: bl }; + setTimeout(() => { if (this._lastWeeklyTap?.target === bl) this._lastWeeklyTap = { t: 0, target: null }; }, 350); + } + }); + + track.append(bl); + } + rowEl.append(track); rowsEl.append(rowEl); + } + __weeklyBuilt = true; + } + } + } catch {} + + // Default: render one row per room + if (!__weeklyBuilt) { for (const eid of this._config.entities) { + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + const rowEl = document.createElement('div'); + rowEl.className = 'row'; + const head = document.createElement('div'); + head.className = 'row-head'; + const meta = document.createElement('div'); + meta.className = 'meta'; + const name = document.createElement('span'); + // Brug brugerdefineret label hvis sat i config.labels, ellers fallback til friendly_name/_prettyName + const customLabel = (this._config.labels && this._config.labels[eid]) ? this._config.labels[eid] : null; + name.textContent = customLabel || this._prettyName(eid); + meta.append(name); + // Per-room Default °C input (optional) + try { + if (this._config?.per_room_defaults) { + const inp = document.createElement('input'); + inp.type = 'number'; + inp.step = '0.5'; + try { inp.min = String(this._minDisplay()); inp.max = String(this._maxDisplay()); } catch {} + const base = Number(row.defaultTemp ?? this._config.default_temp ?? 20); + inp.value = String(this._toDisplayTemp(base)); + inp.title = this._t('editor.default_c').replace('°C', this._unitSymbol()); + inp.addEventListener('change', async () => { + const before = this._config.apply_on_default_change ? this._desiredNowSnapshot() : null; + let raw = String(inp.value||'').replace(',','.'); + let vDisp = Number(raw); + let vC = Number.isFinite(vDisp) ? this._fromDisplayTemp(vDisp) : (row.defaultTemp ?? this._config.default_temp ?? 20); + const mx = Number(this._config?.max_temp ?? 25); + const mn = Number(this._config?.min_temp ?? 5); + vC = Math.max(mn, Math.min(mx, vC)); + row.defaultTemp = vC; + try { inp.value = String(this._toDisplayTemp(vC)); } catch {} + this._debouncedSaveStore(); + if (before) { try { await this._applyIfDesiredChanged(before); } catch {} } + this._scheduleNextApply(); + }); + meta.append(inp); + } + } catch {} + // inline default temperature input removed from timeline to keep UI clean + // Current room temperature bubble: show climate.current_temperature + // If room is merged with additional thermostats, show the average temperature + try { + if (this._config?.show_room_temp ?? true) { + const curC = this._roomCurrentTemperatureC ? this._roomCurrentTemperatureC(eid) : NaN; + if (Number.isFinite(curC)) { + const vDisp = this._toDisplayTemp(curC); + const sb = document.createElement('span'); + sb.className = 'sensor-bubble'; + sb.textContent = `${this._t('ui.current_temp')} ${String(Math.round(Number(vDisp) * 10) / 10)} ${this._unitSymbol()}`; + meta.append(sb); + } + } + } catch {} + // (Global Away bypass button moved to header; no per-room button) + // (manual indicator removed) + head.append(meta); + // Collapsible toolbar: tools slide out from a small arrow button + const toolbar = document.createElement('div'); + toolbar.className = 'toolbar' + (this._toolsOpen[eid] ? ' open' : ''); + // Keep header height stable regardless of open/closed + toolbar.style.minHeight = '32px'; + const toolsWrap = document.createElement('div'); + toolsWrap.className = 'tools-wrap'; + const toggleBtn = document.createElement('button'); + toggleBtn.className = 'tools-toggle'; + toggleBtn.type = 'button'; + toggleBtn.setAttribute('aria-label', 'Toggle actions'); + toggleBtn.textContent = this._toolsOpen[eid] ? '✕' : '❮'; + toggleBtn.addEventListener('click', () => { + const bar = toggleBtn.parentElement; + const wrap = toolsWrap; + const willOpen = !this._toolsOpen[eid]; + this._toolsOpen[eid] = willOpen; + if (willOpen) { + // prepare then expand + try { wrap.style.maxWidth = '0px'; } catch {} + bar && bar.classList.add('open'); + toggleBtn.textContent = '✕'; + requestAnimationFrame(() => { + try { const w = wrap.scrollWidth || 0; wrap.style.maxWidth = (w + 2) + 'px'; } catch {} + }); + } else { + // freeze width then collapse + try { const w = wrap.scrollWidth || 0; wrap.style.maxWidth = (w + 2) + 'px'; } catch {} + bar && bar.classList.remove('open'); + toggleBtn.textContent = '❮'; + requestAnimationFrame(() => { try { wrap.style.maxWidth = '0px'; } catch {} }); + } + }); + // (resume schedule button removed) + const addBtn = document.createElement('button'); + addBtn.className = 'btn primary'; + addBtn.textContent = '+ ' + this._t('ui.add_block'); + try { addBtn.dataset.role = 'addbtn'; if (this._disablePopups) addBtn.style.display = 'none'; } catch {} + addBtn.addEventListener('click', () => { + // If weekdays are enabled, open the block editor directly for today's weekly plan + try { + const row = this._schedules[eid]; + if (this._config?.weekdays_enabled && row) { + // Ensure weekly structures exist + this._ensureWeeklyStruct(row); + const mode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + const today = this._timelineDisplayDayKey(); + const isWeekday = ['mon','tue','wed','thu','fri'].includes(today); + let sel = today; + if (mode === 'weekday_weekend') sel = isWeekday ? 'weekdays' : 'weekend'; + else if (mode === 'weekday_sat_sun') sel = isWeekday ? 'weekdays' : today; // sat/sun + // Prepare an in-memory weekly draft without opening the weekdays popup + try { + const src = row.weekly_modes?.[mode]?.days || {}; + const names = this._weekdayNames(); + const draftDays = JSON.parse(JSON.stringify(src)); + for (const k of names){ if (!Array.isArray(draftDays[k])) draftDays[k] = []; } + this._weeklyDraft = { mode, days: draftDays }; + } catch { + this._weeklyDraft = JSON.parse(JSON.stringify(row.weekly)); + } + this._weeklyEntity = eid; + this._weeklyDayKey = sel; + this._autoSaveWeeklyFromTimeline = true; + const now = this._getNowMin(); + this._openWeeklyBlockEditor(null, now); + return; + } + } catch {} + // Fallback: non-weekly add + this._openNewEditor(eid); + }); + toolsWrap.append(addBtn); + // Copy/Paste schedule button + const copyBtn = document.createElement('button'); + copyBtn.className = 'btn ghost'; + const applyCopyStyle = (mode)=>{ + // mode: 'copy' | 'paste' | 'cancel' + copyBtn.className = 'btn'; + if (mode === 'copy') copyBtn.classList.add('ghost'); + if (mode === 'paste') copyBtn.classList.add('success'); + if (mode === 'cancel') copyBtn.classList.add('danger'); + }; + const updateCopyBtn = ()=>{ + if (!this._copyFrom) { copyBtn.textContent = this._t('ui.copy_plan'); applyCopyStyle('copy'); } + else if (this._copyFrom === eid) { copyBtn.textContent = this._t('ui.cancel'); applyCopyStyle('cancel'); } + else { copyBtn.textContent = this._t('ui.paste_plan'); applyCopyStyle('paste'); } + }; + updateCopyBtn(); + copyBtn.addEventListener('click', async () => { + // Cancel on source row + if (this._copyFrom && this._copyFrom === eid) { + this._copyFrom = null; this._copyPayload = null; this._render(); return; + } + // Paste into this row when copy active + if (this._copyFrom && this._copyPayload) { + const before = this._desiredNowSnapshot(); + const src = this._copyPayload; + try { + const tgt = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + // Deep copy + const clone = JSON.parse(JSON.stringify(src)); + tgt.defaultTemp = Number.isFinite(clone.defaultTemp) ? clone.defaultTemp : (tgt.defaultTemp || this._config.default_temp); + tgt.blocks = Array.isArray(clone.blocks) ? clone.blocks.map(b=>({ ...b, id: Math.random().toString(36).slice(2,9) })) : []; + // Weekday structures (keep modes if present) + if (clone.weekly) tgt.weekly = JSON.parse(JSON.stringify(clone.weekly)); + if (clone.weekly_modes) tgt.weekly_modes = JSON.parse(JSON.stringify(clone.weekly_modes)); + this._schedules[eid] = tgt; + await this._saveStore(); + this._render(); + if (this._config.apply_on_edit) await this._applyIfDesiredChanged(before); + this._scheduleNextApply(); + } catch(e){} + return; + } + // Start copy from this row + try { + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + const payload = { defaultTemp: row.defaultTemp, blocks: (row.blocks||[]).map(b=>({ startMin:b.startMin, endMin:b.endMin, temp:b.temp })), weekly: row.weekly ? JSON.parse(JSON.stringify(row.weekly)) : undefined, weekly_modes: row.weekly_modes ? JSON.parse(JSON.stringify(row.weekly_modes)) : undefined }; + this._copyFrom = eid; this._copyPayload = payload; + this._render(); + } catch(e){} + }); + toolsWrap.append(copyBtn); + // Weekdays button moved to header tools; hidden on rows to avoid duplication + // Row-level Manual schedule button removed – profiles button now lives in the header only + try { /* intentionally no-op */ } catch {} + // (Profiles button moved to header) + toolbar.append(toolsWrap, toggleBtn); + // Hide toggle button if toolsWrap is empty + if (toolsWrap.children.length === 0) { + toggleBtn.style.display = 'none'; + } + // Ensure correct initial width for open state without rerendering + try { + if (this._toolsOpen[eid]) { + requestAnimationFrame(() => { try { toolsWrap.style.maxWidth = (toolsWrap.scrollWidth + 2) + 'px'; } catch {} }); + } else { + toolsWrap.style.maxWidth = '0px'; + } + } catch {} + head.append(toolbar); + rowEl.append(head); + const track = document.createElement('div'); + track.className = 'track'; + track.style.setProperty('--row-height', `${this._config.row_height}px`); + // Use browsed day in weekdays view (all rooms • one day) + const displayDayKey = this._timelineDisplayDayKey(); + let dayKey = displayDayKey; + try { + if (this._config?.weekdays_enabled && row?.weekly) { + const mode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + dayKey = this._weekdaysStorageDayKeyForMode(displayDayKey, mode); + } + } catch { dayKey = displayDayKey; } + this._ensureHolidayStruct(row); + this._ensurePresenceStruct(row); + let showBlocks = (row.blocks || []); + try { + if (this._isHolidayActive() && row?.holiday?.blocks?.length) { + showBlocks = row.holiday.blocks || []; + } else { + // Advanced Presence overrides visual track as well (when a combo is active) + try { + const key = this._activePresenceComboKey && this._activePresenceComboKey(); + if (key) { + const roomBlocks = (row.presence && Array.isArray(row.presence[key]?.blocks)) ? (row.presence[key].blocks || []) : []; + if (roomBlocks.length) { + showBlocks = roomBlocks; + } else { + // Presence combo active but no schedule for this room -> show a virtual all-day Away block + const awayC = Number(this._config?.away?.target_c ?? 17); + const fallback = Number.isFinite(awayC) ? awayC : this._rowDefaultTemp(row); + showBlocks = [{ id: '__presence_away__', startMin: 0, endMin: 1440, temp: fallback }]; + } + } else if (this._config?.profiles_enabled) { + this._ensureProfilesStruct(row); + const ap = row.activeProfile; + if (ap && row.profiles && Array.isArray(row.profiles[ap]?.blocks) && (row.profiles[ap].blocks||[]).length) { + showBlocks = row.profiles[ap].blocks || []; + } else if (this._config?.weekdays_enabled) { + showBlocks = this._getBlocksForDay(row, dayKey) || []; + } + } else if (this._config?.weekdays_enabled) { + showBlocks = this._getBlocksForDay(row, dayKey) || []; + } + } catch { /* fallback below */ } + } + } catch {} + for (const b of showBlocks) { + const pctStart = (b.startMin / 1440) * 100; + const pctWidth = ((b.endMin - b.startMin) / 1440) * 100; + const bl = document.createElement('div'); + bl.className = 'block'; + bl.style.left = `${pctStart}%`; + bl.style.width = `${pctWidth}%`; + try { + const clr = this._colorFor(eid, b.temp); + if (clr) { + bl.style.background = clr; + bl.style.borderColor = clr; + const txt = this._contrastTextColor(clr); + if (txt) bl.style.color = txt; + } + } catch {} + if (this._active?.entity === eid && this._active?.id === b.id) bl.classList.add('active'); + // Time will be shown in a hover tooltip (like the weekdays popup). Do not render time pill here. + UiHelper.genBlockPill( + this, + bl, + `${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}` + ); + track.append(bl); + + // Ensure a tooltip element exists for main timeline (reuse weekly style) + try { + let tooltip = this.shadowRoot.querySelector('.wk-tooltip.main'); + const rowsEl = this.shadowRoot.querySelector('.rows'); + const modalHost = rowsEl || this.shadowRoot.querySelector('ha-card'); + if (!tooltip) { + tooltip = document.createElement('div'); + tooltip.className = 'wk-tooltip main'; + tooltip.style.display = 'none'; + // append to rows so positioning is relative to timeline + if (modalHost) modalHost.append(tooltip); + } + const showTip = () => { + const txt = `${this._label(b.startMin)} - ${this._label(b.endMin)} • ${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}`; + if (!tooltip) return; + // Cancel any pending hide + try { if (tooltip._hideTimer) { clearTimeout(tooltip._hideTimer); tooltip._hideTimer = null; } } catch {} + // Do not show while editor popup is open + try { + const ov = this.shadowRoot.querySelector('.overlay'); + if (this._editing || (ov && ov.classList.contains('open'))) return; + } catch {} + tooltip.textContent = txt; + const box = bl.getBoundingClientRect(); + const cont = (modalHost && modalHost.getBoundingClientRect) ? modalHost.getBoundingClientRect() : { left: 0, top: 0 }; + const left = box.left + box.width / 2 - (cont.left || 0); + const top = (box.top - (cont.top || 0)) - 8; + tooltip.style.left = left + 'px'; + tooltip.style.top = top + 'px'; + tooltip.style.transform = 'translate(-50%,-100%)'; + tooltip.style.display = ''; + }; + const hideTip = () => { + if (!tooltip) return; + // On coarse pointers (tablet) keep tooltip a bit longer + const delay = (window.matchMedia && window.matchMedia('(pointer:coarse)').matches) ? 3000 : 120; + try { if (tooltip._hideTimer) clearTimeout(tooltip._hideTimer); } catch {} + tooltip._hideTimer = setTimeout(() => { try { tooltip.style.display = 'none'; tooltip._hideTimer = null; } catch {} }, delay); + }; + bl.addEventListener('mouseenter', showTip); + bl.addEventListener('mouseleave', hideTip); + } catch (e) { /* ignore tooltip errors */ } + + // Open editor only on double interaction: + // - Desktop: double-click + // - Touch/iPad: double-tap within 350ms on the same block + const openEditor = () => { + // If a manual profile is active, edit that profile directly instead of weekdays + try { + if (this._config?.profiles_enabled) { + this._ensureProfilesStruct(row); + const ap = row?.activeProfile; + if (ap && row?.profiles && Array.isArray(row.profiles[ap]?.blocks) && (row.profiles[ap].blocks||[]).length) { + // Configure profile context and open profile block editor + this._profilesEntity = eid; + this._profilesSelected = ap; + this._profilesRoom = eid; + this._autoSaveProfileFromTimeline = true; + this._openProfileBlockEditor(b.id); + return; + } + } + } catch {} + + // Else if weekdays are enabled, edit inline (no weekday popup); auto‑save back to weekdays + try { + const rowHasWeekly = !!(this._config?.weekdays_enabled && row?.weekly); + if (rowHasWeekly) { + // Prepare an in‑memory weekly draft for the current mode + this._ensureWeeklyStruct(row); + const mode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + const today = displayDayKey; // computed earlier + const isWeekday = ['mon','tue','wed','thu','fri'].includes(today); + let sel = today; + if (mode === 'weekday_weekend') sel = isWeekday ? 'weekdays' : 'weekend'; + else if (mode === 'weekday_sat_sun') sel = isWeekday ? 'weekdays' : today; // sat/sun + // Build draft from the selected mode bucket + try { + const src = row.weekly_modes?.[mode]?.days || {}; + const names = this._weekdayNames(); + const draftDays = JSON.parse(JSON.stringify(src)); + for (const k of names){ if (!Array.isArray(draftDays[k])) draftDays[k] = []; } + this._weeklyDraft = { mode, days: draftDays }; + } catch { this._weeklyDraft = JSON.parse(JSON.stringify(row.weekly)); } + this._weeklyEntity = eid; + this._weeklyDayKey = sel; + this._autoSaveWeeklyFromTimeline = true; + // Locate corresponding block in effective day using midpoint + const eff = this._effectiveDayKey(sel); + const mid = Math.floor((b.startMin + b.endMin) / 2); + const effArr = (this._weeklyDraft?.days?.[eff] || []); + let targetId = b.id; + if (eff !== today) { + const match = effArr.find(x => mid >= x.startMin && mid < x.endMin); + targetId = match ? match.id : null; + } + if (targetId) this._openWeeklyBlockEditor(targetId); + else this._openWeeklyBlockEditor(null, mid); + return; + } + } catch {} + // Fallback to normal (non-weekly/non-profile) editor + this._openEditor(eid, b.id); + }; + + // Desktop/mouse + bl.addEventListener('dblclick', (e) => { + try { e.preventDefault(); e.stopPropagation(); } catch {} + openEditor(); + }); + + // Touch/iPad: detect double-tap + bl.addEventListener('touchend', (e) => { + const now = Date.now(); + const last = this._lastTap || { t: 0, target: null }; + if (last.target === bl && (now - last.t) < 350) { + try { e.preventDefault(); e.stopPropagation(); } catch {} + this._lastTap = { t: 0, target: null }; + openEditor(); + } else { + this._lastTap = { t: now, target: bl }; + // Clear after timeout to avoid stale reference + setTimeout(() => { + if (this._lastTap && this._lastTap.target === bl && (Date.now() - this._lastTap.t) > 350) { + this._lastTap = { t: 0, target: null }; + } + }, 360); + } + }, { passive: false }); + // Note: single click/tap no longer opens the editor + } + // Single-line NOW: rely on .now with extend; remove extra top pin + /* now-top removed */ + rowEl.append(track); + rowsEl.append(rowEl); + } + } + // Single global NOW line across all rows + { + const nowEl = document.createElement('div'); + nowEl.className = 'now'; + // left will be set in pixels to account for row padding + rowsEl.append(nowEl); + + const updateNowGeom = () => { + try { + const tracks = Array.from(this.shadowRoot.querySelectorAll('.track')); + const rowsBox = rowsEl.getBoundingClientRect(); + // Vertical placement/height + if (tracks.length) { + const overhang = 12; + const firstBox = tracks[0].getBoundingClientRect(); + const lastBox = tracks[tracks.length - 1].getBoundingClientRect(); + const top = Math.round(firstBox.top - rowsBox.top - overhang); + const bottom = Math.round(lastBox.bottom - rowsBox.top); + nowEl.style.top = top + 'px'; + nowEl.style.height = (bottom - top) + 'px'; + } + // Horizontal left: respect rows padding so it matches .track coordinate system + const cs = getComputedStyle(rowsEl); + const pl = parseFloat(cs.paddingLeft || '0') || 0; + const pr = parseFloat(cs.paddingRight || '0') || 0; + const contentW = rowsEl.clientWidth - pl - pr; + const leftPx = pl + (nowMin / 1440) * contentW; + nowEl.style.left = leftPx + 'px'; + // Hide NOW line in "All days • one room" view (multi-day timeline) + try { + const perRoomAllDays = !!(this._config?.weekdays_enabled) && String(this._config?.weekdays_view||'all_rooms_one_day') === 'one_room_all_days'; + if (perRoomAllDays) { nowEl.style.display = 'none'; return; } + nowEl.style.display = ''; + } catch {} + // Set visibility of this NowGeo elt depending on the selected day key + let selectedDayKey = ''; + try { + // In normal timeline, the current display day is derived from the timeline browsing state. + // In weekdays view, _weekdaysViewDayKey may be used. + selectedDayKey = String((this._timelineDisplayDayKey && this._timelineDisplayDayKey()) || this._weekdaysViewDayKey || '').toLowerCase(); + } catch { selectedDayKey = String(this._weekdaysViewDayKey || '').toLowerCase(); } + const todayKey = String(this._todayKey && this._todayKey() || '').toLowerCase(); + // If we don't have a selected day (common in normal view), assume today. + if (!selectedDayKey) selectedDayKey = todayKey; + nowEl.style.visibility = (selectedDayKey === todayKey) ? "visible" : "hidden"; + } catch {} + }; + + updateNowGeom(); + + const ro = new ResizeObserver(() => { + updateNowGeom(); + }); + try { ro.observe(rowsEl); } catch {} + } + + // Update footer labels to reflect time format + try { + const s = this.shadowRoot.querySelector('.lbl-start'); + const e = this.shadowRoot.querySelector('.lbl-end'); + if (s) s.textContent = this._label(0); + if (e) e.textContent = this._label(0); + } catch {} + + // Editor modal events + const overlay = qs('.overlay'); + const edSave = qs('.ed-save'); + const edCancel = qs('.ed-cancel'); + const edDelete = qs('.ed-delete'); + if (edSave) edSave.addEventListener('click', () => this._saveEditor()); + if (edCancel) edCancel.addEventListener('click', () => this._closeEditor()); + if (edDelete) edDelete.addEventListener('click', () => this._deleteFromEditor()); + // Do not close editor when clicking outside; require explicit buttons + overlay.addEventListener('click', (e) => { if (e.target === overlay) { e.preventDefault(); e.stopPropagation(); } }); + // Week overlay events + const wov = qs('.overlay-week'); + const wsave = qs('.wk-save'); + const wcancel = qs('.wk-cancel'); + if (wsave) wsave.addEventListener('click', () => this._weeklySaveAll()); + if (wcancel) wcancel.addEventListener('click', () => this._closeWeeklyEditor()); + // Do not close weekly editor on outside click either + if (wov) wov.addEventListener('click', (e) => { if (e.target === wov) { e.preventDefault(); e.stopPropagation(); } }); + + // Profiles overlay events + const pov = qs('.overlay-profiles'); + const pclose = qs('.prof-close'); + const psave = qs('.prof-save'); + if (psave) psave.addEventListener('click', () => { this._profilesSave(); }); + const _maybeCloseProfiles = () => { + try { if (this._profilesDirty) { this._openConfirmCloseProfiles(); return false; } } catch {} + this._closeProfilesEditor(); return true; + }; + if (pclose) pclose.addEventListener('click', _maybeCloseProfiles); + if (pov) pov.addEventListener('click', (e) => { + // Close profiles overlay when clicking outside the modal (with guard) + if (e.target === pov) { e.preventDefault(); _maybeCloseProfiles(); } + }); + + // Holidays overlay events + const hov = qs('.overlay-holiday'); + const hsave = qs('.holiday-save'); + const hclose = qs('.holiday-close'); + if (hsave) hsave.addEventListener('click', () => this._holidaySave()); + if (hclose) hclose.addEventListener('click', () => { this._closeHolidayEditor(); this._render(); }); + if (hov) hov.addEventListener('click', (e)=>{ if (e.target === hov) { e.preventDefault(); this._closeHolidayEditor(); } }); + + // Holiday copy popup overlay events + const hcov = qs('.overlay-hcopy'); + if (hcov) hcov.addEventListener('click', (e)=>{ if (e.target === hcov) { e.preventDefault(); this._closeHolidayCopy(); } }); + + // Onboarding controls + try { + const next = qs('.onb-next'); + const prev = qs('.onb-prev'); + const skip = qs('.onb-skip'); + const done = qs('.onb-done'); + const ov = qs('.overlay-onboard'); + next && (next.onclick = ()=>{ this._onboardStep = Math.min(this._onboardSlides.length-1, this._onboardStep+1); this._renderOnboard(); }); + prev && (prev.onclick = ()=>{ this._onboardStep = Math.max(0, this._onboardStep-1); this._renderOnboard(); }); + skip && (skip.onclick = ()=>{ this._closeOnboard(true); }); + done && (done.onclick = ()=>{ this._closeOnboard(true); }); + ov && ov.addEventListener('click', (e)=>{ if (e.target === ov) { e.preventDefault(); e.stopPropagation(); } }); + } catch {} + + // Test popup events + try { + const tov = qs('.overlay-test'); + const tclose = qs('.test-close'); + const texport = qs('.test-export'); + if (tclose) tclose.onclick = ()=>{ try { tov?.classList.remove('open'); } catch {} }; + if (texport) texport.onclick = ()=>{ try { this._exportTestCommands(); } catch {} }; + if (tov) tov.onclick = (e)=>{ if (e.target === tov) { e.preventDefault(); e.stopPropagation(); } }; + } catch {} + + // Settings popup events + try { + const sov = qs('.overlay-settings'); + const sclose = qs('.settings-close'); + const ssave = qs('.settings-save'); + const stabs = qs('.settings-tabs'); + if (sclose) sclose.onclick = ()=>{ try { this._closeSettingsPopup(false); } catch {} }; + if (ssave) ssave.onclick = ()=>{ try { this._saveSettingsPopup(); } catch {} }; + if (stabs) stabs.onclick = (ev)=>{ + try { + // In Shadow DOM, the click target can be a Text node inside the button. + // Use composedPath() to reliably find the originating button. + const path = typeof ev.composedPath === 'function' ? ev.composedPath() : []; + let btn = null; + for (const n of path) { + if (n && n.classList && n.classList.contains('settings-tab-btn')) { btn = n; break; } + } + if (!btn) btn = (ev.target && ev.target.closest) ? ev.target.closest('.settings-tab-btn') : null; + if (!btn) return; + const tab = btn.getAttribute('data-tab') || 'settings'; + this._setSettingsPopupTab(tab); + } catch {} + }; + if (sov) sov.onclick = (e)=>{ if (e.target === sov) { e.preventDefault(); e.stopPropagation(); } }; + + // Prevent scroll wheel from accidentally changing number inputs (which triggers "unsaved" state) + try { + const scroller = qs('.overlay-settings .settings-body') || sov; + const onWheel = (ev)=>{ + try { + const t = ev?.target; + if (!t || t.tagName !== 'INPUT' || t.type !== 'number') return; + // Stop number increment/decrement and treat it as scroll + try { t.blur?.(); } catch {} + ev.preventDefault(); + const s = t.closest('.settings-body') || scroller; + if (s && typeof s.scrollTop === 'number') s.scrollTop += (Number(ev.deltaY) || 0); + } catch {} + }; + try { + if (this._settingsPopupWheelHandler?.el && this._settingsPopupWheelHandler?.fn) { + this._settingsPopupWheelHandler.el.removeEventListener('wheel', this._settingsPopupWheelHandler.fn); + } + } catch {} + if (scroller) { + scroller.addEventListener('wheel', onWheel, { passive: false }); + this._settingsPopupWheelHandler = { el: scroller, fn: onWheel }; + } + } catch {} + + // Settings tab controls (draft-only) + const onNum = (key) => (e)=>{ + try { + if (!this._settingsDraft) return; + const v = e && e.target ? e.target.value : ''; + if (v === '') return; + const n = Number(v); + if (!Number.isFinite(n)) return; + this._settingsDraft[key] = n; + } catch {} + }; + const onTempNum = (key) => (e)=>{ + try { + if (!this._settingsDraft) return; + const v = e && e.target ? e.target.value : ''; + if (v === '') return; + const raw = String(v).trim().replace(',', '.'); + const nDisp = Number(raw); + if (!Number.isFinite(nDisp)) return; + // Convert based on draft unit (config unit may not match while popup is open) + const wantF = String(this._settingsDraft?.temp_unit || 'C').toUpperCase() === 'F'; + const nC = wantF ? ((nDisp - 32) * 5/9) : nDisp; + if (!Number.isFinite(nC)) return; + this._settingsDraft[key] = nC; + } catch {} + }; + const onSw = (key) => (e)=>{ + try { + if (!this._settingsDraft) return; + const checked = !!(e && e.target && e.target.checked); + this._settingsDraft[key] = checked; + } catch {} + }; + + const spDef = qs('.overlay-settings .sp-def'); + const spRowH = qs('.overlay-settings .sp-rowh'); + const spMinC = qs('.overlay-settings .sp-minc'); + const spMaxC = qs('.overlay-settings .sp-maxc'); + if (spDef) spDef.oninput = onTempNum('default_temp'); + if (spRowH) spRowH.oninput = onNum('row_height'); + if (spMinC) spMinC.oninput = onTempNum('min_temp'); + if (spMaxC) spMaxC.oninput = onTempNum('max_temp'); + + const swAuto = qs('.overlay-settings .sp-auto'); + const swApplyEdit = qs('.overlay-settings .sp-applyedit'); + const swApplyDef = qs('.overlay-settings .sp-applydef'); + const swPerRoom = qs('.overlay-settings .sp-perroom'); + const swShowRoomTemp = qs('.overlay-settings .sp-showroomtemp'); + if (swAuto) swAuto.onchange = onSw('auto_apply'); + if (swApplyEdit) swApplyEdit.onchange = onSw('apply_on_edit'); + if (swApplyDef) swApplyDef.onchange = onSw('apply_on_default_change'); + if (swPerRoom) swPerRoom.onchange = onSw('per_room_defaults'); + if (swShowRoomTemp) swShowRoomTemp.onchange = onSw('show_room_temp'); + + // Segmented controls + profiles enable + const fmt24 = qs('.overlay-settings .sp-fmt24'); + const fmt12 = qs('.overlay-settings .sp-fmt12'); + const srcB = qs('.overlay-settings .sp-srcBrowser'); + const srcH = qs('.overlay-settings .sp-srcHa'); + const unitC = qs('.overlay-settings .sp-unitC'); + const unitF = qs('.overlay-settings .sp-unitF'); + const swProfiles = qs('.overlay-settings .sp-profiles'); + const swInstance = qs('.overlay-settings .sp-instance-enable'); + const btnRegenerateInstance = qs('.overlay-settings .sp-instance-regenerate-btn'); + if (fmt24) fmt24.onclick = ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.time_12h = false; this._renderSettingsPopupSettingsTab(); } catch {} }; + if (fmt12) fmt12.onclick = ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.time_12h = true; this._renderSettingsPopupSettingsTab(); } catch {} }; + if (srcB) srcB.onclick = ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.time_source = 'browser'; this._renderSettingsPopupSettingsTab(); } catch {} }; + if (srcH) srcH.onclick = ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.time_source = 'ha'; this._renderSettingsPopupSettingsTab(); } catch {} }; + if (unitC) unitC.onclick = ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.temp_unit = 'C'; this._renderSettingsPopupTab(); } catch {} }; + if (unitF) unitF.onclick = ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.temp_unit = 'F'; this._renderSettingsPopupTab(); } catch {} }; + if (swProfiles) swProfiles.onchange = onSw('profiles_enabled'); + + // Configuration ID (instance) toggle (persists outside of the popup draft) + if (swInstance) { + swInstance.onchange = async (e)=>{ + try { + const on = !!e?.target?.checked; + if (!on) { + try { this._config.instance_enabled = false; } catch {} + try { this._persistInstanceCfg(false, String(this._config?.instance_id||'')); } catch {} + try { this._renderSettingsPopupSettingsTab(); } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + return; + } + let cur = String(this._config?.instance_id || '').trim(); + if (!cur) cur = this._genInstanceId(); + cur = this._normInstanceId(cur); + try { this._config.instance_enabled = true; this._config.instance_id = cur; } catch {} + try { this._persistInstanceCfg(true, cur); } catch {} + + // Best-effort migration: copy legacy store into this instance + try { + if (this._config?.storage_enabled && this._hass?.callApi) { + const api = await this._hass.callApi('GET', 'thermostat_timeline/state'); + const payload = { force: true, instance_id: cur, activate: false }; + if (api?.schedules) payload.schedules = api.schedules; + if (api?.settings) payload.settings = api.settings; + if (api?.colors) payload.colors = api.colors; + if (api?.weekdays) payload.weekdays = api.weekdays; + if (api?.profiles) payload.profiles = api.profiles; + await this._hass.callService('thermostat_timeline','set_store', payload); + } else { + const srcKey = 'thermostat_timeline_store'; + const dstKey = this._localStoreKey(); + const src = localStorage.getItem(srcKey) || ''; + const dst = localStorage.getItem(dstKey) || ''; + if (src && (!dst || dst.length < 3)) { + try { localStorage.setItem(dstKey, src); } catch {} + } + } + } catch {} + + try { this._renderSettingsPopupSettingsTab(); } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + } catch {} + }; + } + + // Regenerate Configuration ID (instance_id) - renames/moves data to a new id (no duplicate instance) + if (btnRegenerateInstance) { + btnRegenerateInstance.onclick = async ()=>{ + try { + if (!this._config?.instance_enabled) return; + const oldId = this._normInstanceId(this._config?.instance_id || ''); + if (!oldId) return; + const newId = this._genInstanceId(); + if (!newId || newId === oldId) return; + + // Rename/move old -> new (avoids creating an extra instance/section) + const renamed = await this._renameInstanceData(oldId, newId); + if (!renamed) return; + + // Switch this card to the new id + persist outside YAML + try { this._config.instance_id = newId; } catch {} + try { this._persistInstanceCfg(true, newId); } catch {} + + try { this._renderSettingsPopupSettingsTab(); } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + } catch {} + }; + } + } catch {} + } + + // --- Settings popup (in-card, not HA editor) --- + _openSettingsPopup(){ + try { + if (this._disablePopups) return; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings'); + if (!ov) return; + ov.classList.add('open'); + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + try { if (!this._settingsPopupTab) this._settingsPopupTab = 'settings'; } catch {} + try { this._initSettingsPopupDraft(); } catch {} + try { this._applyCardI18n(); } catch {} + try { this._renderSettingsPopupTabs(); } catch {} + try { this._renderSettingsPopupTab(); } catch {} + try { + const onKey = (ev) => { if (ev.key === 'Escape') { this._closeSettingsPopup(false); } }; + try { if (this._settingsPopupKeyHandler) window.removeEventListener('keydown', this._settingsPopupKeyHandler); } catch {} + this._settingsPopupKeyHandler = onKey; + window.addEventListener('keydown', onKey); + } catch {} + } catch {} + } + + _setSettingsPopupTab(tab){ + try { + const allowed = new Set(['settings','pause','weekdays','rooms','boiler','colors','presence_sensor','owd','holidays','away','reset']); + const next = allowed.has(String(tab)) ? String(tab) : 'settings'; + this._settingsPopupTab = next; + this._renderSettingsPopupTabs(); + this._renderSettingsPopupTab(); + } catch {} + } + + _renderSettingsPopupTabs(){ + try { + const active = String(this._settingsPopupTab || 'settings'); + const btns = this.shadowRoot ? Array.from(this.shadowRoot.querySelectorAll('.overlay-settings .settings-tab-btn')) : []; + for (const b of btns){ + const isActive = (b.getAttribute('data-tab') === active); + try { b.classList.toggle('active', isActive); } catch {} + try { b.setAttribute('aria-selected', isActive ? 'true' : 'false'); } catch {} + } + } catch {} + } + + _renderSettingsPopupTab(){ + try { + const active = String(this._settingsPopupTab || 'settings'); + const pages = this.shadowRoot ? Array.from(this.shadowRoot.querySelectorAll('.overlay-settings .settings-page')) : []; + for (const p of pages){ + const isActive = (p.getAttribute('data-page') === active); + try { p.classList.toggle('active', isActive); } catch {} + // Some tab renderers apply inline display styles (e.g. grid). Clear those on inactive tabs + // so CSS `.settings-page{display:none}` can hide them properly. + if (!isActive) { + try { p.style.display = ''; } catch {} + } + } + if (active === 'settings') { + this._renderSettingsPopupSettingsTab(); + } else if (active === 'pause') { + this._renderSettingsPopupPauseTab(); + } else if (active === 'weekdays') { + this._renderSettingsPopupWeekdaysTab(); + } else if (active === 'rooms') { + this._renderSettingsPopupRoomsTab(); + } else if (active === 'boiler') { + this._renderSettingsPopupBoilerTab(); + } else if (active === 'colors') { + this._renderSettingsPopupColorsTab(); + } else if (active === 'presence_sensor') { + this._renderSettingsPopupPresenceSensorTab(); + } else if (active === 'owd') { + this._renderSettingsPopupOwdTab(); + } else if (active === 'holidays') { + this._renderSettingsPopupHolidaysTab(); + } else if (active === 'away') { + this._renderSettingsPopupAwayTab(); + } else if (active === 'reset') { + this._renderSettingsPopupResetTab(); + } + // Some pages (like Weekdays) are built dynamically; re-apply i18n after rendering. + try { this._applyCardI18n(); } catch {} + } catch {} + } + + _initSettingsPopupDraft(){ + try { + try { this._hydrateInstanceCfgFromLocal(); } catch {} + const cfg = this._config || {}; + const ow = (cfg.open_window && typeof cfg.open_window === 'object') ? cfg.open_window : {}; + const owEnabled = (typeof ow.enabled === 'boolean') ? ow.enabled : false; + const owSensors = (ow.sensors && typeof ow.sensors === 'object') ? ow.sensors : {}; + const owOpenDelay = Number.isFinite(Number(ow.open_delay_min)) ? Math.max(0, Math.min(1440, Math.round(Number(ow.open_delay_min)))) : 2; + const owCloseDelay = Number.isFinite(Number(ow.close_delay_min)) ? Math.max(0, Math.min(1440, Math.round(Number(ow.close_delay_min)))) : 5; + let owSensorsClone = {}; + try { + for (const k of Object.keys(owSensors||{})) { + const arr = Array.isArray(owSensors[k]) ? owSensors[k].filter(Boolean).map(String) : []; + if (arr.length) owSensorsClone[String(k)] = Array.from(new Set(arr)); + } + } catch {} + + // Holidays + let holDates = []; + try { + holDates = Array.isArray(cfg.holidays_dates) ? cfg.holidays_dates.filter(Boolean).map(String) : []; + holDates = Array.from(new Set(holDates)).sort(); + } catch {} + + // Away + const awayCfg = (cfg.away && typeof cfg.away === 'object') ? cfg.away : {}; + const awayPersons = Array.isArray(awayCfg.persons) ? awayCfg.persons.filter(Boolean).map(String) : []; + const awayCombos = (awayCfg.combos && typeof awayCfg.combos === 'object') ? awayCfg.combos : {}; + const awayCombosClone = (()=>{ + try { + const out = {}; + for (const k of Object.keys(awayCombos||{})) { + const v = awayCombos[k]; + if (v && typeof v === 'object' && v.enabled) out[String(k)] = { enabled: true }; + } + return out; + } catch { return {}; } + })(); + + this._settingsDraft = { + // Rooms (edited in popup, persisted via store) + entities: (()=>{ + try { return Array.isArray(cfg.entities) ? cfg.entities.map(String) : []; } catch { return []; } + })(), + room_use_input_number: (()=>{ + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.map(String) : []; + const modes = Array.isArray(cfg.room_use_input_number) ? cfg.room_use_input_number.slice() : []; + while (modes.length < ents.length) modes.push(false); + return modes.map(v => !!v).slice(0, ents.length); + } catch { return []; } + })(), + room_use_temp_sensor: (()=>{ + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.map(String) : []; + const modes = Array.isArray(cfg.room_use_temp_sensor) ? cfg.room_use_temp_sensor.slice() : []; + while (modes.length < ents.length) modes.push(false); + return modes.map(v => !!v).slice(0, ents.length); + } catch { return []; } + })(), + labels: (()=>{ try { return (cfg.labels && typeof cfg.labels === 'object') ? JSON.parse(JSON.stringify(cfg.labels)) : {}; } catch { return {}; } })(), + merges: (()=>{ + try { + const src = (cfg.merges && typeof cfg.merges === 'object') ? cfg.merges : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const arr = Array.isArray(src[k]) ? src[k].filter(Boolean).map(String) : []; + if (arr.length) out[String(k)] = Array.from(new Set(arr)); + } + return out; + } catch { return {}; } + })(), + temp_sensors: (()=>{ try { return (cfg.temp_sensors && typeof cfg.temp_sensors === 'object') ? JSON.parse(JSON.stringify(cfg.temp_sensors)) : {}; } catch { return {}; } })(), + turn_on: (()=>{ try { return (cfg.turn_on && typeof cfg.turn_on === 'object') ? JSON.parse(JSON.stringify(cfg.turn_on)) : {}; } catch { return {}; } })(), + + default_temp: this._ttCoerceNum(cfg.default_temp, 20), + row_height: this._ttCoerceNum(cfg.row_height, 64), + min_temp: this._ttCoerceNum(cfg.min_temp, 5), + max_temp: this._ttCoerceNum(cfg.max_temp, 25), + auto_apply: !!cfg.auto_apply, + apply_on_edit: !!cfg.apply_on_edit, + apply_on_default_change: !!cfg.apply_on_default_change, + per_room_defaults: !!cfg.per_room_defaults, + show_pause_button: cfg.show_pause_button !== false, + show_room_temp: cfg.show_room_temp !== false, + pause_sensor_enabled: !!cfg.pause_sensor_enabled, + pause_sensor_entity: String(cfg.pause_sensor_entity || ''), + time_12h: !!cfg.time_12h, + time_source: (cfg.time_source === 'ha') ? 'ha' : 'browser', + temp_unit: (String(cfg.temp_unit || 'C').toUpperCase() === 'F') ? 'F' : 'C', + profiles_enabled: !!cfg.profiles_enabled, + + presence_sensor_enabled: !!cfg.presence_sensor_enabled, + + presence_sensors: (()=>{ try { return (cfg.presence_sensors && typeof cfg.presence_sensors === 'object') ? JSON.parse(JSON.stringify(cfg.presence_sensors)) : {}; } catch { return {}; } })(), + + presence_sensor_temps: (()=>{ try { return (cfg.presence_sensor_temps && typeof cfg.presence_sensor_temps === 'object') ? JSON.parse(JSON.stringify(cfg.presence_sensor_temps)) : {}; } catch { return {}; } })(), + + presence_sensor_delays: (()=>{ try { return (cfg.presence_sensor_delays && typeof cfg.presence_sensor_delays === 'object') ? JSON.parse(JSON.stringify(cfg.presence_sensor_delays)) : {}; } catch { return {}; } })(), + + presence_sensor_delay_units: (()=>{ try { return (cfg.presence_sensor_delay_units && typeof cfg.presence_sensor_delay_units === 'object') ? JSON.parse(JSON.stringify(cfg.presence_sensor_delay_units)) : {}; } catch { return {}; } })(), + + weekdays_enabled: !!cfg.weekdays_enabled, + weekdays_view: String(cfg.weekdays_view || 'all_rooms_one_day'), + weekdays_view_switch_in_timeline: !!(cfg.weekdays_view_switch_in_timeline ?? false), + weekdays_selected_room: String(cfg.weekdays_selected_room || ''), + + boiler_enabled: !!cfg.boiler_enabled, + boiler_switch: String(cfg.boiler_switch || ''), + boiler_switch_domain: (()=>{ + try { + const sw = String(cfg.boiler_switch || ''); + const fromEid = (()=>{ + const dom = String(sw||'').split('.')[0]; + return (dom === 'input_boolean') ? 'input_boolean' : 'switch'; + })(); + const raw = String((cfg.boiler_switch_domain ?? fromEid) || '').toLowerCase().trim(); + return (raw === 'input_boolean') ? 'input_boolean' : 'switch'; + } catch { return 'switch'; } + })(), + boiler_rooms: (()=>{ + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.map(String) : []; + const climateRooms = ents.filter(e=>String(e).startsWith('climate.')); + const raw = cfg.boiler_rooms; + if (Array.isArray(raw)) { + const want = new Set(raw.filter(Boolean).map(String)); + return climateRooms.filter(eid => want.has(eid)); + } + return climateRooms; + } catch { return []; } + })(), + boiler_on_offset: (()=>{ try { const v = Number(cfg.boiler_on_offset); return Number.isFinite(v) ? v : 0; } catch { return 0; } })(), + boiler_off_offset: (()=>{ try { const v = Number(cfg.boiler_off_offset); return Number.isFinite(v) ? v : 0; } catch { return 0; } })(), + boiler_temp_sensor: String(cfg.boiler_temp_sensor || ''), + boiler_min_temp: this._ttCoerceNum(cfg.boiler_min_temp, 20), + boiler_max_temp: this._ttCoerceNum(cfg.boiler_max_temp, 25), + + color_ranges: (()=>{ + try { + const src = (cfg.color_ranges && typeof cfg.color_ranges === 'object') ? cfg.color_ranges : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const arr = Array.isArray(src[k]) ? src[k] : []; + out[String(k)] = arr.map(r => ({ + from: Number.isFinite(Number(r?.from)) ? Number(r.from) : (this._ttCoerceNum(cfg.default_temp, 20)), + to: Number.isFinite(Number(r?.to)) ? Number(r.to) : (this._ttCoerceNum(cfg.default_temp, 20)), + color: String(r?.color || '#ffb347') + })); + } + return out; + } catch { return {}; } + })(), + color_global: !!cfg.color_global, + + open_window: { + enabled: !!owEnabled, + sensors: owSensorsClone, + open_delay_min: owOpenDelay, + close_delay_min: owCloseDelay, + }, + + holidays_enabled: !!cfg.holidays_enabled, + holidays_source: (cfg.holidays_source === 'manual') ? 'manual' : 'calendar', + holidays_entity: String(cfg.holidays_entity || ''), + holidays_dates: holDates, + holidays_groups: (()=>{ try { return Array.isArray(cfg.holidays_groups) ? JSON.parse(JSON.stringify(cfg.holidays_groups)) : []; } catch { return []; } })(), + + away: { + enabled: !!awayCfg.enabled, + target_c: this._ttCoerceNum(awayCfg.target_c, 17), + persons: Array.from(new Set(awayPersons)), + advanced_enabled: !!awayCfg.advanced_enabled, + combos: awayCombosClone, + }, + presence_live_header: !!(cfg.presence_live_header ?? true), + // Away delay (defaults) + away_delay_enabled: !!(awayCfg.delay_enabled ?? false), + away_delay_value: Number.isFinite(Number(awayCfg.delay_value)) ? Number(awayCfg.delay_value) : 0, + away_delay_unit: String(awayCfg.delay_unit || 'minutes'), + }; + } catch { this._settingsDraft = null; } + } + + _applySettingsPopupDraftToConfig(){ + try { + const d = this._settingsDraft; + if (!d) return; + const cfg = { ...(this._config || {}) }; + + // Rooms settings (entities + per-room mode + merges/labels) + try { + const rawEnts = Array.isArray(d.entities) ? d.entities.map(x => String(x||'').trim()) : []; + const ents = rawEnts.filter(Boolean); + const rawModes = Array.isArray(d.room_use_input_number) ? d.room_use_input_number.slice() : []; + while (rawModes.length < rawEnts.length) rawModes.push(false); + const modes = ents.map((eid, idx) => { + // If the entity domain is explicit, prefer it to avoid inconsistent mode state + if (String(eid).startsWith('input_number.')) return true; + if (String(eid).startsWith('climate.')) return false; + // Otherwise, take draft mode + return !!rawModes[idx]; + }); + cfg.entities = ents; + cfg.room_use_input_number = modes; + + // Per-room: use temperature sensor for room temp + try { + const rawTemp = Array.isArray(d.room_use_temp_sensor) ? d.room_use_temp_sensor.slice() : []; + while (rawTemp.length < rawEnts.length) rawTemp.push(false); + cfg.room_use_temp_sensor = ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return false; + if (!String(eid).startsWith('climate.')) return false; + return !!rawTemp[idx]; + }); + } catch { cfg.room_use_temp_sensor = []; } + + // Labels: keep only for active rooms and only non-empty strings + try { + const src = (d.labels && typeof d.labels === 'object') ? d.labels : {}; + const out = {}; + for (const eid of ents) { + const v = (src && Object.prototype.hasOwnProperty.call(src, eid)) ? String(src[eid]||'').trim() : ''; + if (v) out[eid] = v; + } + cfg.labels = out; + } catch {} + + // Merges: only keep mappings for active primaries, only climate rooms, and only linked climate.* entities + try { + const src = (d.merges && typeof d.merges === 'object') ? d.merges : {}; + const out = {}; + const primSet = new Set(ents); + for (let i=0;i String(x).startsWith('climate.') && !primSet.has(String(x))))); + if (cleaned.length) out[primary] = cleaned; + } + cfg.merges = out; + } catch {} + + // Temp sensors: keep only for active rooms + try { + const src = (d.temp_sensors && typeof d.temp_sensors === 'object') ? d.temp_sensors : {}; + const out = {}; + for (const eid of ents) { + const v = (src && Object.prototype.hasOwnProperty.call(src, eid)) ? String(src[eid]||'').trim() : ''; + if (v) out[eid] = v; + } + cfg.temp_sensors = out; + } catch {} + + // Per-room turn_on behavior: keep only for active climate rooms + try { + const src = (d.turn_on && typeof d.turn_on === 'object') ? d.turn_on : {}; + const out = {}; + for (let i=0;i cfg.max_temp) { + const tmp = cfg.min_temp; + cfg.min_temp = cfg.max_temp; + cfg.max_temp = tmp; + } + + cfg.auto_apply = !!d.auto_apply; + cfg.apply_on_edit = !!d.apply_on_edit; + cfg.apply_on_default_change = !!d.apply_on_default_change; + cfg.per_room_defaults = !!d.per_room_defaults; + cfg.show_pause_button = !!d.show_pause_button; + cfg.show_room_temp = !!d.show_room_temp; + cfg.pause_sensor_enabled = !!d.pause_sensor_enabled; + cfg.pause_sensor_entity = String(d.pause_sensor_entity || ''); + + cfg.time_12h = !!d.time_12h; + cfg.time_source = (d.time_source === 'ha') ? 'ha' : 'browser'; + cfg.temp_unit = (String(d.temp_unit || 'C').toUpperCase() === 'F') ? 'F' : 'C'; + cfg.profiles_enabled = !!d.profiles_enabled; + + cfg.presence_sensor_enabled = !!d.presence_sensor_enabled; + + // Presence sensors: keep only for active rooms + only binary_sensor.* + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.filter(Boolean).map(String) : []; + const src = (d.presence_sensors && typeof d.presence_sensors === 'object') ? d.presence_sensors : {}; + const out = {}; + for (const eid of ents) { + const v = (src && Object.prototype.hasOwnProperty.call(src, eid)) ? String(src[eid]||'').trim() : ''; + if (v && String(v).startsWith('binary_sensor.')) out[eid] = v; + } + cfg.presence_sensors = out; + } catch {} + + // Presence sensor temps (°C): keep only for active rooms + only numeric + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.filter(Boolean).map(String) : []; + const src = (d.presence_sensor_temps && typeof d.presence_sensor_temps === 'object') ? d.presence_sensor_temps : {}; + const out = {}; + const minC = Number(cfg.min_temp); + const maxC = Number(cfg.max_temp); + for (const eid of ents) { + const v0 = (src && Object.prototype.hasOwnProperty.call(src, eid)) ? Number(src[eid]) : NaN; + if (!Number.isFinite(v0)) continue; + let v = v0; + if (Number.isFinite(minC)) v = Math.max(minC, v); + if (Number.isFinite(maxC)) v = Math.min(maxC, v); + out[eid] = v; + } + cfg.presence_sensor_temps = out; + } catch {} + + // Presence sensor delays (seconds): keep only for active rooms + only numeric >= 0 + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.filter(Boolean).map(String) : []; + const src = (d.presence_sensor_delays && typeof d.presence_sensor_delays === 'object') ? d.presence_sensor_delays : {}; + const out = {}; + for (const eid of ents) { + const v = src?.[eid]; + if (!v || typeof v !== 'object') continue; + const onS = Number(v.on_s); + const offS = Number(v.off_s); + const obj = {}; + if (Number.isFinite(onS) && onS >= 0) obj.on_s = onS; + if (Number.isFinite(offS) && offS >= 0) obj.off_s = offS; + if (Object.keys(obj).length) out[eid] = obj; + } + cfg.presence_sensor_delays = out; + } catch {} + + // Presence sensor delay units: keep only for active rooms + only 'minutes'|'seconds' + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.filter(Boolean).map(String) : []; + const src = (d.presence_sensor_delay_units && typeof d.presence_sensor_delay_units === 'object') ? d.presence_sensor_delay_units : {}; + const out = {}; + for (const eid of ents) { + const u = (src && Object.prototype.hasOwnProperty.call(src, eid)) ? String(src[eid]||'').toLowerCase().trim() : ''; + out[eid] = (u === 'seconds') ? 'seconds' : 'minutes'; + } + cfg.presence_sensor_delay_units = out; + } catch {} + + cfg.weekdays_enabled = !!d.weekdays_enabled; + cfg.weekdays_view = (String(d.weekdays_view || 'all_rooms_one_day') === 'one_room_all_days') ? 'one_room_all_days' : 'all_rooms_one_day'; + cfg.weekdays_view_switch_in_timeline = !!d.weekdays_view_switch_in_timeline; + // If per-room view, ensure we have a selected room + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.filter(Boolean) : []; + const want = String(d.weekdays_selected_room || ''); + if (cfg.weekdays_view === 'one_room_all_days') { + cfg.weekdays_selected_room = (want && ents.includes(want)) ? want : (ents[0] || ''); + } else { + cfg.weekdays_selected_room = ''; + } + } catch {} + + // Boiler settings + cfg.boiler_enabled = !!d.boiler_enabled; + cfg.boiler_switch = String(d.boiler_switch || ''); + try { + const raw = String(d.boiler_switch_domain || '').toLowerCase().trim(); + cfg.boiler_switch_domain = (raw === 'input_boolean') ? 'input_boolean' : 'switch'; + // Keep entity + selected domain consistent + const swDom = String(cfg.boiler_switch || '').split('.')[0]; + if (cfg.boiler_switch && (swDom === 'switch' || swDom === 'input_boolean') && swDom !== cfg.boiler_switch_domain) { + cfg.boiler_switch = ''; + } + } catch { cfg.boiler_switch_domain = 'switch'; } + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.filter(Boolean).map(String) : []; + const climateRooms = ents.filter(e=>String(e).startsWith('climate.')); + const wantRaw = Array.isArray(d.boiler_rooms) ? d.boiler_rooms.filter(Boolean).map(String) : climateRooms; + const wantSet = new Set(wantRaw); + cfg.boiler_rooms = climateRooms.filter(eid => wantSet.has(eid)); + } catch { cfg.boiler_rooms = []; } + const clampOffset = (n, fallback)=>{ + const v = Number(n); + if (!Number.isFinite(v)) return fallback; + return Math.max(-10, Math.min(10, v)); + }; + cfg.boiler_on_offset = clampOffset(d.boiler_on_offset, (cfg.boiler_on_offset ?? 0)); + cfg.boiler_off_offset = clampOffset(d.boiler_off_offset, (cfg.boiler_off_offset ?? 0)); + cfg.boiler_temp_sensor = String(d.boiler_temp_sensor || ''); + const clampBoiler = (n, fallback)=>{ + const v = Number(n); + if (!Number.isFinite(v)) return fallback; + return Math.max(0, Math.min(120, v)); + }; + cfg.boiler_min_temp = clampBoiler(d.boiler_min_temp, cfg.boiler_min_temp); + cfg.boiler_max_temp = clampBoiler(d.boiler_max_temp, cfg.boiler_max_temp); + // Normalize min/max if user swapped them + if (Number.isFinite(Number(cfg.boiler_min_temp)) && Number.isFinite(Number(cfg.boiler_max_temp)) && Number(cfg.boiler_min_temp) > Number(cfg.boiler_max_temp)) { + const tmpB = cfg.boiler_min_temp; + cfg.boiler_min_temp = cfg.boiler_max_temp; + cfg.boiler_max_temp = tmpB; + } + + // Colors settings + try { + cfg.color_global = !!d.color_global; + const src = (d.color_ranges && typeof d.color_ranges === 'object') ? d.color_ranges : {}; + const out = {}; + const minC = Number.isFinite(Number(cfg.min_temp)) ? Number(cfg.min_temp) : 5; + const maxC = Number.isFinite(Number(cfg.max_temp)) ? Number(cfg.max_temp) : 25; + const clampC = (n, fallback)=>{ + const v = Number(n); + if (!Number.isFinite(v)) return fallback; + return Math.max(minC, Math.min(maxC, v)); + }; + for (const k of Object.keys(src||{})) { + const arr = Array.isArray(src[k]) ? src[k] : []; + const cleaned = []; + for (const r of arr) { + const def = Number.isFinite(Number(cfg.default_temp)) ? Number(cfg.default_temp) : 20; + const from = clampC(r?.from, def); + const to = clampC(r?.to, def); + const color = String(r?.color || '#ffb347'); + cleaned.push({ from, to, color }); + } + out[String(k)] = cleaned; + } + cfg.color_ranges = out; + } catch {} + + // Open Window Detection settings + try { + const ow = (d.open_window && typeof d.open_window === 'object') ? d.open_window : {}; + const out = {}; + out.enabled = !!ow.enabled; + out.open_delay_min = Number.isFinite(Number(ow.open_delay_min)) ? Math.max(0, Math.min(1440, Math.round(Number(ow.open_delay_min)))) : 2; + out.close_delay_min = Number.isFinite(Number(ow.close_delay_min)) ? Math.max(0, Math.min(1440, Math.round(Number(ow.close_delay_min)))) : 5; + out.sensors = {}; + const sMap = (ow.sensors && typeof ow.sensors === 'object') ? ow.sensors : {}; + for (const k of Object.keys(sMap||{})) { + const arr = Array.isArray(sMap[k]) ? sMap[k].filter(Boolean).map(String) : []; + if (arr.length) out.sensors[String(k)] = Array.from(new Set(arr)); + } + cfg.open_window = out; + } catch {} + + // Holidays settings + try { + cfg.holidays_enabled = !!d.holidays_enabled; + cfg.holidays_source = (d.holidays_source === 'manual') ? 'manual' : 'calendar'; + cfg.holidays_entity = String(d.holidays_entity || ''); + let dates = []; + try { + dates = Array.isArray(d.holidays_dates) ? d.holidays_dates.filter(Boolean).map(String) : []; + // Keep only YYYY-MM-DD (simple/strict) + dates = dates.filter(x => /^\d{4}-\d{2}-\d{2}$/.test(String(x))); + dates = Array.from(new Set(dates)).sort(); + } catch {} + cfg.holidays_dates = dates; + try { + const groups = Array.isArray(d.holidays_groups) ? d.holidays_groups : []; + const cleaned = groups.map(g=>({ + id: String(g?.id || ('hg_'+Date.now())), + from: String(g?.from||''), + to: String(g?.to||''), + dates: Array.isArray(g?.dates) ? Array.from(new Set(g.dates.filter(Boolean).map(String))).sort() : [] + })); + cfg.holidays_groups = cleaned; + } catch {} + } catch {} + + // Away settings + try { + const src = (d.away && typeof d.away === 'object') ? d.away : {}; + const cur = { ...(cfg.away || {}) }; + cur.enabled = !!src.enabled; + + // Clamp target C to min/max from draft (stored in C) + const mn = Number.isFinite(Number(d.min_temp)) ? Number(d.min_temp) : (Number.isFinite(Number(cfg.min_temp)) ? Number(cfg.min_temp) : 5); + const mx = Number.isFinite(Number(d.max_temp)) ? Number(d.max_temp) : (Number.isFinite(Number(cfg.max_temp)) ? Number(cfg.max_temp) : 25); + const tC = Number(src.target_c); + const safeTC = Number.isFinite(tC) ? Math.max(mn, Math.min(mx, tC)) : (Number.isFinite(Number(cur.target_c)) ? Number(cur.target_c) : 17); + cur.target_c = safeTC; + + const persons = Array.isArray(src.persons) ? src.persons.filter(Boolean).map(String) : []; + cur.persons = Array.from(new Set(persons)); + cur.advanced_enabled = !!src.advanced_enabled; + // Delay fields from draft + try { + cur.delay_enabled = !!(d.away_delay_enabled ?? cur.delay_enabled ?? false); + const v = Number(d.away_delay_value); + cur.delay_value = Number.isFinite(v) ? Math.max(0, Math.round(v)) : (Number.isFinite(cur.delay_value) ? cur.delay_value : 0); + const u = String(d.away_delay_unit || cur.delay_unit || 'minutes'); + cur.delay_unit = (u === 'seconds') ? 'seconds' : 'minutes'; + } catch {} + + // Normalize combos (only keep enabled entries) + const combosSrc = (src.combos && typeof src.combos === 'object') ? src.combos : {}; + const combosOut = {}; + for (const k of Object.keys(combosSrc||{})) { + const v = combosSrc[k]; + if (v && typeof v === 'object' && v.enabled) combosOut[String(k)] = { enabled: true }; + } + cur.combos = combosOut; + + // If Away is turned OFF, disable live presence chips (and advanced) + if (!cur.enabled) { + cur.advanced_enabled = false; + cfg.presence_live_header = false; + } else { + // Presence live header is available whenever Away mode is enabled + const live = !!(d.presence_live_header ?? true); + cfg.presence_live_header = live; + } + + cfg.away = cur; + } catch {} + + this._config = cfg; + } catch {} + } + + _renderSettingsPopupRoomsTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="rooms"]'); + if (!page) return; + + // Build base markup once + if (!page.dataset.built) { + page.dataset.built = '1'; + page.innerHTML = ` +
    +
    +
    +
    +
    +
    + +
    +
    +
    + `; + } + + // Ensure draft shape + if (!Array.isArray(d.entities)) d.entities = []; + if (!Array.isArray(d.room_use_input_number)) d.room_use_input_number = []; + while (d.room_use_input_number.length < d.entities.length) d.room_use_input_number.push(false); + if (!Array.isArray(d.room_use_temp_sensor)) d.room_use_temp_sensor = []; + while (d.room_use_temp_sensor.length < d.entities.length) d.room_use_temp_sensor.push(false); + if (!d.labels || typeof d.labels !== 'object') d.labels = {}; + if (!d.merges || typeof d.merges !== 'object') d.merges = {}; + if (!d.temp_sensors || typeof d.temp_sensors !== 'object') d.temp_sensors = {}; + try { if (!this._openPopupRooms) this._openPopupRooms = new Set(); } catch {} + + const q = (sel)=> this.shadowRoot.querySelector(sel); + const list = q('.overlay-settings .sp-rooms-list'); + if (!list) return; + list.innerHTML = ''; + + const ents = d.entities.map(x => String(x||'')); + const usedPrimary = new Set(ents.filter(Boolean)); + const usedLinked = new Set(Object.values(d.merges||{}).flat().filter(Boolean).map(String)); + + const titleEl = q('.overlay-settings .sp-rooms-title'); + if (titleEl) titleEl.textContent = this._t('editor.tabs.rooms') || 'Rooms'; + const descEl = q('.overlay-settings .sp-rooms-desc'); + if (descEl) descEl.textContent = this._t('editor.select_entities') || 'Select rooms'; + const addBtn = q('.overlay-settings .sp-rooms-add'); + if (addBtn) addBtn.textContent = this._t('editor.add_entity') || 'Add room'; + + const getRowKey = (idx)=>{ + const eid = ents[idx] || ''; + return eid ? String(eid) : `#idx:${idx}`; + }; + + const pretty = (eid)=>{ + try { + const lbl = d.labels?.[eid]; + if (lbl) return String(lbl); + } catch {} + return this._prettyName(eid); + }; + + const mkFoldout = (idx)=>{ + const line = document.createElement('div'); + line.className = 'inline'; + + const summary = document.createElement('div'); + summary.className = 'summary'; + const sumLeft = document.createElement('div'); + sumLeft.className = 'summary-left'; + const exp = document.createElement('button'); + exp.type = 'button'; + exp.className = 'expander'; + exp.innerHTML = ''; + + const txtWrap = document.createElement('div'); + txtWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); + titleSpan.className = 'summary-title'; + const subSpan = document.createElement('span'); + subSpan.className = 'summary-sub'; + + const actions = document.createElement('div'); + actions.className = 'actions'; + const handle = document.createElement('button'); + handle.type = 'button'; + handle.className = 'drag-handle'; + try { handle.setAttribute('title', this._t('editor.drag_reorder') || 'Drag to reorder'); } catch {} + handle.innerHTML = ''; + const del = document.createElement('button'); + del.type = 'button'; + del.className = 'remove-btn'; + del.innerHTML = '' + (this._t('editor.remove') || 'Remove') + ''; + actions.append(handle, del); + + txtWrap.append(titleSpan, subSpan); + sumLeft.append(exp, txtWrap); + summary.append(sumLeft, actions); + line.append(summary); + + const details = document.createElement('div'); + details.className = 'details'; + line.append(details); + + // Inline styles (keep consistent with other popup foldouts) + try { + summary.style.display = 'flex'; + summary.style.alignItems = 'center'; + summary.style.justifyContent = 'space-between'; + summary.style.padding = '6px'; + summary.style.border = '1px dashed var(--divider-color)'; + summary.style.borderRadius = '8px'; + summary.style.background = 'var(--secondary-background-color, transparent)'; + sumLeft.style.display = 'flex'; + sumLeft.style.alignItems = 'center'; + sumLeft.style.gap = '8px'; + sumLeft.style.minWidth = '0'; + txtWrap.style.display = 'grid'; + txtWrap.style.gap = '2px'; + txtWrap.style.minWidth = '0'; + titleSpan.style.fontWeight = '600'; + titleSpan.style.overflow = 'hidden'; + titleSpan.style.textOverflow = 'ellipsis'; + titleSpan.style.whiteSpace = 'nowrap'; + titleSpan.style.maxWidth = '320px'; + subSpan.style.fontSize = '.85rem'; + subSpan.style.color = 'var(--secondary-text-color)'; + subSpan.style.overflow = 'hidden'; + subSpan.style.textOverflow = 'ellipsis'; + subSpan.style.whiteSpace = 'nowrap'; + subSpan.style.maxWidth = '320px'; + + // Match the Colors foldout expander styling + exp.style.width = '28px'; + exp.style.height = '28px'; + exp.style.borderRadius = '8px'; + exp.style.border = '1px solid var(--divider-color)'; + exp.style.background = 'var(--card-background-color)'; + exp.style.display = 'inline-flex'; + exp.style.alignItems = 'center'; + exp.style.justifyContent = 'center'; + exp.style.cursor = 'pointer'; + } catch {} + try { + details.style.display = 'none'; + details.style.gridTemplateColumns = 'minmax(250px, 520px)'; + details.style.gap = '8px'; + details.style.padding = '8px'; + details.style.border = '1px dashed var(--divider-color)'; + details.style.borderRadius = '8px'; + details.style.background = 'var(--secondary-background-color, transparent)'; + } catch {} + + const rowKey = getRowKey(idx); + const applyOpen = (isOpen)=>{ + try { + if (isOpen) { + line.classList.add('open'); + details.style.display = 'grid'; + exp.style.transform = 'rotate(180deg)'; + } else { + line.classList.remove('open'); + details.style.display = 'none'; + exp.style.transform = ''; + } + } catch {} + }; + const toggle = (ev)=>{ + try { ev?.stopPropagation?.(); } catch {} + const nextOpen = !line.classList.contains('open'); + applyOpen(nextOpen); + try { + if (!this._openPopupRooms) this._openPopupRooms = new Set(); + if (nextOpen) this._openPopupRooms.add(String(rowKey)); + else this._openPopupRooms.delete(String(rowKey)); + } catch {} + }; + exp.addEventListener('click', toggle); + summary.style.cursor = 'pointer'; + summary.addEventListener('click', (ev)=>{ + try { if (ev?.target?.closest?.('button, a, input, select, textarea, tt-entity-picker, ha-entity-picker, ha-switch')) return; } catch {} + toggle(ev); + }); + try { applyOpen(!!this._openPopupRooms?.has(String(rowKey))); } catch {} + + // Build details content + const left = document.createElement('div'); + left.style.display = 'grid'; + left.style.gap = '6px'; + + const useInput = (()=>{ + try { + const eid = String(ents[idx]||''); + if (eid.startsWith('input_number.')) return true; + if (eid.startsWith('climate.')) return false; + return !!d.room_use_input_number?.[idx]; + } catch { return false; } + })(); + + const modeRow = document.createElement('div'); + modeRow.style.display = 'flex'; + modeRow.style.alignItems = 'center'; + modeRow.style.justifyContent = 'space-between'; + modeRow.style.gap = '10px'; + const modeText = document.createElement('div'); + modeText.style.display = 'grid'; + modeText.style.gap = '2px'; + const modeTitle = document.createElement('div'); + modeTitle.style.fontWeight = '600'; + modeTitle.textContent = this._t('editor.room_mode.title') || 'Use input_number instead of climate'; + const modeDesc = document.createElement('div'); + modeDesc.style.fontSize = '.85rem'; + modeDesc.style.color = 'var(--secondary-text-color)'; + modeDesc.textContent = this._t('editor.room_mode.desc') || ''; + modeText.append(modeTitle, modeDesc); + const modeSwitch = document.createElement('ha-switch'); + modeSwitch.className = 'sp-room-mode-switch'; + modeSwitch.checked = !!useInput; + modeSwitch.addEventListener('change', (e)=>{ + try { + const on = !!e.target.checked; + if (!Array.isArray(this._settingsDraft?.room_use_input_number)) this._settingsDraft.room_use_input_number = []; + while (this._settingsDraft.room_use_input_number.length < this._settingsDraft.entities.length) this._settingsDraft.room_use_input_number.push(false); + this._settingsDraft.room_use_input_number[idx] = on; + + // Switching to input_number mode means room temp sensor override is not applicable + try { + if (!Array.isArray(this._settingsDraft?.room_use_temp_sensor)) this._settingsDraft.room_use_temp_sensor = []; + while (this._settingsDraft.room_use_temp_sensor.length < this._settingsDraft.entities.length) this._settingsDraft.room_use_temp_sensor.push(false); + if (on) this._settingsDraft.room_use_temp_sensor[idx] = false; + } catch {} + + const oldEid = String(this._settingsDraft.entities[idx]||'').trim(); + const mismatch = !!oldEid && ((on && !oldEid.startsWith('input_number.')) || (!on && !oldEid.startsWith('climate.'))); + if (mismatch) { + // Clean mappings tied to old primary + try { if (this._settingsDraft.labels && this._settingsDraft.labels[oldEid]) { const next = { ...(this._settingsDraft.labels||{}) }; delete next[oldEid]; this._settingsDraft.labels = next; this._labelsTouched = true; } } catch {} + try { + const merges = { ...(this._settingsDraft.merges||{}) }; + if (merges[oldEid]) delete merges[oldEid]; + for (const k of Object.keys(merges)) { + merges[k] = (merges[k] || []).filter(x => String(x) !== String(oldEid)); + if (!merges[k].length) delete merges[k]; + } + this._settingsDraft.merges = merges; + } catch {} + try { if (this._settingsDraft.temp_sensors && this._settingsDraft.temp_sensors[oldEid]) { const next = { ...(this._settingsDraft.temp_sensors||{}) }; delete next[oldEid]; this._settingsDraft.temp_sensors = next; } } catch {} + try { if (this._settingsDraft.turn_on && this._settingsDraft.turn_on[oldEid]) { const next = { ...(this._settingsDraft.turn_on||{}) }; delete next[oldEid]; this._settingsDraft.turn_on = next; } } catch {} + try { if (this._settingsDraft.presence_sensors && this._settingsDraft.presence_sensors[oldEid]) { const next = { ...(this._settingsDraft.presence_sensors||{}) }; delete next[oldEid]; this._settingsDraft.presence_sensors = next; } } catch {} + this._settingsDraft.entities[idx] = ''; + } + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + modeRow.append(modeText, modeSwitch); + + const pick = document.createElement('tt-entity-picker'); + try { pick.style.display = 'block'; pick.style.minHeight = '48px'; pick.style.maxWidth = '440px'; } catch {} + try { pick.hass = this._hass; } catch {} + pick.setAttribute('include-domains', useInput ? '["input_number"]' : '["climate"]'); + pick.setAttribute('label', this._t('editor.entity_placeholder') || 'Entity'); + try { pick.value = String(ents[idx] || ''); } catch {} + pick.entityFilter = (st) => { + try { + const id = st?.entity_id || st?.entityId || st?.entityID || ''; + if (!id) return false; + const current = String(ents[idx] || ''); + const blocked = new Set([...usedPrimary, ...usedLinked]); + return !blocked.has(id) || id === current; + } catch { return true; } + }; + + const nameLabel = document.createElement('div'); + nameLabel.className = 'label'; + nameLabel.textContent = this._t('editor.display_name_optional') || 'Room name (optional)'; + + const nameInp = document.createElement('input'); + nameInp.type = 'text'; + nameInp.className = 'label-input tt-input tt-input-lg'; + try { nameInp.style.width = '100%'; nameInp.style.maxWidth = '440px'; } catch {} + nameInp.setAttribute('placeholder', this._t('editor.display_name_optional') || 'Room name (optional)'); + try { + const curEid = String(ents[idx] || ''); + nameInp.value = (d.labels && curEid && d.labels[curEid]) ? String(d.labels[curEid]) : ''; + } catch {} + const setDraftLabel = (eid, value)=>{ + try { + if (!eid) return; + const labels = { ...(this._settingsDraft.labels || {}) }; + if (value) labels[eid] = value; + else delete labels[eid]; + this._settingsDraft.labels = labels; + // Mark labels as edited so an empty labels object should clear stored labels + this._labelsTouched = true; + } catch {} + }; + // Important: don't re-render the whole Rooms tab on every keystroke, + // otherwise the input element gets recreated and loses focus. + nameInp.addEventListener('input', (e)=>{ + try { + const eid = String(this._settingsDraft?.entities?.[idx] || '').trim(); + if (!eid) return; + const raw = String(e?.target?.value ?? ''); + setDraftLabel(eid, raw ? raw : ''); + } catch {} + }); + nameInp.addEventListener('change', (e)=>{ + try { + const eid = String(this._settingsDraft?.entities?.[idx] || '').trim(); + if (!eid) return; + const val = String(e?.target?.value ?? '').trim(); + setDraftLabel(eid, val); + try { e.target.value = val; } catch {} + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + + // Per-room: optional turn_on behavior + const turnOnWrap = document.createElement('div'); + turnOnWrap.style.display = 'grid'; + turnOnWrap.style.gap = '6px'; + const turnOnRow = document.createElement('div'); + turnOnRow.style.display = 'flex'; + turnOnRow.style.alignItems = 'center'; + turnOnRow.style.justifyContent = 'space-between'; + turnOnRow.style.gap = '10px'; + const turnOnText = document.createElement('div'); + turnOnText.style.display = 'grid'; + turnOnText.style.gap = '2px'; + const turnOnTitle = document.createElement('div'); + turnOnTitle.style.fontWeight = '600'; + turnOnTitle.textContent = this._t('editor.turn_on.title') || 'Send turn_on command'; + const turnOnDesc = document.createElement('div'); + turnOnDesc.style.fontSize = '.85rem'; + turnOnDesc.style.color = 'var(--secondary-text-color)'; + turnOnDesc.textContent = this._t('editor.turn_on.desc') || ''; + turnOnText.append(turnOnTitle, turnOnDesc); + const turnOnSwitch = document.createElement('ha-switch'); + turnOnSwitch.className = 'turn-on-switch'; + + // Dropdown (only visible when enabled) + const turnOnOrderRow = document.createElement('div'); + turnOnOrderRow.style.display = 'grid'; + turnOnOrderRow.style.gap = '2px'; + const orderLbl = document.createElement('div'); + orderLbl.className = 'slabel'; + orderLbl.textContent = this._t('editor.turn_on.order') || 'Order'; + const orderSel = document.createElement('select'); + orderSel.className = 'turn-on-order'; + try { + orderSel.style.height = '32px'; + orderSel.style.border = '1px solid var(--divider-color)'; + orderSel.style.borderRadius = '8px'; + orderSel.style.background = 'var(--card-background-color)'; + orderSel.style.color = 'var(--primary-text-color)'; + orderSel.style.padding = '0 10px'; + orderSel.style.width = '100%'; + orderSel.style.maxWidth = '440px'; + } catch {} + const optBefore = document.createElement('option'); + optBefore.value = 'before'; + optBefore.textContent = this._t('editor.turn_on.order.before') || 'Before set_temperature'; + const optAfter = document.createElement('option'); + optAfter.value = 'after'; + optAfter.textContent = this._t('editor.turn_on.order.after') || 'After set_temperature'; + orderSel.append(optBefore, optAfter); + + const readTurnOnCfg = ()=>{ + try { + const eid = String(this._settingsDraft?.entities?.[idx] || '').trim(); + const map = (this._settingsDraft?.turn_on && typeof this._settingsDraft.turn_on === 'object') ? this._settingsDraft.turn_on : {}; + const cfg0 = (eid && map[eid] && typeof map[eid] === 'object') ? map[eid] : {}; + const enabled = !!cfg0.enabled; + const order = String(cfg0.order || 'before'); + return { eid, enabled, order: (order === 'after' ? 'after' : 'before') }; + } catch { return { eid: String(this._settingsDraft?.entities?.[idx] || '').trim(), enabled: false, order: 'before' }; } + }; + const writeTurnOnCfg = (eid, patch)=>{ + try { + if (!eid) return; + const prev = (this._settingsDraft.turn_on && typeof this._settingsDraft.turn_on === 'object') ? this._settingsDraft.turn_on : {}; + const cur = (prev[eid] && typeof prev[eid] === 'object') ? prev[eid] : {}; + this._settingsDraft.turn_on = { ...prev, [eid]: { ...cur, ...patch } }; + } catch {} + }; + const applyTurnOnUi = ()=>{ + const { enabled, order } = readTurnOnCfg(); + try { turnOnSwitch.checked = !!enabled; } catch {} + try { orderSel.value = (order === 'after' ? 'after' : 'before'); } catch {} + try { turnOnOrderRow.style.display = enabled ? '' : 'none'; } catch {} + }; + turnOnSwitch.addEventListener('change', (e)=>{ + try { + const { eid, order } = readTurnOnCfg(); + const enabled = !!e.target.checked; + writeTurnOnCfg(eid, { enabled, order }); + applyTurnOnUi(); + } catch {} + }); + orderSel.addEventListener('change', ()=>{ + try { + const { eid, enabled } = readTurnOnCfg(); + const order = String(orderSel.value || 'before'); + writeTurnOnCfg(eid, { enabled: !!enabled, order: (order === 'after' ? 'after' : 'before') }); + applyTurnOnUi(); + } catch {} + }); + applyTurnOnUi(); + turnOnRow.append(turnOnText, turnOnSwitch); + turnOnOrderRow.append(orderLbl, orderSel); + turnOnWrap.append(turnOnRow, turnOnOrderRow); + try { if (useInput) turnOnWrap.style.display = 'none'; } catch {} + + // Per-room: temperature sensor override (for display + boiler) + const tempWrap = document.createElement('div'); + tempWrap.style.display = 'grid'; + tempWrap.style.gap = '6px'; + + const tempRow = document.createElement('div'); + tempRow.style.display = 'flex'; + tempRow.style.alignItems = 'center'; + tempRow.style.justifyContent = 'space-between'; + tempRow.style.gap = '10px'; + + const tempText = document.createElement('div'); + tempText.style.display = 'grid'; + tempText.style.gap = '2px'; + const tempTitle = document.createElement('div'); + tempTitle.style.fontWeight = '600'; + tempTitle.textContent = this._t('editor.room_temp_sensor_override.title') || 'Use room temperature sensor'; + const tempDesc = document.createElement('div'); + tempDesc.style.fontSize = '.85rem'; + tempDesc.style.color = 'var(--secondary-text-color)'; + tempDesc.textContent = this._t('editor.room_temp_sensor_override.desc') || ''; + tempText.append(tempTitle, tempDesc); + + const tempSwitch = document.createElement('ha-switch'); + tempSwitch.className = 'room-temp-sensor-switch'; + + const tempPicker = document.createElement('tt-entity-picker'); + try { tempPicker.style.display = 'block'; tempPicker.style.minHeight = '48px'; tempPicker.style.maxWidth = '440px'; } catch {} + tempPicker.setAttribute('include-domains', '["sensor"]'); + tempPicker.setAttribute('label', this._t('editor.room_temp_sensor_override.entity') || (this._t('editor.temp_sensor') || 'Temperature sensor')); + try { tempPicker.hass = this._hass; } catch {} + + const applyTempUi = ()=>{ + try { + const primary = String(this._settingsDraft?.entities?.[idx] || '').trim(); + const can = !useInput && primary.startsWith('climate.'); + const enabled = can && !!this._settingsDraft?.room_use_temp_sensor?.[idx]; + tempRow.style.display = can ? '' : 'none'; + tempPicker.style.display = (can && enabled) ? '' : 'none'; + tempSwitch.checked = !!enabled; + tempSwitch.disabled = !can; + if (can) { + const cur = String(this._settingsDraft?.temp_sensors?.[primary] || '').trim(); + tempPicker.value = cur; + } else { + tempPicker.value = ''; + } + } catch {} + }; + + tempSwitch.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + if (!Array.isArray(this._settingsDraft.room_use_temp_sensor)) this._settingsDraft.room_use_temp_sensor = []; + while (this._settingsDraft.room_use_temp_sensor.length < this._settingsDraft.entities.length) this._settingsDraft.room_use_temp_sensor.push(false); + this._settingsDraft.room_use_temp_sensor[idx] = !!e.target.checked; + applyTempUi(); + } catch {} + }); + + tempPicker.addEventListener('value-changed', (e)=>{ + try { + if (!this._settingsDraft) return; + const primary = String(this._settingsDraft?.entities?.[idx] || '').trim(); + if (!primary) return; + const v = String(e?.detail?.value || '').trim(); + const next = { ...(this._settingsDraft.temp_sensors || {}) }; + if (v) next[primary] = v; else delete next[primary]; + this._settingsDraft.temp_sensors = next; + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + + tempRow.append(tempText, tempSwitch); + tempWrap.append(tempRow, tempPicker); + applyTempUi(); + + // Merge UI + const linkWrap = document.createElement('div'); + linkWrap.style.display = 'grid'; + linkWrap.style.gap = '6px'; + const linkLabel = document.createElement('div'); + linkLabel.className = 'label'; + linkLabel.textContent = this._t('editor.merge_label') || 'Merge with'; + const linkPicker = document.createElement('tt-entity-picker'); + try { linkPicker.style.display = 'block'; linkPicker.style.minHeight = '48px'; linkPicker.style.width = '100%'; linkPicker.style.maxWidth = '440px'; } catch {} + linkPicker.className = 'merge-picker'; + linkPicker.setAttribute('include-domains', '["climate"]'); + linkPicker.setAttribute('label', this._t('editor.merge_label') || 'Merge with'); + try { linkPicker.hass = this._hass; } catch {} + linkPicker.entityFilter = (st) => { + try { + const id = st?.entity_id || st?.entityId || st?.entityID || ''; + if (!id) return false; + const prims = (this._settingsDraft?.entities || []).filter(Boolean).map(String); + const allLinked = Object.values(this._settingsDraft?.merges || {}).flat().filter(Boolean).map(String); + const used = new Set([...prims, ...allLinked]); + const cur = String(this._settingsDraft?.entities?.[idx] || ''); + return !used.has(id) && id !== cur; + } catch { return true; } + }; + + const chips = document.createElement('div'); + chips.className = 'linked-chips'; + + const refreshSubtitle = ()=>{ + try { + const eid = String(this._settingsDraft?.entities?.[idx] || ''); + const links = (this._settingsDraft?.merges?.[eid] || []).map(String); + const linkedNames = links.map(id => this._prettyName(id)); + const parts = []; + if (eid) parts.push(eid); + if (linkedNames.length) parts.push(`${this._t('editor.merged_with') || 'Merged with'}: ${linkedNames.join(', ')}`); + subSpan.textContent = parts.join(' • '); + } catch { subSpan.textContent = ''; } + }; + + const refreshChips = ()=>{ + try { + chips.innerHTML = ''; + const primary = String(this._settingsDraft?.entities?.[idx] || ''); + const links = (this._settingsDraft?.merges?.[primary] || []).map(String); + for (const l of links) { + const chip = document.createElement('span'); + chip.className = 'pill-chip'; + const txt = document.createElement('span'); + txt.textContent = this._prettyName(l); + const rm = document.createElement('button'); + rm.type = 'button'; + rm.className = 'rm'; + rm.textContent = '×'; + try { rm.setAttribute('aria-label', this._t('editor.remove') || 'Remove'); } catch {} + rm.addEventListener('click', ()=>{ + try { + const merges = { ...(this._settingsDraft.merges || {}) }; + merges[primary] = (merges[primary] || []).filter(x => String(x) !== String(l)); + if (!merges[primary]?.length) delete merges[primary]; + this._settingsDraft.merges = merges; + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + chip.append(txt, rm); + chips.append(chip); + } + refreshSubtitle(); + } catch {} + }; + + linkPicker.addEventListener('value-changed', (e)=>{ + try { + const toAdd = String(e?.detail?.value || ''); + try { linkPicker.value = ''; } catch {} + const primary = String(this._settingsDraft?.entities?.[idx] || ''); + if (!primary || !toAdd) return; + const merges = { ...(this._settingsDraft.merges || {}) }; + const arr = Array.from(new Set([...(merges[primary] || []).map(String), toAdd])); + merges[primary] = arr; + this._settingsDraft.merges = merges; + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + + left.append(modeRow, pick, nameLabel, nameInp, tempWrap, turnOnWrap, linkWrap, chips); + try { linkWrap.append(linkLabel, linkPicker); } catch {} + details.append(left); + + // Summary title + try { + const eid = String(ents[idx] || '').trim(); + titleSpan.textContent = eid ? pretty(eid) : (this._t('editor.entity_placeholder') || 'Select entity'); + } catch { titleSpan.textContent = this._t('editor.entity_placeholder') || 'Select entity'; } + refreshSubtitle(); + + // Remove room + del.addEventListener('click', (ev)=>{ + try { ev.stopPropagation?.(); } catch {} + try { + const arr = [...(this._settingsDraft.entities || [])]; + const modes = Array.isArray(this._settingsDraft.room_use_input_number) ? [...this._settingsDraft.room_use_input_number] : []; + const tmodes = Array.isArray(this._settingsDraft.room_use_temp_sensor) ? [...this._settingsDraft.room_use_temp_sensor] : []; + const primary = String(arr[idx] || ''); + arr.splice(idx, 1); + try { if (modes.length) modes.splice(idx, 1); } catch {} + try { if (tmodes.length) tmodes.splice(idx, 1); } catch {} + + // Clean labels + try { + const labels = { ...(this._settingsDraft.labels || {}) }; + if (primary && labels[primary]) delete labels[primary]; + this._settingsDraft.labels = labels; + } catch {} + // Clean merges + try { + const merges = { ...(this._settingsDraft.merges || {}) }; + if (primary && merges[primary]) delete merges[primary]; + for (const k of Object.keys(merges)) { + merges[k] = (merges[k] || []).filter(e => String(e) !== String(primary)); + if (!merges[k].length) delete merges[k]; + } + this._settingsDraft.merges = merges; + } catch {} + // Clean temp sensors + try { + const sensors = { ...(this._settingsDraft.temp_sensors || {}) }; + if (primary && sensors[primary]) delete sensors[primary]; + this._settingsDraft.temp_sensors = sensors; + } catch {} + + // Clean presence sensors + try { + const ps = { ...(this._settingsDraft.presence_sensors || {}) }; + if (primary && ps[primary]) delete ps[primary]; + this._settingsDraft.presence_sensors = ps; + } catch {} + + // Clean turn_on + try { + const t = { ...(this._settingsDraft.turn_on || {}) }; + if (primary && t[primary]) delete t[primary]; + this._settingsDraft.turn_on = t; + } catch {} + + this._settingsDraft.entities = arr; + this._settingsDraft.room_use_input_number = modes; + this._settingsDraft.room_use_temp_sensor = tmodes; + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + + // Primary picker change + pick.addEventListener('value-changed', (e)=>{ + try { + const newPrimary = String(e?.detail?.value || '').trim(); + const oldPrimary = String(this._settingsDraft?.entities?.[idx] || '').trim(); + + // Auto-sync mode based on chosen domain + try { + while (this._settingsDraft.room_use_input_number.length < this._settingsDraft.entities.length) this._settingsDraft.room_use_input_number.push(false); + if (newPrimary.startsWith('input_number.')) this._settingsDraft.room_use_input_number[idx] = true; + else if (newPrimary.startsWith('climate.')) this._settingsDraft.room_use_input_number[idx] = false; + } catch {} + + // If switching away from climate, disable temp sensor override for this row + try { + if (!Array.isArray(this._settingsDraft.room_use_temp_sensor)) this._settingsDraft.room_use_temp_sensor = []; + while (this._settingsDraft.room_use_temp_sensor.length < this._settingsDraft.entities.length) this._settingsDraft.room_use_temp_sensor.push(false); + if (!newPrimary.startsWith('climate.')) this._settingsDraft.room_use_temp_sensor[idx] = false; + } catch {} + + // Migrate merges/temp sensors when primary changes + if (oldPrimary && newPrimary && oldPrimary !== newPrimary) { + try { + const merges = { ...(this._settingsDraft.merges || {}) }; + if (merges[oldPrimary] && !merges[newPrimary]) { merges[newPrimary] = merges[oldPrimary]; delete merges[oldPrimary]; } + else if (merges[oldPrimary] && merges[newPrimary]) { + merges[newPrimary] = Array.from(new Set([...(merges[newPrimary]||[]), ...(merges[oldPrimary]||[])])); + delete merges[oldPrimary]; + } + this._settingsDraft.merges = merges; + } catch {} + try { + const sensors = { ...(this._settingsDraft.temp_sensors || {}) }; + if (sensors[oldPrimary] && !sensors[newPrimary]) sensors[newPrimary] = sensors[oldPrimary]; + if (sensors[oldPrimary]) delete sensors[oldPrimary]; + this._settingsDraft.temp_sensors = sensors; + } catch {} + try { + const labels = { ...(this._settingsDraft.labels || {}) }; + if (labels[oldPrimary] && !labels[newPrimary]) labels[newPrimary] = labels[oldPrimary]; + if (labels[oldPrimary]) delete labels[oldPrimary]; + this._settingsDraft.labels = labels; + } catch {} + + try { + const t = { ...(this._settingsDraft.turn_on || {}) }; + if (t[oldPrimary] && !t[newPrimary]) t[newPrimary] = t[oldPrimary]; + if (t[oldPrimary]) delete t[oldPrimary]; + this._settingsDraft.turn_on = t; + } catch {} + + try { + const ps = { ...(this._settingsDraft.presence_sensors || {}) }; + if (ps[oldPrimary] && !ps[newPrimary]) ps[newPrimary] = ps[oldPrimary]; + if (ps[oldPrimary]) delete ps[oldPrimary]; + this._settingsDraft.presence_sensors = ps; + } catch {} + // Move open-state key old -> new + try { + if (this._openPopupRooms && this._openPopupRooms.has(oldPrimary)) { this._openPopupRooms.delete(oldPrimary); this._openPopupRooms.add(newPrimary); } + } catch {} + } + + const next = [...(this._settingsDraft.entities || [])]; + next[idx] = newPrimary; + this._settingsDraft.entities = next; + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + + // Drag reorder + handle.addEventListener('mousedown', (ev)=>{ + try { ev.preventDefault(); } catch {} + try { + const wrap = line.parentElement; + const curIdx = wrap ? Array.prototype.indexOf.call(wrap.children, line) : idx; + this._startSettingsPopupRoomsPointerDnd(line, curIdx, ev); + } catch { this._startSettingsPopupRoomsPointerDnd(line, idx, ev); } + }); + handle.addEventListener('touchstart', (ev)=>{ + try { ev.preventDefault(); } catch {} + try { + const wrap = line.parentElement; + const curIdx = wrap ? Array.prototype.indexOf.call(wrap.children, line) : idx; + this._startSettingsPopupRoomsPointerDnd(line, curIdx, ev); + } catch { this._startSettingsPopupRoomsPointerDnd(line, idx, ev); } + }, { passive: false }); + + return line; + }; + + // Add rows + for (let i=0;i{ + try { + if (!this._settingsDraft) return; + this._settingsDraft.entities = [...(this._settingsDraft.entities || []), '']; + const modes = Array.isArray(this._settingsDraft.room_use_input_number) ? [...this._settingsDraft.room_use_input_number] : []; + modes.push(false); + this._settingsDraft.room_use_input_number = modes; + const tmodes = Array.isArray(this._settingsDraft.room_use_temp_sensor) ? [...this._settingsDraft.room_use_temp_sensor] : []; + tmodes.push(false); + this._settingsDraft.room_use_temp_sensor = tmodes; + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + } catch {} + } + } catch {} + } + + _startSettingsPopupRoomsPointerDnd(line, idx, startEv){ + try { + const wrap = line.parentElement; + if (!wrap) return; + if (this._settingsPopupPointerDndActive) return; + this._settingsPopupPointerDndActive = true; + + const rect = line.getBoundingClientRect(); + const ghost = line.cloneNode(true); + ghost.classList.add('drag-ghost'); + ghost.style.width = rect.width + 'px'; + ghost.style.height = rect.height + 'px'; + ghost.style.top = rect.top + 'px'; + ghost.style.left = rect.left + 'px'; + + const placeholder = document.createElement('div'); + placeholder.className = 'placeholder'; + placeholder.style.height = rect.height + 'px'; + + // place placeholder where line was, and hide line + wrap.insertBefore(placeholder, line); + line.style.display = 'none'; + document.body.appendChild(ghost); + + const readClient = (e)=>{ + if (e?.touches && e.touches[0]) return { x: e.touches[0].clientX, y: e.touches[0].clientY }; + return { x: e?.clientX, y: e?.clientY }; + }; + + const onMove = (ev)=>{ + try { ev.preventDefault(); } catch {} + const { y, x } = readClient(ev); + if (!Number.isFinite(y) || !Number.isFinite(x)) return; + ghost.style.top = (y - rect.height/2) + 'px'; + + // find element under pointer within this shadowRoot + let el = this.shadowRoot && this.shadowRoot.elementFromPoint(x, y); + // climb to row in the same wrap + while (el && el !== wrap && !el.classList?.contains('inline')) el = el.parentElement; + if (!el || el===placeholder || el===wrap) return; + const targetRect = el.getBoundingClientRect(); + const before = (y - targetRect.top) < targetRect.height/2; + if (before) wrap.insertBefore(placeholder, el); + else wrap.insertBefore(placeholder, el.nextSibling); + }; + + const endDrag = ()=>{ + window.removeEventListener('mousemove', onMove, true); + window.removeEventListener('mouseup', onUp, true); + window.removeEventListener('touchmove', onMove, true); + window.removeEventListener('touchend', onUp, true); + try { ghost.remove(); } catch {} + + try { + const from = idx; + const to = Array.prototype.indexOf.call(wrap.children, placeholder); + const arr = [...(this._settingsDraft?.entities || [])]; + const modes = Array.isArray(this._settingsDraft?.room_use_input_number) ? [...this._settingsDraft.room_use_input_number] : []; + const tmodes = Array.isArray(this._settingsDraft?.room_use_temp_sensor) ? [...this._settingsDraft.room_use_temp_sensor] : []; + while (modes.length < arr.length) modes.push(false); + while (tmodes.length < arr.length) tmodes.push(false); + const [item] = arr.splice(from,1); + const [mItem] = modes.splice(from,1); + const [tItem] = tmodes.splice(from,1); + const correctedTo = Math.max(0, Math.min(to, arr.length)); + arr.splice(correctedTo, 0, item); + modes.splice(correctedTo, 0, mItem); + tmodes.splice(correctedTo, 0, tItem); + this._settingsDraft.entities = arr; + this._settingsDraft.room_use_input_number = modes; + this._settingsDraft.room_use_temp_sensor = tmodes; + } catch {} + + try { placeholder.remove(); } catch {} + try { line.style.display = ''; } catch {} + this._settingsPopupPointerDndActive = false; + try { this._renderSettingsPopupRoomsTab(); } catch {} + }; + + const onUp = (ev)=>{ try { ev.preventDefault?.(); } catch {} endDrag(); }; + + window.addEventListener('mousemove', onMove, true); + window.addEventListener('mouseup', onUp, true); + window.addEventListener('touchmove', onMove, true); + window.addEventListener('touchend', onUp, true); + + // Initial nudge so the ghost doesn't feel stuck under finger + try { + const { x, y } = readClient(startEv || {}); + if (Number.isFinite(y) && Number.isFinite(x)) ghost.style.top = (y - rect.height/2) + 'px'; + } catch {} + } catch { this._settingsPopupPointerDndActive = false; } + } + + _renderSettingsPopupAwayTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="away"]'); + if (!page) return; + + // Build markup once + if (!page.dataset.built) { + page.dataset.built = '1'; + page.innerHTML = ` +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    + + + + +
    +
    +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + `; + } + + // Ensure draft shape + if (!d.away || typeof d.away !== 'object') d.away = { enabled:false, target_c:17, persons:[], advanced_enabled:false, combos:{} }; + if (!Array.isArray(d.away.persons)) d.away.persons = []; + if (!d.away.combos || typeof d.away.combos !== 'object') d.away.combos = {}; + + const q = (sel)=> this.shadowRoot.querySelector(sel); + const awayOn = !!d.away.enabled; + const advOn = !!d.away.advanced_enabled; + + // Show/hide dependent controls based on master toggle + try { + const tempRow = q('.overlay-settings .sp-away-temp-row'); + const personsRow = q('.overlay-settings .sp-away-persons-row'); + const advRow = q('.overlay-settings .sp-away-adv-row'); + const delayRow = q('.overlay-settings .sp-away-delay-row'); + const liveRow = q('.overlay-settings .sp-presence-live-row'); + const combosRow = q('.overlay-settings .sp-away-combos-row'); + [tempRow, personsRow, delayRow, advRow, liveRow, combosRow].forEach(r=>{ if (r) r.style.display = awayOn ? '' : 'none'; }); + } catch {} + + // Toggle states + try { + const sw = q('.overlay-settings .sp-away-enable'); + if (sw) sw.checked = awayOn; + } catch {} + try { + const adv = q('.overlay-settings .sp-away-adv-enable'); + if (adv) adv.checked = advOn; + } catch {} + try { + const pl = q('.overlay-settings .sp-presence-live-enable'); + if (pl) { + pl.checked = !!(d.presence_live_header ?? true); + // Enabled whenever Away mode is enabled + pl.disabled = !awayOn; + } + } catch {} + + // Temperature field formatting based on draft unit + try { + const inp = q('.overlay-settings .sp-away-temp'); + if (inp) { + const wantF = String(d.temp_unit || 'C').toUpperCase() === 'F'; + const toDisp = (c)=> wantF ? ((Number(c) * 9/5) + 32) : Number(c); + const mnC = Number.isFinite(Number(d.min_temp)) ? Number(d.min_temp) : (Number.isFinite(Number(this._config?.min_temp)) ? Number(this._config.min_temp) : 5); + const mxC = Number.isFinite(Number(d.max_temp)) ? Number(d.max_temp) : (Number.isFinite(Number(this._config?.max_temp)) ? Number(this._config.max_temp) : 25); + inp.setAttribute('min', String(toDisp(mnC))); + inp.setAttribute('max', String(toDisp(mxC))); + if (inp !== this.shadowRoot.activeElement) { + const v = Number.isFinite(Number(d.away.target_c)) ? Number(d.away.target_c) : 17; + inp.value = String(Math.round(toDisp(v) * 2) / 2); + } + } + } catch {} + + // Delay controls + try { + const en = q('.overlay-settings .sp-away-delay-enable'); if (en) en.checked = !!d.away_delay_enabled; + const val = q('.overlay-settings .sp-away-delay-value'); if (val && val !== this.shadowRoot.activeElement) val.value = String(Number.isFinite(Number(d.away_delay_value)) ? Number(d.away_delay_value) : 0); + const unit = q('.overlay-settings .sp-away-delay-unit'); if (unit) unit.value = (String(d.away_delay_unit || 'minutes')); + } catch {} + + // Entity picker + try { + const picker = q('.overlay-settings .sp-away-person-picker'); + if (picker) { + picker.hass = this._hass; + picker.value = ''; + try { picker.setAttribute('label', this._t('away.add_person') || 'Add person'); } catch {} + } + } catch {} + + // Chips + try { + const chips = q('.overlay-settings .sp-away-chips'); + if (chips) { + chips.innerHTML = ''; + const arr = Array.isArray(d.away.persons) ? d.away.persons.filter(Boolean).map(String) : []; + for (const p of arr) { + const chip = document.createElement('span'); chip.className = 'pill-chip'; + const txt = document.createElement('span'); txt.textContent = this._hass?.states?.[p]?.attributes?.friendly_name || (p.split('.')[1]||p); + const rm = document.createElement('button'); rm.type='button'; rm.className='rm'; rm.textContent='×'; + try { rm.setAttribute('aria-label', this._t('editor.remove') || 'Remove'); } catch {} + rm.addEventListener('click', ()=>{ + try { + if (!this._settingsDraft?.away) return; + this._settingsDraft.away.persons = (this._settingsDraft.away.persons||[]).filter(x=>String(x)!==String(p)); + this._renderSettingsPopupAwayTab(); + } catch {} + }); + chip.append(txt, rm); + chips.append(chip); + } + } + } catch {} + + // Presence combinations checklist + try { + const comboWrap = q('.overlay-settings .sp-presence-combo-list'); + if (comboWrap) { + comboWrap.innerHTML = ''; + const persons = (d.away.persons || []).map(String); + const enabled = !!d.away.advanced_enabled; + const conf = (d.away.combos && typeof d.away.combos === 'object') ? d.away.combos : {}; + const n = persons.length; + const labelOf = (arr)=> arr.map(e => (this._hass?.states?.[e]?.attributes?.friendly_name || (e.split('.')[1]||e))).join(', '); + for (let mask=0; mask < (1<>i)&1) ? home.push(persons[i]) : away.push(persons[i]); } + // Skip "All home" entry; fallback to main/weekdays when all are home + if (home.length===n) continue; + const key = `H:${home.slice().sort().join(',')}|A:${away.slice().sort().join(',')}`; + const row = document.createElement('label'); + row.style.display='flex'; + row.style.alignItems='center'; + row.style.gap='8px'; + row.style.opacity = enabled ? '' : '.6'; + const cb = document.createElement('input'); + cb.type='checkbox'; + cb.disabled = !enabled || !persons.length; + cb.checked = !!conf[key]?.enabled; + cb.addEventListener('change', ()=>{ + try { + if (!this._settingsDraft?.away) return; + const map = { ...(this._settingsDraft.away.combos || {}) }; + if (cb.checked) map[key] = { ...(map[key]||{}), enabled:true }; + else delete map[key]; + this._settingsDraft.away.combos = map; + } catch {} + }); + const sp = document.createElement('span'); + let lab; + if (home.length===0) lab = this._t('presence.none_home'); + else { + const h = (this._t('presence.home') || 'Home: {names}').replace('{names}', labelOf(home)); + const a = (this._t('presence.away') || 'Away: {names}').replace('{names}', labelOf(away)); + lab = `${h} • ${a}`; + } + sp.textContent = lab; + row.append(cb, sp); + comboWrap.append(row); + } + } + } catch {} + + // Bind events once + if (!page.dataset.bound) { + page.dataset.bound = '1'; + + try { + const sw = q('.overlay-settings .sp-away-enable'); + if (sw) sw.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft?.away) return; + const enabled = !!e.target.checked; + this._settingsDraft.away.enabled = enabled; + // If Away is turned OFF, also disable Advanced Away and live presence chips + if (!enabled) { + this._settingsDraft.away.advanced_enabled = false; + this._settingsDraft.presence_live_header = false; + } + this._renderSettingsPopupAwayTab(); + } catch {} + }); + } catch {} + + try { + const inp = q('.overlay-settings .sp-away-temp'); + if (inp) inp.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft?.away) return; + let raw = String(e.target.value||'').trim().replace(',', '.'); + const vDisp = Number(raw); + if (!Number.isFinite(vDisp)) return; + const wantF = String(this._settingsDraft.temp_unit || 'C').toUpperCase() === 'F'; + const fromDisp = (x)=> wantF ? ((Number(x) - 32) * 5/9) : Number(x); + let vC = fromDisp(vDisp); + const mn = Number.isFinite(Number(this._settingsDraft.min_temp)) ? Number(this._settingsDraft.min_temp) : 5; + const mx = Number.isFinite(Number(this._settingsDraft.max_temp)) ? Number(this._settingsDraft.max_temp) : 25; + vC = Math.max(mn, Math.min(mx, vC)); + this._settingsDraft.away.target_c = vC; + } catch {} + }); + } catch {} + + try { + const picker = q('.overlay-settings .sp-away-person-picker'); + if (picker) picker.addEventListener('value-changed', (e)=>{ + try { + const val = e?.detail?.value; + if (!val || !this._settingsDraft?.away) return; + picker.value = ''; + const set = new Set((this._settingsDraft.away.persons || []).map(String)); + set.add(String(val)); + this._settingsDraft.away.persons = Array.from(set); + this._renderSettingsPopupAwayTab(); + } catch {} + }); + } catch {} + + try { + const adv = q('.overlay-settings .sp-away-adv-enable'); + if (adv) adv.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft?.away) return; + this._settingsDraft.away.advanced_enabled = !!e.target.checked; + // Do not force presence chips off when advanced is disabled; + // availability is controlled by Away mode. + this._renderSettingsPopupAwayTab(); + } catch {} + }); + } catch {} + + try { + const pl = q('.overlay-settings .sp-presence-live-enable'); + if (pl) pl.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft?.away?.enabled) { + e.preventDefault?.(); + pl.checked = false; + this._settingsDraft.presence_live_header = false; + return; + } + this._settingsDraft.presence_live_header = !!e.target.checked; + } catch {} + }); + } catch {} + + // Delay bindings + try { + const en = q('.overlay-settings .sp-away-delay-enable'); + if (en) en.addEventListener('change', (e)=>{ try { this._settingsDraft.away_delay_enabled = !!e.target.checked; } catch {} }); + } catch {} + try { + const unit = q('.overlay-settings .sp-away-delay-unit'); + if (unit) unit.addEventListener('change', (e)=>{ try { this._settingsDraft.away_delay_unit = String(e.target.value||'minutes'); } catch {} }); + } catch {} + try { + const val = q('.overlay-settings .sp-away-delay-value'); + if (val) val.addEventListener('change', (e)=>{ try { const n=Number(e.target.value); this._settingsDraft.away_delay_value = Number.isFinite(n)? Math.max(0,Math.round(n)) : 0; } catch {} }); + } catch {} + } + } catch {} + } + + _renderSettingsPopupResetTab(){ + try { + if (!this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="reset"]'); + if (!page) return; + + if (!page.dataset.built) { + page.dataset.built = '1'; + page.innerHTML = ` +
    +
    +
    +
    +
    +
    + + +
    +
    + `; + } + + const q = (sel)=> this.shadowRoot.querySelector(sel); + const ttl = q('.overlay-settings .sp-reset-title'); + if (ttl) ttl.textContent = this._t('editor.tabs.reset') || 'Reset'; + const desc = q('.overlay-settings .sp-reset-desc'); + if (desc) { + let msg = String(this._t('editor.reset.desc') || '').trim(); + if (!msg) { + let short = String(this._t('editor.clear_all_confirm') || '').trim(); + short = short.replace(/\?\s*$/, ''); + msg = short || 'Clear storage + local/browser data.'; + } + desc.textContent = msg; + } + + const bAll = q('.overlay-settings .sp-reset-clear-all'); + if (bAll) { + const s = bAll.querySelector('span') || bAll; + if (s) s.textContent = this._t('editor.clear_all') || 'Clear all data'; + } + const bLocal = q('.overlay-settings .sp-reset-clear-local'); + if (bLocal) { + const s = bLocal.querySelector('span') || bLocal; + if (s) s.textContent = this._t('editor.clear_local_only') || 'Clear ALL local data (local only)'; + } + + if (!page.dataset.bound) { + page.dataset.bound = '1'; + try { + const btn = page.querySelector('.sp-reset-clear-all'); + if (btn) btn.addEventListener('click', async ()=>{ + const msg = this._t('editor.clear_all_confirm'); + if (!confirm(msg)) return; + try { await this._clearAllDataEverywhereConfirmed(); } catch {} + }); + } catch {} + try { + const btn = page.querySelector('.sp-reset-clear-local'); + if (btn) btn.addEventListener('click', ()=>{ + const msg = this._t('editor.clear_local_only_confirm'); + if (!confirm(msg)) return; + try { this._clearAllLocalOnlyConfirmed(); } catch {} + }); + } catch {} + } + } catch {} + } + + _renderSettingsPopupOwdTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="owd"]'); + if (!page) return; + page.innerHTML = ''; + try { page.style.display = ''; page.style.gap = ''; } catch {} + + // Ensure draft shape + const cur = (d.open_window && typeof d.open_window === 'object') ? d.open_window : (d.open_window = { enabled:false, sensors:{}, open_delay_min:2, close_delay_min:5 }); + if (typeof cur.enabled !== 'boolean') cur.enabled = false; + if (!cur.sensors || typeof cur.sensors !== 'object') cur.sensors = {}; + if (!Number.isFinite(Number(cur.open_delay_min))) cur.open_delay_min = 2; + if (!Number.isFinite(Number(cur.close_delay_min))) cur.close_delay_min = 5; + cur.open_delay_min = Math.max(0, Math.min(1440, Math.round(Number(cur.open_delay_min)))); + cur.close_delay_min = Math.max(0, Math.min(1440, Math.round(Number(cur.close_delay_min)))); + + // Card with all OWD content + const enableCard = document.createElement('div'); enableCard.className = 'settings-card'; + const enRow = document.createElement('div'); enRow.className = 'setting'; + const enText = document.createElement('div'); enText.className = 'text'; + const enTitle = document.createElement('div'); enTitle.className = 'title sp-owd-enable-title'; + const enDesc = document.createElement('div'); enDesc.className = 'desc sp-owd-enable-desc'; + enText.append(enTitle, enDesc); + const enSwitch = document.createElement('ha-switch'); enSwitch.className = 'sp-owd-enable'; enSwitch.checked = !!cur.enabled; + enSwitch.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft?.open_window) return; + this._settingsDraft.open_window.enabled = !!e.target.checked; + try { contentWrap.style.display = this._settingsDraft.open_window.enabled ? 'grid' : 'none'; } catch {} + } catch {} + }); + enRow.append(enText, enSwitch); + enableCard.append(enRow); + + // Remaining content shown only when enabled (inside the same card, like other tabs) + const contentWrap = document.createElement('div'); contentWrap.className = 'owd-content'; + try { contentWrap.style.display = cur.enabled ? 'grid' : 'none'; contentWrap.style.gap = '16px'; contentWrap.style.marginTop = '6px'; } catch {} + enableCard.append(contentWrap); + page.append(enableCard); + + // Rooms foldouts, wrapped in a setting row (like Boiler) + const ents = Array.isArray(this._config?.entities) ? this._config.entities : []; + const sensorsRow = document.createElement('div'); sensorsRow.className = 'setting'; sensorsRow.style.gridTemplateColumns = '1fr'; + const sensorsText = document.createElement('div'); sensorsText.className = 'text'; + const sensorsTitle = document.createElement('div'); sensorsTitle.className = 'title sp-owd-sensors-label'; + sensorsText.append(sensorsTitle); + sensorsRow.append(sensorsText); + + const roomsWrap = document.createElement('div'); + try { roomsWrap.style.display = 'grid'; roomsWrap.style.gap = '10px'; } catch {} + sensorsRow.append(roomsWrap); + contentWrap.append(sensorsRow); + + if (!ents.length) { + const box = document.createElement('div'); box.className = 'empty-box'; + const p = document.createElement('div'); p.textContent = this._t('editor.select_entities') || 'Rooms'; + const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'add-entity-btn'; + btn.innerHTML = '' + (this._t('editor.add_entity') || 'Add room') + ''; + btn.onclick = ()=>{ this._setSettingsPopupTab('rooms'); }; + box.append(p, btn); + roomsWrap.append(box); + } + + // Remember open/closed state while popup is open + try { if (!this._openOwdRooms) this._openOwdRooms = new Set(); } catch {} + + for (const eid of ents) { + if (!eid) continue; + // Foldout (same UI pattern as HA editor sections) + const line = document.createElement('div'); line.className = 'inline'; + const summary = document.createElement('div'); summary.className = 'summary'; + const sumLeft = document.createElement('div'); sumLeft.className = 'summary-left'; + const exp = document.createElement('button'); exp.type = 'button'; exp.className = 'expander'; + exp.innerHTML = ''; + const txtWrap = document.createElement('div'); txtWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); titleSpan.className = 'summary-title'; + + // Inline styles to match the editor foldout appearance (robust across contexts) + try { + summary.style.display = 'flex'; + summary.style.alignItems = 'center'; + summary.style.justifyContent = 'space-between'; + summary.style.padding = '6px'; + summary.style.border = '1px dashed var(--divider-color)'; + summary.style.borderRadius = '8px'; + summary.style.background = 'var(--secondary-background-color, transparent)'; + sumLeft.style.display = 'flex'; + sumLeft.style.alignItems = 'center'; + sumLeft.style.gap = '8px'; + sumLeft.style.minWidth = '0'; + txtWrap.style.display = 'grid'; + txtWrap.style.gap = '2px'; + txtWrap.style.minWidth = '0'; + titleSpan.style.fontWeight = '600'; + titleSpan.style.overflow = 'hidden'; + titleSpan.style.textOverflow = 'ellipsis'; + titleSpan.style.whiteSpace = 'nowrap'; + titleSpan.style.maxWidth = '320px'; + exp.style.width = '28px'; + exp.style.height = '28px'; + exp.style.borderRadius = '8px'; + exp.style.border = '1px solid var(--divider-color)'; + exp.style.background = 'var(--card-background-color)'; + exp.style.display = 'inline-flex'; + exp.style.alignItems = 'center'; + exp.style.justifyContent = 'center'; + exp.style.cursor = 'pointer'; + } catch {} + try { + const labels = this._config.labels || {}; + const display = labels[eid] || this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid); + titleSpan.textContent = display; + } catch { titleSpan.textContent = this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid); } + txtWrap.append(titleSpan); + sumLeft.append(exp, txtWrap); + summary.append(sumLeft); + line.append(summary); + + const details = document.createElement('div'); details.className = 'details'; + line.append(details); + + try { + details.style.display = 'none'; + details.style.gridTemplateColumns = 'minmax(250px, 440px)'; + details.style.gap = '6px'; + details.style.padding = '6px'; + details.style.border = '1px dashed var(--divider-color)'; + details.style.borderRadius = '8px'; + details.style.background = 'var(--secondary-background-color, transparent)'; + } catch {} + + const applyOpen = (isOpen)=>{ + try { + if (isOpen) { + line.classList.add('open'); + details.style.display = 'grid'; + exp.style.transform = 'rotate(180deg)'; + } else { + line.classList.remove('open'); + details.style.display = 'none'; + exp.style.transform = ''; + } + } catch {} + }; + + const toggle = (ev)=>{ + try { ev?.stopPropagation?.(); } catch {} + const nextOpen = !line.classList.contains('open'); + applyOpen(nextOpen); + try { + if (!this._openOwdRooms) this._openOwdRooms = new Set(); + if (nextOpen) this._openOwdRooms.add(String(eid)); + else this._openOwdRooms.delete(String(eid)); + } catch {} + }; + exp.addEventListener('click', toggle); + summary.style.cursor = 'pointer'; + summary.addEventListener('click', (ev)=>{ + // Allow clicking the row header to toggle as well + try { if (ev?.target?.closest?.('button, a, input, select, textarea, tt-entity-picker, ha-entity-picker')) return; } catch {} + toggle(ev); + }); + try { applyOpen(!!this._openOwdRooms?.has(String(eid))); } catch {} + + const wrap = document.createElement('div'); + wrap.style.display = 'grid'; + wrap.style.gap = '8px'; + const picker = document.createElement('tt-entity-picker'); + picker.className = 'sp-owd-sensor-picker'; + picker.setAttribute('include-domains', '["binary_sensor"]'); + try { picker.hass = this._hass; } catch {} + try { picker.value = ''; } catch {} + const chips = document.createElement('div'); chips.className = 'linked-chips sp-owd-chips'; + + const renderChips = ()=>{ + chips.innerHTML = ''; + const map = (this._settingsDraft?.open_window?.sensors && typeof this._settingsDraft.open_window.sensors === 'object') ? this._settingsDraft.open_window.sensors : {}; + const arr = Array.isArray(map[eid]) ? map[eid] : []; + for (const sEid of arr) { + const chip = document.createElement('span'); chip.className = 'pill-chip'; + const txt = document.createElement('span'); txt.textContent = this._hass?.states?.[sEid]?.attributes?.friendly_name || (sEid.split('.')[1]||sEid); + const rm = document.createElement('button'); rm.type='button'; rm.className='rm'; rm.textContent='×'; rm.setAttribute('aria-label', this._t('editor.remove')); + rm.addEventListener('click', ()=>{ + try { + const ow = this._settingsDraft?.open_window; + if (!ow || !ow.sensors) return; + const next = { ...(ow.sensors || {}) }; + const curArr = Array.isArray(next[eid]) ? next[eid] : []; + const out = curArr.filter(x => x !== sEid); + if (out.length) next[eid] = out; else delete next[eid]; + ow.sensors = next; + renderChips(); + } catch {} + }); + chip.append(txt, rm); + chips.append(chip); + } + }; + renderChips(); + + picker.addEventListener('value-changed', (e)=>{ + const val = e.detail?.value || ''; + if (!val) return; + try { picker.value = ''; } catch {} + try { + const ow = this._settingsDraft?.open_window; + if (!ow) return; + const next = { ...(ow.sensors || {}) }; + const set = new Set(Array.isArray(next[eid]) ? next[eid] : []); + set.add(String(val)); + next[eid] = Array.from(set); + ow.sensors = next; + renderChips(); + } catch {} + }); + + wrap.append(picker, chips); + details.append(wrap); + roomsWrap.append(line); + } + + // Global delay settings + // Delay row inside same card + const row = document.createElement('div'); row.className = 'setting'; row.style.gridTemplateColumns = '1fr'; + const txt = document.createElement('div'); txt.className = 'text'; + const title = document.createElement('div'); title.className = 'title sp-owd-delays-title'; + const desc = document.createElement('div'); desc.className = 'desc sp-owd-delays-desc'; + txt.append(title, desc); + row.append(txt); + contentWrap.append(row); + const grid = document.createElement('div'); + grid.style.display = 'flex'; + grid.style.gap = '10px'; + grid.style.flexWrap = 'wrap'; + grid.style.alignItems = 'flex-end'; + + const mkField = (cls, labelKey, value, onChange)=>{ + const f = document.createElement('div'); f.className = 'sfield'; + const l = document.createElement('div'); l.className = 'slabel ' + cls + '-label'; l.textContent = (this._t(labelKey) || labelKey); + const inp = document.createElement('input'); inp.className = cls + ' settings-input'; inp.type = 'number'; inp.min='0'; inp.max='1440'; inp.step='1'; + if (inp !== this.shadowRoot.activeElement) inp.value = String(value); + inp.addEventListener('change', (e)=>{ + let v = Math.round(Number(String(e.target.value||'').replace(',','.'))); + if (!Number.isFinite(v)) v = 0; + v = Math.max(0, Math.min(1440, v)); + e.target.value = String(v); + onChange(v); + }); + f.append(l, inp); + return f; + }; + + const openField = mkField('sp-owd-open-delay', 'owd.open_delay', Number(cur.open_delay_min||0), (v)=>{ try { if (!this._settingsDraft?.open_window) return; this._settingsDraft.open_window.open_delay_min = v; } catch {} }); + const closeField = mkField('sp-owd-close-delay', 'owd.close_delay', Number(cur.close_delay_min||0), (v)=>{ try { if (!this._settingsDraft?.open_window) return; this._settingsDraft.open_window.close_delay_min = v; } catch {} }); + grid.append(openField, closeField); + contentWrap.append(grid); + } catch {} + } + + _renderSettingsPopupHolidaysTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="holidays"]'); + if (!page) return; + + // Build markup once + if (!page.dataset.built) { + page.dataset.built = '1'; + page.innerHTML = ` +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    + `; + } + + const q = (sel)=> this.shadowRoot.querySelector(sel); + + // Ensure draft shape + if (!Array.isArray(d.holidays_dates)) d.holidays_dates = []; + if (d.holidays_source !== 'manual') d.holidays_source = 'calendar'; + + // Visibility of dependent controls + const holOn = !!d.holidays_enabled; + const src = (d.holidays_source === 'manual') ? 'manual' : 'calendar'; + try { + const editRow = q('.overlay-settings .sp-holidays-edit-setting'); + const srcRow = q('.overlay-settings .sp-holidays-source-setting'); + const entRow = q('.overlay-settings .sp-holidays-entity-setting'); + const datesRow = q('.overlay-settings .sp-holidays-dates-setting'); + if (editRow) editRow.style.display = holOn ? '' : 'none'; + if (srcRow) srcRow.style.display = holOn ? '' : 'none'; + if (entRow) entRow.style.display = (holOn && src === 'calendar') ? '' : 'none'; + if (datesRow) datesRow.style.display = (holOn && src === 'manual') ? '' : 'none'; + } catch {} + + // Segmented UI state + try { + const bCal = q('.overlay-settings .sp-holidays-source-cal'); + const bMan = q('.overlay-settings .sp-holidays-source-man'); + if (bCal) bCal.classList.toggle('active', src === 'calendar'); + if (bMan) bMan.classList.toggle('active', src === 'manual'); + } catch {} + + // Master toggle state + try { + const sw = q('.overlay-settings .sp-holidays-enable'); + if (sw) sw.checked = !!d.holidays_enabled; + } catch {} + + // Entity picker + try { + const p = q('.overlay-settings .sp-holidays-entity-picker'); + if (p) { + p.hass = this._hass; + p.value = String(d.holidays_entity || ''); + try { p.setAttribute('label', this._t('holidays.entity') || 'Holiday entity'); } catch {} + } + } catch {} + + // Dates chips + const chips = q('.overlay-settings .sp-holidays-date-chips'); + const renderChips = ()=>{ + try { + if (!chips) return; + chips.innerHTML = ''; + const arr = Array.isArray(this._settingsDraft?.holidays_dates) ? this._settingsDraft.holidays_dates.filter(Boolean).map(String) : []; + const groups = Array.isArray(this._settingsDraft?.holidays_groups) ? this._settingsDraft.holidays_groups : []; + const groupDates = new Set([].concat(...groups.map(g=>g.dates||[])).map(String)); + const sorted = Array.from(new Set(arr.filter(x => /^\d{4}-\d{2}-\d{2}$/.test(String(x)) && !groupDates.has(String(x))))).sort(); + for (const dt of sorted) { + const chip = document.createElement('span'); chip.className = 'pill-chip'; + const txt = document.createElement('span'); txt.textContent = dt; + const rm = document.createElement('button'); rm.type = 'button'; rm.className = 'rm'; rm.textContent = '×'; + try { rm.setAttribute('aria-label', this._t('editor.remove') || 'Remove'); } catch {} + rm.addEventListener('click', ()=>{ + try { + if (!this._settingsDraft) return; + const cur = Array.isArray(this._settingsDraft.holidays_dates) ? this._settingsDraft.holidays_dates : []; + this._settingsDraft.holidays_dates = cur.filter(x => String(x) !== dt); + renderChips(); + } catch {} + }); + chip.append(txt, rm); + chips.append(chip); + } + } catch {} + }; + renderChips(); + + // Range group chips + const grpChips = q('.overlay-settings .sp-holidays-range-chips'); + const renderGrpChips = ()=>{ + try { + if (!grpChips) return; + grpChips.innerHTML = ''; + const groups = Array.isArray(this._settingsDraft?.holidays_groups) ? this._settingsDraft.holidays_groups : []; + for (const g of groups) { + const chip = document.createElement('span'); chip.className='pill-chip'; + const txt = document.createElement('span'); txt.textContent = `${g.from} – ${g.to}`; + const rm = document.createElement('button'); rm.type='button'; rm.className='rm'; rm.textContent='×'; + rm.addEventListener('click', ()=>{ + try { + if (!this._settingsDraft) return; + const all = Array.isArray(this._settingsDraft.holidays_groups) ? this._settingsDraft.holidays_groups : []; + const others = all.filter(x=>x!==g); + const othersDates = new Set([].concat(...others.map(o=>o.dates||[])).map(String)); + const cur = new Set((this._settingsDraft.holidays_dates||[]).map(String)); + for (const d of (g.dates||[])) { if (!othersDates.has(String(d))) cur.delete(String(d)); } + this._settingsDraft.holidays_groups = others; + this._settingsDraft.holidays_dates = Array.from(cur).sort(); + renderChips(); renderGrpChips(); + } catch {} + }); + chip.append(txt, rm); grpChips.append(chip); + } + } catch {} + }; + renderGrpChips(); + + // Bind events once + if (!page.dataset.bound) { + page.dataset.bound = '1'; + + try { + const sw = q('.overlay-settings .sp-holidays-enable'); + if (sw) sw.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + this._settingsDraft.holidays_enabled = !!e.target.checked; + this._holidaysToggleTouched = true; + this._renderSettingsPopupHolidaysTab(); + } catch {} + }); + } catch {} + + try { + const btn = q('.overlay-settings .sp-holidays-edit-btn'); + if (btn) btn.addEventListener('click', ()=>{ + try { + const eid = (this._config?.entities || [])[0] || null; + if (!eid) return; + this._resumeSettingsAfterHoliday = true; + this._suspendSettingsPopup(); + this._openHolidayEditor(eid); + } catch {} + }); + } catch {} + + try { + const bCal = q('.overlay-settings .sp-holidays-source-cal'); + const bMan = q('.overlay-settings .sp-holidays-source-man'); + if (bCal) bCal.addEventListener('click', ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.holidays_source = 'calendar'; this._renderSettingsPopupHolidaysTab(); } catch {} }); + if (bMan) bMan.addEventListener('click', ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.holidays_source = 'manual'; this._renderSettingsPopupHolidaysTab(); } catch {} }); + } catch {} + + try { + const p = q('.overlay-settings .sp-holidays-entity-picker'); + if (p) p.addEventListener('value-changed', (e)=>{ try { if (!this._settingsDraft) return; this._settingsDraft.holidays_entity = e.detail?.value || ''; } catch {} }); + } catch {} + + try { + const inp = q('.overlay-settings .sp-holidays-date-input'); + const add = q('.overlay-settings .sp-holidays-add-date'); + const msg = q('.overlay-settings .sp-holidays-add-date-msg'); + const onAdd = ()=>{ + try { + if (!this._settingsDraft || !inp) return; + const v = String(inp.value || '').trim(); + if (!v) return; + if (!/^\d{4}-\d{2}-\d{2}$/.test(v)) return; + // Show info if v is inside any existing range; do not add + try { + const groups = Array.isArray(this._settingsDraft.holidays_groups) ? this._settingsDraft.holidays_groups : []; + const covered = groups.some(g => Array.isArray(g?.dates) && g.dates.map(String).includes(String(v))); + if (covered) { if (msg) msg.textContent = this._t('holidays.date_in_range') || 'Date already covered by a range'; return; } + } catch {} + const cur = Array.isArray(this._settingsDraft.holidays_dates) ? this._settingsDraft.holidays_dates.filter(Boolean).map(String) : []; + const next = Array.from(new Set(cur.concat([v]))).sort(); + this._settingsDraft.holidays_dates = next; + try { inp.value = ''; if (msg) msg.textContent = ''; } catch {} + renderChips(); + } catch {} + }; + if (add) add.addEventListener('click', onAdd); + if (inp) inp.addEventListener('keydown', (e)=>{ if (e.key === 'Enter') { try { e.preventDefault(); } catch {} onAdd(); } }); + } catch {} + + // Range add in settings overlay + try { + const inpf = q('.overlay-settings .sp-holidays-range-from'); + const inpt = q('.overlay-settings .sp-holidays-range-to'); + const addr = q('.overlay-settings .sp-holidays-add-range'); + const onAddRange = ()=>{ + try { + if (!this._settingsDraft || !inpf || !inpt) return; + const f = String(inpf.value||'').trim(); + const t = String(inpt.value||'').trim(); + if (!f || !t) return; + if (!/^\d{4}-\d{2}-\d{2}$/.test(f) || !/^\d{4}-\d{2}-\d{2}$/.test(t)) return; + let d1 = new Date(f+'T00:00:00'); + let d2 = new Date(t+'T00:00:00'); + if (isNaN(d1.getTime()) || isNaN(d2.getTime())) return; + if (d1.getTime() > d2.getTime()) { const tmp=d1; d1=d2; d2=tmp; } + const fmt = (d)=>{ const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`; }; + const curArr = Array.isArray(this._settingsDraft.holidays_dates) ? this._settingsDraft.holidays_dates.filter(Boolean).map(String) : []; + const cur = new Set(curArr); + const grpDates = []; + for (let d=new Date(d1); d.getTime()<=d2.getTime(); d.setDate(d.getDate()+1)) { const s=fmt(d); cur.add(s); grpDates.push(s); } + this._settingsDraft.holidays_dates = Array.from(cur).sort(); + // Add a group tag for UI + const gid = 'hg_'+Date.now()+'_'+Math.floor(Math.random()*1e6); + const groups = Array.isArray(this._settingsDraft.holidays_groups) ? [...this._settingsDraft.holidays_groups] : []; + groups.push({ id: gid, from: fmt(d1), to: fmt(d2), dates: grpDates }); + this._settingsDraft.holidays_groups = groups; + // Mirror into live config so the main card view updates as well + try { + this._config.holidays_dates = Array.from(cur).sort(); + const liveGroups = Array.isArray(this._config.holidays_groups) ? [...this._config.holidays_groups] : []; + liveGroups.push({ id: gid, from: fmt(d1), to: fmt(d2), dates: grpDates.slice() }); + this._config.holidays_groups = liveGroups; + } catch {} + try { inpf.value=''; inpt.value=''; } catch {} + renderChips(); + try { renderGrpChips(); } catch {} + } catch {} + }; + if (addr) addr.addEventListener('click', onAddRange); + if (inpf) inpf.addEventListener('keydown', (e)=>{ if (e.key==='Enter') { try { e.preventDefault(); } catch {} onAddRange(); } }); + if (inpt) inpt.addEventListener('keydown', (e)=>{ if (e.key==='Enter') { try { e.preventDefault(); } catch {} onAddRange(); } }); + } catch {} + } + } catch {} + } + + _renderSettingsPopupColorsTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="colors"]'); + if (!page) return; + page.innerHTML = ''; + + // Wrap the entire Colors tab in one settings-card (border around everything) + const outerCard = document.createElement('div'); + outerCard.className = 'settings-card'; + try { + outerCard.style.padding = '10px'; + outerCard.style.display = 'grid'; + outerCard.style.gap = '10px'; + } catch {} + page.append(outerCard); + + // Ensure draft shape + try { + if (typeof d.color_global !== 'boolean') d.color_global = !!(this._config?.color_global); + if (!d.color_ranges || typeof d.color_ranges !== 'object') d.color_ranges = {}; + } catch {} + + const wantF = String(d.temp_unit || 'C').toUpperCase() === 'F'; + const toDisp = (c)=>{ const v = Number(c); if (!Number.isFinite(v)) return 0; return wantF ? Math.round((v*9/5+32)*10)/10 : v; }; + const fromDisp = (v)=>{ const n = Number(v); if (!Number.isFinite(n)) return NaN; return wantF ? ((n-32)*5/9) : n; }; + const minC = Number.isFinite(Number(d.min_temp)) ? Number(d.min_temp) : (Number.isFinite(Number(this._config?.min_temp)) ? Number(this._config.min_temp) : 5); + const maxC = Number.isFinite(Number(d.max_temp)) ? Number(d.max_temp) : (Number.isFinite(Number(this._config?.max_temp)) ? Number(this._config.max_temp) : 25); + const minDisp = toDisp(minC); + const maxDisp = toDisp(maxC); + const defC = Number.isFinite(Number(d.default_temp)) ? Number(d.default_temp) : (Number.isFinite(Number(this._config?.default_temp)) ? Number(this._config.default_temp) : 20); + + // Mode toggle (Per room | All rooms) + const modeWrap = document.createElement('div'); + modeWrap.style.display = 'flex'; + modeWrap.style.alignItems = 'center'; + modeWrap.style.justifyContent = 'space-between'; + modeWrap.style.gap = '10px'; + const modeText = document.createElement('div'); + modeText.style.display = 'grid'; + modeText.style.gap = '2px'; + const modeTitle = document.createElement('div'); + modeTitle.className = 'colors-mode-title'; + modeTitle.style.fontWeight = '600'; + modeTitle.textContent = this._t('editor.colors.mode') || 'Color mode'; + const modeDesc = document.createElement('div'); + modeDesc.className = 'colors-mode-desc'; + modeDesc.style.fontSize = '.85rem'; + modeDesc.style.color = 'var(--secondary-text-color)'; + modeDesc.textContent = this._t('editor.colors.mode.desc') || 'Choose per-room colors or one set for all rooms.'; + modeText.append(modeTitle, modeDesc); + const seg = document.createElement('div'); seg.className = 'seg colors-mode'; + const bRoom = document.createElement('button'); bRoom.type = 'button'; bRoom.className = 'mode-room'; + bRoom.textContent = this._t('editor.colors.mode_per_room') || 'Per room'; + const bGlob = document.createElement('button'); bGlob.type = 'button'; bGlob.className = 'mode-global'; + bGlob.textContent = this._t('editor.colors.mode_global') || 'All rooms'; + const applySegUi = ()=>{ + const on = !!d.color_global; + bGlob.classList.toggle('active', on); + bRoom.classList.toggle('active', !on); + }; + bRoom.onclick = ()=>{ try { d.color_global = false; applySegUi(); this._renderSettingsPopupColorsTab(); } catch {} }; + bGlob.onclick = ()=>{ try { d.color_global = true; applySegUi(); this._renderSettingsPopupColorsTab(); } catch {} }; + seg.append(bRoom, bGlob); + applySegUi(); + modeWrap.append(modeText, seg); + outerCard.append(modeWrap); + + const isGlobal = !!d.color_global; + const ents = Array.isArray(this._config?.entities) ? this._config.entities.filter(Boolean) : []; + + // If per-room mode and there are no rooms yet, show helper box + if (!isGlobal && ents.length === 0) { + const box = document.createElement('div'); box.className = 'empty-box'; + const p = document.createElement('div'); p.textContent = this._t('editor.colors.empty') || 'No rooms added yet.'; + const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'add-entity-btn'; + btn.innerHTML = '' + (this._t('editor.colors.go_add') || 'Add room') + ''; + btn.onclick = ()=>{ this._setSettingsPopupTab('rooms'); }; + box.append(p, btn); + outerCard.append(box); + } + + // Remember open/closed state while popup is open + try { if (!this._openPopupColorRows) this._openPopupColorRows = new Set(); } catch {} + + const roomsWrap = document.createElement('div'); + try { roomsWrap.style.display = 'grid'; roomsWrap.style.gap = '10px'; } catch {} + outerCard.append(roomsWrap); + + const buildFoldout = (key, titleText)=>{ + const line = document.createElement('div'); line.className = 'inline'; + const summary = document.createElement('div'); summary.className = 'summary'; + const sumLeft = document.createElement('div'); sumLeft.className = 'summary-left'; + const exp = document.createElement('button'); exp.type = 'button'; exp.className = 'expander'; + exp.innerHTML = ''; + const txtWrap = document.createElement('div'); txtWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); titleSpan.className = 'summary-title'; + titleSpan.textContent = titleText; + + // Inline styles to match the editor foldout appearance (robust across contexts) + try { + summary.style.display = 'flex'; + summary.style.alignItems = 'center'; + summary.style.justifyContent = 'space-between'; + summary.style.padding = '6px'; + summary.style.border = '1px dashed var(--divider-color)'; + summary.style.borderRadius = '8px'; + summary.style.background = 'var(--secondary-background-color, transparent)'; + sumLeft.style.display = 'flex'; + sumLeft.style.alignItems = 'center'; + sumLeft.style.gap = '8px'; + sumLeft.style.minWidth = '0'; + txtWrap.style.display = 'grid'; + txtWrap.style.gap = '2px'; + txtWrap.style.minWidth = '0'; + titleSpan.style.fontWeight = '600'; + titleSpan.style.overflow = 'hidden'; + titleSpan.style.textOverflow = 'ellipsis'; + titleSpan.style.whiteSpace = 'nowrap'; + titleSpan.style.maxWidth = '320px'; + exp.style.width = '28px'; + exp.style.height = '28px'; + exp.style.borderRadius = '8px'; + exp.style.border = '1px solid var(--divider-color)'; + exp.style.background = 'var(--card-background-color)'; + exp.style.display = 'inline-flex'; + exp.style.alignItems = 'center'; + exp.style.justifyContent = 'center'; + exp.style.cursor = 'pointer'; + } catch {} + + txtWrap.append(titleSpan); + sumLeft.append(exp, txtWrap); + summary.append(sumLeft); + line.append(summary); + + const details = document.createElement('div'); details.className = 'details'; + line.append(details); + + try { + details.style.display = 'none'; + details.style.gridTemplateColumns = 'minmax(250px, 440px)'; + details.style.gap = '6px'; + details.style.padding = '6px'; + details.style.border = '1px dashed var(--divider-color)'; + details.style.borderRadius = '8px'; + details.style.background = 'var(--secondary-background-color, transparent)'; + } catch {} + + const applyOpen = (isOpen)=>{ + try { + if (isOpen) { + line.classList.add('open'); + details.style.display = 'grid'; + exp.style.transform = 'rotate(180deg)'; + } else { + line.classList.remove('open'); + details.style.display = 'none'; + exp.style.transform = ''; + } + } catch {} + }; + const toggle = (ev)=>{ + try { ev?.stopPropagation?.(); } catch {} + const nextOpen = !line.classList.contains('open'); + applyOpen(nextOpen); + try { + if (!this._openPopupColorRows) this._openPopupColorRows = new Set(); + if (nextOpen) this._openPopupColorRows.add(String(key)); + else this._openPopupColorRows.delete(String(key)); + } catch {} + }; + exp.addEventListener('click', toggle); + summary.style.cursor = 'pointer'; + summary.addEventListener('click', (ev)=>{ + try { if (ev?.target?.closest?.('button, a, input, select, textarea')) return; } catch {} + toggle(ev); + }); + try { applyOpen(!!this._openPopupColorRows?.has(String(key))); } catch {} + + return { line, details }; + }; + + const renderRangesForKey = (key, details)=>{ + details.innerHTML = ''; + const card = document.createElement('div'); + card.className = 'settings-card'; + try { card.style.padding = '10px'; card.style.gap = '8px'; } catch {} + details.append(card); + + const head = document.createElement('div'); + head.className = 'colors-head'; + try { + head.style.display = 'grid'; + head.style.gridTemplateColumns = '88px 88px 70px 1fr'; + head.style.gap = '8px'; + head.style.alignItems = 'end'; + } catch {} + const mkHead = (k)=>{ + const el = document.createElement('div'); + el.className = 'slabel'; + el.textContent = this._t(k) || k; + return el; + }; + head.append( + mkHead('editor.colors.col_from'), + mkHead('editor.colors.col_to'), + mkHead('editor.colors.col_color'), + document.createElement('div') + ); + + const getArr = ()=>{ + const map = (d.color_ranges && typeof d.color_ranges === 'object') ? d.color_ranges : {}; + return Array.isArray(map[key]) ? map[key] : []; + }; + const setArr = (arr)=>{ + try { + const map = (d.color_ranges && typeof d.color_ranges === 'object') ? d.color_ranges : {}; + d.color_ranges = { ...map, [key]: arr }; + } catch {} + }; + + const ranges = getArr(); + if (ranges.length > 0) card.append(head); + + ranges.forEach((r, ri)=>{ + const outer = document.createElement('div'); outer.className = 'color-item'; + const row = document.createElement('div'); row.className = 'colors-row'; + try { + row.style.display = 'grid'; + row.style.gridTemplateColumns = '88px 88px 70px 1fr'; + row.style.gap = '8px'; + row.style.alignItems = 'center'; + } catch {} + const f = document.createElement('input'); + f.type = 'number'; f.step = '0.5'; + f.className = 'settings-input'; + f.value = String(toDisp(r?.from ?? defC)); + f.setAttribute('aria-label', this._t('editor.colors.col_from')); + f.title = this._t('editor.colors.col_from'); + const t = document.createElement('input'); + t.type = 'number'; t.step = '0.5'; + t.className = 'settings-input'; + t.value = String(toDisp(r?.to ?? defC)); + t.setAttribute('aria-label', this._t('editor.colors.col_to')); + t.title = this._t('editor.colors.col_to'); + const c = document.createElement('input'); + c.type = 'color'; c.value = String(r?.color || '#ffb347'); + c.setAttribute('aria-label', this._t('editor.colors.col_color')); + c.title = this._t('editor.colors.col_color'); + try { + c.style.height = '28px'; + c.style.width = '70px'; + c.style.padding = '0'; + c.style.borderRadius = '8px'; + c.style.border = '1px solid var(--divider-color)'; + c.style.background = 'var(--card-background-color)'; + c.style.boxSizing = 'border-box'; + } catch {} + + f.min = String(minDisp); t.min = String(minDisp); + f.max = String(maxDisp); t.max = String(maxDisp); + + const rm = document.createElement('button'); + rm.type = 'button'; + rm.className = 'btn ghost danger'; + rm.innerHTML = '' + (this._t('editor.remove') || 'Remove') + ''; + try { + rm.style.height = '28px'; + rm.style.padding = '2px 10px'; + rm.style.borderRadius = '8px'; + rm.style.justifySelf = 'end'; + rm.style.width = 'max-content'; + rm.style.display = 'inline-flex'; + rm.style.alignItems = 'center'; + rm.style.gap = '6px'; + rm.querySelector?.('ha-icon')?.style && (rm.querySelector('ha-icon').style.setProperty('--mdc-icon-size','16px')); + } catch {} + + const updateAt = (idx, patch)=>{ + const arr = getArr(); + const next = arr.map((x, i)=> (i===idx ? ({ ...x, ...patch }) : x)); + setArr(next); + }; + + f.onchange = ()=>{ + let v = parseFloat(String(f.value||'').replace(',','.')); + if (!Number.isFinite(v)) v = minDisp; + v = Math.max(minDisp, Math.min(maxDisp, v)); + f.value = String(v); + const vC = fromDisp(v); + const clampC = Math.max(minC, Math.min(maxC, Number.isFinite(vC) ? vC : defC)); + updateAt(ri, { from: clampC }); + }; + t.onchange = ()=>{ + let v = parseFloat(String(t.value||'').replace(',','.')); + if (!Number.isFinite(v)) v = minDisp; + v = Math.max(minDisp, Math.min(maxDisp, v)); + t.value = String(v); + const vC = fromDisp(v); + const clampC = Math.max(minC, Math.min(maxC, Number.isFinite(vC) ? vC : defC)); + updateAt(ri, { to: clampC }); + }; + c.onchange = ()=>{ updateAt(ri, { color: c.value }); }; + rm.onclick = ()=>{ + const arr = getArr(); + const next = [...arr]; + next.splice(ri, 1); + setArr(next); + renderRangesForKey(key, details); + }; + + row.append(f, t, c, rm); + outer.append(row); + card.append(outer); + }); + + const add = document.createElement('button'); + add.className = 'add-entity-btn'; + add.type = 'button'; + add.innerHTML = '' + (this._t('editor.heat_colors.add') || 'Add color range') + ''; + try { add.style.width = 'max-content'; add.style.justifySelf = 'start'; } catch {} + add.onclick = ()=>{ + const arr = getArr(); + const next = [...arr, { from: defC, to: defC, color: '#ffb347' }]; + setArr(next); + renderRangesForKey(key, details); + }; + card.append(add); + }; + + if (isGlobal) { + const key = '*'; + const f = buildFoldout(key, this._t('editor.colors.global_title') || 'All rooms'); + roomsWrap.append(f.line); + renderRangesForKey(key, f.details); + } else { + for (const eid of ents) { + const labels = this._config?.labels || {}; + const title = labels?.[eid] || this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid); + const f = buildFoldout(eid, title); + roomsWrap.append(f.line); + renderRangesForKey(eid, f.details); + } + } + + // Actions (Update schedule + Clear all) + const actions = document.createElement('div'); + actions.className = 'colors-actions'; + actions.style.display = 'flex'; + actions.style.gap = '8px'; + actions.style.alignItems = 'center'; + actions.style.flexWrap = 'wrap'; + + const clrAll = document.createElement('button'); + clrAll.className = 'remove-btn clr-colors-all-btn'; + clrAll.type = 'button'; + try { clrAll.style.width = 'max-content'; clrAll.style.justifySelf = 'start'; } catch {} + clrAll.innerHTML = '' + (this._t('editor.colors.clear_all') || 'Clear all color data') + ''; + clrAll.onclick = ()=>{ + const ok = confirm(this._t('editor.colors.clear_all_confirm') || 'This will delete all color ranges. Continue?'); + if (!ok) return; + try { d.color_ranges = {}; } catch {} + this._renderSettingsPopupColorsTab(); + }; + + actions.append(clrAll); + outerCard.append(actions); + } catch {} + } + + _renderSettingsPopupBoilerTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="boiler"]'); + if (!page) return; + + const wantF = String(d?.temp_unit || 'C').toUpperCase() === 'F'; + const toDispDelta = (nC)=>{ + const v = Number(nC); + if (!Number.isFinite(v)) return 0; + return wantF ? (v * 9/5) : v; + }; + const fromDispDelta = (nDisp)=>{ + const v = Number(nDisp); + if (!Number.isFinite(v)) return NaN; + return wantF ? (v * 5/9) : v; + }; + const round1 = (n)=>{ + const v = Number(n); + if (!Number.isFinite(v)) return 0; + return Math.round(v * 10) / 10; + }; + + if (!page.dataset.built) { + page.dataset.built = '1'; + page.innerHTML = ` +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    + `; + } + + const q = (sel)=> this.shadowRoot.querySelector(sel); + + // Visibility of dependent controls + const boilerOn = !!d.boiler_enabled; + try { + const switchRow = q('.overlay-settings .sp-boiler-switch-setting'); + const roomsRow = q('.overlay-settings .sp-boiler-rooms-setting'); + const offRow = q('.overlay-settings .sp-boiler-offsets-setting'); + [switchRow, roomsRow, offRow].forEach(r=>{ if (r) r.style.display = boilerOn ? '' : 'none'; }); + } catch {} + + // Master toggle + try { + const sw = q('.overlay-settings .sp-boiler-enable'); + if (sw) sw.checked = !!d.boiler_enabled; + } catch {} + + // Pickers + try { + const domSel = q('.overlay-settings .sp-boiler-switch-domain'); + if (domSel) { + const raw = String(d.boiler_switch_domain || '').toLowerCase().trim(); + domSel.value = (raw === 'input_boolean') ? 'input_boolean' : 'switch'; + } + const p = q('.overlay-settings .sp-boiler-switch-picker'); + if (p) { + const raw = String(d.boiler_switch_domain || '').toLowerCase().trim(); + const dom = (raw === 'input_boolean') ? 'input_boolean' : 'switch'; + try { p.setAttribute('include-domains', JSON.stringify([dom])); } catch {} + p.hass = this._hass; + p.value = String(d.boiler_switch || ''); + } + } catch {} + + // Rooms list (checkboxes) + try { + const list = q('.overlay-settings .sp-boiler-rooms-list'); + if (list) { + list.innerHTML = ''; + const ents = Array.isArray(d.entities) ? d.entities.map(String) : []; + const rooms = ents.filter(e=>String(e).startsWith('climate.')); + const selected = Array.isArray(d.boiler_rooms) ? new Set(d.boiler_rooms.map(String)) : new Set(rooms); + const labels = (this._config && this._config.labels && typeof this._config.labels === 'object') ? this._config.labels : {}; + const byName = (eid)=>{ + try { + const lbl = labels && Object.prototype.hasOwnProperty.call(labels, eid) ? String(labels[eid]||'').trim() : ''; + if (lbl) return lbl; + return String(this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid)); + } catch { return eid; } + }; + for (const eid of rooms) { + const row = document.createElement('label'); + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.checked = selected.has(eid); + cb.setAttribute('data-eid', eid); + cb.addEventListener('change', (ev)=>{ + try { + if (!this._settingsDraft) return; + const cur = Array.isArray(this._settingsDraft.boiler_rooms) ? this._settingsDraft.boiler_rooms.map(String) : rooms; + const curSet = new Set(cur); + const on = !!ev?.target?.checked; + if (on) curSet.add(eid); else curSet.delete(eid); + // Preserve order as in entities[] + this._settingsDraft.boiler_rooms = rooms.filter(x => curSet.has(x)); + } catch {} + }); + const sp = document.createElement('span'); + sp.textContent = byName(eid); + row.append(cb, sp); + list.append(row); + } + } + } catch {} + + // Offsets + try { + const onIn = q('.overlay-settings .sp-boiler-on-offset'); + if (onIn) { + const lim = wantF ? 18 : 10; + onIn.min = String(-lim); + onIn.max = String(lim); + onIn.value = String(round1(toDispDelta(d.boiler_on_offset ?? 0))); + } + } catch {} + try { + const offIn = q('.overlay-settings .sp-boiler-off-offset'); + if (offIn) { + const lim = wantF ? 18 : 10; + offIn.min = String(-lim); + offIn.max = String(lim); + offIn.value = String(round1(toDispDelta(d.boiler_off_offset ?? 0))); + } + } catch {} + + // Bind events once + if (!page.dataset.bound) { + page.dataset.bound = '1'; + try { + const sw = q('.overlay-settings .sp-boiler-enable'); + if (sw) sw.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + this._settingsDraft.boiler_enabled = !!e.target.checked; + this._renderSettingsPopupBoilerTab(); + } catch {} + }); + } catch {} + + try { + const p = q('.overlay-settings .sp-boiler-switch-picker'); + if (p) p.addEventListener('value-changed', (e)=>{ + try { + if (!this._settingsDraft) return; + const v = String(e.detail?.value || ''); + this._settingsDraft.boiler_switch = v; + const dom = String(v||'').split('.')[0]; + if (dom === 'switch' || dom === 'input_boolean') { + this._settingsDraft.boiler_switch_domain = (dom === 'input_boolean') ? 'input_boolean' : 'switch'; + this._renderSettingsPopupBoilerTab(); + } + } catch {} + }); + } catch {} + + try { + const s = q('.overlay-settings .sp-boiler-switch-domain'); + if (s) s.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + const v = String(e?.target?.value || '').toLowerCase().trim(); + this._settingsDraft.boiler_switch_domain = (v === 'input_boolean') ? 'input_boolean' : 'switch'; + // Clear selected entity when changing type to avoid mismatch + this._settingsDraft.boiler_switch = ''; + this._renderSettingsPopupBoilerTab(); + } catch {} + }); + } catch {} + + try { + const onIn = q('.overlay-settings .sp-boiler-on-offset'); + if (onIn) onIn.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + const raw = String(e?.target?.value ?? '').trim().replace(',', '.'); + if (raw === '') return; + const nDisp = Number(raw); + const nC = fromDispDelta(nDisp); + if (!Number.isFinite(nC)) return; + this._settingsDraft.boiler_on_offset = Math.max(-10, Math.min(10, nC)); + } catch {} + }); + } catch {} + try { + const offIn = q('.overlay-settings .sp-boiler-off-offset'); + if (offIn) offIn.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + const raw = String(e?.target?.value ?? '').trim().replace(',', '.'); + if (raw === '') return; + const nDisp = Number(raw); + const nC = fromDispDelta(nDisp); + if (!Number.isFinite(nC)) return; + this._settingsDraft.boiler_off_offset = Math.max(-10, Math.min(10, nC)); + } catch {} + }); + } catch {} + } + } catch {} + } + + _renderSettingsPopupWeekdaysTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="weekdays"]'); + if (!page) return; + + // Build markup once + if (!page.dataset.built) { + page.dataset.built = '1'; + page.innerHTML = ` +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    + `; + } + + const q = (sel)=> this.shadowRoot.querySelector(sel); + const setChk = (sel, v)=>{ const el=q(sel); if (el) el.checked = !!v; }; + setChk('.overlay-settings .sp-week-enable', d.weekdays_enabled); + setChk('.overlay-settings .sp-week-view-switch', d.weekdays_view_switch_in_timeline); + + const view = (String(d.weekdays_view || 'all_rooms_one_day') === 'one_room_all_days') ? 'one_room_all_days' : 'all_rooms_one_day'; + try { + const bRooms = q('.overlay-settings .sp-week-view-rooms'); + const bDays = q('.overlay-settings .sp-week-view-days'); + bRooms && bRooms.classList.toggle('active', view === 'all_rooms_one_day'); + bDays && bDays.classList.toggle('active', view === 'one_room_all_days'); + // Disable when weekdays are off (match editor behavior) + const on = !!d.weekdays_enabled; + const row = q('.overlay-settings .sp-week-view-setting'); + if (row) row.style.opacity = on ? '' : '.5'; + if (bRooms) { bRooms.disabled = !on; bRooms.setAttribute('aria-disabled', (!on).toString()); } + if (bDays) { bDays.disabled = !on; bDays.setAttribute('aria-disabled', (!on).toString()); } + + const swRow = q('.overlay-settings .sp-week-view-switch-setting'); + if (swRow) swRow.style.opacity = on ? '' : '.5'; + const swInline = q('.overlay-settings .sp-week-view-switch'); + if (swInline) { swInline.disabled = !on; swInline.setAttribute('aria-disabled', (!on).toString()); } + + // Open weekdays editor button + const hasEnts = Array.isArray(this._config?.entities) && this._config.entities.filter(Boolean).length > 0; + const openRow = q('.overlay-settings .sp-week-open-setting'); + if (openRow) openRow.style.display = (on && hasEnts) ? '' : 'none'; + const openBtn = q('.overlay-settings .sp-week-open-btn'); + if (openBtn) { openBtn.disabled = !(on && hasEnts); openBtn.setAttribute('aria-disabled', (!(on && hasEnts)).toString()); } + } catch {} + + // Bind events once + if (!page.dataset.bound) { + page.dataset.bound = '1'; + try { + const sw = q('.overlay-settings .sp-week-enable'); + if (sw) sw.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + const on = !!e.target.checked; + this._settingsDraft.weekdays_enabled = on; + // When enabling, default view to all rooms + if (on && (!this._settingsDraft.weekdays_view || this._settingsDraft.weekdays_view === 'undefined')) { + this._settingsDraft.weekdays_view = 'all_rooms_one_day'; + } + this._renderSettingsPopupWeekdaysTab(); + } catch {} + }); + } catch {} + + try { + const swInline = q('.overlay-settings .sp-week-view-switch'); + if (swInline) swInline.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + this._settingsDraft.weekdays_view_switch_in_timeline = !!e.target.checked; + this._renderSettingsPopupWeekdaysTab(); + } catch {} + }); + } catch {} + + const applyView = (mode)=>{ + try { + if (!this._settingsDraft) return; + if (!this._settingsDraft.weekdays_enabled) return; + this._settingsDraft.weekdays_view = (mode === 'one_room_all_days') ? 'one_room_all_days' : 'all_rooms_one_day'; + this._renderSettingsPopupWeekdaysTab(); + } catch {} + }; + try { + const bRooms = q('.overlay-settings .sp-week-view-rooms'); + const bDays = q('.overlay-settings .sp-week-view-days'); + if (bRooms) bRooms.addEventListener('click', ()=>applyView('all_rooms_one_day')); + if (bDays) bDays.addEventListener('click', ()=>applyView('one_room_all_days')); + } catch {} + try { + const seg = q('.overlay-settings .sp-week-view'); + if (seg) seg.addEventListener('click', (ev)=>{ + try { + const el = ev.target && ev.target.closest ? ev.target.closest('.sp-week-view-rooms, .sp-week-view-days') : null; + if (!el) return; + if (el.classList.contains('sp-week-view-rooms')) applyView('all_rooms_one_day'); + else if (el.classList.contains('sp-week-view-days')) applyView('one_room_all_days'); + } catch {} + }); + } catch {} + + try { + const btn = q('.overlay-settings .sp-week-open-btn'); + if (btn) btn.addEventListener('click', ()=>{ + try { + const ents = Array.isArray(this._config?.entities) ? this._config.entities.filter(Boolean).map(String) : []; + if (!ents.length) return; + const want = String(this._settingsDraft?.weekdays_selected_room || this._config?.weekdays_selected_room || ''); + const eid = (want && ents.includes(want)) ? want : (ents[0] || null); + if (!eid) return; + this._resumeSettingsAfterWeekdays = true; + this._suspendSettingsPopup(); + this._openWeeklyEditor(eid); + } catch {} + }); + } catch {} + } + } catch {} + } + + _renderSettingsPopupPresenceSensorTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="presence_sensor"]'); + if (!page) return; + page.innerHTML = ''; + + const card = document.createElement('div'); + card.className = 'settings-card'; + page.append(card); + + // Toggle + const row = document.createElement('div'); + row.className = 'setting'; + const text = document.createElement('div'); + text.className = 'text'; + const title = document.createElement('div'); + title.className = 'title'; + title.textContent = this._t('editor.presence_sensor.enable.title') || 'Presence sensor'; + const desc = document.createElement('div'); + desc.className = 'desc'; + desc.textContent = this._t('editor.presence_sensor.enable.desc') || 'Enable per-room presence sensor configuration.'; + text.append(title, desc); + const sw = document.createElement('ha-switch'); + const on = !!d.presence_sensor_enabled; + try { sw.checked = on; sw.disabled = false; } catch {} + sw.addEventListener('change', ()=>{ + try { + d.presence_sensor_enabled = !!sw.checked; + this._renderSettingsPopupPresenceSensorTab(); + } catch {} + }); + row.append(text, sw); + card.append(row); + + // Rooms list + const ents = (()=>{ + try { + const arr = Array.isArray(d.entities) ? d.entities : (Array.isArray(this._config?.entities) ? this._config.entities : []); + return arr.filter(Boolean).map(String); + } catch { return []; } + })(); + + if (!ents.length) { + const box = document.createElement('div'); box.className = 'empty-box'; + const p = document.createElement('div'); p.textContent = this._t('editor.colors.empty') || 'No rooms added yet.'; + const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'add-entity-btn'; + btn.innerHTML = '' + (this._t('editor.colors.go_add') || 'Add room') + ''; + btn.onclick = ()=>{ this._setSettingsPopupTab('rooms'); }; + box.append(p, btn); + card.append(box); + return; + } + + try { if (!this._openPopupPresenceSensorRows) this._openPopupPresenceSensorRows = new Set(); } catch {} + const wrap = document.createElement('div'); + try { wrap.style.display = 'grid'; wrap.style.gap = '10px'; } catch {} + card.append(wrap); + + const buildFoldout = (key, titleText)=>{ + const line = document.createElement('div'); line.className = 'inline'; + const summary = document.createElement('div'); summary.className = 'summary'; + const sumLeft = document.createElement('div'); sumLeft.className = 'summary-left'; + const exp = document.createElement('button'); exp.type = 'button'; exp.className = 'expander'; + exp.innerHTML = ''; + const txtWrap = document.createElement('div'); txtWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); titleSpan.className = 'summary-title'; + titleSpan.textContent = titleText; + + try { + summary.style.display = 'flex'; + summary.style.alignItems = 'center'; + summary.style.justifyContent = 'space-between'; + summary.style.padding = '6px'; + summary.style.border = '1px dashed var(--divider-color)'; + summary.style.borderRadius = '8px'; + summary.style.background = 'var(--secondary-background-color, transparent)'; + sumLeft.style.display = 'flex'; + sumLeft.style.alignItems = 'center'; + sumLeft.style.gap = '8px'; + sumLeft.style.minWidth = '0'; + txtWrap.style.display = 'grid'; + txtWrap.style.gap = '2px'; + txtWrap.style.minWidth = '0'; + titleSpan.style.fontWeight = '600'; + titleSpan.style.overflow = 'hidden'; + titleSpan.style.textOverflow = 'ellipsis'; + titleSpan.style.whiteSpace = 'nowrap'; + titleSpan.style.maxWidth = '320px'; + exp.style.width = '28px'; + exp.style.height = '28px'; + exp.style.borderRadius = '8px'; + exp.style.border = '1px solid var(--divider-color)'; + exp.style.background = 'var(--card-background-color)'; + exp.style.display = 'inline-flex'; + exp.style.alignItems = 'center'; + exp.style.justifyContent = 'center'; + exp.style.cursor = 'pointer'; + } catch {} + + txtWrap.append(titleSpan); + sumLeft.append(exp, txtWrap); + summary.append(sumLeft); + line.append(summary); + + const details = document.createElement('div'); details.className = 'details'; + line.append(details); + + try { + details.style.display = 'none'; + details.style.gridTemplateColumns = 'minmax(250px, 440px)'; + details.style.gap = '6px'; + details.style.padding = '6px'; + details.style.border = '1px dashed var(--divider-color)'; + details.style.borderRadius = '8px'; + details.style.background = 'var(--secondary-background-color, transparent)'; + } catch {} + + const applyOpen = (isOpen)=>{ + try { + if (isOpen) { + line.classList.add('open'); + details.style.display = 'grid'; + exp.style.transform = 'rotate(180deg)'; + } else { + line.classList.remove('open'); + details.style.display = 'none'; + exp.style.transform = ''; + } + } catch {} + }; + const toggle = (ev)=>{ + try { ev?.stopPropagation?.(); } catch {} + const nextOpen = !line.classList.contains('open'); + applyOpen(nextOpen); + try { + if (!this._openPopupPresenceSensorRows) this._openPopupPresenceSensorRows = new Set(); + if (nextOpen) this._openPopupPresenceSensorRows.add(String(key)); + else this._openPopupPresenceSensorRows.delete(String(key)); + } catch {} + }; + exp.addEventListener('click', toggle); + summary.style.cursor = 'pointer'; + summary.addEventListener('click', (ev)=>{ + try { if (ev?.target?.closest?.('button, a, input, select, textarea')) return; } catch {} + toggle(ev); + }); + try { applyOpen(!!this._openPopupPresenceSensorRows?.has(String(key))); } catch {} + + return { line, details }; + }; + + try { if (!d.presence_sensors || typeof d.presence_sensors !== 'object') d.presence_sensors = {}; } catch {} + try { if (!d.presence_sensor_temps || typeof d.presence_sensor_temps !== 'object') d.presence_sensor_temps = {}; } catch {} + try { if (!d.presence_sensor_delays || typeof d.presence_sensor_delays !== 'object') d.presence_sensor_delays = {}; } catch {} + try { if (!d.presence_sensor_delay_units || typeof d.presence_sensor_delay_units !== 'object') d.presence_sensor_delay_units = {}; } catch {} + + for (const eid of ents) { + const labels = (d.labels && typeof d.labels === 'object') ? d.labels : (this._config?.labels || {}); + const friendly = this._hass?.states?.[eid]?.attributes?.friendly_name; + const titleText = labels?.[eid] || friendly || (eid.split('.')[1] || eid); + const { line, details } = buildFoldout(eid, titleText); + + try { + const fieldWrap = document.createElement('div'); + fieldWrap.style.display = 'grid'; + fieldWrap.style.gap = '2px'; + + const tTitle = document.createElement('div'); + tTitle.style.fontWeight = '600'; + tTitle.textContent = this._t('editor.presence_sensor.entity.title') || 'Presence entity'; + const tDesc = document.createElement('div'); + tDesc.style.fontSize = '.85rem'; + tDesc.style.color = 'var(--secondary-text-color)'; + tDesc.textContent = this._t('editor.presence_sensor.entity.desc') || 'Select the binary_sensor that indicates presence in this room.'; + fieldWrap.append(tTitle, tDesc); + + const picker = document.createElement('tt-entity-picker'); + try { picker.style.display = 'block'; picker.style.minHeight = '48px'; picker.style.maxWidth = '440px'; } catch {} + picker.setAttribute('include-domains', '["binary_sensor"]'); + picker.setAttribute('label', this._t('editor.presence_sensor.entity.title') || 'Presence entity'); + try { picker.hass = this._hass; } catch {} + try { picker.value = String(d.presence_sensors?.[eid] || ''); } catch {} + try { picker.disabled = !on; picker.setAttribute('aria-disabled', String(!on)); } catch {} + picker.addEventListener('value-changed', (e)=>{ + try { + const val = String(e?.detail?.value || '').trim(); + const next = { ...(d.presence_sensors || {}) }; + if (val && String(val).startsWith('binary_sensor.')) next[eid] = val; + else delete next[eid]; + d.presence_sensors = next; + } catch {} + }); + + details.append(fieldWrap, picker); + + // Temperature override when presence is ON + try { + const wantF = String(d.temp_unit || this._config?.temp_unit || 'C').toUpperCase() === 'F'; + const toDisp = (c)=>{ const v = Number(c); if (!Number.isFinite(v)) return ''; return wantF ? Math.round((v*9/5+32)*10)/10 : v; }; + const fromDisp = (x)=>{ const v = Number(x); if (!Number.isFinite(v)) return NaN; return wantF ? ((v-32)*5/9) : v; }; + const minC = Number.isFinite(Number(d.min_temp)) ? Number(d.min_temp) : Number(this._config?.min_temp ?? 5); + const maxC = Number.isFinite(Number(d.max_temp)) ? Number(d.max_temp) : Number(this._config?.max_temp ?? 25); + + const tWrap = document.createElement('div'); + tWrap.style.display = 'grid'; + tWrap.style.gap = '2px'; + + const tT = document.createElement('div'); + tT.style.fontWeight = '600'; + tT.textContent = this._t('editor.presence_sensor.on_temp.title') || 'Temperature when presence is ON'; + const tD = document.createElement('div'); + tD.style.fontSize = '.85rem'; + tD.style.color = 'var(--secondary-text-color)'; + tD.textContent = this._t('editor.presence_sensor.on_temp.desc') || 'If the sensor is ON, this room will use this temperature instead of the schedule.'; + tWrap.append(tT, tD); + + const inp = document.createElement('input'); + inp.type = 'number'; + inp.step = '0.5'; + inp.className = 'settings-input'; + try { + if (Number.isFinite(minC)) inp.min = String(toDisp(minC)); + if (Number.isFinite(maxC)) inp.max = String(toDisp(maxC)); + } catch {} + try { + const defaultC = 21; + let curC = Number(d.presence_sensor_temps?.[eid]); + if (!Number.isFinite(curC)) { + // Preset default (stored in °C) + let v = Number(defaultC); + if (Number.isFinite(minC)) v = Math.max(minC, v); + if (Number.isFinite(maxC)) v = Math.min(maxC, v); + const next = { ...(d.presence_sensor_temps || {}) }; + next[eid] = v; + d.presence_sensor_temps = next; + curC = v; + } + inp.value = String(toDisp(curC)); + } catch {} + try { inp.disabled = !on; } catch {} + + inp.addEventListener('change', (e)=>{ + try { + const raw = String(e?.target?.value ?? '').trim(); + const next = { ...(d.presence_sensor_temps || {}) }; + if (!raw) { + delete next[eid]; + d.presence_sensor_temps = next; + return; + } + const parsed = fromDisp(String(raw).replace(',', '.')); + if (!Number.isFinite(parsed)) { + e.target.value = ''; + delete next[eid]; + d.presence_sensor_temps = next; + return; + } + let v = parsed; + if (Number.isFinite(minC)) v = Math.max(minC, v); + if (Number.isFinite(maxC)) v = Math.min(maxC, v); + next[eid] = v; + d.presence_sensor_temps = next; + try { e.target.value = String(toDisp(v)); } catch {} + } catch {} + }); + + details.append(tWrap, inp); + } catch {} + + // Delay settings (stored as seconds) + try { + const defaultDelaySec = 180; // 3 minutes + const dm = (d.presence_sensor_delays && typeof d.presence_sensor_delays === 'object') ? d.presence_sensor_delays : {}; + const cur = (dm?.[eid] && typeof dm?.[eid] === 'object') ? dm[eid] : {}; + + const um = (d.presence_sensor_delay_units && typeof d.presence_sensor_delay_units === 'object') ? d.presence_sensor_delay_units : {}; + const unit = (String(um?.[eid] || 'minutes').toLowerCase().trim() === 'seconds') ? 'seconds' : 'minutes'; + const fmtVal = (sec)=>{ + const v = Number(sec); + if (!Number.isFinite(v)) return ''; + if (unit === 'seconds') return String(Math.round(v)); + return String(Math.round((v/60)*10)/10); + }; + const parseToSeconds = (raw)=>{ + const s = String(raw||'').trim(); + if (!s) return NaN; + const n = Number(s.replace(',', '.')); + if (!Number.isFinite(n)) return NaN; + if (unit === 'seconds') return Math.max(0, n); + return Math.max(0, n) * 60; + }; + + const setDelaySeconds = (which, seconds)=>{ + try { + const nextAll = { ...(d.presence_sensor_delays || {}) }; + const curObj = (nextAll[eid] && typeof nextAll[eid] === 'object') ? { ...nextAll[eid] } : {}; + if (!Number.isFinite(seconds)) { + delete curObj[which]; + } else { + curObj[which] = Math.max(0, Math.round(Number(seconds))); + } + const out = {}; + const onS = Number(curObj.on_s); const offS = Number(curObj.off_s); + if (Number.isFinite(onS) && onS >= 0) out.on_s = onS; + if (Number.isFinite(offS) && offS >= 0) out.off_s = offS; + if (Object.keys(out).length) nextAll[eid] = out; + else delete nextAll[eid]; + d.presence_sensor_delays = nextAll; + } catch {} + }; + + // Unit dropdown (per room) + try { + const uWrap = document.createElement('div'); + uWrap.style.display = 'grid'; + uWrap.style.gap = '2px'; + const uT = document.createElement('div'); + uT.style.fontWeight = '600'; + uT.textContent = this._t('away.delay_unit') || 'Unit'; + uWrap.append(uT); + + const sel = document.createElement('select'); + sel.className = 'settings-input'; + const optM = document.createElement('option'); + optM.value = 'minutes'; + optM.textContent = this._t('away.delay_unit.minutes') || 'Minutes'; + const optS = document.createElement('option'); + optS.value = 'seconds'; + optS.textContent = this._t('away.delay_unit.seconds') || 'Seconds'; + sel.append(optM, optS); + try { sel.value = unit; } catch {} + try { sel.disabled = !on; } catch {} + sel.addEventListener('change', ()=>{ + try { + const v = String(sel.value||'').toLowerCase().trim(); + const next = { ...(d.presence_sensor_delay_units || {}) }; + next[eid] = (v === 'seconds') ? 'seconds' : 'minutes'; + d.presence_sensor_delay_units = next; + this._renderSettingsPopupPresenceSensorTab(); + } catch {} + }); + + details.append(uWrap, sel); + } catch {} + + const mkField = (titleKey, descKey, which, currentSec)=>{ + const w = document.createElement('div'); + w.style.display = 'grid'; + w.style.gap = '2px'; + + const tt = document.createElement('div'); + tt.style.fontWeight = '600'; + tt.textContent = this._t(titleKey) || ''; + const dd = document.createElement('div'); + dd.style.fontSize = '.85rem'; + dd.style.color = 'var(--secondary-text-color)'; + dd.textContent = this._t(descKey) || ''; + w.append(tt, dd); + + const inp = document.createElement('input'); + inp.type = 'number'; + inp.min = '0'; + inp.step = (unit === 'seconds') ? '1' : '0.1'; + inp.className = 'settings-input'; + try { + const sec = Number.isFinite(Number(currentSec)) ? Number(currentSec) : defaultDelaySec; + inp.value = fmtVal(sec); + } catch {} + try { inp.disabled = !on; } catch {} + inp.addEventListener('change', (e)=>{ + try { + if (!String(e?.target?.value || '').trim()) { + // Treat empty as 0 (no delay) + setDelaySeconds(which, 0); + try { e.target.value = fmtVal(0); } catch {} + return; + } + const sec = parseToSeconds(e?.target?.value); + if (!Number.isFinite(sec)) { + e.target.value = ''; + setDelaySeconds(which, 0); + return; + } + setDelaySeconds(which, sec); + try { e.target.value = fmtVal(Math.round(sec)); } catch {} + } catch {} + }); + return { w, inp }; + }; + + const fOn = mkField('editor.presence_sensor.on_delay.title', 'editor.presence_sensor.on_delay.desc', 'on_s', cur?.on_s); + const fOff = mkField('editor.presence_sensor.off_delay.title', 'editor.presence_sensor.off_delay.desc', 'off_s', cur?.off_s); + details.append(fOn.w, fOn.inp, fOff.w, fOff.inp); + } catch {} + } catch {} + + wrap.append(line); + } + } catch {} + } + + _renderSettingsPopupSettingsTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const q = (sel)=> this.shadowRoot.querySelector(sel); + const setVal = (sel, v)=>{ const el=q(sel); if (el) el.value = (v ?? '') + ''; }; + const setChk = (sel, v)=>{ const el=q(sel); if (el) el.checked = !!v; }; + // Show temps in display unit based on draft.temp_unit, but keep draft values in °C + const wantF = String(d.temp_unit || 'C').toUpperCase() === 'F'; + const toDisp = (c)=>{ try { const v = Number(c); if (!Number.isFinite(v)) return c; return wantF ? this._toDisplayTemp(v) : v; } catch { return c; } }; + setVal('.overlay-settings .sp-def', toDisp(d.default_temp)); + setVal('.overlay-settings .sp-rowh', d.row_height); + setVal('.overlay-settings .sp-minc', toDisp(d.min_temp)); + setVal('.overlay-settings .sp-maxc', toDisp(d.max_temp)); + setChk('.overlay-settings .sp-auto', d.auto_apply); + setChk('.overlay-settings .sp-applyedit', d.apply_on_edit); + setChk('.overlay-settings .sp-applydef', d.apply_on_default_change); + setChk('.overlay-settings .sp-perroom', d.per_room_defaults); + setChk('.overlay-settings .sp-showroomtemp', d.show_room_temp); + + // Segmented controls: active state + try { + const b24 = q('.overlay-settings .sp-fmt24'); + const b12 = q('.overlay-settings .sp-fmt12'); + const is12 = !!d.time_12h; + b24 && b24.classList.toggle('active', !is12); + b12 && b12.classList.toggle('active', is12); + } catch {} + try { + const bB = q('.overlay-settings .sp-srcBrowser'); + const bH = q('.overlay-settings .sp-srcHa'); + const useHa = String(d.time_source||'browser')==='ha'; + bB && bB.classList.toggle('active', !useHa); + bH && bH.classList.toggle('active', useHa); + } catch {} + try { + const bC = q('.overlay-settings .sp-unitC'); + const bF = q('.overlay-settings .sp-unitF'); + bC && bC.classList.toggle('active', !wantF); + bF && bF.classList.toggle('active', wantF); + } catch {} + setChk('.overlay-settings .sp-profiles', d.profiles_enabled); + + // Configuration ID (instance) toggle + label (not part of the popup draft) + try { + this._hydrateInstanceCfgFromLocal(); + const sw = q('.overlay-settings .sp-instance-enable'); + if (sw) sw.checked = !!this._config?.instance_enabled; + const row = q('.overlay-settings .sp-instance-id-setting'); + if (row) row.style.display = this._config?.instance_enabled ? '' : 'none'; + const idEl = q('.overlay-settings .sp-instance-id'); + if (idEl) { + const id = String(this._config?.instance_id || ''); + idEl.textContent = id ? `ID: ${id}` : ''; + } + const b = q('.overlay-settings .sp-instance-regenerate-btn'); + if (b) b.style.display = this._config?.instance_enabled ? '' : 'none'; + } catch {} + + // Keep temp field labels in sync with draft unit + try { + const unitSym = wantF ? '°F' : '°C'; + const t = (k)=> (this._t ? this._t(k) : k); + const setLbl = (sel, key)=>{ const el=q(sel); if (!el) return; const s = String(t(key)||''); el.textContent = s.includes('°C') ? s.replace('°C', unitSym) : (s + (s.includes('(') ? '' : '')); }; + setLbl('.overlay-settings .sp-slabel-def', 'editor.default_c'); + setLbl('.overlay-settings .sp-slabel-minc', 'editor.min_c'); + setLbl('.overlay-settings .sp-slabel-maxc', 'editor.max_c'); + } catch {} + } catch {} + } + + _renderSettingsPopupPauseTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const q = (sel)=> this.shadowRoot.querySelector(sel); + const setChk = (sel, v)=>{ const el=q(sel); if (el) el.checked = !!v; }; + + setChk('.overlay-settings .sp-pausebtn', d.show_pause_button); + setChk('.overlay-settings .sp-pausesensor', d.pause_sensor_enabled); + + // Pause sensor entity row visibility + picker value + try { + const row = q('.overlay-settings .sp-pausesensor-entity-setting'); + if (row) row.style.display = d.pause_sensor_enabled ? '' : 'none'; + } catch {} + try { + const p = q('.overlay-settings .sp-pausesensor-entity'); + if (p) { + p.hass = this._hass; + p.value = String(d.pause_sensor_entity || ''); + try { p.setAttribute('label', this._t('editor.pause_sensor.entity_title') || 'Pause sensor'); } catch {} + } + } catch {} + + // Wire Pause tab controls + const onSw = (key) => (e)=>{ + try { + if (!this._settingsDraft) return; + const checked = !!(e && e.target && e.target.checked); + this._settingsDraft[key] = checked; + } catch {} + }; + + try { + const swPauseBtn = q('.overlay-settings .sp-pausebtn'); + if (swPauseBtn) swPauseBtn.onchange = onSw('show_pause_button'); + } catch {} + + try { + const swPauseSensor = q('.overlay-settings .sp-pausesensor'); + if (swPauseSensor) swPauseSensor.onchange = (e)=>{ try { if (!this._settingsDraft) return; this._settingsDraft.pause_sensor_enabled = !!e?.target?.checked; this._renderSettingsPopupPauseTab(); } catch {} }; + } catch {} + + // Pause sensor entity picker + try { + const p = q('.overlay-settings .sp-pausesensor-entity'); + if (p) { + if (!p._ttBoundPausePicker) { + p._ttBoundPausePicker = true; + p.addEventListener('value-changed', (ev)=>{ + try { + if (!this._settingsDraft) return; + const v = (ev?.detail && ev.detail.value != null) ? String(ev.detail.value) : ''; + this._settingsDraft.pause_sensor_entity = v; + } catch {} + }); + } + } + } catch {} + } catch {} + } + + _ttCoerceNum(v, fallback){ + const n = Number(v); + return Number.isFinite(n) ? n : fallback; + } + _ttClampNum(v, min, max, fallback){ + const n = Number(v); + if (!Number.isFinite(n)) return fallback; + return Math.max(min, Math.min(max, n)); + } + _settingsPopupHasUnsaved(){ + try { + const d = this._settingsDraft; + const c = this._config || {}; + if (!d) return false; + const eqNum = (a,b)=>{ + const na = Number(a); + const nb = Number(b); + if (!Number.isFinite(na) && !Number.isFinite(nb)) return true; + return Math.abs(na - nb) < 0.0001; + }; + // Normalize undefined config fields to the same defaults used in _initSettingsPopupDraft + const cDefaultTemp = this._ttCoerceNum(c.default_temp, 20); + const cRowHeight = this._ttCoerceNum(c.row_height, 64); + const cMinTemp = this._ttCoerceNum(c.min_temp, 5); + const cMaxTemp = this._ttCoerceNum(c.max_temp, 25); + + if (!eqNum(d.default_temp, cDefaultTemp)) return true; + if (!eqNum(d.row_height, cRowHeight)) return true; + if (!eqNum(d.min_temp, cMinTemp)) return true; + if (!eqNum(d.max_temp, cMaxTemp)) return true; + if (!!d.auto_apply !== !!c.auto_apply) return true; + if (!!d.apply_on_edit !== !!c.apply_on_edit) return true; + if (!!d.apply_on_default_change !== !!c.apply_on_default_change) return true; + if (!!d.per_room_defaults !== !!c.per_room_defaults) return true; + if (!!d.show_pause_button !== !!(c.show_pause_button !== false)) return true; + if (!!d.show_room_temp !== !!(c.show_room_temp !== false)) return true; + if (!!d.pause_sensor_enabled !== !!c.pause_sensor_enabled) return true; + if (String(d.pause_sensor_entity||'') !== String(c.pause_sensor_entity||'')) return true; + if (!!d.time_12h !== !!c.time_12h) return true; + if (String(d.time_source||'browser') !== String(c.time_source||'browser')) return true; + if (String(d.temp_unit||'C').toUpperCase() !== String(c.temp_unit||'C').toUpperCase()) return true; + if (!!d.profiles_enabled !== !!c.profiles_enabled) return true; + + if (!!d.presence_sensor_enabled !== !!c.presence_sensor_enabled) return true; + + if (!!d.weekdays_enabled !== !!c.weekdays_enabled) return true; + if (String(d.weekdays_view||'all_rooms_one_day') !== String(c.weekdays_view||'all_rooms_one_day')) return true; + if (String(d.weekdays_selected_room||'') !== String(c.weekdays_selected_room||'')) return true; + + if (!!d.boiler_enabled !== !!c.boiler_enabled) return true; + if (String(d.boiler_switch||'') !== String(c.boiler_switch||'')) return true; + + // boiler_switch_domain: normalize to switch/input_boolean + try { + const normDom = (eid, raw)=>{ + const fromEid = (()=>{ const dom = String(eid||'').split('.')[0]; return (dom === 'input_boolean') ? 'input_boolean' : 'switch'; })(); + const s = String((raw ?? fromEid) || '').toLowerCase().trim(); + return (s === 'input_boolean') ? 'input_boolean' : 'switch'; + }; + const dDom = normDom(d.boiler_switch, d.boiler_switch_domain); + const cDom = normDom(c.boiler_switch, c.boiler_switch_domain); + if (dDom !== cDom) return true; + } catch {} + + // boiler_rooms: compare sets; default is "all climate rooms" + try { + const cEnts = Array.isArray(c.entities) ? c.entities.filter(Boolean).map(String) : []; + const climateRooms = cEnts.filter(e=>String(e).startsWith('climate.')); + const norm = (arr)=>{ + const set = new Set((Array.isArray(arr) ? arr : climateRooms).filter(Boolean).map(String)); + return climateRooms.filter(eid => set.has(eid)); + }; + const dRooms = norm(d.boiler_rooms); + const cRooms = norm(c.boiler_rooms); + if (dRooms.length !== cRooms.length) return true; + for (let i=0;i{ + const n = Number(v); + if (!Number.isFinite(n)) return fallback; + return Math.max(0, Math.min(1440, Math.round(n))); + }; + if (!eqNum(dn.open_delay_min, clampOw(cn.open_delay_min, 2))) return true; + if (!eqNum(dn.close_delay_min, clampOw(cn.close_delay_min, 5))) return true; + const ds = (dn.sensors && typeof dn.sensors === 'object') ? dn.sensors : {}; + const cs = (cn.sensors && typeof cn.sensors === 'object') ? cn.sensors : {}; + const keys = new Set([...Object.keys(ds||{}), ...Object.keys(cs||{})]); + for (const k of keys) { + const a = Array.isArray(ds[k]) ? ds[k].filter(Boolean).map(String) : []; + const b = Array.isArray(cs[k]) ? cs[k].filter(Boolean).map(String) : []; + const sa = Array.from(new Set(a)).sort(); + const sb = Array.from(new Set(b)).sort(); + if (sa.length !== sb.length) return true; + for (let i=0;i /^\d{4}-\d{2}-\d{2}$/.test(String(x))))).sort(); + const sc = Array.from(new Set(ca.filter(x => /^\d{4}-\d{2}-\d{2}$/.test(String(x))))).sort(); + if (sd.length !== sc.length) return true; + for (let i=0;i !!dcmb[k]?.enabled).map(String).sort(); + const ck = Object.keys(ccmb||{}).filter(k => !!ccmb[k]?.enabled).map(String).sort(); + if (dk.length !== ck.length) return true; + for (let i=0;iString(x||'').trim()).filter(Boolean) : []; + const ce = Array.isArray(c.entities) ? c.entities.map(x=>String(x||'').trim()).filter(Boolean) : []; + if (JSON.stringify(de) !== JSON.stringify(ce)) return true; + + const dm0 = Array.isArray(d.room_use_input_number) ? d.room_use_input_number.map(v=>!!v) : []; + const cm0 = Array.isArray(c.room_use_input_number) ? c.room_use_input_number.map(v=>!!v) : []; + const normModes = (ents, raw)=> ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return true; + if (String(eid).startsWith('climate.')) return false; + return !!raw[idx]; + }); + const dm = normModes(de, dm0); + const cm = normModes(ce, cm0); + if (JSON.stringify(dm) !== JSON.stringify(cm)) return true; + + const dl = (d.labels && typeof d.labels === 'object') ? d.labels : {}; + const cl = (c.labels && typeof c.labels === 'object') ? c.labels : {}; + const normMap = (src)=>{ + const out = {}; + for (const eid of de) { + const v = Object.prototype.hasOwnProperty.call(src, eid) ? String(src[eid]||'').trim() : ''; + if (v) out[eid] = v; + } + return out; + }; + if (JSON.stringify(normMap(dl)) !== JSON.stringify(normMap(cl))) return true; + + const dmrg = (d.merges && typeof d.merges === 'object') ? d.merges : {}; + const cmrg = (c.merges && typeof c.merges === 'object') ? c.merges : {}; + const primSet = new Set(de); + const normMerges = (src)=>{ + const out = {}; + for (let i=0;iString(x).startsWith('climate.') && !primSet.has(String(x))))).sort(); + if (cleaned.length) out[primary] = cleaned; + } + return out; + }; + if (JSON.stringify(normMerges(dmrg)) !== JSON.stringify(normMerges(cmrg))) return true; + + const dts = (d.temp_sensors && typeof d.temp_sensors === 'object') ? d.temp_sensors : {}; + const cts = (c.temp_sensors && typeof c.temp_sensors === 'object') ? c.temp_sensors : {}; + if (JSON.stringify(normMap(dts)) !== JSON.stringify(normMap(cts))) return true; + + // Presence sensors: one binary_sensor per active room + const dps = (d.presence_sensors && typeof d.presence_sensors === 'object') ? d.presence_sensors : {}; + const cps = (c.presence_sensors && typeof c.presence_sensors === 'object') ? c.presence_sensors : {}; + const normPresence = (src)=>{ + const out = {}; + for (const eid of de) { + const v = Object.prototype.hasOwnProperty.call(src, eid) ? String(src[eid]||'').trim() : ''; + if (v && String(v).startsWith('binary_sensor.')) out[eid] = v; + } + return out; + }; + if (JSON.stringify(normPresence(dps)) !== JSON.stringify(normPresence(cps))) return true; + + // Presence override temps (°C): compare numeric mappings for active rooms + try { + const dpt = (d.presence_sensor_temps && typeof d.presence_sensor_temps === 'object') ? d.presence_sensor_temps : {}; + const cpt = (c.presence_sensor_temps && typeof c.presence_sensor_temps === 'object') ? c.presence_sensor_temps : {}; + const normTemps = (src)=>{ + const out = {}; + for (const eid of de) { + const v0 = Object.prototype.hasOwnProperty.call(src, eid) ? Number(src[eid]) : NaN; + if (!Number.isFinite(v0)) continue; + out[eid] = v0; + } + return out; + }; + if (JSON.stringify(normTemps(dpt)) !== JSON.stringify(normTemps(cpt))) return true; + } catch {} + + // Presence delays (seconds): compare { on_s, off_s } per active room + try { + const dpd = (d.presence_sensor_delays && typeof d.presence_sensor_delays === 'object') ? d.presence_sensor_delays : {}; + const cpd = (c.presence_sensor_delays && typeof c.presence_sensor_delays === 'object') ? c.presence_sensor_delays : {}; + const normDelays = (src)=>{ + const out = {}; + for (const eid of de) { + const v = src?.[eid]; + if (!v || typeof v !== 'object') continue; + const onS = Number(v.on_s); + const offS = Number(v.off_s); + const obj = {}; + if (Number.isFinite(onS) && onS >= 0) obj.on_s = onS; + if (Number.isFinite(offS) && offS >= 0) obj.off_s = offS; + if (Object.keys(obj).length) out[eid] = obj; + } + return out; + }; + if (JSON.stringify(normDelays(dpd)) !== JSON.stringify(normDelays(cpd))) return true; + } catch {} + + // Delay unit per room: default 'minutes' + try { + const dpu = (d.presence_sensor_delay_units && typeof d.presence_sensor_delay_units === 'object') ? d.presence_sensor_delay_units : {}; + const cpu = (c.presence_sensor_delay_units && typeof c.presence_sensor_delay_units === 'object') ? c.presence_sensor_delay_units : {}; + const normUnits = (src)=>{ + const out = {}; + for (const eid of de) { + const u0 = Object.prototype.hasOwnProperty.call(src, eid) ? String(src[eid]||'').toLowerCase().trim() : ''; + out[eid] = (u0 === 'seconds') ? 'seconds' : 'minutes'; + } + return out; + }; + if (JSON.stringify(normUnits(dpu)) !== JSON.stringify(normUnits(cpu))) return true; + } catch {} + } catch {} + return false; + } catch { return false; } + } + + _closeSettingsPopup(force){ + try { + const mustConfirm = !force && this._settingsPopupHasUnsaved(); + if (mustConfirm) { + const msg = this._t('ui.unsaved_confirm') || 'You have unsaved changes. Close anyway?'; + const ok = window.confirm(msg); + if (!ok) return; + } + try { + if (this._settingsPopupKeyHandler) window.removeEventListener('keydown', this._settingsPopupKeyHandler); + } catch {} + this._settingsPopupKeyHandler = null; + try { + if (this._settingsPopupWheelHandler?.el && this._settingsPopupWheelHandler?.fn) { + this._settingsPopupWheelHandler.el.removeEventListener('wheel', this._settingsPopupWheelHandler.fn); + } + } catch {} + this._settingsPopupWheelHandler = null; + this._settingsPopupSuspended = false; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings'); + if (ov) ov.classList.remove('open'); + try { this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} + this._settingsDraft = null; + } catch {} + } + + _suspendSettingsPopup(){ + try { + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings'); + if (ov) ov.classList.remove('open'); + try { this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} + this._settingsPopupSuspended = true; + try { + if (this._settingsPopupKeyHandler) window.removeEventListener('keydown', this._settingsPopupKeyHandler); + } catch {} + } catch {} + } + + _resumeSettingsPopup(){ + try { + if (this._disablePopups) return; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings'); + if (!ov) return; + ov.classList.add('open'); + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + this._settingsPopupSuspended = false; + try { this._applyCardI18n(); } catch {} + try { this._renderSettingsPopupTabs(); } catch {} + try { this._renderSettingsPopupTab(); } catch {} + try { + const onKey = (ev) => { if (ev.key === 'Escape') { this._closeSettingsPopup(false); } }; + try { if (this._settingsPopupKeyHandler) window.removeEventListener('keydown', this._settingsPopupKeyHandler); } catch {} + this._settingsPopupKeyHandler = onKey; + window.addEventListener('keydown', onKey); + } catch {} + } catch {} + } + + async _saveSettingsPopup(){ + try { + // IMPORTANT: do not persist on toggle/input; only commit+save here. + this._applySettingsPopupDraftToConfig(); + if (this._config?.storage_enabled && typeof this._saveStore === 'function') { + await this._saveStore(true); + // Pull fresh settings back from the integration to avoid any race where UI shows defaults after a hard refresh + try { + const api = await this._apiFetchState(); + const s = api?.settings || null; + if (s && typeof s === 'object') { + if (typeof s.holidays_enabled === 'boolean') this._config.holidays_enabled = !!s.holidays_enabled; + if (typeof s.holidays_source === 'string') this._config.holidays_source = (s.holidays_source === 'manual') ? 'manual' : 'calendar'; + if (typeof s.holidays_entity === 'string') this._config.holidays_entity = String(s.holidays_entity || ''); + if (Array.isArray(s.holidays_dates)) this._config.holidays_dates = [...s.holidays_dates]; + if (s.open_window && typeof s.open_window === 'object') this._config.open_window = { ...s.open_window }; + } + } catch {} + } else { + try { if (typeof this._debouncedSaveStore === 'function') this._debouncedSaveStore(); } catch {} + } + } catch {} + try { this._closeSettingsPopup(true); } catch {} + try { this._render(); } catch {} + // Ensure other card instances update immediately after Save (used for Colors "Update schedule") + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + try { document.querySelectorAll('thermostat-timeline-card').forEach(el=>el?.devRefreshFromEditor?.()); } catch {} + } + + // --- Test popup (entity picker + available services) --- + _openTestPopup(){ + try { + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-test'); + const picker = this.shadowRoot && this.shadowRoot.querySelector('.test-picker'); + const select = this.shadowRoot && this.shadowRoot.querySelector('.test-select'); + if (!ov || !picker) return; + ov.classList.add('open'); + // If HA's entity picker web component isn't loaded, show a simple to avoid menu overlays escaping the modal + const hasPicker = false && !!customElements.get('ha-entity-picker'); + if (hasPicker) { + try { picker.style.display = ''; } catch {} + try { if (select) select.style.display = 'none'; } catch {} + // Bind hass to entity picker and set current value + try { picker.hass = this._hass; } catch {} + try { if (this._testEntity) picker.value = this._testEntity; } catch {} + // Attach one-time listener (avoid duplicates) + try { + if (!picker._ttBound) { + picker._ttBound = true; + picker.addEventListener('value-changed', (e)=>{ try { this._testEntity = e.detail?.value || e.target?.value || ''; this._renderTestCommands(); } catch {} }); + picker.addEventListener('change', (e)=>{ try { this._testEntity = e.target?.value || ''; this._renderTestCommands(); } catch {} }); + } + } catch {} + } else { + // Fallback: populate select with all entity ids + try { picker.style.display = 'none'; } catch {} + if (select) { + try { select.style.display = ''; } catch {} + // Build options once per open + select.innerHTML = ''; + const states = this._hass?.states || {}; + const items = Object.keys(states).map(id=>{ + const fn = states[id]?.attributes?.friendly_name || id; + return { id, fn: String(fn) }; + }).sort((a,b)=> a.fn.localeCompare(b.fn)); + const ph = document.createElement('option'); ph.value=''; ph.textContent = this._t('editor.test_select_placeholder'); select.append(ph); + for (const it of items){ const o=document.createElement('option'); o.value=it.id; o.textContent=`${it.fn} (${it.id})`; if (this._testEntity===it.id) o.selected = true; select.append(o); } + if (!select._ttBound){ select._ttBound = true; select.addEventListener('change',(e)=>{ try { this._testEntity = e.target?.value || ''; this._renderTestCommands(); } catch {} }); } + } + } + this._renderTestCommands(); + this._updateTestExportEnabled(); + } catch {} + } + + _renderTestCommands(){ + try { + const list = this.shadowRoot && this.shadowRoot.querySelector('.test-list'); + if (!list) return; + list.innerHTML = ''; + const eid = String(this._testEntity || '').trim(); + if (!eid) { + const empty = document.createElement('div'); empty.className = 'test-empty'; empty.textContent = this._t('editor.test_empty'); list.append(empty); return; + } + const domain = eid.split('.')[0]; + const all = (this._hass && this._hass.services) ? this._hass.services : {}; + const svcObj = all[domain] || {}; + // If chosen entity is a climate, also show generic homeassistant "set_state" fields? Keep to domain services for now + const names = Object.keys(svcObj).sort(); + if (!names.length) { + const empty = document.createElement('div'); empty.className = 'test-empty'; empty.textContent = `Ingen services fundet for domænet "${domain}".`; list.append(empty); return; + } + for (const s of names){ + const def = svcObj[s] || {}; + const item = document.createElement('div'); item.className = 'test-item'; + const h = document.createElement('h4'); h.textContent = `${domain}.${s}`; item.append(h); + const ul = document.createElement('ul'); ul.className = 'test-fields'; + const fields = def.fields || {}; + const fNames = Object.keys(fields); + if (!fNames.length) { + const li = document.createElement('li'); li.textContent = this._t('editor.test_no_fields'); ul.append(li); + } else { + for (const fn of fNames){ + const fd = fields[fn] || {}; + const li = document.createElement('li'); + const req = fd.required ? ' (påkrævet)' : ''; + const desc = fd.description ? ` – ${fd.description}` : ''; + const ex = typeof fd.example !== 'undefined' ? ` Eksempel: ${JSON.stringify(fd.example)}` : ''; + li.textContent = `${fn}${req}${desc}${ex}`; + ul.append(li); + } + } + item.append(ul); + list.append(item); + } + this._updateTestExportEnabled(); + } catch {} + } + + _updateTestExportEnabled(){ + try { + const btn = this.shadowRoot && this.shadowRoot.querySelector('.test-export'); + if (!btn) return; + const eid = String(this._testEntity || '').trim(); + const domain = eid && eid.includes('.') ? eid.split('.')[0] : ''; + const has = domain && this._hass && this._hass.services && this._hass.services[domain] && Object.keys(this._hass.services[domain]).length; + btn.disabled = !(eid && has); + } catch {} + } + + _exportTestCommands(){ + try { + if (this._exporting) return; // throttle to avoid accidental multi-download + this._exporting = true; setTimeout(()=>{ this._exporting = false; }, 600); + const text = this._buildTestExportText(); + if (!text) return; + const eid = String(this._testEntity || 'entity').replace(/[^a-zA-Z0-9_.-]+/g,'_'); + const name = `${eid}-commands.md`; + const blob = new Blob([text], { type: 'text/markdown;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = name; document.body.appendChild(a); a.click(); + setTimeout(()=>{ try { document.body.removeChild(a); URL.revokeObjectURL(url); } catch {} }, 0); + } catch {} + } + + _buildTestExportText(){ + try { + const eid = String(this._testEntity || '').trim(); + if (!eid) return ''; + const st = this._hass?.states?.[eid]; + const friendly = st?.attributes?.friendly_name || ''; + const domain = eid.split('.')[0]; + const date = new Date(); + const lines = []; + lines.push(`Entity: ${eid}${friendly?` (${friendly})`:''}`); + lines.push(`Domain: ${domain}`); + lines.push(`Generated: ${date.toISOString()}`); + lines.push(''); + const svcObj = (this._hass?.services?.[domain]) || {}; + const names = Object.keys(svcObj).sort(); + if (!names.length) { lines.push('No services for this domain.'); return lines.join('\n'); } + for (const s of names){ + // Mark command in Markdown-style bold for easier reading + lines.push(`**${domain}.${s}**`); + const fields = (svcObj[s] && svcObj[s].fields) || {}; + const fNames = Object.keys(fields); + if (!fNames.length) { lines.push(' (no fields)'); continue; } + for (const fn of fNames){ + const fd = fields[fn] || {}; + const req = fd.required ? 'required' : 'optional'; + const desc = fd.description ? ` - ${fd.description}` : ''; + const ex = typeof fd.example !== 'undefined' ? ` Example: ${JSON.stringify(fd.example)}` : ''; + lines.push(` ${fn} (${req})${desc}${ex}`); + } + lines.push(''); + } + return lines.join('\n'); + } catch { return ''; } + } + + // Allow editor to nudge a repaint explicitly + devRefreshFromEditor(){ + try { this._ensureSchedules(); } catch {} + try { if (!this._inlineEditing && !this._editing) this._render(); } catch {} + } + + _openEditor(entity, blockId) { + this._ensureEditModeOn(); + if (!blockId) return this._openNewEditor(entity); + this._editing = { entity, blockId }; + // Hide any tooltips while editing + try { + const tips = this.shadowRoot.querySelectorAll('.wk-tooltip'); + tips.forEach(t => { try { if (t._hideTimer) clearTimeout(t._hideTimer); } catch {} t.style.display = 'none'; }); + } catch {} + const row = this._schedules[entity]; + if (!row) return; + const b = row.blocks.find(x => x.id === blockId); + if (!b) return; + const edTemp = this.shadowRoot.querySelector(".ed-temp"); + const edFrom = this.shadowRoot.querySelector(".ed-from"); + const edTo = this.shadowRoot.querySelector(".ed-to"); + const fromMer = this.shadowRoot.querySelector('.ed-from-mer'); + const toMer = this.shadowRoot.querySelector('.ed-to-mer'); + const overlay = this.shadowRoot.querySelector(".overlay"); + const title = this.shadowRoot.querySelector('.modal h3'); + const delBtn = this.shadowRoot.querySelector('.ed-delete'); + if (edTemp) { edTemp.value = String(this._toDisplayTemp(b.temp)); edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } + if (this._config?.time_12h) { + if (edFrom) edFrom.value = this._toTimeInput12h(b.startMin); + if (edTo) edTo.value = this._toTimeInput12h(b.endMin); + if (fromMer) { fromMer.style.display=''; fromMer.value = (b.startMin>=720)?'PM':'AM'; } + if (toMer) { toMer.style.display=''; toMer.value = (b.endMin>=720 && b.endMin<1440)?'PM':'AM'; } + } else { + if (edFrom) edFrom.value = this._toTimeInput(b.startMin); + if (edTo) edTo.value = this._toTimeInput(b.endMin); + if (fromMer) fromMer.style.display='none'; + if (toMer) toMer.style.display='none'; + } + if (title) title.textContent = this._t('ui.edit_block'); + if (delBtn) delBtn.style.display = ''; + const err = this.shadowRoot.querySelector(".ed-error"); + if (err) { err.style.display = "none"; err.textContent = ""; } + + try { const modalEl = this.shadowRoot.querySelector('.overlay .modal:not(.modal-week)'); if (modalEl) modalEl.classList.toggle('modal-12h', !!this._config?.time_12h); } catch {} + overlay.classList.add("open"); + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + try { + const onKey = (ev) => { if (ev.key === 'Escape') { this._closeEditor(); } }; + window.addEventListener('keydown', onKey, { once: true }); + } catch {} + // Improved keyboard navigation between time fields (two-step Tab in "Fra") + try { + const fromInput = this.shadowRoot.querySelector(".ed-from"); + const toInput = this.shadowRoot.querySelector(".ed-to"); + if (fromInput && toInput) { + // Focus "Fra" on open for fast editing + setTimeout(() => { try { fromInput.focus(); fromInput.select?.(); } catch {} }, 0); + // Two-step Tab inside "Fra": hours -> minutes -> move to "Til" + let fraStep = 0; + fromInput.addEventListener("keydown", (e) => { + if (e.key === "Tab" && !e.shiftKey) { + e.preventDefault(); + try { + const val = fromInput.value || ""; + if (fraStep === 0) { + if (fromInput.setSelectionRange) { + setTimeout(() => { try { fromInput.setSelectionRange(3, 5); } catch {} }, 0); + } + fraStep = 1; + } else { + fraStep = 0; + toInput.focus(); + toInput.select?.(); + } + } catch { + if (fraStep == 0) { fraStep = 1; } else { fraStep = 0; toInput.focus(); toInput.select?.(); } + } + } + }); + // Shift+Tab from Til -> Fra + toInput.addEventListener("keydown", (e) => { + if (e.key === "Tab" && e.shiftKey) { + e.preventDefault(); + fraStep = 1; + fromInput.focus(); + try { fromInput.setSelectionRange?.(3,5); } catch {} + } + }); + fromInput.addEventListener("input", () => { fraStep = 0; }); + } + } catch {} + + try { + const onKey = (ev) => { if (ev.key === 'Escape') { this._closeEditor(); } }; + window.addEventListener('keydown', onKey, { once: true }); + } catch {} + // Improved keyboard navigation between time fields + try { + const fromInput = this.shadowRoot.querySelector(".ed-from"); + const toInput = this.shadowRoot.querySelector(".ed-to"); + if (fromInput && toInput) { + // Focus "Fra" on open for fast editing + setTimeout(() => { try { fromInput.focus(); fromInput.select?.(); } catch {} }, 0); + // Tab from Fra -> Til (skip any intermediary focusable elements like clock icons) + fromInput.addEventListener("keydown", (e) => { + if (e.key === "Tab" && !e.shiftKey) { + e.preventDefault(); + toInput.focus(); + toInput.select?.(); + } + }); + // Shift+Tab from Til -> Fra + toInput.addEventListener("keydown", (e) => { + if (e.key === "Tab" && e.shiftKey) { + e.preventDefault(); + fromInput.focus(); + fromInput.select?.(); + } + }); + } + } catch {} + + try { + const onKey = (ev) => { if (ev.key === 'Escape') { this._closeEditor(); } }; + // Add once to avoid stacking listeners across multiple opens + window.addEventListener('keydown', onKey, { once: true }); + } catch {} + + // Also reflect the current option on the global select entity instantly + try { + const selOpt = (()=>{ + const all = this._hass?.states || {}; + for (const eid in all){ + if (!Object.prototype.hasOwnProperty.call(all, eid)) continue; + if (!eid.startsWith('select.')) continue; + const st = all[eid]; + const fn = String(st?.attributes?.friendly_name || '').toLowerCase(); + if (fn === 'thermostat profile' || fn.startsWith('thermostat profile')){ + return String(st?.state || ''); + } + } + return null; + })(); + if (selOpt !== null) { + const norm = (v)=>{ const s = String(v||'').trim(); return (s.toLowerCase()==='none' || s==='') ? null : s; }; + const want = norm(selOpt); + const changed = want !== this._globalProfile; + if (changed) { + this._globalProfile = want; + if (want) this._config.profiles_enabled = true; + // Update local schedules so UI reflects change instantly (no save) + try { + for (const eid of (this._config?.entities || [])){ + const r = this._schedules[eid]; if (!r) continue; this._ensureProfilesStruct(r); + if (!want) { r.activeProfile = null; continue; } + const has = !!(r.profiles && r.profiles[want]); + r.activeProfile = has ? want : null; + } + } catch {} + if (!this._inlineEditing && !this._editing) this._render(); + this._scheduleNextApply(); + // Persist back to integration so sensor version updates immediately + try { this._debouncedSaveStore(); } catch {} + } + } + } catch {} + + } + _closeEditor(){ + this._editing = null; + this.shadowRoot.querySelector(".overlay")?.classList.remove("open"); + try { this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} + // If we hid the weekday modal for editing, decide whether to restore it + // Only restore when the edit was initiated from inside the weekdays popup. + // If the edit was initiated from the main timeline (auto-save flow), do not reopen the weekdays popup. + if (this._weeklyOverlayHiddenForEditor && this._weeklyDraft) { + const cameFromTimeline = !!this._autoSaveWeeklyFromTimeline; + this._weeklyOverlayHiddenForEditor = false; + if (cameFromTimeline) { + // Do not bounce back into weekdays popup on cancel; reset the auto-save flag and close weekly context + this._autoSaveWeeklyFromTimeline = false; + this._closeWeeklyEditor(); + } else { + const wov = this.shadowRoot.querySelector('.overlay-week'); + if (wov) wov.classList.add('open'); + this._renderWeeklyModal(); + } + } + // If we hid the profiles modal for editing, restore it on close + if (this._profilesOverlayHiddenForEditor && this._profilesOpen) { + this._profilesOverlayHiddenForEditor = false; + const pov = this.shadowRoot.querySelector('.overlay-profiles'); + if (pov) pov.classList.add('open'); + this._renderProfilesModal(); + } + // If we hid the holiday modal for editing, restore it on close + if (this._holidayOverlayHiddenForEditor && this._holidayOpen) { + this._holidayOverlayHiddenForEditor = false; + const hov = this.shadowRoot.querySelector('.overlay-holiday'); + if (hov) hov.classList.add('open'); + this._renderHolidayModal(); + } + // Restore presence modal if it is open + try { + if (this._presenceOpen) { + const pov = this.shadowRoot.querySelector('.overlay-presence'); + if (pov) pov.classList.add('open'); + this._renderPresenceModal(); + } + } catch {} + } + + async _deleteFromEditor(){ if (!this._editing) return; const { entity, blockId, weeklyDay, profileName }=this._editing; if (!blockId) { this._closeEditor(); return; } + if (weeklyDay && this._weeklyDraft) { + // Delete from both _weeklyDraft.days AND _weeklyDraft._weeklyModes to ensure complete removal + + // First, try to delete from _weeklyDraft.days + let foundInDays = false; + if (this._weeklyDraft.days?.[weeklyDay]) { + const arr = this._weeklyDraft.days[weeklyDay]; + const idx = arr.findIndex(x=>x.id===blockId); + if (idx >= 0) { + arr.splice(idx, 1); + foundInDays = true; + } + } + + // Also delete from _weeklyDraft._weeklyModes if it exists (should delete from ALL modes to be safe) + let foundInModes = false; + if (this._weeklyDraft?._weeklyModes) { + for (const modeName in this._weeklyDraft._weeklyModes) { + const modeBlocks = this._weeklyDraft._weeklyModes[modeName]?.days?.[weeklyDay]; + if (Array.isArray(modeBlocks)) { + const idx = modeBlocks.findIndex(x=>x.id===blockId); + if (idx >= 0) { + modeBlocks.splice(idx, 1); + foundInModes = true; + } + } + } + } + + // If the weekly editor was opened implicitly from the main timeline, + // save directly without applying group sync (which would re-copy deleted blocks) + if (this._autoSaveWeeklyFromTimeline) { + this._autoSaveWeeklyFromTimeline = false; + // Save without applying group propagation + const entity = this._weeklyEntity; + const row = this._schedules[entity]; + if (row) { + const mode = this._weeklyDraft.mode || 'weekday_weekend'; + this._ensureWeeklyStruct(row); + // Save to both weekly and weekly_modes without syncing across group days. + // IMPORTANT: main timeline builds _weeklyDraft from weekly_modes, so if we only + // update row.weekly.days here, the next edit will reload stale blocks from + // row.weekly_modes and "revive" deleted blocks. + const daysCopy = JSON.parse(JSON.stringify(this._weeklyDraft.days || {})); + // Keep any existing modes, but ensure the active mode reflects this draft. + try { + if (this._weeklyDraft._weeklyModes) { + row.weekly_modes = JSON.parse(JSON.stringify(this._weeklyDraft._weeklyModes)); + } + } catch {} + try { + if (!row.weekly_modes || typeof row.weekly_modes !== 'object') row.weekly_modes = {}; + row.weekly_modes[mode] = { days: JSON.parse(JSON.stringify(daysCopy)) }; + } catch { + try { row.weekly_modes = row.weekly_modes || {}; row.weekly_modes[mode] = { days: daysCopy }; } catch {} + } + row.weekly = { mode, days: JSON.parse(JSON.stringify(daysCopy)) }; + this._schedules[entity] = row; + this._config.weekdays_mode = mode; + const before = this._desiredNowSnapshot(); + try { this._persistDraftNow(); } catch {} + this._markPendingSave(); + if (this._config.apply_on_edit) await this._applyIfDesiredChanged(before); + this._scheduleNextApply(); + } + this._closeEditor(); + // Close weekly overlay when editing from main timeline + const wov = this.shadowRoot.querySelector('.overlay-week'); + if (wov) wov.classList.remove('open'); + this._weeklyOpen = false; + this._render(); + } else { + // Regular flow when user is inside the weekdays popup: update preview and stay there + this._renderWeeklyModal(); + this._closeEditor(); + } + return; + } + if (this._editing.presence && this._presenceDraft && this._presenceDraft.rooms) { + try { + const eid = this._editing.entity; + const arr = (this._presenceDraft.rooms||{})[eid] || []; + const idx = arr.findIndex(x=>x.id===blockId); + if (idx>=0) arr.splice(idx,1); + (this._presenceDraft.rooms||{})[eid] = arr; + this._render(); this._closeEditor(); this._renderPresenceModal(); + } catch { this._closeEditor(); } + return; + } + if (this._editing.holiday && this._holidayDraft && this._holidayDraft.rooms) { + try { + const eid = this._editing.entity; + const arr = (this._holidayDraft.rooms||{})[eid] || []; + const idx = arr.findIndex(x=>x.id===blockId); + if (idx>=0) arr.splice(idx,1); + (this._holidayDraft.rooms||{})[eid] = arr; + this._render(); this._closeEditor(); this._renderHolidayModal(); + } catch { this._closeEditor(); } + return; + } + if (profileName) { + try { + // Ensure draft exists and operate on the current room's draft blocks map + const isNewProf = (profileName === '__new__'); + const curRoom = this._profilesRoom || this._profilesEntity || entity; + if (!this._profilesDraft || (!isNewProf && this._profilesDraft.name !== profileName)) { + const row = this._schedules[entity]; + const roomsMap = {}; + for (const e of (this._config?.entities||[])){ + const src = !isNewProf ? (this._schedules[e]?.profiles?.[profileName]?.blocks || []) : []; + roomsMap[e] = JSON.parse(JSON.stringify(src)); + } + this._profilesDraft = { name: (isNewProf? null : profileName), rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + } + const arr = (this._profilesDraft.rooms||{})[curRoom] || []; + const idx = arr.findIndex(x=>x.id===blockId); + if (idx>=0) arr.splice(idx,1); + (this._profilesDraft.rooms||{})[curRoom] = arr; + this._profilesDirty = true; + // If deletion was initiated from the main timeline, persist immediately + if (this._autoSaveProfileFromTimeline) { + try { this._autoSaveProfileFromTimeline = false; await this._profilesSaveDraft(); } catch {} + } + this._render(); this._closeEditor(); if (this._profilesOpen) this._renderProfilesModal(); + } catch { this._closeEditor(); } + return; + } + this._deleteBlock(entity, blockId); this._closeEditor(); } + + _openNewEditor(entity){ this._ensureEditModeOn(); this._editing = { entity, blockId: null }; const row = this._schedules[entity]; if (!row) return; const edTemp = this.shadowRoot.querySelector(".ed-temp"); const edFrom = this.shadowRoot.querySelector(".ed-from"); const edTo = this.shadowRoot.querySelector(".ed-to"); const overlay = this.shadowRoot.querySelector(".overlay"); const now = this._getNowMin(); const start = this._clamp(Math.round(now), 0, 1380); const end = this._clamp(start + 60, start + 15, 1440); if (edTemp) edTemp.value = String(this._toDisplayTemp(row.defaultTemp ?? 20)); if (edFrom) edFrom.value = this._toTimeInput(start); if (edTo) edTo.value = this._toTimeInput(end); const title = this.shadowRoot.querySelector('.modal h3'); if (title) title.textContent = this._t('ui.add_block'); const delBtn = this.shadowRoot.querySelector('.ed-delete'); if (delBtn) delBtn.style.display = 'none'; const err = this.shadowRoot.querySelector(".ed-error"); if (err) { err.style.display = "none"; err.textContent = ""; } + /* popups visible: continue */ + // Hide any tooltips while editing + try { const tips = this.shadowRoot.querySelectorAll('.wk-tooltip'); tips.forEach(t => { try { if (t._hideTimer) clearTimeout(t._hideTimer); } catch {} t.style.display='none'; }); } catch {} + try { const modalEl = this.shadowRoot.querySelector('.overlay .modal:not(.modal-week)'); if (modalEl) modalEl.classList.toggle('modal-12h', !!this._config?.time_12h); } catch {} + // Open after a frame to avoid half-rendered UI flash + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { overlay.classList.add('open'); })); } catch { overlay.classList.add('open'); } + try { const fromMer = this.shadowRoot.querySelector('.ed-from-mer'); const toMer = this.shadowRoot.querySelector('.ed-to-mer'); if (this._config?.time_12h) { if (edFrom) edFrom.value = this._toTimeInput12h(start); if (edTo) edTo.value = this._toTimeInput12h(end); if (fromMer){ fromMer.style.display=''; fromMer.value = (start>=720)?'PM':'AM'; } if (toMer){ toMer.style.display=''; toMer.value = (end>=720 && end<1440)?'PM':'AM'; } } else { if (fromMer) fromMer.style.display='none'; if (toMer) toMer.style.display='none'; } } catch {} + if (edTemp) { edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } + try { + const onKey = (ev) => { if (ev.key === 'Escape') { this._closeEditor(); } }; + window.addEventListener('keydown', onKey, { once: true }); + } catch {} + // Improved keyboard navigation between time fields (two-step Tab in "Fra") + try { + const fromInput = this.shadowRoot.querySelector(".ed-from"); + const toInput = this.shadowRoot.querySelector(".ed-to"); + if (fromInput && toInput) { + // Focus "Fra" on open for fast editing + setTimeout(() => { try { fromInput.focus(); fromInput.select?.(); } catch {} }, 0); + // Two-step Tab inside "Fra": hours -> minutes -> move to "Til" + let fraStep = 0; + fromInput.addEventListener("keydown", (e) => { + if (e.key === "Tab" && !e.shiftKey) { + e.preventDefault(); + try { + const val = fromInput.value || ""; + if (fraStep === 0) { + if (fromInput.setSelectionRange) { + setTimeout(() => { try { fromInput.setSelectionRange(3, 5); } catch {} }, 0); + } + fraStep = 1; + } else { + fraStep = 0; + toInput.focus(); + toInput.select?.(); + } + } catch { + if (fraStep == 0) { fraStep = 1; } else { fraStep = 0; toInput.focus(); toInput.select?.(); } + } + } + }); + // Shift+Tab from Til -> Fra + toInput.addEventListener("keydown", (e) => { + if (e.key === "Tab" && e.shiftKey) { + e.preventDefault(); + fraStep = 1; + fromInput.focus(); + try { fromInput.setSelectionRange?.(3,5); } catch {} + } + }); + fromInput.addEventListener("input", () => { fraStep = 0; }); + } + } catch {} + } + + // ---------- Weekdays modal ---------- + _openWeeklyEditor(entity){ + this._ensureEditModeOn(); + this._weeklyEntity = entity; + const row = this._schedules[entity] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureWeeklyStruct(row); + // Reset any pending cross-room copies for this editing session + this._weeklyCopyTargets = []; + + // Away delay tracking + this._awayWant = null; // boolean | null + this._awayWantSinceMs = 0; // timestamp when away want last changed + // Deep copy as draft from the dataset for current mode + try { + const curMode = (row.weekly?.mode) || (this._config?.weekdays_mode) || 'weekday_weekend'; + const src = row.weekly_modes?.[curMode]?.days || {}; + const names = this._weekdayNames(); + const draftDays = JSON.parse(JSON.stringify(src)); + for (const k of names){ if (!Array.isArray(draftDays[k])) draftDays[k] = []; } + this._weeklyDraft = { mode: curMode, days: draftDays }; + } catch { + this._weeklyDraft = JSON.parse(JSON.stringify(row.weekly)); + } + this._weeklyDayKey = this._todayKey(); + this._weeklyOpen = true; + const ov = this.shadowRoot.querySelector('.overlay-week'); + // Mark overlaying first to hide background artifacts + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + // Build the modal while hidden + this._renderWeeklyModal(); + // Open after a paint + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { if (ov) ov.classList.add('open'); })); } catch { if (ov) ov.classList.add('open'); } + } + _closeWeeklyEditor(){ + try { this._weeklyOpen = false; this._weeklyEntity = null; this._weeklyDraft = null; this._weeklyDayKey = null; } catch {} + try { this.shadowRoot.querySelector('.overlay-week')?.classList.remove('open'); } catch {} + try { this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} + try { + if (this._resumeSettingsAfterWeekdays) { + this._resumeSettingsAfterWeekdays = false; + this._resumeSettingsPopup?.(); + } + } catch {} + } + _weeklySaveAll(){ try { if (!this._weeklyEntity || !this._weeklyDraft) { this._closeWeeklyEditor(); return; } const entity = this._weeklyEntity; const row = this._schedules[entity]; if (!row) { this._closeWeeklyEditor(); return; } + // Normalize by mode: propagate grouped days within the selected mode only + const mode = this._weeklyDraft.mode || 'weekday_weekend'; + const d = this._weeklyDraft.days || {}; + const applyGroup = (keys, srcKey)=>{ const src = JSON.parse(JSON.stringify(d[srcKey]||[])); for (const k of keys){ d[k] = JSON.parse(JSON.stringify(src)); } }; + if (mode === 'weekday_weekend') { + applyGroup(['tue','wed','thu','fri'], 'mon'); + applyGroup(['sun'], 'sat'); + } else if (mode === 'weekday_sat_sun') { + applyGroup(['tue','wed','thu','fri'], 'mon'); + } // all_7 -> keep individual + this._ensureWeeklyStruct(row); + // Save into mode bucket and set active weekly to this mode + try { row.weekly_modes[mode] = { days: JSON.parse(JSON.stringify(d)) }; } catch { row.weekly_modes[mode] = { days: d }; } + row.weekly = { mode, days: JSON.parse(JSON.stringify(d)) }; + this._schedules[entity] = row; + // Also update the global fallback mode so other rooms without per-room override follow this selection + this._config.weekdays_mode = mode; + // Also apply to any pending target rooms selected via "copy week to rooms" + const targets = Array.isArray(this._weeklyCopyTargets) ? this._weeklyCopyTargets : []; + if (targets.length) { + // Make a fresh copy for targets (don't reuse the modified 'd' which already has applyGroup applied) + const targetData = JSON.parse(JSON.stringify(this._weeklyDraft.days || {})); + for (const eid of targets){ + try { + const trow = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureWeeklyStruct(trow); + // Apply group propagation on the target copy (independent from source) + const tdata = JSON.parse(JSON.stringify(targetData)); + const applyGroupTarget = (keys, srcKey)=>{ const src = JSON.parse(JSON.stringify(tdata[srcKey]||[])); for (const k of keys){ tdata[k] = JSON.parse(JSON.stringify(src)); } }; + if (mode === 'weekday_weekend') { + applyGroupTarget(['tue','wed','thu','fri'], 'mon'); + applyGroupTarget(['sun'], 'sat'); + } else if (mode === 'weekday_sat_sun') { + applyGroupTarget(['tue','wed','thu','fri'], 'mon'); + } + trow.weekly_modes[mode] = { days: JSON.parse(JSON.stringify(tdata)) }; + trow.weekly = { mode, days: JSON.parse(JSON.stringify(tdata)) }; + this._schedules[eid] = trow; + } catch {} + } + } + // Clear pending list after Save + this._weeklyCopyTargets = []; + const before = this._desiredNowSnapshot(); + try { this._persistDraftNow(); } catch {} + this._markPendingSave(); + this._closeWeeklyEditor(); + this._render(); + if (this._config.apply_on_edit) this._applyIfDesiredChanged(before); + this._scheduleNextApply(); + } catch(e){ this._closeWeeklyEditor(); } + } + _renderWeeklyModal(){ try { + const modeSel = this.shadowRoot.querySelector('.week-mode'); + const titleEl = this.shadowRoot.querySelector('.week-title'); + const roomEl = this.shadowRoot.querySelector('.week-room'); + const roomTabs = this.shadowRoot.querySelector('.week-roomtabs'); + const note = this.shadowRoot.querySelector('.week-note'); + const tabsWrap = this.shadowRoot.querySelector('.week-tabs'); + const pasteBar = this.shadowRoot.querySelector('.week-pastebar'); + const actionsWrap = this.shadowRoot.querySelector('.wk-actions'); + const track = this.shadowRoot.querySelector('.week-track'); + const scale = this.shadowRoot.querySelector('.week-scale-inner'); + const modalWeek = this.shadowRoot.querySelector('.modal-week'); + if (!this._weeklyDraft) return; + // i18n + if (titleEl) titleEl.textContent = this._t('week.editor.title'); + // Room name on the same header line + try { + if (roomEl) { + const eid = this._weeklyEntity; + let name = ''; + if (eid) { + const custom = (this._config?.labels && this._config.labels[eid]) ? this._config.labels[eid] : null; + name = custom || this._prettyName(eid); + } + roomEl.textContent = name || ''; + } + } catch {} + // Build room tabs (switch room in weekdays editor) + try { + if (roomTabs) { + roomTabs.innerHTML = ''; + const ents = this._config?.entities || []; + if (!this._weeklyEntity || !ents.includes(this._weeklyEntity)) this._weeklyEntity = ents[0] || null; + + for (const eid of ents){ + const btn = document.createElement('button'); + btn.type='button'; + btn.className = 'wk-tab' + (eid===this._weeklyEntity ? ' active' : ''); + let label = ''; + try { label = this._config?.labels?.[eid] || (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1]||eid)); } catch { label = eid; } + btn.textContent = label; + btn.addEventListener('click', ()=>{ if (eid!==this._weeklyEntity) this._openWeeklyEditor(eid); }); + roomTabs.append(btn); + } + + // Hide tabs if only one room + roomTabs.style.display = (ents.length>1) ? 'flex' : 'none'; + } + } catch (e) {} + const modeLab = this.shadowRoot.querySelector('.week-mode-label'); + if (modeLab) modeLab.textContent = this._t('week.mode'); + if (note) note.textContent = this._t('week.editor.preview_note'); + if (modeSel) { + modeSel.value = this._weeklyDraft.mode || 'weekday_weekend'; + // set option labels i18n + for (const opt of Array.from(modeSel.options)){ + if (opt.value==='weekday_weekend') opt.textContent = this._t('week.mode.weekday_weekend'); + if (opt.value==='weekday_sat_sun') opt.textContent = this._t('week.mode.weekday_sat_sun'); + if (opt.value==='all_7') opt.textContent = this._t('week.mode.all_7'); + } + modeSel.onchange = () => { + const newMode = modeSel.value; + this._weeklyDraft.mode = newMode; + // Update global fallback so per-room view reflects latest choice + this._config.weekdays_mode = newMode; + try { + const row = this._schedules[this._weeklyEntity]; + this._ensureWeeklyStruct(row); + const src = row.weekly_modes?.[newMode]?.days || {}; + const names = this._weekdayNames(); + const draftDays = JSON.parse(JSON.stringify(src)); + for (const k of names){ if (!Array.isArray(draftDays[k])) draftDays[k] = []; } + this._weeklyDraft.days = draftDays; + } catch {} + this._renderWeeklyModal(); + }; + } + // Copy/Paste day logic + // Helper to resolve which real days are in a group for current mode + const groupDaysForMode = (mode, key)=>{ + if (mode === 'weekday_weekend') { + if (key==='weekdays') return ['mon','tue','wed','thu','fri']; + if (key==='weekend') return ['sat','sun']; + } else if (mode === 'weekday_sat_sun') { + if (key==='weekdays') return ['mon','tue','wed','thu','fri']; + if (key==='sat') return ['sat']; + if (key==='sun') return ['sun']; + } else if (mode === 'all_7') { + return [key]; + } + return [this._effectiveDayKey(key)]; + }; + // Build per-day action buttons (unique DOM per tab) + const renderActions = () => { + if (!actionsWrap) return; + actionsWrap.innerHTML = ''; + const curGroup = this._weeklyDayKey || 'weekdays'; + const mode = this._weeklyDraft.mode || 'weekday_weekend'; + // Global action: copy entire week to other rooms + const copyAllBtn = document.createElement('button'); + copyAllBtn.type = 'button'; + copyAllBtn.className = 'btn ghost wk-copyall'; + copyAllBtn.textContent = this._t('week.copy_to_rooms'); + copyAllBtn.onclick = (e)=>{ e.preventDefault(); this._openCopyWeekToRooms(); }; + actionsWrap.append(copyAllBtn); + const itemsKeys = []; + if (this._weeklyDraft.mode === 'weekday_weekend') itemsKeys.push('weekdays','weekend'); + else if (this._weeklyDraft.mode === 'weekday_sat_sun') itemsKeys.push('weekdays','sat','sun'); + else itemsKeys.push('mon','tue','wed','thu','fri','sat','sun'); + for (const key of itemsKeys){ + const box = document.createElement('div'); + box.style.display = key === curGroup ? 'flex' : 'none'; + box.style.gap = '8px'; + box.setAttribute('data-key', key); + // Copy/Paste button (contextual) + const cp = document.createElement('button'); + cp.type = 'button'; + cp.className = 'btn'; + const setCpStyle = (m)=>{ cp.className='btn'; if (m==='copy') cp.classList.add('ghost'); if (m==='paste') cp.classList.add('success'); }; + if (!this._weeklyCopyFromGroup) { cp.textContent = this._t('ui.copy_day'); setCpStyle('copy'); } + else if (this._weeklyCopyFromGroup === key) { cp.textContent = this._t('ui.copy_day'); setCpStyle('copy'); } + else { cp.textContent = this._t('ui.paste_day'); setCpStyle('paste'); } + // Disable on source day while copy mode is active + cp.disabled = !!this._weeklyCopyFromGroup && this._weeklyCopyFromGroup === key; + cp.onclick = (ev)=>{ + ev.preventDefault(); ev.stopPropagation(); + if (cp.disabled) return; + const modeNow = this._weeklyDraft.mode || 'weekday_weekend'; + if (!this._weeklyCopyFromGroup) { + const gDays = groupDaysForMode(modeNow, key); + const srcDay = (modeNow==='all_7') ? this._effectiveDayKey(key) : gDays[0]; + const arr = (this._weeklyDraft?.days?.[srcDay] || []); + this._weeklyCopyFromGroup = key; + this._weeklyCopyPayload = JSON.parse(JSON.stringify(arr)); + renderActions(); + return; + } + if (this._weeklyCopyFromGroup === key) { + // Refresh payload from same source, stay in copy mode + const gDays = groupDaysForMode(modeNow, key); + const srcDay = (modeNow==='all_7') ? this._effectiveDayKey(key) : gDays[0]; + const arr = (this._weeklyDraft?.days?.[srcDay] || []); + this._weeklyCopyPayload = JSON.parse(JSON.stringify(arr)); + return; + } + // Paste into this key + try { + const payload = Array.isArray(this._weeklyCopyPayload) ? this._weeklyCopyPayload.map(x=>({ ...x, id: Math.random().toString(36).slice(2,9) })) : []; + const targets = (modeNow==='all_7') ? [this._effectiveDayKey(key)] : groupDaysForMode(modeNow, key); + const daysClone = { ...(this._weeklyDraft.days || {}) }; + for (const dk of targets){ + const arr = payload.map(x=>({ ...x })); + arr.sort((a,b)=>a.startMin-b.startMin||a.endMin-b.endMin); + daysClone[dk] = arr; + } + this._weeklyDraft.days = daysClone; + this._ensureWeeklyStruct(this._schedules[this._weeklyEntity] || { weekly:{} }); + } catch(e){} + this._weeklyDayKey = key; this._renderWeeklyModal(); + }; + // Add block button (per day) — doubles as Cancel while in copy mode + const add = document.createElement('button'); + add.type = 'button'; + const applyAddState = ()=>{ + const inCopy = !!this._weeklyCopyFromGroup; + add.className = 'btn ' + (inCopy ? 'danger' : 'primary'); + add.textContent = inCopy ? this._t('ui.cancel') : ('+ ' + this._t('ui.add_block')); + }; + applyAddState(); + add.onclick = ()=>{ + if (this._weeklyCopyFromGroup) { + // Cancel copy mode + this._weeklyCopyFromGroup = null; this._weeklyCopyPayload = null; renderActions(); + } else { + this._weeklyDayKey = key; this._openWeeklyBlockEditor(null); + } + }; + box.append(cp, add); + actionsWrap.append(box); + } + }; + renderActions(); + // Ensure pastebar stays hidden with the new contextual control + try { const pb = this.shadowRoot.querySelector('.week-pastebar'); if (pb) { pb.style.display='none'; pb.innerHTML=''; } } catch{} + // Paste target bar (always real days mon..sun). Visible only in copy mode + if (pasteBar) { + if (!this._weeklyCopyFrom) { + pasteBar.style.display = 'none'; pasteBar.innerHTML = ''; + } else { + pasteBar.style.display = 'flex'; pasteBar.innerHTML = ''; + const names = this._weekdayNames(); + for (const k of names){ + const btn = document.createElement('button'); btn.type='button'; + if (k === this._weeklyCopyFrom) { + btn.className = 'btn danger'; btn.textContent = this._t('ui.cancel'); + btn.onclick = ()=>{ this._weeklyCopyFrom = null; this._weeklyCopyPayload = null; this._renderWeeklyModal(); }; + } else { + btn.className = 'btn success'; + const lab = (this._t('week.days_full.'+k) || this._t('week.days.'+k) || k); + btn.textContent = this._t('ui.paste_day') + ' ' + lab; + btn.onclick = ()=>{ + try { + const payload = Array.isArray(this._weeklyCopyPayload) ? this._weeklyCopyPayload.map(x=>({ ...x, id: Math.random().toString(36).slice(2,9) })) : []; + this._weeklyDraft.days[k] = payload.sort((a,b)=>a.startMin-b.startMin||a.endMin-b.endMin); + this._renderWeeklyModal(); + } catch(e){} + }; + } + pasteBar.append(btn); + } + } + } + // Build group/day tabs reflecting mode grouping + tabsWrap.innerHTML = ''; + const editable = this._weeklyEditableKeys(this._weeklyDraft.mode); + const items = []; + if (this._weeklyDraft.mode === 'weekday_weekend') { + items.push({key:'weekdays', label: (this._t('week.group.weekdays')||'Weekdays')}); + items.push({key:'weekend', label: (this._t('week.group.weekend')||'Weekend')}); + } else if (this._weeklyDraft.mode === 'weekday_sat_sun') { + items.push({key:'weekdays', label: (this._t('week.group.weekdays')||'Weekdays')}); + items.push({key:'sat', label: this._t('week.days_full.sat') || this._t('week.days.sat')}); + items.push({key:'sun', label: this._t('week.days_full.sun') || this._t('week.days.sun')}); + } else { // all_7 + for (const k of this._weekdayNames()) items.push({key:k, label:(this._t('week.days_full.'+k) || this._t('week.days.'+k))}); + } + if (!editable.includes(this._weeklyDayKey)) this._weeklyDayKey = editable[0] || 'weekdays'; + // Helper: group -> array of real day keys + const groupDays2 = (mode, key)=>{ + if (mode === 'weekday_weekend') { + if (key==='weekdays') return ['mon','tue','wed','thu','fri']; + if (key==='weekend') return ['sat','sun']; + } else if (mode === 'weekday_sat_sun') { + if (key==='weekdays') return ['mon','tue','wed','thu','fri']; + if (key==='sat') return ['sat']; + if (key==='sun') return ['sun']; + } else if (mode === 'all_7') { + return [key]; + } + return [this._effectiveDayKey(key)]; + }; + for (const it of items){ + const tab = document.createElement('button'); + tab.type = 'button'; + tab.className = 'wk-tab' + (it.key === this._weeklyDayKey ? ' active' : ''); + tab.textContent = it.label; + tab.addEventListener('click', ()=>{ this._weeklyDayKey = it.key; this._renderWeeklyModal(); }); + tabsWrap.append(tab); + } + // Rebuild actions so the visible set matches active tab + renderActions(); + // Compact scale ticks (every 2 hours) + if (scale) { + scale.innerHTML = ''; + for (let i=0;i<=24;i+=2){ + const t = document.createElement('div'); + t.style.position='absolute'; + t.style.left = (i/24*100)+'%'; + t.style.top='4px'; t.style.bottom='4px'; + t.style.borderLeft='1px solid var(--divider-color)'; + const lab = document.createElement('div'); lab.style.position='absolute'; lab.style.top='-2px'; lab.style.transform='translate(-50%,0)'; lab.style.left='0'; lab.style.fontSize='.75rem'; lab.style.color='var(--secondary-text-color)'; + if (this._config?.time_12h){ const p=this._timeParts(i===24?0:i*60); lab.innerHTML = `
    ${p.main}
    ${p.mer||''}
    `; } else { lab.textContent = (i===24? this._label(0) : this._label(i*60)); } + t.append(lab); scale.append(t); + } + } + // Render preview track with today's selected day blocks + track.innerHTML = ''; + const effKey = this._effectiveDayKey(this._weeklyDayKey); + const blocks = (this._weeklyDraft.days?.[effKey] || []); + // Ensure tooltip exists (scoped to the weekly modal to avoid conflicts with other tooltips) + let tooltip = (modalWeek?.querySelector('.wk-tooltip.week')) || this.shadowRoot.querySelector('.wk-tooltip.week'); + if (!tooltip) { tooltip = document.createElement('div'); tooltip.className='wk-tooltip week'; tooltip.style.display='none'; modalWeek?.append(tooltip); } + for (const b of blocks){ + const div = document.createElement('div'); + div.className = 'block'; + div.style.position='absolute'; + div.style.top='6px'; div.style.bottom='6px'; + div.style.left = (b.startMin/1440*100)+'%'; + div.style.width = ((b.endMin-b.startMin)/1440*100)+'%'; + // Only show temperature in the block in the weekly popup (hide time) + try { + const clr = this._colorFor(this._weeklyEntity, b.temp); + if (clr) { + div.style.background = clr; + div.style.borderColor = clr; + const txt = this._contrastTextColor(clr); + if (txt) div.style.color = txt; + } + } catch {} + UiHelper.genBlockPill( + this, + div, + `${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}` + ); + // double-click to edit (prevent bubbling to track handler) + div.addEventListener('dblclick', (ev)=>{ try { ev.stopPropagation(); ev.preventDefault(); } catch {} this._openWeeklyBlockEditor(b.id); }); + // Hover -> show tooltip with full time range + temp (no click needed) + const showTip = ()=>{ + const txt = `${this._label(b.startMin)} - ${this._label(b.endMin)} • ${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}`; + if (!tooltip) return; + try { if (tooltip._hideTimer) { clearTimeout(tooltip._hideTimer); tooltip._hideTimer = null; } } catch {} + // Do not show while editor popup is open + try { + const ov = this.shadowRoot.querySelector('.overlay'); + if (this._editing || (ov && ov.classList.contains('open'))) return; + } catch {} + tooltip.textContent = txt; + const box = div.getBoundingClientRect(); + const cont = modalWeek?.getBoundingClientRect(); + const left = box.left + box.width/2 - (cont?.left||0); + const top = (box.top - (cont?.top||0)) - 8; // a little above + tooltip.style.left = left + 'px'; + tooltip.style.top = top + 'px'; + tooltip.style.transform = 'translate(-50%,-100%)'; + tooltip.style.display = ''; + }; + const hideTip = ()=>{ + if (!tooltip) return; + const delay = (window.matchMedia && window.matchMedia('(pointer:coarse)').matches) ? 3000 : 120; + try { if (tooltip._hideTimer) clearTimeout(tooltip._hideTimer); } catch {} + tooltip._hideTimer = setTimeout(() => { try { tooltip.style.display='none'; tooltip._hideTimer = null; } catch {} }, delay); + }; + div.addEventListener('mouseenter', showTip); + div.addEventListener('mouseleave', hideTip); + track.append(div); + } + // Add click on empty area to add + track.addEventListener('dblclick', (e)=>{ + try { + if (e.target && (e.target.closest && e.target.closest('.block'))) return; + const box = track.getBoundingClientRect(); + const rel = (e.clientX - box.left) / box.width; const min = this._clamp(Math.round(rel*1440),0,1439); + this._openWeeklyBlockEditor(null, min); + } catch { this._openWeeklyBlockEditor(null); } + }); + track.addEventListener('mouseleave', ()=>{ + const tip = modalWeek?.querySelector('.wk-tooltip.week'); + if (!tip) return; + try { if (tip._hideTimer) { clearTimeout(tip._hideTimer); tip._hideTimer = null; } } catch {} + const delay = (window.matchMedia && window.matchMedia('(pointer:coarse)').matches) ? 3000 : 120; + tip._hideTimer = setTimeout(()=>{ try { tip.style.display='none'; tip._hideTimer = null; } catch{} }, delay); + }); + } catch(e){} + } + // ---------- Profiles modal ---------- + _openProfilesEditor(entity){ + try { + this._ensureEditModeOn(); + this._profilesEntity = entity; + const row = this._schedules[entity] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureProfilesStruct(row); + // New creation by default: empty timeline + this._profilesSelected = null; + this._profilesEditingExisting = false; + // Initialize empty draft for ALL rooms + const roomsMap = {}; for (const eid of (this._config?.entities||[])) roomsMap[eid] = []; + this._profilesDraft = { name: null, rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + this._profilesDirty = false; + this._profilesEditingNew = true; + this._profilesRoom = (this._config?.entities||[])[0] || entity; + this._profilesOpen = true; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-profiles'); + // Mark overlaying, render while hidden, then open after paint + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + this._renderProfilesModal(); + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { if (ov) ov.classList.add('open'); })); } catch { if (ov) ov.classList.add('open'); } + } catch { /* no-op */ } + } + _openConfirmCloseProfiles(){ try { + const pov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-profiles'); if (!pov) return; + // If an existing confirm is open, do nothing + if (pov.querySelector('.mini-confirm')) return; + const wrap = document.createElement('div'); wrap.className='mini-confirm'; + const box = document.createElement('div'); box.className='box'; + const h = document.createElement('h4'); h.textContent = this._t('ui.unsaved_confirm'); + const btns = document.createElement('div'); btns.className='btns'; + const keep = document.createElement('button'); keep.type='button'; keep.className='btn ghost'; keep.textContent = this._t('ui.keep_editing') || 'Keep editing'; + const discard = document.createElement('button'); discard.type='button'; discard.className='btn danger'; discard.textContent = this._t('ui.discard_changes') || 'Discard changes'; + keep.addEventListener('click', ()=>{ try { wrap.remove(); } catch {} }); + discard.addEventListener('click', ()=>{ try { wrap.remove(); } catch {}; this._closeProfilesEditor(); }); + btns.append(keep, discard); box.append(h, btns); wrap.append(box); pov.append(wrap); + } catch {} } + _closeProfilesEditor(){ + this._profilesOpen = false; this._profilesEntity = null; this._profilesSelected = null; this.shadowRoot.querySelector('.overlay-profiles')?.classList.remove('open'); + try { this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} + } + _renderProfilesModal(){ try { + const title = this.shadowRoot.querySelector('.prof-title'); if (title) title.textContent = this._t('profiles.editor.title'); + const roomEl = this.shadowRoot.querySelector('.prof-room'); if (roomEl) { const eid = this._profilesEntity; const custom = (this._config?.labels?.[eid]) || null; roomEl.textContent = custom || this._prettyName(eid||''); } + const note = this.shadowRoot.querySelector('.prof-note'); if (note) note.textContent = this._t('profiles.preview_note'); + const list = this.shadowRoot.querySelector('.prof-list'); + const addBtn = this.shadowRoot.querySelector('.prof-add'); + const closeBtn = this.shadowRoot.querySelector('.prof-close'); + const saveBtn = this.shadowRoot.querySelector('.prof-save'); + const modalProf = this.shadowRoot.querySelector('.modal-profiles'); + const scale = modalProf?.querySelector('.week-scale-inner') || this.shadowRoot.querySelector('.week-scale-inner'); + const track = modalProf?.querySelector('.week-track') || this.shadowRoot.querySelector('.week-track'); + const roomTabs = modalProf?.querySelector('.prof-roomtabs'); + if (!list || !track || !this._profilesEntity) return; + + // Keep the Save button label in sync with current mode + try { + if (saveBtn) { + const isExisting = !!(this._profilesEditingExisting && this._profilesSelected); + const key = isExisting ? 'profiles.save_existing' : 'profiles.save_new'; + saveBtn.textContent = this._t(key) || this._t('ui.save') || 'Save'; + } + } catch {} + const row = this._schedules[this._profilesEntity] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureProfilesStruct(row); + // Build room tabs (one per configured entity) + if (roomTabs) { + roomTabs.innerHTML = ''; + const ents = this._config?.entities || []; + if (!this._profilesRoom || !ents.includes(this._profilesRoom)) this._profilesRoom = ents[0] || null; + for (const eid of ents){ + const btn = document.createElement('button'); btn.type='button'; btn.className='wk-tab' + (eid===this._profilesRoom?' active':''); + let label = this._config?.labels?.[eid] || (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1]||eid)); + btn.textContent = label; btn.addEventListener('click', ()=>{ this._profilesRoom = eid; this._renderProfilesModal(); }); + roomTabs.append(btn); + } + } + // Build list + list.innerHTML = ''; + const names = Object.keys(row.profiles || {}); + if (!names.length) { + const p = document.createElement('div'); p.style.fontSize = '.9rem'; p.style.color='var(--secondary-text-color)'; p.textContent = this._t('profiles.none'); list.append(p); + } + for (const name of names){ + const item = document.createElement('div'); item.className = 'prof-item' + (this._profilesSelected===name ? ' active' : ''); + // Name label: intentionally non-interactive (only action buttons should do something) + const left = document.createElement('div'); left.className='prof-chip'; left.textContent = name; + const actions = document.createElement('div'); actions.className = 'prof-actions'; + const editBtn = document.createElement('button'); editBtn.type='button'; editBtn.className='btn ghost'; editBtn.textContent=this._t('profiles.edit'); + // Highlight Edit button when this profile is the one currently being edited + try { + if (this._profilesEditingExisting && this._profilesSelected === name) { + editBtn.classList.remove('ghost'); + editBtn.classList.add('success'); + editBtn.setAttribute('aria-pressed','true'); + } else { + editBtn.setAttribute('aria-pressed','false'); + } + } catch {} + editBtn.addEventListener('click', ()=>{ + // Enter edit mode explicitly (overwriting the selected profile on Save) + this._profilesSelected = name; + this._profilesEditingExisting = true; + this._profilesEditingNew = false; + try { + const roomsMap = {}; + for (const eid of (this._config?.entities||[])){ + const src = this._schedules[eid]?.profiles?.[name]?.blocks || []; + roomsMap[eid] = JSON.parse(JSON.stringify(src)); + } + this._profilesDraft = { name, rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + this._profilesDirty = false; + } catch { + this._profilesDraft = null; + this._profilesDirty = false; + } + this._renderProfilesModal(); + try { const trk = modalProf?.querySelector('.week-track'); trk?.scrollIntoView({ behavior:'smooth', block:'center' }); } catch {} + }); + const act = document.createElement('button'); act.type='button'; act.className = 'btn ' + (row.activeProfile===name?'success':'ghost'); act.textContent = (row.activeProfile===name)? this._t('profiles.deactivate') : this._t('profiles.activate'); + act.addEventListener('click', async ()=>{ + const before = this._desiredNowSnapshot(); + // Toggle global activation: if already globally selected, turn off; else activate this name for all rooms + const enable = (this._globalProfile !== name); + if (enable) { + this._globalProfile = name; + this._config.profiles_enabled = true; + } else { + this._globalProfile = null; + // Keep profiles_enabled as-is; user can still have per-room profiles + } + // Apply across all configured rooms + for (const eid of (this._config?.entities || [])){ + const r = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureProfilesStruct(r); + if (enable) { + r.activeProfile = (r.profiles && r.profiles[name]) ? name : null; + } else { + r.activeProfile = null; + } + this._schedules[eid] = r; + } + this._markPendingSave(); + this._render(); + this._renderProfilesModal(); + try { + if (this._config?.auto_apply && !this._config?.storage_enabled && !this._isPaused()) { + await this._applyIfDesiredChanged(before); + } + } catch {} + this._scheduleNextApply(); + // Auto-close profiles modal when a profile is (de)activated + try { this._closeProfilesEditor(); } catch {} + }); + const ren = document.createElement('button'); ren.type='button'; ren.className='btn ghost'; ren.textContent=this._t('profiles.rename'); ren.addEventListener('click', async ()=>{ + const cur = name; const txt = prompt(this._t('profiles.rename'), cur) || ''; + const nn = txt.trim(); if (!nn || nn===cur) return; + if (row.profiles[nn]) { alert(this._t('profiles.name_exists') || 'Name exists'); return; } + row.profiles[nn] = row.profiles[cur]; delete row.profiles[cur]; if (row.activeProfile===cur) row.activeProfile = nn; if (this._profilesSelected===cur) this._profilesSelected = nn; this._markPendingSave(); this._renderProfilesModal(); this._render(); + }); + const del = document.createElement('button'); del.type='button'; del.className='remove-btn'; del.innerHTML = ''+this._t('profiles.delete')+''; + del.addEventListener('click', async ()=>{ if (!confirm(this._t('profiles.delete')+'?')) return; delete row.profiles[name]; if (row.activeProfile===name) row.activeProfile=null; if (this._profilesSelected===name) this._profilesSelected = Object.keys(row.profiles||{})[0]||null; this._markPendingSave(); this._renderProfilesModal(); this._render(); this._scheduleNextApply(); }); + actions.append(editBtn, act, ren, del); item.append(left, actions); list.append(item); + } + if (addBtn) { + try { const sp = addBtn.querySelector('span'); if (sp) sp.textContent = this._t('profiles.add'); } catch {} + addBtn.classList.remove('btn','primary'); addBtn.classList.add('add-entity-btn'); + addBtn.onclick = ()=>{ + // Reset to a brand‑new draft (empty timeline) + try { + const eid = this._profilesEntity; const row = this._schedules[eid] || { defaultTemp: this._config.default_temp }; + this._profilesSelected = null; this._profilesEditingNew = true; + this._profilesEditingExisting = false; + this._profilesDraft = { name: null, blocks: [], defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + this._profilesDirty = false; this._renderProfilesModal(); + } catch {} + }; + } + if (closeBtn) { try { closeBtn.textContent = this._t('ui.close') || 'Close'; } catch {} } + // Draw scale + try { + if (scale) { + scale.innerHTML = ''; + // Match the weekly preview: ticks every 2 hours using simple borders and small labels + for (let i=0;i<=24;i+=2){ + const t = document.createElement('div'); + t.style.position='absolute'; + t.style.left = (i/24*100)+'%'; + t.style.top='4px'; t.style.bottom='4px'; + t.style.borderLeft='1px solid var(--divider-color)'; + const lab = document.createElement('div'); + lab.style.position='absolute'; lab.style.top='-2px'; lab.style.transform='translate(-50%,0)'; lab.style.left='0'; lab.style.fontSize='.75rem'; lab.style.color='var(--secondary-text-color)'; + if (this._config?.time_12h){ const p=this._timeParts(i===24?0:i*60); lab.innerHTML = `
    ${p.main}
    ${p.mer||''}
    `; } else { lab.textContent = (i===24 ? this._label(0) : this._label(i*60)); } + t.append(lab); scale.append(t); + } + } + } catch {} + // Draw track for selected profile + track.innerHTML = ''; + const sel = this._profilesSelected; + let pblocks = []; + const curRoom = this._profilesRoom || this._profilesEntity; + if (this._profilesDraft && (!sel || this._profilesDraft.name===sel)) { + const rooms = this._profilesDraft.rooms || {}; pblocks = rooms[curRoom] || []; + } else if (sel) { + // Fallback: show blocks from the current room's saved profile + const r = this._schedules[curRoom] || {}; pblocks = (r.profiles?.[sel]?.blocks) || []; + } + const addBtnInline = document.createElement('button'); addBtnInline.type='button'; addBtnInline.className='btn primary'; addBtnInline.innerHTML = ''+this._t('ui.add_block')+''; addBtnInline.addEventListener('click', ()=>{ + this._openProfileBlockEditor(null, this._getNowMin()); + }); + const toolbar = document.createElement('div'); toolbar.className='prof-toolbar'; toolbar.style.display='flex'; toolbar.style.justifyContent='flex-end'; toolbar.style.gap='8px'; toolbar.append(addBtnInline); + const topbar = document.createElement('div'); topbar.className='prof-topbar'; + // Move the existing note into the topbar so note (left) + add button (right) are on the same line + try { if (note) { note.style.whiteSpace = 'normal'; topbar.append(note); } } catch {} + topbar.append(toolbar); + // Place topbar ABOVE the time scale (before the .week-scale container) + try { + const outerScale = (scale && scale.closest) ? scale.closest('.week-scale') : (modalProf?.querySelector('.week-scale') || null); + const parent = outerScale?.parentElement || track.parentElement; + if (parent) { + Array.from(parent.querySelectorAll('.prof-topbar')).forEach(el=>el.remove()); + Array.from(parent.querySelectorAll('.prof-toolbar')).forEach(el=>el.remove()); + if (outerScale) parent.insertBefore(topbar, outerScale); else parent.insertBefore(topbar, track); + } + } catch {} + // Tooltip (same style as weekdays) + let tooltip = this.shadowRoot.querySelector('.wk-tooltip.prof'); + if (!tooltip) { tooltip = document.createElement('div'); tooltip.className='wk-tooltip prof'; tooltip.style.display='none'; modalProf?.append(tooltip); } + for (const b of pblocks){ + const pctStart = (b.startMin/1440)*100; const pctW = ((b.endMin-b.startMin)/1440)*100; + const bl = document.createElement('div'); bl.className='block'; bl.style.left=`${pctStart}%`; bl.style.width=`${pctW}%`; + try { const clr = this._colorFor(this._profilesEntity, b.temp); if (clr){ bl.style.background=clr; bl.style.borderColor=clr; const txt=this._contrastTextColor(clr); if (txt) bl.style.color=txt; } } catch {} + // Hide pill via CSS; use hover tooltip like weekdays + UiHelper.genBlockPill( + this, bl, + `${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}` + ); + bl.addEventListener('dblclick', ()=> this._openProfileBlockEditor(b.id)); + const showTip = ()=>{ + const txt = `${this._label(b.startMin)} - ${this._label(b.endMin)} • ${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}`; + if (!tooltip) return; + try { if (tooltip._hideTimer) { clearTimeout(tooltip._hideTimer); tooltip._hideTimer = null; } } catch {} + // Suppress while block editor is open + try { const ov = this.shadowRoot.querySelector('.overlay'); if (this._editing || (ov && ov.classList.contains('open'))) return; } catch {} + tooltip.textContent = txt; + const box = bl.getBoundingClientRect(); const cont = modalProf?.getBoundingClientRect(); + const left = box.left + box.width/2 - (cont?.left||0); const top = (box.top - (cont?.top||0)) - 8; + tooltip.style.left = left + 'px'; tooltip.style.top = top + 'px'; tooltip.style.transform = 'translate(-50%,-100%)'; tooltip.style.display=''; + }; + const hideTip = ()=>{ if (!tooltip) return; const delay = (window.matchMedia && window.matchMedia('(pointer:coarse)').matches) ? 3000 : 120; try { if (tooltip._hideTimer) clearTimeout(tooltip._hideTimer); } catch {}; tooltip._hideTimer = setTimeout(()=>{ try { tooltip.style.display='none'; tooltip._hideTimer=null; } catch{} }, delay); }; + bl.addEventListener('mouseenter', showTip); + bl.addEventListener('mouseleave', hideTip); + track.append(bl); + } + } catch { /* ignore */ } + } + _openProfileBlockEditor(blockId, atMin=null){ + try { + this._ensureEditModeOn(); + const eid = this._profilesRoom || this._profilesEntity; const name = this._profilesSelected; if (!eid) return; + const row = this._schedules[eid]; this._ensureProfilesStruct(row); + // Use draft while editing + if (!this._profilesDraft || this._profilesDraft.name !== name) { + try { + const roomsMap = {}; + for (const e of (this._config?.entities||[])){ + const src = name ? (this._schedules[e]?.profiles?.[name]?.blocks || []) : []; + roomsMap[e] = JSON.parse(JSON.stringify(src)); + } + this._profilesDraft = { name, rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + this._profilesDirty = false; + } catch { + const roomsMap = {}; for (const e of (this._config?.entities||[])) roomsMap[e] = []; + this._profilesDraft = { name, rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; this._profilesDirty = false; + } + } + const arr = (this._profilesDraft.rooms||{})[eid] || []; + const edTemp = this.shadowRoot.querySelector('.ed-temp'); const edFrom = this.shadowRoot.querySelector('.ed-from'); const edTo = this.shadowRoot.querySelector('.ed-to'); const overlay = this.shadowRoot.querySelector('.overlay'); const title = this.shadowRoot.querySelector('.modal h3'); const delBtn = this.shadowRoot.querySelector('.ed-delete'); const err = this.shadowRoot.querySelector('.ed-error'); const fromMer = this.shadowRoot.querySelector('.ed-from-mer'); const toMer = this.shadowRoot.querySelector('.ed-to-mer'); + if (!blockId){ + // New block default around now + const now = Number.isFinite(atMin) ? atMin : this._getNowMin(); const start = this._clamp(Math.round(now), 0, 1380); const end = this._clamp(start+60, start+15, 1440); + this._editing = { entity: eid, blockId: null, profileName: (name || '__new__') }; + if (edTemp) { const base = Number(row.defaultTemp||this._config.default_temp||20); edTemp.value = String(this._toDisplayTemp(base)); edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } + if (this._config?.time_12h) { if (edFrom) edFrom.value=this._toTimeInput12h(start); if (edTo) edTo.value=this._toTimeInput12h(end); if (fromMer){ fromMer.style.display=''; fromMer.value=(start>=720)?'PM':'AM'; } if (toMer){ toMer.style.display=''; toMer.value=(end>=720 && end<1440)?'PM':'AM'; } } else { if (edFrom) edFrom.value=this._toTimeInput(start); if (edTo) edTo.value=this._toTimeInput(end); if (fromMer) fromMer.style.display='none'; if (toMer) toMer.style.display='none'; } + if (title) title.textContent = this._t('ui.add_block'); if (delBtn) delBtn.style.display='none'; if (err){ err.style.display='none'; err.textContent=''; } + } else { + const b = arr.find(x=>x.id===blockId); if (!b) return; + this._editing = { entity: eid, blockId, profileName: (name || '__new__') }; + if (edTemp) { edTemp.value = String(this._toDisplayTemp(b.temp)); edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } + if (this._config?.time_12h) { if (edFrom) edFrom.value=this._toTimeInput12h(b.startMin); if (edTo) edTo.value=this._toTimeInput12h(b.endMin); if (fromMer){ fromMer.style.display=''; fromMer.value=(b.startMin>=720)?'PM':'AM'; } if (toMer){ toMer.style.display=''; toMer.value=(b.endMin>=720 && b.endMin<1440)?'PM':'AM'; } } else { if (edFrom) edFrom.value=this._toTimeInput(b.startMin); if (edTo) edTo.value=this._toTimeInput(b.endMin); if (fromMer) fromMer.style.display='none'; if (toMer) toMer.style.display='none'; } + if (title) title.textContent = this._t('ui.edit_block'); if (delBtn) delBtn.style.display=''; if (err){ err.style.display='none'; err.textContent=''; } + } + // Hide profiles overlay while editing + const pov = this.shadowRoot.querySelector('.overlay-profiles'); if (pov) pov.classList.remove('open'); this._profilesOverlayHiddenForEditor = true; + try { const modalEl = this.shadowRoot.querySelector('.overlay .modal:not(.modal-week)'); if (modalEl) modalEl.classList.toggle('modal-12h', !!this._config?.time_12h); } catch {} + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { overlay.classList.add('open'); })); } catch { overlay.classList.add('open'); } + } catch {} + } + async _profilesSaveDraft(){ + try { + const eid = this._profilesEntity; const sel = this._profilesSelected; const d = this._profilesDraft; + if (!eid || !sel || !d || d.name !== sel) return; + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureProfilesStruct(row); + // Persist current draft into storage structure + const blocks = (d.rooms && d.rooms[eid]) ? d.rooms[eid] : []; + row.profiles[sel] = { defaultTemp: row.defaultTemp, blocks: JSON.parse(JSON.stringify(blocks)) }; + this._schedules[eid] = row; + this._markPendingSave(); + // Clear dirty flag and revert Add/Save button immediately + this._profilesDirty = false; + try { + const btn = this.shadowRoot && this.shadowRoot.querySelector('.prof-add'); + const sp = btn && btn.querySelector && btn.querySelector('span'); + if (btn && sp) { sp.textContent = this._t('profiles.add'); btn.classList.remove('btn','primary'); btn.classList.add('add-entity-btn'); btn.onclick = null; } + } catch {} + // Refresh UI and reschedule transitions + this._render(); + this._renderProfilesModal(); + this._scheduleNextApply(); + } catch (e) {} + } + async _profilesSave(){ + try { + // Prevent double-trigger when user presses Enter to confirm the prompt + if (this._profilesSaveLock) return; this._profilesSaveLock = true; + const eid = this._profilesEntity; if (!eid) return; + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; this._ensureProfilesStruct(row); + const draft = this._profilesDraft || { name:null, rooms:{} }; + // New profile: prompt for name + if (this._profilesEditingNew || !this._profilesSelected) { + const base = this._t('profiles.name_placeholder'); + const txt = prompt(base||'Profile name', 'Profile'); + const name = (txt||'').trim(); if (!name) return; + if (row.profiles[name]) { alert(this._t('profiles.name_exists') || 'Name exists'); return; } + // Save this profile for ALL rooms from the draft map + for (const rid of (this._config?.entities||[])){ + const rrow = this._schedules[rid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureProfilesStruct(rrow); + const blocks = (draft.rooms?.[rid] || []); + rrow.profiles = rrow.profiles || {}; rrow.profiles[name] = { defaultTemp: rrow.defaultTemp, blocks: JSON.parse(JSON.stringify(blocks)) }; + this._schedules[rid] = rrow; + } + // Persist and reset timeline for a fresh new draft (ready for next) + this._profilesDirty = false; + this._markPendingSave(); + try { + this._profilesSelected = null; this._profilesEditingNew = true; + this._profilesEditingExisting = false; + const roomsMap = {}; for (const rid of (this._config?.entities||[])) roomsMap[rid] = []; + this._profilesDraft = { name: null, rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + } catch {} + // Move focus away from Save to avoid immediate re-click from Enter + try { (this.shadowRoot && this.shadowRoot.activeElement && this.shadowRoot.activeElement.blur && this.shadowRoot.activeElement.blur()); } catch {} + this._renderProfilesModal(); this._render(); this._scheduleNextApply(); + return; + } + // Edit existing: save without renaming, then reset timeline for next + const sel = this._profilesSelected; + for (const rid of (this._config?.entities||[])){ + const rrow = this._schedules[rid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureProfilesStruct(rrow); + const blocks = (draft.rooms?.[rid] || []); + rrow.profiles = rrow.profiles || {}; rrow.profiles[sel] = { defaultTemp: rrow.defaultTemp, blocks: JSON.parse(JSON.stringify(blocks)) }; + this._schedules[rid] = rrow; + } + this._profilesDirty = false; + this._markPendingSave(); + try { (this.shadowRoot && this.shadowRoot.activeElement && this.shadowRoot.activeElement.blur && this.shadowRoot.activeElement.blur()); } catch {} + // Clear selection and show empty preview ready for a new schedule + try { + this._profilesSelected = null; this._profilesEditingNew = true; + this._profilesEditingExisting = false; + const roomsMap = {}; for (const rid of (this._config?.entities||[])) roomsMap[rid] = []; + this._profilesDraft = { name: null, rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + } catch {} + this._renderProfilesModal(); this._render(); this._scheduleNextApply(); + } catch (e) {} + finally { setTimeout(()=>{ this._profilesSaveLock = false; }, 300); } + } + _openCopyWeekToRooms(){ try { + // Build list of target rooms (exclude current) + const list = this.shadowRoot.querySelector('.copyrooms-list'); + const title = this.shadowRoot.querySelector('.copyrooms-title'); + const desc = this.shadowRoot.querySelector('.copyrooms-desc'); + const ov = this.shadowRoot.querySelector('.overlay-copyrooms'); + if (title) title.textContent = this._t('week.copy_to_rooms.title'); + if (desc) desc.textContent = this._t('week.copy_to_rooms.select'); + if (list) { + list.innerHTML = ''; + const ents = (this._config?.entities || []); + for (const eid of ents){ + if (eid === this._weeklyEntity) continue; + const lbl = (this._config?.labels && this._config.labels[eid]) ? this._config.labels[eid] : this._prettyName(eid); + const row = document.createElement('label'); + const cb = document.createElement('input'); cb.type='checkbox'; cb.value=eid; + const span = document.createElement('span'); span.textContent = lbl; + row.append(cb, span); list.append(row); + } + } + const cancel = this.shadowRoot.querySelector('.copyrooms-cancel'); + const apply = this.shadowRoot.querySelector('.copyrooms-apply'); + if (cancel) cancel.onclick = ()=>{ ov?.classList.remove('open'); }; + if (apply) apply.onclick = ()=> this._applyCopyWeekToRooms(); + ov?.classList.add('open'); + } catch(e){} + } + async _applyCopyWeekToRooms(){ try { + const list = this.shadowRoot.querySelector('.copyrooms-list'); + const ov = this.shadowRoot.querySelector('.overlay-copyrooms'); + if (!this._weeklyDraft) { ov?.classList.remove('open'); return; } + const checked = Array.from(list?.querySelectorAll('input[type=checkbox]:checked') || []).map(x=>x.value); + // Queue the targets; actual copy occurs on Save in weekly editor + this._weeklyCopyTargets = checked; + ov?.classList.remove('open'); + // Optionally re-render header/note to reflect queued action (no persistence yet) + this._renderWeeklyModal(); + } catch(e){} + } + _openWeeklyBlockEditor(blockId, atMin=null){ if (!this._weeklyDraft) return; this._ensureEditModeOn(); const day = this._effectiveDayKey(this._weeklyDayKey || 'mon'); const arr = this._weeklyDraft.days?.[day] || []; if (!blockId){ + // open new + const start = Number.isFinite(atMin) ? this._clamp(Math.round(atMin),0,1380) : this._clamp(Math.round(this._getNowMin()),0,1380); + const end = this._clamp(start+60, start+15, 1440); + const row = this._schedules[this._weeklyEntity] || { defaultTemp: this._config.default_temp }; + const edTemp = this.shadowRoot.querySelector('.ed-temp'); const edFrom = this.shadowRoot.querySelector('.ed-from'); const edTo = this.shadowRoot.querySelector('.ed-to'); const overlay = this.shadowRoot.querySelector('.overlay'); const title = this.shadowRoot.querySelector('.modal h3'); const delBtn = this.shadowRoot.querySelector('.ed-delete'); const err = this.shadowRoot.querySelector('.ed-error'); const fromMer = this.shadowRoot.querySelector('.ed-from-mer'); const toMer = this.shadowRoot.querySelector('.ed-to-mer'); + this._editing = { entity: this._weeklyEntity, blockId: null, weeklyDay: day }; + // Hide any tooltips while editing + try { const tips = this.shadowRoot.querySelectorAll('.wk-tooltip'); tips.forEach(t => { try { if (t._hideTimer) clearTimeout(t._hideTimer); } catch {} t.style.display='none'; }); } catch {} + if (edTemp) { edTemp.value = String(this._toDisplayTemp(row.defaultTemp||20)); edTemp.max= String(this._maxDisplay()); } + if (this._config?.time_12h) { + if (edFrom) edFrom.value = this._toTimeInput12h(start); + if (edTo) edTo.value = this._toTimeInput12h(end); + if (fromMer){ fromMer.style.display=''; fromMer.value = (start>=720)?'PM':'AM'; } + if (toMer){ toMer.style.display=''; toMer.value = (end>=720 && end<1440)?'PM':'AM'; } + } else { + if (edFrom) edFrom.value = this._toTimeInput(start); + if (edTo) edTo.value = this._toTimeInput(end); + if (fromMer) fromMer.style.display='none'; + if (toMer) toMer.style.display='none'; + } + if (title) title.textContent = this._t('ui.add_block'); if (delBtn) delBtn.style.display='none'; if (err){ err.style.display='none'; err.textContent=''; } + // Hide weekday overlay while editing to avoid double-popups + const wov = this.shadowRoot.querySelector('.overlay-week'); if (wov) wov.classList.remove('open'); this._weeklyOverlayHiddenForEditor = true; + try { const modalEl = this.shadowRoot.querySelector('.overlay .modal:not(.modal-week)'); if (modalEl) modalEl.classList.toggle('modal-12h', !!this._config?.time_12h); } catch {} + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { overlay.classList.add('open'); })); } catch { overlay.classList.add('open'); } + } else { + let b = arr.find(x=>x.id===blockId); + + // If not found in weekly.days, try searching in _weeklyModes + if (!b && this._weeklyDraft?._weeklyModes) { + for (const modeName in this._weeklyDraft._weeklyModes) { + const modeBlocks = this._weeklyDraft._weeklyModes[modeName]?.days?.[day]; + if (Array.isArray(modeBlocks)) { + b = modeBlocks.find(x=>x.id===blockId); + if (b) break; + } + } + } + + if (!b) return; + const edTemp = this.shadowRoot.querySelector('.ed-temp'); const edFrom = this.shadowRoot.querySelector('.ed-from'); const edTo = this.shadowRoot.querySelector('.ed-to'); const overlay = this.shadowRoot.querySelector('.overlay'); const title = this.shadowRoot.querySelector('.modal h3'); const delBtn = this.shadowRoot.querySelector('.ed-delete'); const err = this.shadowRoot.querySelector('.ed-error'); const fromMer = this.shadowRoot.querySelector('.ed-from-mer'); const toMer = this.shadowRoot.querySelector('.ed-to-mer'); + this._editing = { entity: this._weeklyEntity, blockId, weeklyDay: day }; + // Hide any tooltips while editing + try { const tips = this.shadowRoot.querySelectorAll('.wk-tooltip'); tips.forEach(t => { try { if (t._hideTimer) clearTimeout(t._hideTimer); } catch {} t.style.display='none'; }); } catch {} + if (edTemp) { edTemp.value = String(this._toDisplayTemp(b.temp)); edTemp.max= String(this._maxDisplay()); } + if (this._config?.time_12h) { + if (edFrom) edFrom.value = this._toTimeInput12h(b.startMin); + if (edTo) edTo.value = this._toTimeInput12h(b.endMin); + if (fromMer){ fromMer.style.display=''; fromMer.value = (b.startMin>=720)?'PM':'AM'; } + if (toMer){ toMer.style.display=''; toMer.value = (b.endMin>=720 && b.endMin<1440)?'PM':'AM'; } + } else { + if (edFrom) edFrom.value = this._toTimeInput(b.startMin); + if (edTo) edTo.value = this._toTimeInput(b.endMin); + if (fromMer) fromMer.style.display='none'; + if (toMer) toMer.style.display='none'; + } + if (title) title.textContent = this._t('ui.edit_block'); if (delBtn) delBtn.style.display=''; if (err){ err.style.display='none'; err.textContent=''; } + const wov = this.shadowRoot.querySelector('.overlay-week'); if (wov) wov.classList.remove('open'); this._weeklyOverlayHiddenForEditor = true; + try { const modalEl = this.shadowRoot.querySelector('.overlay .modal:not(.modal-week)'); if (modalEl) modalEl.classList.toggle('modal-12h', !!this._config?.time_12h); } catch {} + overlay.classList.add('open'); + } + } + + // ---------- Holidays modal ---------- + _openHolidayEditor(entity){ + try { + this._ensureEditModeOn(); + const ents = this._config?.entities || []; + this._holidayRoom = entity || ents[0] || null; + // Build a rooms map draft from all rooms + const roomsMap = {}; + for (const eid of ents){ + const r = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureHolidayStruct(r); + roomsMap[eid] = Array.isArray(r.holiday?.blocks) ? JSON.parse(JSON.stringify(r.holiday.blocks)) : []; + } + this._holidayDraft = { rooms: roomsMap }; + this._holidayOpen = true; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-holiday'); + // Mark card overlay state first, then render while hidden + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + this._renderHolidayModal(); + // Open after a paint so timeline is fully laid out + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { if (ov) ov.classList.add('open'); })); } catch { if (ov) ov.classList.add('open'); } + } catch {} + } + _closeHolidayEditor(){ + try { this._holidayOpen = false; this._holidayEntity = null; this._holidayRoom = null; this._holidayDraft = null; } catch {} + try { this.shadowRoot.querySelector('.overlay-holiday')?.classList.remove('open'); } catch {} + try { this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} + try { + if (this._resumeSettingsAfterHoliday) { + this._resumeSettingsAfterHoliday = false; + this._resumeSettingsPopup?.(); + } + } catch {} + } + _renderHolidayModal(){ try { + const title = this.shadowRoot && this.shadowRoot.querySelector('.holiday-title'); if (title) title.textContent = this._t('holidays.editor.title'); + const roomEl = this.shadowRoot && this.shadowRoot.querySelector('.holiday-room'); if (roomEl) { const eid = this._holidayRoom; const custom = (this._config?.labels?.[eid]) || null; roomEl.textContent = custom || this._prettyName(eid||''); } + const note = this.shadowRoot && this.shadowRoot.querySelector('.holiday-note'); if (note) note.textContent = this._t('week.editor.preview_note'); + try { const sv = this.shadowRoot.querySelector('.holiday-save'); if (sv) sv.textContent = this._t('ui.save'); const cl = this.shadowRoot.querySelector('.holiday-close'); if (cl) cl.textContent = this._t('ui.cancel'); } catch {} + const modalHost = this.shadowRoot && this.shadowRoot.querySelector('.modal-holiday'); + const scale = modalHost?.querySelector('.week-scale-inner') || this.shadowRoot.querySelector('.week-scale-inner'); + const track = modalHost?.querySelector('.week-track') || this.shadowRoot.querySelector('.week-track'); + if (!track) return; + // Build room tabs + const tabs = modalHost?.querySelector('.holiday-roomtabs'); + if (tabs) { + tabs.innerHTML = ''; + const ents = this._config?.entities || []; + if (!this._holidayRoom || !ents.includes(this._holidayRoom)) this._holidayRoom = ents[0] || null; + for (const eid of ents){ const btn=document.createElement('button'); btn.type='button'; btn.className='wk-tab'+(eid===this._holidayRoom?' active':''); let label = this._config?.labels?.[eid] || (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1]||eid)); btn.textContent=label; btn.addEventListener('click', ()=>{ this._holidayRoom=eid; this._renderHolidayModal(); }); tabs.append(btn); } + } + // Draw scale every 2 hours + if (scale) { + scale.innerHTML = ''; + for (let i=0;i<=24;i+=2){ + const t = document.createElement('div'); t.style.position='absolute'; t.style.left=(i/24*100)+'%'; t.style.top='4px'; t.style.bottom='4px'; t.style.borderLeft='1px solid var(--divider-color)'; + const lab = document.createElement('div'); lab.style.position='absolute'; lab.style.top='-2px'; lab.style.transform='translate(-50%,0)'; lab.style.left='0'; lab.style.fontSize='.75rem'; lab.style.color='var(--secondary-text-color)'; + if (this._config?.time_12h){ const p=this._timeParts(i===24?0:i*60); lab.innerHTML = `
    ${p.main}
    ${p.mer||''}
    `; } else { lab.textContent = (i===24 ? this._label(0) : this._label(i*60)); } + t.append(lab); scale.append(t); + } + } + // Toolbar with Add + const addBtnInline = document.createElement('button'); addBtnInline.type='button'; addBtnInline.className='btn primary'; addBtnInline.innerHTML = ''+this._t('ui.add_block')+''; + addBtnInline.addEventListener('click', ()=>{ this._openHolidayBlockEditor(null, this._getNowMin()); }); + const copyBtn = document.createElement('button'); copyBtn.type='button'; copyBtn.className='btn ghost'; copyBtn.innerHTML = ''+ (this._t('holidays.copy_from') || this._t('ui.copy_plan')) +''; + copyBtn.addEventListener('click', ()=> this._openHolidayCopy()); + const toolbar = document.createElement('div'); toolbar.className='prof-toolbar'; toolbar.style.display='flex'; toolbar.style.justifyContent='flex-end'; toolbar.style.gap='8px'; toolbar.append(copyBtn, addBtnInline); + try { + const outerScale = (scale && scale.closest) ? scale.closest('.week-scale') : (modalHost?.querySelector('.week-scale') || null); + const parent = outerScale?.parentElement || track.parentElement; + if (parent) { + Array.from(parent.querySelectorAll('.prof-toolbar')).forEach(el=>el.remove()); + if (outerScale) parent.insertBefore(toolbar, outerScale); else parent.insertBefore(toolbar, track); + } + } catch {} + // Draw blocks + track.innerHTML = ''; + const blocks = (this._holidayDraft && this._holidayDraft.rooms) ? (this._holidayDraft.rooms[this._holidayRoom] || []) : []; + // Tooltip like weekly + let tooltip = this.shadowRoot.querySelector('.wk-tooltip.holiday'); + if (!tooltip) { tooltip = document.createElement('div'); tooltip.className='wk-tooltip holiday'; tooltip.style.display='none'; modalHost?.append(tooltip); } + for (const b of blocks){ + const pctStart=(b.startMin/1440)*100, pctW=((b.endMin-b.startMin)/1440)*100; const bl=document.createElement('div'); bl.className='block'; bl.style.left=pctStart+'%'; bl.style.width=pctW+'%'; + try { const clr=this._colorFor(this._holidayRoom, b.temp); if (clr){ bl.style.background=clr; bl.style.borderColor=clr; const txt=this._contrastTextColor(clr); if (txt) bl.style.color=txt; } } catch {} + UiHelper.genBlockPill( + this, b, + `${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}` + ); + bl.addEventListener('dblclick', ()=> this._openHolidayBlockEditor(b.id)); + const showTip = ()=>{ const txt=`${this._label(b.startMin)} - ${this._label(b.endMin)} • ${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}`; if (!tooltip) return; try { if (tooltip._hideTimer) clearTimeout(tooltip._hideTimer); tooltip._hideTimer=null; } catch {} const box=bl.getBoundingClientRect(); const cont=modalHost?.getBoundingClientRect(); const left=box.left+box.width/2-(cont?.left||0); const top=(box.top-(cont?.top||0))-8; tooltip.textContent=txt; tooltip.style.left=left+'px'; tooltip.style.top=top+'px'; tooltip.style.transform='translate(-50%,-100%)'; tooltip.style.display=''; }; + const hideTip = ()=>{ if (!tooltip) return; const delay=(window.matchMedia&&window.matchMedia('(pointer:coarse)').matches)?3000:120; try{ if(tooltip._hideTimer) clearTimeout(tooltip._hideTimer);}catch{} tooltip._hideTimer=setTimeout(()=>{ try{ tooltip.style.display='none'; tooltip._hideTimer=null; }catch{} }, delay); }; + bl.addEventListener('mouseenter', showTip); bl.addEventListener('mouseleave', hideTip); + track.append(bl); + } + // Add on empty area + track.addEventListener('dblclick', (e)=>{ try { if (e.target && (e.target.closest && e.target.closest('.block'))) return; const box=track.getBoundingClientRect(); const rel=(e.clientX-box.left)/box.width; const min=this._clamp(Math.round(rel*1440),0,1439); this._openHolidayBlockEditor(null, min); } catch { this._openHolidayBlockEditor(null); } }); + } catch {} + } + + // ---------- Presence (advanced away) modal ---------- + _openPresenceEditor(entity){ + try { + this._ensureEditModeOn(); + // Default to first room + const ents = this._config?.entities || []; + this._presenceRoom = entity || ents[0] || null; + // Select current active combo if any; else first combo + const combos = this._presenceAllCombos(); + const activeKey = this._activePresenceComboKey(); + this._presenceCombo = activeKey || (combos[0]?.key || null); + // Build draft map for current combo from all rooms + const roomsMap = {}; + for (const eid of ents){ + const r = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensurePresenceStruct(r); + const arr = (this._presenceCombo && r.presence && r.presence[this._presenceCombo]) ? (r.presence[this._presenceCombo].blocks || []) : []; + roomsMap[eid] = JSON.parse(JSON.stringify(arr)); + } + this._presenceDraft = { combo: this._presenceCombo, rooms: roomsMap }; + this._presenceOpen = true; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-presence'); + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + this._renderPresenceModal(); + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { if (ov) ov.classList.add('open'); })); } catch { if (ov) ov.classList.add('open'); } + } catch {} + } + _closePresenceEditor(){ this._presenceOpen = false; this._presenceCombo = null; this._presenceDraft = null; this._presenceRoom = null; this.shadowRoot.querySelector('.overlay-presence')?.classList.remove('open'); try { this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} } + _renderPresenceModal(){ try { + const title = this.shadowRoot && this.shadowRoot.querySelector('.presence-title'); if (title) title.textContent = this._t('presence.editor.title'); + const roomEl = this.shadowRoot && this.shadowRoot.querySelector('.presence-room'); if (roomEl) { const eid = this._presenceRoom; const custom = (this._config?.labels?.[eid]) || null; roomEl.textContent = custom || this._prettyName(eid||''); } + // Hide toggle in modal (selection happens in editor) + try { const advRow = this.shadowRoot && this.shadowRoot.querySelector('.presence-adv-row'); if (advRow) advRow.style.display = 'none'; } catch {} + const list = this.shadowRoot && this.shadowRoot.querySelector('.presence-combos'); + const wrapActive = this.shadowRoot && this.shadowRoot.querySelector('.presence-combo-active'); + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-presence'); + const modalHost = this.shadowRoot && this.shadowRoot.querySelector('.overlay-presence .modal'); + const scale = modalHost?.querySelector('.week-scale-inner'); + const track = modalHost?.querySelector('.week-track'); + const tabs = modalHost?.querySelector('.presence-roomtabs'); + const btnSave = modalHost?.querySelector('.presence-save'); const btnClose = modalHost?.querySelector('.presence-close'); + // Build readonly vertical list of enabled combos (click to select) + if (list) { + list.innerHTML=''; + const all = this._presenceAllCombos(); + const conf = (this._config?.away?.combos) || {}; + const enabled = all.filter(it => !!conf[it.key]?.enabled); + const grid = document.createElement('div'); grid.className = 'presence-rows'; grid.style.border='1px solid var(--divider-color)'; grid.style.borderRadius='8px'; grid.style.padding='6px'; grid.style.maxHeight='180px'; grid.style.overflow='auto'; + const titleRow = document.createElement('div'); titleRow.style.fontSize='.9rem'; titleRow.style.color='var(--secondary-text-color)'; titleRow.textContent = this._t('presence.combos'); list.append(titleRow); + for (const it of enabled){ + const btn = document.createElement('button'); btn.type='button'; btn.className='presence-row' + (it.key===this._presenceCombo?' active':''); + const full = it.label; btn.textContent = full; btn.title = full; + btn.addEventListener('click', ()=>{ + this._presenceCombo = it.key; + const roomsMap={}; for (const eid of (this._config?.entities||[])){ const r=this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; this._ensurePresenceStruct(r); const arr=(r.presence && r.presence[it.key]) ? (r.presence[it.key].blocks || []) : []; roomsMap[eid]=JSON.parse(JSON.stringify(arr)); } + this._presenceDraft = { combo: it.key, rooms: roomsMap }; this._renderPresenceModal(); + }); + grid.append(btn); + } + list.append(grid); + } + // Active combo label + if (wrapActive) { + const combos = this._presenceAllCombos(); const conf=(this._config?.away?.combos)||{}; + const cur = combos.find(x=>x.key===this._presenceCombo && !!conf[x.key]?.enabled); + if (cur) { + const nameOf = (eid)=>{ + try { return this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1]||eid); } catch { return eid; } + }; + const homeNames = (cur.home||[]).map(nameOf).join(', ') || '-'; + const awayNames = (cur.away||[]).map(nameOf).join(', ') || '-'; + const lh = this._t('presence.who_home') || 'Who is home'; + const la = this._t('presence.who_away') || 'Who is not home'; + wrapActive.innerHTML = `
    ${lh}: ${homeNames}
    ${la}: ${awayNames}
    `; + } else { + wrapActive.textContent = ''; + wrapActive.title = ''; + } + } + // (combo tabs removed; selection happens via the list above) + // Room tabs + if (tabs) { + tabs.innerHTML=''; + const ents = this._config?.entities || []; + if (!this._presenceRoom || !ents.includes(this._presenceRoom)) this._presenceRoom = ents[0] || null; + for (const eid of ents){ const btn=document.createElement('button'); btn.type='button'; btn.className='wk-tab'+(eid===this._presenceRoom?' active':''); let label = this._config?.labels?.[eid] || (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1]||eid)); btn.textContent=label; btn.addEventListener('click', ()=>{ this._presenceRoom=eid; this._renderPresenceModal(); }); tabs.append(btn); } + try { tabs.style.display = (ents.length>1) ? 'flex' : 'none'; } catch {} + } + // Scale + if (scale) { scale.innerHTML=''; for (let i=0;i<=24;i+=2){ const t=document.createElement('div'); t.style.position='absolute'; t.style.left=(i/24*100)+'%'; t.style.top='4px'; t.style.bottom='4px'; t.style.borderLeft='1px solid var(--divider-color)'; const lab=document.createElement('div'); lab.style.position='absolute'; lab.style.top='-2px'; lab.style.transform='translate(-50%,0)'; lab.style.left='0'; lab.style.fontSize='.75rem'; lab.style.color='var(--secondary-text-color)'; if (this._config?.time_12h){ const p=this._timeParts(i===24?0:i*60); lab.innerHTML=`
    ${p.main}
    ${p.mer||''}
    `; } else { lab.textContent=(i===24? this._label(0) : this._label(i*60)); } t.append(lab); scale.append(t);} } + // Toolbar Add + if (modalHost) { + const addBtnInline = document.createElement('button'); addBtnInline.type='button'; addBtnInline.className='btn primary'; addBtnInline.innerHTML=''+this._t('ui.add_block')+''; + addBtnInline.addEventListener('click', ()=>{ this._openPresenceBlockEditor(null, this._getNowMin()); }); + const toolbar = document.createElement('div'); toolbar.className='prof-toolbar'; toolbar.style.display='flex'; toolbar.style.justifyContent='flex-end'; toolbar.style.gap='8px'; toolbar.append(addBtnInline); + const outerScale = (scale && scale.closest) ? scale.closest('.week-scale') : (modalHost.querySelector('.week-scale') || null); + const parent = outerScale?.parentElement || track?.parentElement; + if (parent) { Array.from(parent.querySelectorAll('.prof-toolbar')).forEach(el=>el.remove()); if (outerScale) parent.insertBefore(toolbar, outerScale); else if (track) parent.insertBefore(toolbar, track); } + } + // Track blocks for current room/combo + if (track) { + track.innerHTML=''; + const combo = this._presenceCombo; const room = this._presenceRoom; + const arr = (this._presenceDraft && this._presenceDraft.rooms) ? (this._presenceDraft.rooms[room] || []) : []; + // Tooltip reuse + let tooltip = this.shadowRoot.querySelector('.wk-tooltip.presence'); if (!tooltip) { tooltip = document.createElement('div'); tooltip.className='wk-tooltip presence'; tooltip.style.display='none'; (modalHost||document.body).append(tooltip); } + for (const b of arr){ + const pctStart=(b.startMin/1440)*100, pctW=((b.endMin-b.startMin)/1440)*100; + const bl=document.createElement('div'); + bl.className='block'; bl.style.left=pctStart+'%'; + bl.style.width=pctW+'%'; + try { + const clr=this._colorFor(room, b.temp); + if (clr){ + bl.style.background=clr; + bl.style.borderColor=clr; + const txt=this._contrastTextColor(clr); + if (txt) bl.style.color=txt; + } + } catch {} + UiHelper.genBlockPill( + this, bl, + `${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}` + ); + bl.addEventListener('dblclick', ()=> this._openPresenceBlockEditor(b.id)); + const showTip = ()=>{ const txt=`${this._label(b.startMin)} - ${this._label(b.endMin)} • ${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}`; + if (!tooltip) return; + try { + if (tooltip._hideTimer) clearTimeout(tooltip._hideTimer); + tooltip._hideTimer=null; + } catch {} + const box=bl.getBoundingClientRect(); + const cont=modalHost?.getBoundingClientRect(); + const left=box.left+box.width/2-(cont?.left||0); + const top=(box.top-(cont?.top||0))-8; + tooltip.textContent=txt; + tooltip.style.left=left+'px'; + tooltip.style.top=top+'px'; + tooltip.style.transform='translate(-50%,-100%)'; + tooltip.style.display=''; }; + const hideTip = ()=>{ + if (!tooltip) return; + const delay=(window.matchMedia&&window.matchMedia('(pointer:coarse)').matches)?3000:120; + try{ + if(tooltip._hideTimer) clearTimeout(tooltip._hideTimer); + }catch{} + tooltip._hideTimer=setTimeout( + ()=>{ + try{ + tooltip.style.display='none'; + tooltip._hideTimer=null; + } catch{} + }, delay + ); + }; + bl.addEventListener('mouseenter', showTip); + bl.addEventListener('mouseleave', hideTip); + track.append(bl); } + // dblclick to add + track.addEventListener('dblclick', (e)=>{ try { if (e.target && (e.target.closest && e.target.closest('.block'))) return; const box=track.getBoundingClientRect(); const rel=(e.clientX-box.left)/box.width; const min=this._clamp(Math.round(rel*1440),0,1439); this._openPresenceBlockEditor(null, min); } catch { this._openPresenceBlockEditor(null); } }); + } + // Save/Close + if (btnSave) btnSave.onclick = ()=> this._presenceSave(); + if (btnClose) btnClose.onclick = ()=> this._closePresenceEditor(); + } catch {} + } + _openPresenceBlockEditor(blockId, atMin=null){ try { + this._ensureEditModeOn(); + const eid = this._presenceRoom; if (!eid) return; + if (!this._presenceDraft || !this._presenceDraft.rooms) { + const roomsMap={}; for (const rid of (this._config?.entities||[])){ const r=this._schedules[rid] || { defaultTemp: this._config.default_temp, blocks: [] }; this._ensurePresenceStruct(r); const arr=(this._presenceCombo && r.presence && r.presence[this._presenceCombo]) ? (r.presence[this._presenceCombo].blocks || []) : []; roomsMap[rid]=JSON.parse(JSON.stringify(arr)); } + this._presenceDraft = { combo: this._presenceCombo, rooms: roomsMap }; + } + const arr = (this._presenceDraft.rooms||{})[eid] || []; + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + const edTemp = this.shadowRoot.querySelector('.ed-temp'); const edFrom = this.shadowRoot.querySelector('.ed-from'); const edTo = this.shadowRoot.querySelector('.ed-to'); const overlay = this.shadowRoot.querySelector('.overlay'); const title = this.shadowRoot.querySelector('.modal h3'); const delBtn = this.shadowRoot.querySelector('.ed-delete'); const err = this.shadowRoot.querySelector('.ed-error'); const fromMer = this.shadowRoot.querySelector('.ed-from-mer'); const toMer = this.shadowRoot.querySelector('.ed-to-mer'); + if (!blockId){ const now = Number.isFinite(atMin) ? atMin : this._getNowMin(); const start = this._clamp(Math.round(now), 0, 1380); const end = this._clamp(start+60, start+15, 1440); this._editing = { entity: eid, blockId: null, presence: true }; if (edTemp) { const base = Number(row.defaultTemp||this._config.default_temp||20); edTemp.value = String(this._toDisplayTemp(base)); edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } if (this._config?.time_12h) { if (edFrom) edFrom.value=this._toTimeInput12h(start); if (edTo) edTo.value=this._toTimeInput12h(end); if (fromMer){ fromMer.style.display=''; fromMer.value=(start>=720)?'PM':'AM'; } if (toMer){ toMer.style.display=''; toMer.value=(end>=720 && end<1440)?'PM':'AM'; } } else { if (edFrom) edFrom.value=this._toTimeInput(start); if (edTo) edTo.value=this._toTimeInput(end); if (fromMer) fromMer.style.display='none'; if (toMer) toMer.style.display='none'; } if (title) title.textContent = this._t('ui.add_block'); if (delBtn) delBtn.style.display='none'; if (err){ err.style.display='none'; err.textContent=''; } } + else { const b = arr.find(x=>x.id===blockId); if (!b) return; this._editing = { entity: eid, blockId, presence: true }; if (edTemp) { edTemp.value = String(this._toDisplayTemp(b.temp)); edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } if (this._config?.time_12h) { if (edFrom) edFrom.value=this._toTimeInput12h(b.startMin); if (edTo) edTo.value=this._toTimeInput12h(b.endMin); if (fromMer){ fromMer.style.display=''; fromMer.value=(b.startMin>=720)?'PM':'AM'; } if (toMer){ toMer.style.display=''; toMer.value=(b.endMin>=720 && b.endMin<1440)?'PM':'AM'; } } else { if (edFrom) edFrom.value=this._toTimeInput(b.startMin); if (edTo) edTo.value=this._toTimeInput(b.endMin); if (fromMer) fromMer.style.display='none'; if (toMer) toMer.style.display='none'; } if (title) title.textContent = this._t('ui.edit_block'); if (delBtn) delBtn.style.display=''; if (err){ err.style.display='none'; err.textContent=''; } } + // Hide presence overlay while editing + const pov = this.shadowRoot.querySelector('.overlay-presence'); if (pov) pov.classList.remove('open'); this._profilesOverlayHiddenForEditor = false; this._holidayOverlayHiddenForEditor = false; // independent + try { const modalEl = this.shadowRoot.querySelector('.overlay .modal:not(.modal-week)'); if (modalEl) modalEl.classList.toggle('modal-12h', !!this._config?.time_12h); } catch {} + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { overlay.classList.add('open'); })); } catch { overlay.classList.add('open'); } + } catch {} + } + async _presenceSave(){ try { + if (!this._presenceDraft || !this._presenceDraft.rooms || !this._presenceCombo) { this._closePresenceEditor(); return; } + // Persist for each room under row.presence[combo].blocks + for (const eid of (this._config?.entities||[])){ + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensurePresenceStruct(row); + const arr = JSON.parse(JSON.stringify(this._presenceDraft.rooms?.[eid] || [])); + row.presence = row.presence || {}; row.presence[this._presenceCombo] = { blocks: arr }; + this._schedules[eid] = row; + } + const before = this._desiredNowSnapshot(); + this._markPendingSave(); + this._closePresenceEditor(); this._render(); + if (this._config.apply_on_edit) await this._applyIfDesiredChanged(before); this._scheduleNextApply(); + } catch {} + } + _openHolidayBlockEditor(blockId, atMin=null){ try { + this._ensureEditModeOn(); + const eid = this._holidayRoom; if (!eid) return; const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; this._ensureHolidayStruct(row); + if (!this._holidayDraft || !this._holidayDraft.rooms) { + const roomsMap={}; for (const rid of (this._config?.entities||[])){ const src=this._schedules[rid]?.holiday?.blocks || []; roomsMap[rid]=JSON.parse(JSON.stringify(src)); } + this._holidayDraft = { rooms: roomsMap }; + } + const arr = (this._holidayDraft.rooms||{})[eid] || []; + const edTemp = this.shadowRoot.querySelector('.ed-temp'); const edFrom = this.shadowRoot.querySelector('.ed-from'); const edTo = this.shadowRoot.querySelector('.ed-to'); const overlay = this.shadowRoot.querySelector('.overlay'); const title = this.shadowRoot.querySelector('.modal h3'); const delBtn = this.shadowRoot.querySelector('.ed-delete'); const err = this.shadowRoot.querySelector('.ed-error'); const fromMer = this.shadowRoot.querySelector('.ed-from-mer'); const toMer = this.shadowRoot.querySelector('.ed-to-mer'); + if (!blockId){ + const now = Number.isFinite(atMin) ? atMin : this._getNowMin(); const start = this._clamp(Math.round(now), 0, 1380); const end = this._clamp(start+60, start+15, 1440); + this._editing = { entity: eid, blockId: null, holiday: true }; + if (edTemp) { const base = Number(row.defaultTemp||this._config.default_temp||20); edTemp.value = String(this._toDisplayTemp(base)); edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } + if (this._config?.time_12h) { if (edFrom) edFrom.value=this._toTimeInput12h(start); if (edTo) edTo.value=this._toTimeInput12h(end); if (fromMer){ fromMer.style.display=''; fromMer.value=(start>=720)?'PM':'AM'; } if (toMer){ toMer.style.display=''; toMer.value=(end>=720 && end<1440)?'PM':'AM'; } } else { if (edFrom) edFrom.value=this._toTimeInput(start); if (edTo) edTo.value=this._toTimeInput(end); if (fromMer) fromMer.style.display='none'; if (toMer) toMer.style.display='none'; } + if (title) title.textContent = this._t('ui.add_block'); if (delBtn) delBtn.style.display='none'; if (err){ err.style.display='none'; err.textContent=''; } + } else { + const b = arr.find(x=>x.id===blockId); if (!b) return; this._editing = { entity: eid, blockId, holiday: true }; + if (edTemp) { edTemp.value = String(this._toDisplayTemp(b.temp)); edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } + if (this._config?.time_12h) { if (edFrom) edFrom.value=this._toTimeInput12h(b.startMin); if (edTo) edTo.value=this._toTimeInput12h(b.endMin); if (fromMer){ fromMer.style.display=''; fromMer.value=(b.startMin>=720)?'PM':'AM'; } if (toMer){ toMer.style.display=''; toMer.value=(b.endMin>=720 && b.endMin<1440)?'PM':'AM'; } } else { if (edFrom) edFrom.value=this._toTimeInput(b.startMin); if (edTo) edTo.value=this._toTimeInput(b.endMin); if (fromMer) fromMer.style.display='none'; if (toMer) toMer.style.display='none'; } + if (title) title.textContent = this._t('ui.edit_block'); if (delBtn) delBtn.style.display=''; if (err){ err.style.display='none'; err.textContent=''; } + } + // Hide holiday overlay while editing to avoid double-popups + const hov = this.shadowRoot.querySelector('.overlay-holiday'); if (hov) hov.classList.remove('open'); this._holidayOverlayHiddenForEditor = true; + try { const modalEl = this.shadowRoot.querySelector('.overlay .modal:not(.modal-week)'); if (modalEl) modalEl.classList.toggle('modal-12h', !!this._config?.time_12h); } catch {} + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { overlay.classList.add('open'); })); } catch { overlay.classList.add('open'); } + } catch {} + } + async _holidaySave(){ try { + if (!this._holidayDraft || !this._holidayDraft.rooms) { this._closeHolidayEditor(); return; } + const rooms = this._holidayDraft.rooms || {}; + for (const eid of (this._config?.entities||[])){ + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureHolidayStruct(row); + row.holiday = { blocks: JSON.parse(JSON.stringify(rooms[eid] || [])) }; + this._schedules[eid] = row; + } + const before = this._desiredNowSnapshot(); + this._markPendingSave(); + this._closeHolidayEditor(); this._render(); + if (this._config.apply_on_edit) await this._applyIfDesiredChanged(before); + this._scheduleNextApply(); + } catch {} + } + + // ---- Holiday: Copy from other schedule types ---- + _holidayCopySourcesForRoom(eid){ + const out = []; + try { + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureWeeklyStruct(row); this._ensureProfilesStruct(row); this._ensurePresenceStruct(row); this._ensureHolidayStruct(row); + // Main plan + out.push({ group: 'main', label: this._t('holidays.copy.group.main'), items: [{ value: 'main::main', label: this._t('holidays.copy.group.main') }] }); + // Weekdays – list all 7 days, regardless of mode + try { + const days = this._weekdayNames(); + const items = days.map(k=>({ value: `weekday::${k}`, label: (this._t('week.days_full.'+k) || this._t('week.days.'+k) || k) })); + out.push({ group: 'weekdays', label: this._t('holidays.copy.group.weekdays'), items }); + } catch {} + // Profiles + try { + const names = Object.keys(row.profiles || {}); + if (names.length) { + const items = []; + // Put active profile first if any + const ap = row.activeProfile && names.includes(row.activeProfile) ? row.activeProfile : null; + if (ap) items.push({ value: `profile::${ap}`, label: `${(this._t('profiles.button')||'Manual schedule')}: ${ap}` }); + for (const n of names){ if (n!==ap) items.push({ value: `profile::${n}`, label: n }); } + out.push({ group: 'profiles', label: this._t('holidays.copy.group.profiles'), items }); + } + } catch {} + // Presence combinations (show enabled + any with saved blocks) + try { + const conf = (this._config?.away?.combos) || {}; + const all = this._presenceAllCombos(); + const items = []; + for (const it of all){ + const meta = conf[it.key] || {}; + const hasBlocks = !!(row.presence && row.presence[it.key] && Array.isArray(row.presence[it.key].blocks) && row.presence[it.key].blocks.length); + if (meta.enabled || hasBlocks) { + const lab = meta.enabled ? it.label : `${it.label} (off)`; + items.push({ value:`presence::${it.key}`, label: lab }); + } + } + if (items.length) out.push({ group: 'presence', label: this._t('holidays.copy.group.presence'), items }); + } catch {} + } catch {} + return out; + } + _holidayCopyBlocks(eid, value){ + try { + const [type, keyRaw] = String(value||'').split('::'); + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureWeeklyStruct(row); this._ensureProfilesStruct(row); this._ensurePresenceStruct(row); + if (type === 'main') return JSON.parse(JSON.stringify(row.blocks||[])); + if (type === 'weekday') { + const k = keyRaw || 'mon'; + const src = row.weekly?.days?.[k] || []; + return JSON.parse(JSON.stringify(src)); + } + if (type === 'profile') { + const src = row.profiles?.[keyRaw]?.blocks || []; + return JSON.parse(JSON.stringify(src)); + } + if (type === 'presence') { + const src = row.presence?.[keyRaw]?.blocks || null; + if (Array.isArray(src)) return JSON.parse(JSON.stringify(src)); + // Fallback: all‑day away + const awayC = Number(this._config?.away?.target_c ?? 17); + const fallback = Number.isFinite(awayC) ? awayC : (row.defaultTemp || this._config.default_temp || 20); + return [{ id: '__presence_away__', startMin: 0, endMin: 1440, temp: fallback }]; + } + } catch {} + return []; + } + _openHolidayCopy(){ + try { + this._ensureEditModeOn(); + const eid = this._holidayRoom; if (!eid) return; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-hcopy'); + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + this._holidayCopyOpen = true; + this._holidayCopySel = 'main::main'; + this._renderHolidayCopy(); + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { ov?.classList.add('open'); })); } catch { ov?.classList.add('open'); } + } catch {} + } + _closeHolidayCopy(){ try { this._holidayCopyOpen = false; this._holidayCopySel = null; this.shadowRoot.querySelector('.overlay-hcopy')?.classList.remove('open'); this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} } + _renderHolidayCopy(){ try { + const modal = this.shadowRoot && this.shadowRoot.querySelector('.overlay-hcopy .modal-hcopy'); if (!modal) return; + const eid = this._holidayRoom; const rowName = this._config?.labels?.[eid] || (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid?.split?.('.')[1]||eid)); + const title = modal.querySelector('.hcopy-title'); if (title) title.textContent = this._t('holidays.copy.title'); + const roomEl = modal.querySelector('.hcopy-room'); if (roomEl) roomEl.textContent = rowName || ''; + const selLab = modal.querySelector('.hcopy-label'); if (selLab) selLab.textContent = this._t('holidays.copy.select'); + const applyBtn = modal.querySelector('.hcopy-apply'); if (applyBtn) { applyBtn.textContent = this._t('holidays.copy.apply'); applyBtn.onclick = ()=>{ + try { + const cur = (this._holidayDraft?.rooms?.[eid] || []); + if (cur.length) { const ok = confirm(this._t('holidays.copy.replace_confirm')); if (!ok) return; } + } catch {} + const blocks = this._holidayCopyBlocks(eid, this._holidayCopySel); + if (!this._holidayDraft || !this._holidayDraft.rooms) { + const roomsMap = {}; for (const r of (this._config?.entities||[])) { const arr = (this._schedules[r]?.holiday?.blocks || []); roomsMap[r] = JSON.parse(JSON.stringify(arr)); } + this._holidayDraft = { rooms: roomsMap }; + } + this._holidayDraft.rooms[eid] = blocks.map(b=>({ ...b, id: (b.id && String(b.id).startsWith('__')? b.id : Math.random().toString(36).slice(2)) })); + this._renderHolidayModal(); this._closeHolidayCopy(); + }; } + const closeBtn = modal.querySelector('.hcopy-close'); if (closeBtn) { closeBtn.textContent = this._t('ui.cancel'); closeBtn.onclick = ()=> this._closeHolidayCopy(); } + // Build select options + const select = modal.querySelector('.hcopy-select'); + if (select) { + select.innerHTML = ''; + const groups = this._holidayCopySourcesForRoom(eid); + for (const g of groups){ + const og = document.createElement('optgroup'); og.label = g.label || g.group; + for (const it of (g.items||[])){ + const opt = document.createElement('option'); opt.value = it.value; opt.textContent = it.label; if (this._holidayCopySel===it.value) opt.selected = true; og.append(opt); + } + select.append(og); + } + select.onchange = ()=>{ this._holidayCopySel = String(select.value||''); this._renderHolidayCopy(); }; + } + // Draw scale and preview + const scale = modal.querySelector('.week-scale-inner'); const track = modal.querySelector('.week-track'); + if (scale) { + scale.innerHTML = ''; + for (let i=0;i<=24;i+=2){ + const t = document.createElement('div'); t.style.position='absolute'; t.style.left=(i/24*100)+'%'; t.style.top='4px'; t.style.bottom='4px'; t.style.borderLeft='1px solid var(--divider-color)'; + const lab = document.createElement('div'); lab.style.position='absolute'; lab.style.top='-2px'; lab.style.transform='translate(-50%,0)'; lab.style.left='0'; lab.style.fontSize='.75rem'; lab.style.color='var(--secondary-text-color)'; + if (this._config?.time_12h){ const p=this._timeParts(i===24?0:i*60); lab.innerHTML = `
    ${p.main}
    ${p.mer||''}
    `; } else { lab.textContent = (i===24 ? this._label(0) : this._label(i*60)); } + t.append(lab); scale.append(t); + } + } + if (track) { + track.innerHTML = ''; + const blocks = this._holidayCopyBlocks(eid, this._holidayCopySel); + for (const b of blocks){ + const pctStart=(b.startMin/1440)*100, pctW=((b.endMin-b.startMin)/1440)*100; const bl=document.createElement('div'); bl.className='block'; bl.style.left=pctStart+'%'; bl.style.width=pctW+'%'; + try { const clr=this._colorFor(eid, b.temp); if (clr){ bl.style.background=clr; bl.style.borderColor=clr; const txt=this._contrastTextColor(clr); if (txt) bl.style.color=txt; } } catch {} + UiHelper.genBlockPill( + this, bl, + `${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}` + ); + track.append(bl); + } + } + } catch {} + } + + async _saveEditor(){ const errElGlobal = this.shadowRoot?.querySelector('.ed-error'); try { if (!this._editing) return; const { entity, blockId, weeklyDay, profileName, holiday } = this._editing; const row = this._schedules[entity]; if (!row) return; let b = null; let sourceBlocks = row.blocks; if (profileName) { this._ensureProfilesStruct(row); // Use the current draft for overlap/checks instead of stored profile + const curRoom = this._profilesRoom || this._profilesEntity || entity; const draftArr = (this._profilesDraft?.rooms?.[curRoom]) || []; sourceBlocks = draftArr; b = blockId ? draftArr.find(x=>x.id===blockId) : null; } else if (weeklyDay && this._weeklyDraft) { sourceBlocks = (this._weeklyDraft.days?.[weeklyDay] || []); b = blockId ? sourceBlocks.find(x=>x.id===blockId) : null; if (!b && this._weeklyDraft?._weeklyModes) { for (const modeName in this._weeklyDraft._weeklyModes) { const modeBlocks = this._weeklyDraft._weeklyModes[modeName]?.days?.[weeklyDay]; if (Array.isArray(modeBlocks)) { b = modeBlocks.find(x=>x.id===blockId); if (b) { sourceBlocks = modeBlocks; break; } } } } } else if (holiday && this._holidayDraft?.rooms) { sourceBlocks = (this._holidayDraft.rooms?.[entity] || []); b = blockId ? sourceBlocks.find(x=>x.id===blockId) : null; } else { if (blockId) b = row.blocks.find(x => x.id === blockId); } + const edTemp = this.shadowRoot.querySelector(".ed-temp"); const edFrom = this.shadowRoot.querySelector(".ed-from"); const edTo = this.shadowRoot.querySelector(".ed-to"); + const fromMerSel = this.shadowRoot.querySelector('.ed-from-mer'); const toMerSel = this.shadowRoot.querySelector('.ed-to-mer'); + const rawFrom = String(edFrom.value || ""); + const rawTo = String(edTo.value || ""); + let start = this._fromTimeInput(rawFrom); + let end = this._fromTimeInput(rawTo); + // If 12h mode, adjust using AM/PM selects + if (this._config?.time_12h) { + const merF = (fromMerSel?.value || 'AM').toUpperCase(); + const merT = (toMerSel?.value || 'AM').toUpperCase(); + let sh = Math.floor(start/60), sm = start%60; + let eh = Math.floor(end/60), em = end%60; + if (sh <= 12) { if (merF==='AM' && sh===12) sh = 0; if (merF==='PM' && sh<12) sh += 12; } + if (eh <= 12) { if (merT==='AM' && eh===12) eh = 0; if (merT==='PM' && eh<12) eh += 12; } + start = sh*60 + sm; end = eh*60 + em; + } + // Treat end time 00:00 as end of day (24:00 -> 1440 minutes) + if (rawTo === '00:00') end = 1440; + let tempRaw = String(edTemp.value || "").replace(",", "."); let temp = parseFloat(tempRaw); + if (isNaN(start)) start = 0; if (isNaN(end)) end = 60; if (isNaN(temp)) temp = row.defaultTemp || 20; + const maxLimit = this._maxDisplay(); const minLimit = this._minDisplay(); if (Number.isFinite(maxLimit)) temp = Math.min(temp, maxLimit); if (Number.isFinite(minLimit)) temp = Math.max(temp, minLimit); + start = this._clamp(Math.floor(start), 0, 1439); end = this._clamp(Math.ceil(end), 1, 1440); + + // Helper to check overlap in an array of blocks (ignore current 'b' if provided) + const hasOverlap = (arr, s, e, ignoreId=null) => { + const othersArr = (arr||[]).filter(x=>!ignoreId || x.id !== ignoreId); + return othersArr.find(o => !(e <= o.startMin || s >= o.endMin)); + }; + + // If interval crosses midnight (end <= start), split into two blocks + const crossesMidnight = end <= start; + // Early overlap check only for base/weekly editors; profiles and holiday are handled below using the draft state + if (!crossesMidnight && !profileName && !holiday) { + const overlap = hasOverlap(sourceBlocks, start, end, b?.id); + if (overlap) { + const overlapStart = Math.max(start, overlap.startMin); + const overlapEnd = Math.min(end, overlap.endMin); + const errEl = this.shadowRoot.querySelector('.ed-error'); + if (errEl) { + const msg = this._t('ui.overlap_msg').replace('{start}', this._label(overlapStart)).replace('{end}', this._label(overlapEnd)); + const fixStartLab = this._t('ui.fix_start_to').replace('{time}', this._label(overlap.endMin)); + const fixEndLab = this._t('ui.fix_end_to').replace('{time}', this._label(overlap.startMin)); + // Decide which fixes are relevant + const startInside = start >= overlap.startMin && start < overlap.endMin; + const endInside = end > overlap.startMin && end <= overlap.endMin; + const showStart = startInside || (!startInside && endInside && end > overlap.endMin); // overlap at right edge + const showEnd = endInside || (!endInside && startInside && start < overlap.startMin); // overlap at left edge + let btns = ''; + if (showStart) btns += ``; + if (showEnd) btns += ``; + // No extra cancel button here; bottom Cancel handles it + errEl.innerHTML = `
    ${msg}
    ${btns}
    `; + errEl.style.display = 'block'; + const applyFix = (kind)=>{ + // Adjust inputs then re-run save + try { + const edFrom = this.shadowRoot.querySelector('.ed-from'); + const edTo = this.shadowRoot.querySelector('.ed-to'); + const fromMerSel = this.shadowRoot.querySelector('.ed-from-mer'); + const toMerSel = this.shadowRoot.querySelector('.ed-to-mer'); + if (kind==='start') { + const ns = overlap.endMin; + if (this._config?.time_12h) { + if (edFrom) edFrom.value = this._toTimeInput12h(ns); + if (fromMerSel) fromMerSel.value = (ns>=720 && ns<1440)?'PM':'AM'; + } else { + if (edFrom) edFrom.value = this._toTimeInput(ns); + } + } else if (kind==='end') { + const ne = overlap.startMin; + if (this._config?.time_12h) { + if (edTo) edTo.value = this._toTimeInput12h(ne); + if (toMerSel) toMerSel.value = (ne>=720 && ne<1440)?'PM':'AM'; + } else { + if (edTo) edTo.value = this._toTimeInput(ne); + } + } + } catch {} + try { errEl.style.display='none'; errEl.textContent=''; } catch {} + // Defer to allow DOM to update + setTimeout(()=>{ this._saveEditor(); }, 0); + }; + errEl.querySelector('.ed-fix-start')?.addEventListener('click', ()=>applyFix('start')); + errEl.querySelector('.ed-fix-end')?.addEventListener('click', ()=>applyFix('end')); + // No extra cancel handler + } + return; + } + } + temp = this._fromDisplayTemp(temp); + const before = this._desiredNowSnapshot(); + if (profileName) { + // Save into a draft for the selected profile; persist only when user presses Save + const hasOverlap = (arr, s, e, ignoreId=null) => { const othersArr = (arr||[]).filter(x=>!ignoreId || x.id !== ignoreId); return othersArr.find(o => !(e <= o.startMin || s >= o.endMin)); }; + const crossesMidnight = end <= start; + // Ensure draft exists for current room; don't re-init when creating a new (unnamed) profile + const isNewProf = (profileName === '__new__'); + const curRoom = this._profilesRoom || this._profilesEntity || Object.keys(this._schedules)[0]; + if (!this._profilesDraft || (!isNewProf && this._profilesDraft.name !== profileName)) { + try { + const roomsMap = {}; + for (const e of (this._config?.entities||[])){ + const src = !isNewProf ? (this._schedules[e]?.profiles?.[profileName]?.blocks || []) : []; + roomsMap[e] = JSON.parse(JSON.stringify(src)); + } + this._profilesDraft = { name: (isNewProf? null : profileName), rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + } catch { + const roomsMap = {}; for (const e of (this._config?.entities||[])) roomsMap[e] = []; + this._profilesDraft = { name: (isNewProf? null : profileName), rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + } + } + const arr = (this._profilesDraft.rooms||{})[curRoom] || []; + // Find the block within the draft, not the stored profile + let b = blockId ? arr.find(x=>x.id===blockId) : null; + if (crossesMidnight) { + const ov1 = hasOverlap(arr, start, 1440, b?.id); + const ov2 = hasOverlap(arr, 0, end, null); + if (ov1 || ov2) { const errEl = this.shadowRoot.querySelector('.ed-error'); if (errEl) { const overlap = ov1 || ov2; const msg = this._t('ui.overlap_msg').replace('{start}', this._label(ov1?Math.max(start, ov1.startMin):0)).replace('{end}', this._label(ov1?Math.min(1440, ov1.endMin):end)); errEl.innerHTML = `
    ${msg}
    `; errEl.style.display='block'; } return; } + if (!b) { const id1=Math.random().toString(36).slice(2,9); const id2=Math.random().toString(36).slice(2,9); arr.push({ id:id1, startMin:start, endMin:1440, temp: this._fromDisplayTemp(temp) }); arr.push({ id:id2, startMin:0, endMin:end, temp: this._fromDisplayTemp(temp) }); } else { b.temp=this._fromDisplayTemp(temp); b.startMin=start; b.endMin=1440; const id2=Math.random().toString(36).slice(2,9); arr.push({ id:id2, startMin:0, endMin:end, temp: this._fromDisplayTemp(temp) }); } + } else { + const overlap = hasOverlap(arr, start, end, b?.id); + if (overlap) { const errEl = this.shadowRoot.querySelector('.ed-error'); if (errEl) { const msg = this._t('ui.overlap_msg').replace('{start}', this._label(Math.max(start, overlap.startMin))).replace('{end}', this._label(Math.min(end, overlap.endMin))); errEl.innerHTML = `
    ${msg}
    `; errEl.style.display='block'; } return; } + if (!b) { const id=Math.random().toString(36).slice(2,9); arr.push({ id, startMin:start, endMin:end, temp:this._fromDisplayTemp(temp) }); } else { b.temp=this._fromDisplayTemp(temp); b.startMin=start; b.endMin=end; } + } + (this._profilesDraft.rooms||{})[curRoom] = arr.sort((a,bx)=>a.startMin-bx.startMin||a.endMin-bx.endMin); + this._profilesDirty = true; + // If the profile edit originated from the main timeline, persist immediately + if (this._autoSaveProfileFromTimeline) { + try { + this._autoSaveProfileFromTimeline = false; + // Persist draft for current room/profile + await this._profilesSaveDraft(); + // Apply immediately if edit affects current time + try { + if (this._config?.auto_apply && this._config?.apply_on_edit) { + const nowMin = this._getNowMin(); + const arrNow = Array.isArray((this._profilesDraft?.rooms||{})[curRoom]) ? this._profilesDraft.rooms[curRoom] : []; + const hit = arrNow.find(x => nowMin >= x.startMin && nowMin < x.endMin); + if (hit) { await this._applyImmediateForEntity(entity, Number(hit.temp), nowMin); this._scheduleNextApply(); } + } + } catch {} + this._closeEditor(); + return; + } catch { + // Fall back to non-persisting flow + } + } + this._render(); this._closeEditor(); this._renderProfilesModal(); + return; + } + if (weeklyDay && this._weeklyDraft) { + if (crossesMidnight) { + // Split into two parts on the SAME day: [start..1440] and [0..end] + const dayKey = weeklyDay; + const arrToday = this._weeklyDraft.days?.[dayKey] || []; + // Check overlaps for each part against the same day + const ov1 = hasOverlap(arrToday, start, 1440, b?.id); + const ov2 = hasOverlap(arrToday, 0, end, b?.id); + if (ov1 || ov2){ + const errEl = this.shadowRoot.querySelector('.ed-error'); + if (errEl) { + const overlap = ov1 || ov2; + const msg = this._t('ui.overlap_msg').replace('{start}', this._label(Math.max((ov1?start:0), overlap.startMin))).replace('{end}', this._label(Math.min((ov1?1440:end), overlap.endMin))); + errEl.innerHTML = `
    ${msg}
    `; errEl.style.display='block'; + } + return; + } + // Apply changes on same day + if (!b) { + const id1 = Math.random().toString(36).slice(2,9); + const id2 = Math.random().toString(36).slice(2,9); + arrToday.push({ id:id1, startMin:start, endMin:1440, temp }); + arrToday.push({ id:id2, startMin:0, endMin:end, temp }); + } else { + b.temp = temp; b.startMin = start; b.endMin = 1440; + const id2 = Math.random().toString(36).slice(2,9); + arrToday.push({ id:id2, startMin:0, endMin:end, temp }); + } + this._weeklyDraft.days[dayKey] = arrToday.sort((a,bx)=>a.startMin-bx.startMin||a.endMin-bx.endMin); + } else { + if (!b) { + const id = Math.random().toString(36).slice(2,9); + b = { id, startMin: start, endMin: end, temp }; + sourceBlocks.push(b); + } else { + b.temp = temp; b.startMin = start; b.endMin = end; + } + sourceBlocks.sort((a,bx)=>a.startMin - bx.startMin || a.endMin - bx.endMin); + } + // If started from main timeline, auto-save the whole weekly plan now + if (this._autoSaveWeeklyFromTimeline) { + this._autoSaveWeeklyFromTimeline = false; + this._closeEditor(); + // Persist weekly draft (propagate groups) and apply if needed + this._weeklySaveAll(); + } else { + // Close editor and re-render weekly preview; no immediate apply + this._closeEditor(); + this._renderWeeklyModal(); + + // If "apply on edit" is enabled, and this weekly edit affects "right now", + // apply the new temperature immediately without requiring full Week Save. + try { + if (this._config?.auto_apply && this._config?.apply_on_edit) { + const today = this._todayKey(); + // Determine if the edit impacts today given the current week mode and selected group + const mode = (this._weeklyDraft?.mode) || (row.weekly?.mode) || (this._config?.weekdays_mode) || 'weekday_weekend'; + const selKey = this._weeklyDayKey || weeklyDay || today; // group key the user was editing + + const isWeekday = ['mon','tue','wed','thu','fri'].includes(today); + let editImpactsToday = false; + let sourceKeyForToday = today; // which draft day array reflects today after grouping + if (mode === 'weekday_weekend') { + if (selKey === 'weekdays' && isWeekday) { editImpactsToday = true; sourceKeyForToday = 'mon'; } + if (selKey === 'weekend' && (today==='sat' || today==='sun')) { editImpactsToday = true; sourceKeyForToday = 'sat'; } + if (selKey === today) { editImpactsToday = true; sourceKeyForToday = today; } + } else if (mode === 'weekday_sat_sun') { + if (selKey === 'weekdays' && isWeekday) { editImpactsToday = true; sourceKeyForToday = 'mon'; } + if ((selKey === 'sat' && today==='sat') || (selKey==='sun' && today==='sun')) { editImpactsToday = true; sourceKeyForToday = today; } + if (selKey === today) { editImpactsToday = true; sourceKeyForToday = today; } + } else { // all_7 + if (selKey === today) { editImpactsToday = true; sourceKeyForToday = today; } + } + + if (editImpactsToday) { + const nowMin = this._getNowMin(); + const arrNow = Array.isArray(this._weeklyDraft?.days?.[sourceKeyForToday]) ? this._weeklyDraft.days[sourceKeyForToday] : []; + const hit = arrNow.find(x => nowMin >= x.startMin && nowMin < x.endMin); + if (hit) { + // Apply immediately to reflect the new block that currently covers "now" + await this._applyImmediateForEntity(entity, Number(hit.temp), nowMin); + this._scheduleNextApply(); + } + } + } + } catch {} + } + return; + } + if (this._editing.presence && this._presenceDraft) { + // Work on presence draft only; persist on Presence Save + const eid = this._editing.entity; + const arr = (this._presenceDraft.rooms||{})[eid] || []; + // Ensure we reference the correct block when editing (so overlap checks ignore itself) + let b = blockId ? arr.find(x=>x.id===blockId) : null; + if (crossesMidnight) { + const ov1 = hasOverlap(arr, start, 1440, b?.id); + const ov2 = hasOverlap(arr, 0, end, null); + if (ov1 || ov2) { const errEl = this.shadowRoot.querySelector('.ed-error'); if (errEl) { const overlap = ov1 || ov2; const msg = this._t('ui.overlap_msg').replace('{start}', this._label(ov1?Math.max(start, ov1.startMin):0)).replace('{end}', this._label(ov1?Math.min(1440, ov1.endMin):end)); errEl.innerHTML = `
    ${msg}
    `; errEl.style.display='block'; } return; } + if (!b) { const id1=Math.random().toString(36).slice(2,9); const id2=Math.random().toString(36).slice(2,9); arr.push({ id:id1, startMin:start, endMin:1440, temp }); arr.push({ id:id2, startMin:0, endMin:end, temp }); } else { b.temp=temp; b.startMin=start; b.endMin=1440; const id2=Math.random().toString(36).slice(2,9); arr.push({ id:id2, startMin:0, endMin:end, temp }); } + } else { + const overlap = hasOverlap(arr, start, end, b?.id); + if (overlap) { const errEl = this.shadowRoot.querySelector('.ed-error'); if (errEl) { const msg = this._t('ui.overlap_msg').replace('{start}', this._label(Math.max(start, overlap.startMin))).replace('{end}', this._label(Math.min(end, overlap.endMin))); errEl.innerHTML = `
    ${msg}
    `; errEl.style.display='block'; } return; } + if (!b) { const id=Math.random().toString(36).slice(2,9); arr.push({ id, startMin:start, endMin:end, temp }); } else { b.temp=temp; b.startMin=start; b.endMin=end; } + } + (this._presenceDraft.rooms||{})[eid] = arr.sort((a,bx)=>a.startMin-bx.startMin||a.endMin-bx.endMin); + this._render(); this._closeEditor(); if (this._presenceOpen) this._renderPresenceModal(); + return; + } + if (holiday && this._holidayDraft?.rooms) { + // Work on holiday draft only; persist on Holiday Save + const eid = entity; + const arr = (this._holidayDraft.rooms||{})[eid] || []; + if (crossesMidnight) { + const ov1 = hasOverlap(arr, start, 1440, b?.id); + const ov2 = hasOverlap(arr, 0, end, null); + if (ov1 || ov2) { const errEl = this.shadowRoot.querySelector('.ed-error'); if (errEl) { const overlap = ov1 || ov2; const msg = this._t('ui.overlap_msg').replace('{start}', this._label(ov1?Math.max(start, ov1.startMin):0)).replace('{end}', this._label(ov1?Math.min(1440, ov1.endMin):end)); errEl.innerHTML = `
    ${msg}
    `; errEl.style.display='block'; } return; } + if (!b) { const id1=Math.random().toString(36).slice(2,9); const id2=Math.random().toString(36).slice(2,9); arr.push({ id:id1, startMin:start, endMin:1440, temp: this._fromDisplayTemp(temp) }); arr.push({ id:id2, startMin:0, endMin:end, temp: this._fromDisplayTemp(temp) }); } else { b.temp=this._fromDisplayTemp(temp); b.startMin=start; b.endMin=1440; const id2=Math.random().toString(36).slice(2,9); arr.push({ id:id2, startMin:0, endMin:end, temp: this._fromDisplayTemp(temp) }); } + } else { + const overlap = hasOverlap(arr, start, end, b?.id); + if (overlap) { const errEl = this.shadowRoot.querySelector('.ed-error'); if (errEl) { const msg = this._t('ui.overlap_msg').replace('{start}', this._label(Math.max(start, overlap.startMin))).replace('{end}', this._label(Math.min(end, overlap.endMin))); errEl.innerHTML = `
    ${msg}
    `; errEl.style.display='block'; } return; } + if (!b) { const id=Math.random().toString(36).slice(2,9); arr.push({ id, startMin:start, endMin:end, temp:this._fromDisplayTemp(temp) }); } else { b.temp=this._fromDisplayTemp(temp); b.startMin=start; b.endMin=end; } + } + (this._holidayDraft.rooms||{})[eid] = arr.sort((a,bx)=>a.startMin-bx.startMin||a.endMin-bx.endMin); + this._render(); this._closeEditor(); this._renderHolidayModal(); + return; + } + // Normal (non-weekly) save + if (crossesMidnight) { + // Check overlaps for both parts + const ov1 = hasOverlap(row.blocks, start, 1440, b?.id); + const ov2 = hasOverlap(row.blocks, 0, end, null); + if (ov1 || ov2) { + const errEl = this.shadowRoot.querySelector('.ed-error'); + if (errEl) { + const overlap = ov1 || ov2; + const msg = this._t('ui.overlap_msg').replace('{start}', this._label(ov1?Math.max(start, ov1.startMin):0)).replace('{end}', this._label(ov1?Math.min(1440, ov1.endMin):end)); + const fixStartLab = this._t('ui.fix_start_to').replace('{time}', this._label((ov1?1440:overlap.endMin))); + const fixEndLab = this._t('ui.fix_end_to').replace('{time}', this._label((ov2?0:overlap.startMin))); + // Relevance: if first part [start..1440] overlaps -> offer start fix; if second part [0..end] overlaps -> offer end fix + const showStart = !!ov1; + const showEnd = !!ov2; + let btns = ''; + if (showStart) btns += ``; + if (showEnd) btns += ``; + // No extra cancel here; use the modal cancel button + errEl.innerHTML = `
    ${msg}
    ${btns}
    `; + errEl.style.display='block'; + const applyFix = (kind)=>{ + try { + const edFrom = this.shadowRoot.querySelector('.ed-from'); + const edTo = this.shadowRoot.querySelector('.ed-to'); + const fromMerSel = this.shadowRoot.querySelector('.ed-from-mer'); + const toMerSel = this.shadowRoot.querySelector('.ed-to-mer'); + if (kind==='start') { + const ns = ov1 ? Math.max(start, (ov1.endMin||start)) : (overlap.endMin||start); + if (this._config?.time_12h) { if (edFrom) edFrom.value = this._toTimeInput12h(ns); if (fromMerSel) fromMerSel.value = (ns>=720 && ns<1440)?'PM':'AM'; } else { if (edFrom) edFrom.value = this._toTimeInput(ns); } + } else { + const ne = ov2 ? Math.min(end, (ov2.startMin||end)) : (overlap.startMin||end); + if (this._config?.time_12h) { if (edTo) edTo.value = this._toTimeInput12h(ne); if (toMerSel) toMerSel.value = (ne>=720 && ne<1440)?'PM':'AM'; } else { if (edTo) edTo.value = this._toTimeInput(ne); } + } + } catch {} + try { errEl.style.display='none'; errEl.textContent=''; } catch {} + setTimeout(()=>{ this._saveEditor(); }, 0); + }; + errEl.querySelector('.ed-fix-start')?.addEventListener('click', ()=>applyFix('start')); + errEl.querySelector('.ed-fix-end')?.addEventListener('click', ()=>applyFix('end')); + // No extra cancel handler + } + return; + } + if (!b) { + const id1 = Math.random().toString(36).slice(2,9); + const id2 = Math.random().toString(36).slice(2,9); + row.blocks.push({ id:id1, startMin:start, endMin:1440, temp }); + row.blocks.push({ id:id2, startMin:0, endMin:end, temp }); + } else { + // Update current b to first part and add second + b.temp = temp; b.startMin = start; b.endMin = 1440; + const id2 = Math.random().toString(36).slice(2,9); + row.blocks.push({ id:id2, startMin:0, endMin:end, temp }); + } + row.blocks.sort((a,bx)=>a.startMin-bx.startMin||a.endMin-bx.endMin); + } else { + if (!b) { const id = Math.random().toString(36).slice(2,9); b = { id, startMin: start, endMin: end, temp }; row.blocks.push(b); } else { b.temp = temp; b.startMin = start; b.endMin = end; } + this._applyNoOverlapResize(entity, b, "left", b.startMin); this._applyNoOverlapResize(entity, b, "right", b.endMin); + } + // Persist local draft immediately for resilience + try { this._persistDraftNow(); } catch {} + this._markPendingSave(); this._render(); this._closeEditor(); + // Immediate apply if a block now covers the current time (non-weekly path) + try { + if (this._config?.auto_apply && this._config?.apply_on_edit) { + const nowMin = this._getNowMin(); + const arrNow = Array.isArray(row.blocks) ? row.blocks : []; + const hitNow = arrNow.find(x => nowMin >= x.startMin && nowMin < x.endMin); + if (hitNow) await this._applyImmediateForEntity(entity, Number(hitNow.temp), nowMin); + } + } catch {} + if (this._config.apply_on_edit) await this._applyIfDesiredChanged(before); this._scheduleNextApply(); } catch (e) { if (errElGlobal) { errElGlobal.style.display = 'block'; errElGlobal.textContent = this._t('editor.error_prefix') + (e && e.message ? e.message : String(e)); } }} + + _toTimeInput(min){ if (!Number.isFinite(min)) min = 0; const m = ((Math.floor(min) % 1440) + 1440) % 1440; const hh=Math.floor(m/60), mm=Math.floor(m%60); return `${String(hh).padStart(2,"0")}:${String(mm).padStart(2,"0")}`; } + _fromTimeInput(str){ const m=/(\d{1,2}):(\d{2})$/.exec(str||""); if(!m) return 0; const h=Math.max(0,Math.min(23,parseInt(m[1],10))); const mi=Math.max(0,Math.min(59,parseInt(m[2],10))); return h*60+mi; } + _toTimeInput12h(min){ if (!Number.isFinite(min)) min=0; const m=((Math.floor(min)%1440)+1440)%1440; let hh=Math.floor(m/60); const mm=Math.floor(m%60); let h=hh%12; if(h===0) h=12; return `${String(h).padStart(2,'0')}:${String(mm).padStart(2,'0')}`; } + + // ---------- Onboarding ---------- + _onboardStorageKey(){ return 'thermostat_timeline_onboard_seen_v1'; } + _maybeOpenOnboard(){ + try { + // Respect explicit disable flag + if (this._disableOnboard) return; + const flag = localStorage.getItem(this._onboardStorageKey()); + if (!flag) this._openOnboard(); + } catch {} + } + _openOnboard(){ + try { + this._onboardOpen = true; this._onboardStep = 0; this._renderOnboard(); + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-onboard'); + if (ov) ov.classList.add('open'); + } catch {} + } + _closeOnboard(markSeen=false){ + try { + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-onboard'); if (ov) ov.classList.remove('open'); + this._onboardOpen = false; + // store preference + const cb = this.shadowRoot && this.shadowRoot.querySelector('.onb-hide'); + if (markSeen || (cb && cb.checked)) { + try { localStorage.setItem(this._onboardStorageKey(), '1'); } catch {} + } + } catch {} + } + _renderOnboard(){ + try { + const img = this.shadowRoot && this.shadowRoot.querySelector('.onb-img'); + const vid = this.shadowRoot && this.shadowRoot.querySelector('.onb-vid'); + const hero = this.shadowRoot && this.shadowRoot.querySelector('.ob-hero'); + const txt = this.shadowRoot && this.shadowRoot.querySelector('.onb-text'); + const dots = this.shadowRoot && this.shadowRoot.querySelector('.onb-dots'); + const next = this.shadowRoot && this.shadowRoot.querySelector('.onb-next'); + const done = this.shadowRoot && this.shadowRoot.querySelector('.onb-done'); + const step = Math.max(0, Math.min(this._onboardSlides.length-1, this._onboardStep)); + const slide = this._onboardSlides[step] || { key:'onboard.step_main', img:'', fallback:'' }; + if (img && vid) { + // First try HACS/relative asset; on error, try /local fallback if provided + const nextUrl = slide.img || slide.fallback || ''; + if (this._onboardLastUrl !== nextUrl) { + this._onboardLastUrl = nextUrl; + const isVideo = /\.(mp4|webm|mov)(\?|#|$)/i.test(nextUrl); + // Reset state + img.style.display = 'none'; + vid.style.display = 'none'; + // Remove any previous message + try { const m = hero && hero.querySelector('.onb-missing'); if (m) m.remove(); } catch {} + if (isVideo) { + // Setup video + vid.onerror = null; vid.onloadeddata = null; vid.oncanplay = null; + vid.muted = true; vid.playsInline = true; vid.loop = true; vid.autoplay = true; + vid.onloadeddata = ()=>{ try { vid.style.display=''; img.style.display='none'; vid.play().catch(()=>{}); } catch{} }; + vid.onerror = ()=>{ + try { + if (slide.fallback && vid.src !== slide.fallback) { vid.src = slide.fallback; vid.load?.(); return; } + vid.style.display='none'; + if (hero) { let msg = hero.querySelector('.onb-missing'); if (!msg){ msg=document.createElement('div'); msg.className='onb-missing'; msg.style.fontSize='.9rem'; msg.style.color='var(--secondary-text-color)'; msg.style.padding='8px'; hero.append(msg); } msg.textContent=this._t('onboard.video_not_found') || 'Video not found'; } + } catch{} + }; + vid.src = nextUrl; vid.load?.(); + } else { + // Setup image + img.onerror = null; img.onload = null; + img.onerror = ()=>{ + try { + if (slide.fallback && img.src !== slide.fallback) { img.src = slide.fallback; return; } + img.style.display='none'; + if (hero) { let msg = hero.querySelector('.onb-missing'); if (!msg){ msg=document.createElement('div'); msg.className='onb-missing'; msg.style.fontSize='.9rem'; msg.style.color='var(--secondary-text-color)'; msg.style.padding='8px'; hero.append(msg); } msg.textContent=this._t('onboard.image_not_found') || 'Image not found'; } + } catch{} + }; + img.onload = ()=>{ try { img.style.display=''; vid.style.display='none'; } catch{} }; + img.src = nextUrl; + } + } + } + if (txt) txt.textContent = this._t(slide.key); + if (dots) { + dots.innerHTML = ''; + for (let i=0;ib.id!==id); if(this._active&&this._active.entity===entity&&this._active.id===id) this._active=null; this._debouncedSaveStore(); this._render(); if (this._config.apply_on_edit) this._applyIfDesiredChanged(before); this._scheduleNextApply(); } + + _t(k){ return ttLocalize(k, this._hass || this._lang || 'en'); } + + _applyCardI18n(){ + try { + const t = (k)=> this._t(k); + // Main modal: Edit block + const modal = this.shadowRoot && this.shadowRoot.querySelector('.overlay:not(.overlay-week):not(.overlay-profiles):not(.overlay-holiday):not(.overlay-presence):not(.overlay-copyrooms):not(.overlay-onboard):not(.overlay-pause):not(.overlay-test):not(.overlay-settings) .modal'); + if (modal) { + modal.setAttribute('aria-label', t('ui.edit_block')); + const h3 = modal.querySelector('.ed-modal-title'); + if (h3) h3.textContent = t('ui.edit_block'); + const fromLab = modal.querySelector('.ed-from-label'); + if (fromLab) fromLab.textContent = t('ui.from'); + const toLab = modal.querySelector('.ed-to-label'); + if (toLab) toLab.textContent = t('ui.to'); + const tempLab = modal.querySelector('.ed-temp-label'); + if (tempLab) tempLab.textContent = t('ui.temperature') + ' ('+ this._unitSymbol() +')'; + const del = modal.querySelector('.ed-delete'); + if (del) del.textContent = t('ui.delete'); + const cancel = modal.querySelector('.ed-cancel'); + if (cancel) cancel.textContent = t('ui.cancel'); + const save = modal.querySelector('.ed-save'); + if (save) save.textContent = t('ui.save'); + } + + // Week modal + try { + const wt = this.shadowRoot && this.shadowRoot.querySelector('.week-title'); + if (wt) wt.textContent = t('week.editor.title'); + const wml = this.shadowRoot && this.shadowRoot.querySelector('.week-mode-label'); + if (wml) wml.textContent = t('week.mode'); + const wnote = this.shadowRoot && this.shadowRoot.querySelector('.week-note'); + if (wnote) wnote.textContent = t('week.editor.preview_note'); + const wsel = this.shadowRoot && this.shadowRoot.querySelector('.week-mode'); + if (wsel) { + const opt1 = wsel.querySelector('.opt-weekday-weekend'); + if (opt1) opt1.textContent = t('week.mode.weekday_weekend'); + const opt2 = wsel.querySelector('.opt-weekday-sat-sun'); + if (opt2) opt2.textContent = t('week.mode.weekday_sat_sun'); + const opt3 = wsel.querySelector('.opt-all-7'); + if (opt3) opt3.textContent = t('week.mode.all_7'); + } + const wcancelBtn = this.shadowRoot && this.shadowRoot.querySelector('.wk-cancel'); + if (wcancelBtn) wcancelBtn.textContent = t('ui.cancel'); + const wsaveBtn = this.shadowRoot && this.shadowRoot.querySelector('.wk-save'); + if (wsaveBtn) wsaveBtn.textContent = t('ui.save'); + } catch {} + + // Profiles modal + try { + const proftitle = this.shadowRoot && this.shadowRoot.querySelector('.prof-title'); + if (proftitle) proftitle.textContent = t('profiles.editor.title'); + const profadd = this.shadowRoot && this.shadowRoot.querySelector('.prof-add-text'); + if (profadd) profadd.textContent = t('profiles.add'); + const ps = this.shadowRoot && this.shadowRoot.querySelector('.prof-save'); + if (ps) { + const isExisting = !!(this._profilesEditingExisting && this._profilesSelected); + const key = isExisting ? 'profiles.save_existing' : 'profiles.save_new'; + ps.textContent = t(key) || t('ui.save'); + } + const pc = this.shadowRoot && this.shadowRoot.querySelector('.prof-close'); + if (pc) pc.textContent = t('ui.close'); + } catch {} + + // Holiday modal + try { + const ht = this.shadowRoot && this.shadowRoot.querySelector('.holiday-title'); + if (ht) ht.textContent = t('holidays.button'); + const hs = this.shadowRoot && this.shadowRoot.querySelector('.holiday-save'); + if (hs) hs.textContent = t('ui.save'); + const hc = this.shadowRoot && this.shadowRoot.querySelector('.holiday-close'); + if (hc) hc.textContent = t('ui.close'); + } catch {} + + // Presence modal + try { + const pttl = this.shadowRoot && this.shadowRoot.querySelector('.presence-title'); + if (pttl) pttl.textContent = t('presence.editor.title'); + const padv = this.shadowRoot && this.shadowRoot.querySelector('.presence-adv-label'); + if (padv) padv.textContent = t('presence.enable_advanced'); + const ps = this.shadowRoot && this.shadowRoot.querySelector('.presence-save'); + if (ps) ps.textContent = t('ui.save'); + const pcl = this.shadowRoot && this.shadowRoot.querySelector('.presence-close'); + if (pcl) pcl.textContent = t('ui.close'); + } catch {} + + // Copy to rooms modal + try { + const crt = this.shadowRoot && this.shadowRoot.querySelector('.copyrooms-title'); + if (crt) crt.textContent = t('week.copy_to_rooms.title'); + const crd = this.shadowRoot && this.shadowRoot.querySelector('.copyrooms-desc'); + if (crd) crd.textContent = t('week.copy_to_rooms.select'); + const crc = this.shadowRoot && this.shadowRoot.querySelector('.copyrooms-cancel'); + if (crc) crc.textContent = t('ui.cancel'); + const cra = this.shadowRoot && this.shadowRoot.querySelector('.copyrooms-apply'); + if (cra) cra.textContent = t('week.copy_to_rooms.apply'); + } catch {} + + // Onboarding modal + try { + const ttl = this.shadowRoot && this.shadowRoot.querySelector('.onb-title'); + if (ttl) ttl.textContent = t('onboard.title'); + const prev = this.shadowRoot && this.shadowRoot.querySelector('.onb-prev'); + if (prev) prev.textContent = t('onboard.prev'); + const next = this.shadowRoot && this.shadowRoot.querySelector('.onb-next'); + if (next) next.textContent = t('onboard.next'); + const skip = this.shadowRoot && this.shadowRoot.querySelector('.onb-skip'); + if (skip) skip.textContent = t('onboard.skip'); + const done = this.shadowRoot && this.shadowRoot.querySelector('.onb-done'); + if (done) done.textContent = t('onboard.done'); + const hideLbl = this.shadowRoot && this.shadowRoot.querySelector('.onb-hide-lbl'); + if (hideLbl) hideLbl.textContent = t('onboard.dont_show'); + } catch {} + + // Pause modal + try { + const pt = this.shadowRoot && this.shadowRoot.querySelector('.pause-title'); + if (pt) pt.textContent = t('pause.menu_title'); + const p1 = this.shadowRoot && this.shadowRoot.querySelector('.pause-1h'); + if (p1) p1.textContent = t('pause.off_1h'); + const p5 = this.shadowRoot && this.shadowRoot.querySelector('.pause-5h'); + if (p5) p5.textContent = t('pause.off_5h'); + const pcu = this.shadowRoot && this.shadowRoot.querySelector('.pause-custom'); + if (pcu) pcu.textContent = t('pause.off_custom'); + const pi = this.shadowRoot && this.shadowRoot.querySelector('.pause-indef'); + if (pi) pi.textContent = t('pause.off_indef'); + const pr = this.shadowRoot && this.shadowRoot.querySelector('.pause-resume'); + if (pr) pr.textContent = t('pause.resume'); + const px = this.shadowRoot && this.shadowRoot.querySelector('.pause-close'); + if (px) px.textContent = t('pause.close'); + } catch {} + + // Test modal + try { + const testtitle = this.shadowRoot && this.shadowRoot.querySelector('.test-title'); + if (testtitle) testtitle.textContent = t('editor.test_tool') || 'Test tool'; + const testlabel = this.shadowRoot && this.shadowRoot.querySelector('.test-label'); + if (testlabel) testlabel.textContent = t('editor.test_select_entity') || 'Select entity'; + const testempty = this.shadowRoot && this.shadowRoot.querySelector('.test-empty'); + if (testempty) testempty.textContent = t('editor.test_empty') || 'Select an entity to see available commands…'; + const testexport = this.shadowRoot && this.shadowRoot.querySelector('.test-export-btn'); + if (testexport) testexport.textContent = t('editor.test_export') || 'Export .md'; + const testclose = this.shadowRoot && this.shadowRoot.querySelector('.test-close'); + if (testclose) testclose.textContent = t('ui.close'); + } catch {} + + // Settings modal (in-card) + try { + const sm = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .modal'); + if (sm) sm.setAttribute('aria-label', t('editor.tabs.settings')); + const st = this.shadowRoot && this.shadowRoot.querySelector('.settings-title'); + if (st) st.textContent = t('editor.tabs.settings'); + + // Integration availability status chip (moved from HA editor into popup header) + try { + const chip = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-int-status'); + const icon = chip?.querySelector('ha-icon'); + const txt = chip?.querySelector('.text'); + const hasService = !!(this._hass && this._hass.services && this._hass.services['thermostat_timeline']); + if (chip && icon && txt) { + if (hasService) { + chip.classList.remove('bad'); chip.classList.add('ok'); + icon.setAttribute('icon','mdi:cloud-check-outline'); + txt.textContent = t('editor.integration.connected') || 'Connected to integration'; + } else { + chip.classList.remove('ok'); chip.classList.add('bad'); + icon.setAttribute('icon','mdi:cloud-off-outline'); + txt.textContent = t('editor.integration.disconnected') || 'Not connected to integration'; + } + } + } catch {} + + // Use the draft unit for the settings popup (so unit toggles update labels immediately, before Save). + const popupUnit = (() => { + try { + if (this._settingsDraft && this._settingsDraft.temp_unit != null) { + const u = String(this._settingsDraft.temp_unit).toUpperCase(); + if (u === 'F' || u === 'FAHRENHEIT') return '°F'; + if (u === 'C' || u === 'CELSIUS') return '°C'; + } + } catch {} + try { + return this._unitSymbol(); + } catch {} + return '°C'; + })(); + + // Tab strip (same labels as editor tabs; excludes Sync/Backup intentionally) + const tabMap = { + settings: 'editor.tabs.settings', + pause: 'editor.tabs.pause', + weekdays: 'editor.tabs.weekdays', + rooms: 'editor.tabs.rooms', + boiler: 'editor.tabs.boiler', + colors: 'editor.tabs.colors', + presence_sensor: 'editor.tabs.presence_sensor', + owd: 'editor.tabs.owd', + holidays: 'editor.tabs.holidays', + away: 'editor.tabs.away', + reset: 'editor.tabs.reset' + }; + const tabFallback = { + settings: 'Settings', + pause: 'Pause', + weekdays: 'Weekdays', + rooms: 'Rooms', + boiler: 'Boiler', + colors: 'Colors', + presence_sensor: 'Presence sensor', + owd: 'Open Window Detection', + holidays: 'Holidays', + away: 'Away from home', + reset: 'Reset' + }; + const btns = this.shadowRoot ? Array.from(this.shadowRoot.querySelectorAll('.overlay-settings .settings-tab-btn')) : []; + for (const b of btns){ + const tab = String(b.getAttribute('data-tab') || ''); + const key = tabMap[tab] || null; + if (!key) continue; + b.textContent = t(key) || tabFallback[tab] || tab; + } + const ss = this.shadowRoot && this.shadowRoot.querySelector('.settings-save'); + if (ss) ss.textContent = t('ui.save'); + const sc = this.shadowRoot && this.shadowRoot.querySelector('.settings-close'); + if (sc) sc.textContent = t('ui.close'); + + // Settings tab content labels + const defLab = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-slabel-def'); + if (defLab) defLab.textContent = String(t('editor.default_c')||'').replace('°C', popupUnit); + const rowHLab = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-slabel-rowh'); + if (rowHLab) rowHLab.textContent = t('editor.row_height_px'); + const minLab = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-slabel-minc'); + if (minLab) minLab.textContent = String(t('editor.min_c')||'').replace('°C', popupUnit); + const maxLab = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-slabel-maxc'); + if (maxLab) maxLab.textContent = String(t('editor.max_c')||'').replace('°C', popupUnit); + + const at = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-auto-title'); + if (at) at.textContent = t('editor.auto_apply.title'); + const ad = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-auto-desc'); + if (ad) ad.textContent = t('editor.auto_apply.desc'); + + const et = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-applyedit-title'); + if (et) et.textContent = t('editor.apply_edit.title'); + const ed = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-applyedit-desc'); + if (ed) ed.textContent = t('editor.apply_edit.desc'); + + const dt = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-applydef-title'); + if (dt) dt.textContent = t('editor.apply_default.title'); + const dd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-applydef-desc'); + if (dd) dd.textContent = t('editor.apply_default.desc'); + + const prt = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-perroom-title'); + if (prt) prt.textContent = t('editor.perroom.title'); + const prd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-perroom-desc'); + if (prd) prd.textContent = t('editor.perroom.desc'); + + const srt = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-showroomtemp-title'); + if (srt) srt.textContent = t('editor.show_room_temp.title'); + const srd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-showroomtemp-desc'); + if (srd) srd.textContent = t('editor.show_room_temp.desc'); + + const pbt = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-pausebtn-title'); + if (pbt) pbt.textContent = t('editor.pausebtn.title'); + const pbd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-pausebtn-desc'); + if (pbd) pbd.textContent = t('editor.pausebtn.desc'); + + const pst = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-pausesensor-title'); + if (pst) pst.textContent = t('editor.pause_sensor.title'); + const psd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-pausesensor-desc'); + if (psd) psd.textContent = t('editor.pause_sensor.desc'); + const psEt = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-pausesensor-entity-title'); + if (psEt) psEt.textContent = t('editor.pause_sensor.entity_title'); + const psEd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-pausesensor-entity-desc'); + if (psEd) psEd.textContent = t('editor.pause_sensor.entity_desc'); + + // New popup-only settings (moved from HA editor) + const tft = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-timefmt-title'); + if (tft) tft.textContent = t('editor.timefmt.title'); + const tfd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-timefmt-desc'); + if (tfd) tfd.textContent = t('editor.timefmt.desc'); + const tst = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-timesrc-title'); + if (tst) tst.textContent = t('editor.timesrc.title'); + const tsd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-timesrc-desc'); + if (tsd) tsd.textContent = t('editor.timesrc.desc'); + const sb = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-srcBrowser'); + if (sb) sb.textContent = t('editor.timesrc.browser'); + const sh = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-srcHa'); + if (sh) sh.textContent = t('editor.timesrc.ha'); + const tut = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-tempfmt-title'); + if (tut) tut.textContent = t('editor.tempfmt.title'); + const tud = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-tempfmt-desc'); + if (tud) tud.textContent = t('editor.tempfmt.desc'); + const pt = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-profiles-title'); + if (pt) pt.textContent = t('profiles.enable'); + const pd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-profiles-desc'); + if (pd) pd.textContent = t('profiles.enable.desc'); + + const instTitle = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-instance-title'); + if (instTitle) instTitle.textContent = t('editor.instance.title'); + const instDesc = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-instance-desc'); + if (instDesc) instDesc.textContent = t('editor.instance.desc'); + const instBtn = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-instance-regenerate-btn'); + if (instBtn) { + const label = instBtn.querySelector('span') || instBtn; + if (label) label.textContent = t('editor.instance.new_id'); + } + + // Weekdays tab content labels + const weT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-enable-title'); + if (weT) weT.textContent = t('week.enable'); + const weD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-enable-desc'); + if (weD) weD.textContent = t('week.enable.desc'); + const wvT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-view-title'); + if (wvT) wvT.textContent = t('week.view.title'); + const wvR = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-view-rooms'); + if (wvR) wvR.textContent = t('week.view.rooms_one_day'); + const wvD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-view-days'); + if (wvD) wvD.textContent = t('week.view.days_one_room'); + + const wvsT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-view-switch-title'); + if (wvsT) wvsT.textContent = t('week.view_switch.title'); + const wvsD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-view-switch-desc'); + if (wvsD) wvsD.textContent = t('week.view_switch.desc'); + + const woT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-open-title'); + if (woT) woT.textContent = t('week.editor.title'); + const woD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-open-desc'); + if (woD) woD.textContent = t('week.open_editor.desc'); + const woB = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-open-btn'); + if (woB) woB.textContent = t('week.open_editor.btn'); + + // Boiler tab content labels + const beT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-enable-title'); + if (beT) beT.textContent = t('boiler.enable'); + const beD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-enable-desc'); + if (beD) beD.textContent = t('boiler.enable.desc'); + const bsT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-switch-title'); + if (bsT) bsT.textContent = t('boiler.switch'); + const bsdL = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-switch-domain-label'); + if (bsdL) bsdL.textContent = t('boiler.switch_type'); + try { + const sel = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-switch-domain'); + if (sel) { + const oSw = sel.querySelector('option[value="switch"]'); + if (oSw) oSw.textContent = t('boiler.switch_type.switch') || 'Switch'; + const oIb = sel.querySelector('option[value="input_boolean"]'); + if (oIb) oIb.textContent = t('boiler.switch_type.input_boolean') || 'Input boolean'; + } + } catch {} + const brT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-rooms-title'); + if (brT) brT.textContent = t('boiler.rooms'); + const brD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-rooms-desc'); + if (brD) brD.textContent = t('boiler.rooms.desc'); + const boT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-offsets-title'); + if (boT) boT.textContent = t('boiler.offsets'); + const boD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-offsets-desc'); + if (boD) boD.textContent = t('boiler.offsets.desc'); + const bon = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-on-offset-label'); + if (bon) bon.textContent = String(t('boiler.on_offset')||'').replace('°C', popupUnit); + const bof = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-off-offset-label'); + if (bof) bof.textContent = String(t('boiler.off_offset')||'').replace('°C', popupUnit); + const btT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-sensor-title'); + if (btT) btT.textContent = t('boiler.temp_sensor'); + const btD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-sensor-desc'); + if (btD) btD.textContent = t('boiler.temp_sensor.desc'); + const bmin = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-min-temp-label'); + if (bmin) bmin.textContent = String(t('boiler.min_temp')||'').replace('°C', popupUnit); + const bmax = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-max-temp-label'); + if (bmax) bmax.textContent = String(t('boiler.max_temp')||'').replace('°C', popupUnit); + + // Open Window Detection tab content labels + const owdT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-owd-enable-title'); + if (owdT) owdT.textContent = t('owd.enable'); + const owdD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-owd-enable-desc'); + if (owdD) owdD.textContent = t('owd.enable.desc'); + const owdS = this.shadowRoot ? Array.from(this.shadowRoot.querySelectorAll('.overlay-settings .sp-owd-sensors-label')) : []; + for (const el of owdS) { try { el.textContent = t('owd.sensors'); } catch {} } + const owdDT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-owd-delays-title'); + if (owdDT) owdDT.textContent = t('owd.delays.title'); + const owdDD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-owd-delays-desc'); + if (owdDD) owdDD.textContent = t('owd.delays.desc'); + const owdOpen = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-owd-open-delay-label'); + if (owdOpen) owdOpen.textContent = t('owd.open_delay'); + const owdClose = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-owd-close-delay-label'); + if (owdClose) owdClose.textContent = t('owd.close_delay'); + + // Holidays tab content labels + const heT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-enable-title'); + if (heT) heT.textContent = t('holidays.enable'); + const heD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-enable-desc'); + if (heD) heD.textContent = t('holidays.desc'); + const hsT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-source-title'); + if (hsT) hsT.textContent = t('holidays.source'); + const hCal = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-source-cal'); + if (hCal) hCal.textContent = t('holidays.source.calendar'); + const hMan = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-source-man'); + if (hMan) hMan.textContent = t('holidays.source.manual'); + const hEntT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-entity-title'); + if (hEntT) hEntT.textContent = t('holidays.entity'); + const hEntD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-entity-desc'); + if (hEntD) hEntD.textContent = t('holidays.entity_desc'); + const hDatesT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-dates-title'); + if (hDatesT) hDatesT.textContent = t('holidays.dates'); + const hSinglesT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-single-title'); + if (hSinglesT) hSinglesT.textContent = t('holidays.single_dates'); + const hAdd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-add-date'); + if (hAdd) hAdd.textContent = t('holidays.add_date'); + const hAddR = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-add-range'); + if (hAddR) hAddR.textContent = t('holidays.add_range'); + const hRangesT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-ranges-title'); + if (hRangesT) hRangesT.textContent = t('holidays.ranges'); + const hEditT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-edit-title'); + if (hEditT) hEditT.textContent = t('holidays.editor.title'); + const hEditB = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-edit-btn'); + if (hEditB) hEditB.textContent = t('holidays.edit'); + + // Away tab content labels + const aeT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-enable-title'); + if (aeT) aeT.textContent = t('away.enable'); + const aeD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-enable-desc'); + if (aeD) aeD.textContent = t('away.desc'); + const atT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-temp-title'); + if (atT) atT.textContent = String(t('away.temp')||'').replace('°C', popupUnit); + const apT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-persons-title'); + if (apT) apT.textContent = t('away.persons'); + const adT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-delay-title'); + if (adT) adT.textContent = t('away.delay_enable'); + const adD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-delay-desc'); + if (adD) adD.textContent = t('away.delay_desc'); + const adVL = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-delay-value-label'); + if (adVL) adVL.textContent = t('away.delay_value'); + const adUL = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-delay-unit-label'); + if (adUL) adUL.textContent = t('away.delay_unit'); + try { const sel = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-delay-unit'); if (sel) { const opts = sel.querySelectorAll('option'); if (opts[0]) opts[0].textContent = t('away.delay_unit.seconds'); if (opts[1]) opts[1].textContent = t('away.delay_unit.minutes'); } } catch {} + const aaT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-adv-title'); + if (aaT) aaT.textContent = t('presence.enable_advanced'); + const aaD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-adv-desc'); + if (aaD) aaD.textContent = t('presence.enable_advanced.desc'); + const plT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-presence-live-title'); + if (plT) plT.textContent = t('presence.live_header'); + const plD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-presence-live-desc'); + if (plD) plD.textContent = t('presence.live_header.desc'); + const pcT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-presence-combos-title'); + if (pcT) pcT.textContent = t('presence.combos'); + } catch {} + + } catch (e) { /* ignore */ } + } + + _colorFor(eid, tempC){ + try { + + // Onboarding i18n + try { + const ttl = this.shadowRoot && this.shadowRoot.querySelector('.onb-title'); if (ttl) ttl.textContent = t('onboard.title'); + const prev = this.shadowRoot && this.shadowRoot.querySelector('.onb-prev'); if (prev) prev.textContent = t('onboard.prev'); + const next = this.shadowRoot && this.shadowRoot.querySelector('.onb-next'); if (next) next.textContent = t('onboard.next'); + const skip = this.shadowRoot && this.shadowRoot.querySelector('.onb-skip'); if (skip) skip.textContent = t('onboard.skip'); + const done = this.shadowRoot && this.shadowRoot.querySelector('.onb-done'); if (done) done.textContent = t('onboard.done'); + const hideLbl = this.shadowRoot && this.shadowRoot.querySelector('.onb-hide-lbl'); if (hideLbl) hideLbl.textContent = t('onboard.dont_show'); + } catch {} + // Support merged entities by always resolving to primary + const primary = this._groupPrimaryOf ? this._groupPrimaryOf(eid) : eid; + const cr = (this._config?.color_ranges) || {}; + const globalFirst = !!this._config?.color_global; + const candidates = globalFirst ? ['*', 'default', primary, eid] : [primary, eid, '*', 'default']; + let ranges = []; + for (const k of candidates){ if (Array.isArray(cr[k]) && cr[k].length){ ranges = cr[k]; break; } } + for (const r of ranges){ + const from = Number(r.from); + const to = Number(r.to); + if (!Number.isFinite(from) || !Number.isFinite(to)) continue; + if (tempC >= Math.min(from,to) && tempC <= Math.max(from,to)) return r.color || ''; + } + } catch {} + return ''; + } + + // Compute readable text color for a given background + _contrastTextColor(bg){ + try { + const rgb = (c)=>{ + if(!c) return null; + let s=String(c).trim(); + if (s.startsWith('rgb')){ + const m=s.match(/rgba?\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i); if(!m) return null; return {r:+m[1], g:+m[2], b:+m[3]}; + } + if (s[0]==='#') s=s.slice(1); + if (s.length===3) s = s.split('').map(x=>x+x).join(''); + if (s.length!==6) return null; + const r=parseInt(s.slice(0,2),16), g=parseInt(s.slice(2,4),16), b=parseInt(s.slice(4,6),16); + if ([r,g,b].some(x=>Number.isNaN(x))) return null; return {r,g,b}; + }; + const o = rgb(bg); if (!o) return null; + // Perceived luminance (sRGB) + const lum = (v)=>{ v/=255; return v<=0.03928? v/12.92 : Math.pow((v+0.055)/1.055, 2.4); }; + const L = 0.2126*lum(o.r) + 0.7152*lum(o.g) + 0.0722*lum(o.b); + return L > 0.5 ? '#000000' : '#ffffff'; + } catch { return null; } + } + +} +// Avoid hard failures if the resource is loaded multiple times. +try { + if (!customElements.get("thermostat-timeline-card")) { + customElements.define("thermostat-timeline-card", ThermostatTimelineCard); + } +} catch {} + +/* ----------------- LIGHTWEIGHT PICKER (TTEntityPicker) ----------------- */ +class TTEntityPicker extends HTMLElement { + static get observedAttributes(){ return ['include-domains','label','disabled']; } + constructor(){ + super(); + this.attachShadow({ mode: 'open' }); + this._hass = null; + this._value = ''; + this._filter = null; // function + this._domains = []; + this._disabled = false; + } + set hass(h){ this._hass = h; this._render(); } + get hass(){ return this._hass; } + set value(v){ this._value = v || ''; this._render(); } + get value(){ return this._value || ''; } + set entityFilter(fn){ this._filter = typeof fn === 'function' ? fn : null; this._render(); } + get entityFilter(){ return this._filter; } + set disabled(v){ this._disabled = !!v; this._render(); } + get disabled(){ return !!this._disabled; } + attributeChangedCallback(name, _old, val){ + if (name === 'disabled') this._disabled = val !== null; + this._render(); + } + connectedCallback(){ this._render(); } + _parseDomains(){ + try { + const a = this.getAttribute('include-domains'); + if (!a) return []; + const v = typeof a === 'string' ? JSON.parse(a) : a; + return Array.isArray(v) ? v.map(x=>String(x)) : []; + } catch { return []; } + } + _render(){ + const label = this.getAttribute('label') || ''; + this._domains = this._parseDomains(); + const opts = []; + try { + const states = this._hass?.states || {}; + for (const id of Object.keys(states)){ + const dom = id.split('.')[0]; + if (this._domains.length && !this._domains.includes(dom)) continue; + const st = states[id]; + if (this._filter && !this._filter(st)) continue; + const name = st?.attributes?.friendly_name || (id.split('.')[1] || id); + opts.push({ id, name }); + } + } catch {} + opts.sort((a,b)=> a.name.localeCompare(b.name,'en',{sensitivity:'base'})); + const selOpts = [''].concat( + opts.map(o=>``) + ); + const css = ` + :host{ display:block; } + .wrap{ width:100%; min-width:250px; max-width:440px; } + .sel{ position: relative; } + select{ width:100%; height:36px; min-height:36px; appearance:none; -webkit-appearance:none; -moz-appearance:none; padding:6px 34px 6px 10px; border:1px solid var(--divider-color); border-radius:8px; background: var(--card-background-color); color: var(--primary-text-color); box-sizing: border-box; } + .backup-slot-select{ width:auto; min-width:260px; height:40px; min-height:40px; padding:8px 42px 8px 14px; font-size:14px; font-weight:500; border:1px solid var(--divider-color); border-radius:10px; background: var(--card-background-color); color: var(--primary-text-color); box-shadow: 0 1px 2px rgba(0,0,0,0.08) inset, 0 1px 4px rgba(0,0,0,0.12); transition: border-color 120ms ease, box-shadow 120ms ease; } + .backup-slot-select:focus{ outline:none; border-color: var(--primary-color); box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary-color) 60%, transparent), 0 1px 4px rgba(0,0,0,0.14); } + select:focus{ outline:none; border-color: var(--primary-color); box-shadow: 0 0 0 2px color-mix(in oklab, var(--primary-color) 35%, transparent); } + select:hover{ border-color: color-mix(in oklab, var(--primary-text-color) 20%, var(--divider-color)); } + select:disabled{ opacity:.6; cursor: not-allowed; } + .dd-arrow{ position:absolute; right:12px; top:50%; transform: translateY(-50%); width:0; height:0; border-left:6px solid transparent; border-right:6px solid transparent; border-top:6px solid var(--secondary-text-color); pointer-events:none; } + select:disabled + .dd-arrow{ opacity:.6; } + .label{ font-size:.85rem; color: var(--secondary-text-color); margin: 2px 0 4px; display:none; } + `; + this.shadowRoot.innerHTML = `
    `; + const select = this.shadowRoot.querySelector('select'); + if (select) { + try { select.value = this._value || ''; } catch {} + try { select.disabled = !!this._disabled; } catch {} + // Signal picker open early (helps pause editor re-render on macOS in Safari/Chrome) + try { + select.addEventListener('pointerdown', () => { + this.dispatchEvent(new CustomEvent('picker-opened', { bubbles: true, composed: true })); + }, { passive: true }); + select.addEventListener('focus', () => { + this.dispatchEvent(new CustomEvent('picker-opened', { bubbles: true, composed: true })); + }, { passive: true }); + } catch {} + select.onchange = (e)=>{ + this._value = e.target.value || ''; + this.dispatchEvent(new CustomEvent('value-changed', { detail: { value: this._value }, bubbles: true, composed: true })); + // Signal picker closed after a selection is made + try { this.dispatchEvent(new CustomEvent('picker-closed', { bubbles: true, composed: true })); } catch {} + }; + // Also signal closed on blur (if user clicks away without changing) + try { + select.addEventListener('blur', ()=>{ + this.dispatchEvent(new CustomEvent('picker-closed', { bubbles: true, composed: true })); + }); + } catch {} + } + } +} +// Avoid hard failures if another resource already defined the picker. +try { + if (!customElements.get('tt-entity-picker')) { + customElements.define('tt-entity-picker', TTEntityPicker); + } +} catch {} + +/* ----------------- CONFIG EDITOR ----------------- */ +class ThermostatTimelineCardEditor extends HTMLElement { + setConfig(config) { + const hadUid = !!(config && typeof config === 'object' && String(config.instance_uid || '').trim()); + this._config = { ...ThermostatTimelineCard.getStubConfig(), ...(config||{}) }; + this._storageLoaded = false; + this._render(); + // If this card predates instance_uid, persist it immediately so local per-card + // storage keys remain stable across reloads. + if (!hadUid) { + try { this._emit(); } catch {} + } + } + // Helper to check if API is available + _apiSupported(){ try { return !!(this._hass?.callApi); } catch { return false; } } + + _normInstanceId(raw){ + try { + const s = String(raw ?? '').trim(); + if (!s) return 'default'; + const out = []; + for (const ch of s) { + const ok = (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch === '_' || ch === '-' || ch === '.'; + out.push(ok ? ch : '_'); + } + const s2 = out.join('').slice(0, 64); + return s2 || 'default'; + } catch { return 'default'; } + } + + _instanceId(){ + try { + if (!this._config?.instance_enabled) return 'default'; + return this._normInstanceId(this._config?.instance_id || 'default'); + } catch { return 'default'; } + } + + _localStoreKey(){ + try { + const iid = this._instanceId(); + return (this._config?.instance_enabled) ? `thermostat_timeline_store:${iid}` : 'thermostat_timeline_store'; + } catch { return 'thermostat_timeline_store'; } + } + + async _apiFetchState(instanceId=null){ + if (!this._apiSupported()) return null; + try { + const iid = instanceId ? this._normInstanceId(instanceId) : this._instanceId(); + const q = (this._config?.instance_enabled && iid) ? `?instance_id=${encodeURIComponent(iid)}` : ''; + return await this._hass.callApi('GET', `thermostat_timeline/state${q}`); + } catch { return null; } + } + set hass(hass) { this._hass = hass; this._lang = ttGetLangFromHass(hass); + // On first hass set, merge settings from file storage via API + if (!this._storageLoaded && this._config?.storage_enabled) { + this._storageLoaded = true; + (async () => { + try { + const resp = await this._apiFetchState(); + const s = resp?.settings; + if (s && typeof s === 'object') { + if (typeof s.time_source === 'string') this._config.time_source = (s.time_source === 'ha' ? 'ha' : 'browser'); + if (typeof s.time_12h === 'boolean') this._config.time_12h = !!s.time_12h; + if (typeof s.temp_unit === 'string') this._config.temp_unit = (String(s.temp_unit).toUpperCase()==='F' ? 'F' : 'C'); + if (Number.isFinite(s.default_temp)) this._config.default_temp = Number(s.default_temp); + if (Number.isFinite(s.row_height)) this._config.row_height = Math.max(30, Math.min(200, Math.round(Number(s.row_height)))); + if (Number.isFinite(s.min_temp)) this._config.min_temp = Number(s.min_temp); + if (Number.isFinite(s.max_temp)) this._config.max_temp = Number(s.max_temp); + if (typeof s.auto_apply_enabled === 'boolean') this._config.auto_apply = !!s.auto_apply_enabled; + if (typeof s.profiles_enabled === 'boolean') this._config.profiles_enabled = !!s.profiles_enabled; + if (typeof s.weekdays_enabled === 'boolean') this._config.weekdays_enabled = !!s.weekdays_enabled; + if (typeof s.holidays_enabled === 'boolean') this._config.holidays_enabled = !!s.holidays_enabled; + if (typeof s.show_pause_button === 'boolean') this._config.show_pause_button = !!s.show_pause_button; + if (typeof s.pause_sensor_enabled === 'boolean') this._config.pause_sensor_enabled = !!s.pause_sensor_enabled; + if (typeof s.pause_sensor_entity === 'string') this._config.pause_sensor_entity = String(s.pause_sensor_entity || ''); + if (typeof s.apply_on_edit === 'boolean') this._config.apply_on_edit = !!s.apply_on_edit; + if (typeof s.apply_on_default_change === 'boolean') this._config.apply_on_default_change = !!s.apply_on_default_change; + if (typeof s.per_room_defaults === 'boolean') this._config.per_room_defaults = !!s.per_room_defaults; + if (typeof s.backup_auto_enabled === 'boolean') this._config.backup_auto_enabled = !!s.backup_auto_enabled; + if (Number.isFinite(s.backup_interval_days)) this._config.backup_interval_days = s.backup_interval_days; + if (s.open_window && typeof s.open_window === 'object') this._config.open_window = s.open_window; + } + // Re-render after merging settings + try { this._render(); } catch {} + } catch (err) {} + })(); + } + // Avoid clobbering inputs while user is typing (preserve previous behavior) + const ae = this.shadowRoot && this.shadowRoot.activeElement; + if (this._suspendRender) return; + if (ae) { + try { + if (ae.classList?.contains("title") || ae.classList?.contains('label-input') + || ae.classList?.contains('mi-from') || ae.classList?.contains('mi-to') || ae.classList?.contains('mi-temp') + || (ae.closest && ae.closest('.mini-ed'))) return; + if (ae.closest && (ae.closest('ha-entity-picker') || ae.closest('tt-entity-picker'))) return; + } catch {} + } + // Keep title preset in sync with language when user hasn't customized it + try { + if (this._config) { + const cur = String(this._config.title || ''); + if (!cur || this._isDefaultTitle(cur)) { + const next = ttLocalize('card.title_default', this._hass || this._lang || 'en'); + if (next && next !== cur) { this._config.title = next; this._emit(); } + } + } + } catch {} + // Full render and i18n apply (normal path) + this._render(); this._applyEditorI18n(); + // Ensure entity pickers are hydrated with hass after hard refresh + try { this._forcePickerRefresh && this._forcePickerRefresh(); } catch {} + // Also run a lightweight service-availability-only check in case services changed while editor open + try { + const root = this.shadowRoot; + if (root) { + const hasService = !!(this._hass && this._hass.services && this._hass.services['thermostat_timeline']); + if (!hasService) { + const warnText = this._t ? this._t('editor.store_missing') : null; + const msg = warnText && warnText !== 'editor.store_missing' ? warnText : 'Integration "Thermostat Pro Timeline Sync" is not installed. Shared storage is unavailable.'; + let warn = root.querySelector('.store-missing'); + if (!warn) { + warn = document.createElement('div'); warn.className = 'store-missing'; warn.style.color = 'var(--error-color)'; warn.style.fontSize = '.9rem'; warn.style.marginTop = '6px'; + const controls = root.querySelector('.store-controls'); if (controls && controls.parentNode) controls.parentNode.insertBefore(warn, controls); + } + while (warn.firstChild) warn.removeChild(warn.firstChild); + warn.appendChild(document.createTextNode(msg.replace(/\.$/, '') + '. ')); + (function(){ + let link = document.createElement('a'); + // Use my.home-assistant.io deep link to the HACS repository page + link.href = 'https://my.home-assistant.io/redirect/hacs_repository/?owner=qlerup&repository=thermostat-pro-timeline-sync'; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + link.style.color = 'var(--error-color)'; + link.style.textDecoration = 'underline'; + link.style.fontWeight = '500'; + link.textContent = this._t('editor.open_in_hacs'); + warn.appendChild(link); + })(); + const sw = root.querySelector('.store-enable'); if (sw) sw.disabled = true; + const picker = root.querySelector('.storage'); if (picker) picker.disabled = true; + } else { + const rootWarn = root.querySelector('.store-missing'); if (rootWarn && rootWarn.parentNode) rootWarn.parentNode.removeChild(rootWarn); + const sw = root.querySelector('.store-enable'); if (sw) sw.disabled = false; + const picker = root.querySelector('.storage'); if (picker) picker.disabled = false; + } + // Update integration status chip in tabs + try { + const chip = root.querySelector('.int-status'); + const icon = chip?.querySelector('ha-icon'); + const txt = chip?.querySelector('.text'); + if (chip && icon && txt) { + if (hasService) { + chip.classList.remove('bad'); chip.classList.add('ok'); + icon.setAttribute('icon','mdi:cloud-check-outline'); + txt.textContent = this._t ? this._t('editor.integration.connected') : 'Connected to integration'; + } else { + chip.classList.remove('ok'); chip.classList.add('bad'); + icon.setAttribute('icon','mdi:cloud-off-outline'); + txt.textContent = this._t ? this._t('editor.integration.disconnected') : 'Not connected to integration'; + } + // Click to jump to Sync tab + try { chip.style.userSelect='none'; chip.onclick = ()=>{ this._activeTab='sync'; this._render(); }; } catch {} + } + } catch {} + } + } catch {} + } + + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this._openCount = 0; + this._pendingEmit = false; + this._openRows = new Set(); + this._openColorRows = new Set(); + this._openOwdRows = new Set(); + this._suspendRender = false; + this._activeTab = 'settings'; + // Holidays inline editor state + this._holDraft = null; // { rooms: { [eid]: [ {id,startMin,endMin,temp} ] } } + this._holRoom = null; // active room eid + this._holEditing = null; // { room, blockId } + this._holMini = null; // { room, blk } + // Import state (Backup tab) + this._importStoreError = ''; + this._importStoreBusy = false; + this.shadowRoot.innerHTML = ` + +
    +
    +
    Titel
    + + + + + + +
    +
    +
    +
    +
    +
    Shared storage + background control
    +
    Saves schedules to file storage and enables background control.
    +
    + +
    + +
    + +
    + +
    +
    +
    +
    + +
    + + +
    +
    + + `; this._applyEditorI18n(); + + const qs = (s)=>this.shadowRoot.querySelector(s); + qs(".title")?.addEventListener("change", e => this._upd("title", e.target.value)); + // Note: storage_entity_* pickers removed - file-based storage is always used + qs('.store-enable')?.addEventListener('change', e => { + const on = !!e.target.checked; + this._upd('storage_enabled', on); + // Disable the bottom "Clear local only" button when shared storage is ON + try { + const clrLocal = this.shadowRoot.querySelector('.clear-local-only'); + if (clrLocal) { + clrLocal.disabled = on; + clrLocal.setAttribute('aria-disabled', on ? 'true' : 'false'); + clrLocal.title = on ? 'Disabled while shared storage is ON' : ''; + // Hide button entirely when shared storage is enabled (requested) + clrLocal.style.display = on ? 'none' : ''; + } + } catch {} + // Show/hide storage controls when shared storage is toggled + try { + const ctrls = this.shadowRoot.querySelector('.store-controls'); + if (ctrls) ctrls.style.display = on ? 'flex' : 'none'; + } catch {} + // Sync controls removed (instant-only via Save button) + this._applyEditorI18n(); + // Re-render so Colors tab shared-clear button visibility updates immediately + try { this._render(); } catch {} + // Ensure integration side knows current auto-apply state when enabling shared storage + try { if (on) this._pushSettingsToStoreDebounced(); } catch {} + // On enabling shared storage: offer to migrate local browser data to storage + (async ()=>{ + try { + if (on) { + const localRaw = localStorage.getItem(this._localStoreKey()); + const hasLocal = !!(localRaw && localRaw.length > 2); + if (hasLocal) { + const ok = confirm(this._t('editor.migrate_confirm')); + if (ok) { + // Properly unwrap the local payload and send both schedules and settings + let parsed = {}; + try { parsed = JSON.parse(localRaw || '{}'); } catch { parsed = {}; } + let schedules = parsed && typeof parsed === 'object' && parsed.schedules ? parsed.schedules : (parsed || {}); + let settings = parsed && typeof parsed === 'object' && parsed.settings ? parsed.settings : { time_12h: this._config.time_12h, temp_unit: this._config.temp_unit, entities: Array.isArray(this._config.entities) ? this._config.entities : [], room_use_input_number: Array.isArray(this._config.room_use_input_number) ? this._config.room_use_input_number : [], room_use_temp_sensor: Array.isArray(this._config.room_use_temp_sensor) ? this._config.room_use_temp_sensor : [], color_ranges: this._config.color_ranges, color_global: !!this._config.color_global, row_height: Number(this._config.row_height ?? 64), default_temp: Number(this._config.default_temp||20), min_temp: Number(this._config.min_temp ?? 5), max_temp: Number(this._config.max_temp ?? 25), away: this._config.away, merges: this._config.merges, labels: this._config.labels, temp_sensors: this._config.temp_sensors, per_room_defaults: !!(this._config.per_room_defaults ?? false), apply_on_default_change: !!(this._config.apply_on_default_change ?? true), holidays_enabled: !!this._config.holidays_enabled, holidays_source: this._config.holidays_source || 'calendar', holidays_entity: this._config.holidays_entity || '', holidays_dates: Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : [], sync_mode: (this._config.storage_sync_mode||'instant'), sync_delay_min: Number(Math.max(0, Math.round((this._config.storage_sync_sec||0)/60))), sync_delay_sec: Number(this._config.storage_sync_sec||0) }; + let colors = parsed && typeof parsed === 'object' && parsed.colors ? parsed.colors : { color_ranges: settings?.color_ranges || this._config.color_ranges, color_global: typeof settings?.color_global === 'boolean' ? settings.color_global : !!this._config.color_global }; + settings = { ...settings, entities: Array.isArray(settings?.entities) ? settings.entities : (Array.isArray(this._config.entities) ? this._config.entities : []), room_use_input_number: Array.isArray(settings?.room_use_input_number) ? settings.room_use_input_number : (Array.isArray(this._config.room_use_input_number) ? this._config.room_use_input_number : []), room_use_temp_sensor: Array.isArray(settings?.room_use_temp_sensor) ? settings.room_use_temp_sensor : (Array.isArray(this._config.room_use_temp_sensor) ? this._config.room_use_temp_sensor : []), row_height: Number(settings?.row_height ?? (this._config.row_height ?? 64)), min_temp: Number(settings?.min_temp ?? (this._config.min_temp ?? 5)), max_temp: Number(settings?.max_temp ?? (this._config.max_temp ?? 25)), apply_on_default_change: (typeof settings?.apply_on_default_change === 'boolean') ? settings.apply_on_default_change : !!(this._config.apply_on_default_change ?? true), show_pause_button: !!(this._config.show_pause_button ?? true), show_room_temp: !!(this._config.show_room_temp ?? true), pause_sensor_enabled: !!(this._config.pause_sensor_enabled ?? false), pause_sensor_entity: String(this._config.pause_sensor_entity || ''), auto_apply_enabled: !!(this._config.auto_apply ?? true), pause_indef: !!this._pauseIndef, pause_until_ms: Number(this._pauseUntilMs||0) }; + if (this._hass) { + // Use set_store service directly - file-based storage + const p = { schedules, settings, colors }; + if (this._config?.instance_enabled) { p.instance_id = this._instanceId(); p.activate = true; } + await this._hass.callService('thermostat_timeline', 'set_store', p); + } + } else { + // If user cancels, offer to import schedules FROM storage into local browser + try { + // Fetch from API instead of sensor states + const resp = await this._apiFetchState(); + if (resp && typeof resp === 'object') { + let sch = resp.schedules || {}; + let set = resp.settings || { time_12h: this._config.time_12h, temp_unit: this._config.temp_unit, entities: Array.isArray(this._config.entities) ? this._config.entities : [], room_use_input_number: Array.isArray(this._config.room_use_input_number) ? this._config.room_use_input_number : [], room_use_temp_sensor: Array.isArray(this._config.room_use_temp_sensor) ? this._config.room_use_temp_sensor : [], color_ranges: this._config.color_ranges, color_global: !!this._config.color_global, row_height: Number(this._config.row_height ?? 64), default_temp: Number(this._config.default_temp||20), min_temp: Number(this._config.min_temp ?? 5), max_temp: Number(this._config.max_temp ?? 25), away: this._config.away, merges: this._config.merges, labels: this._config.labels, temp_sensors: this._config.temp_sensors, per_room_defaults: !!(this._config.per_room_defaults ?? false), apply_on_default_change: !!(this._config.apply_on_default_change ?? true), holidays_enabled: !!this._config.holidays_enabled, holidays_source: this._config.holidays_source || 'calendar', holidays_entity: this._config.holidays_entity || '', holidays_dates: Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : [], sync_mode: (this._config.storage_sync_mode||'instant'), sync_delay_min: Number(Math.max(0, Math.round((this._config.storage_sync_sec||0)/60))), sync_delay_sec: Number(this._config.storage_sync_sec||0) }; + let colors = resp.colors || { color_ranges: set.color_ranges || this._config.color_ranges, color_global: !!set.color_global }; + if (Object.keys(sch || {}).length) { + const pull = confirm(this._t('editor.migrate_pull_confirm')); + if (pull) { + try { localStorage.setItem(this._localStoreKey(), JSON.stringify({ schedules: sch, settings: set, colors })); } catch {} + } + } + } + } catch {} + } + } + } + } catch {} + })(); + }); + // Sync mode controls removed (instant-only via Save button) + // (reload buttons removed) + // Holidays enable (migrated into in-card settings popup) + // (clear storage only button removed) + + // Clear all data (storage + local) + qs('.clear-all')?.addEventListener('click', async () => { + const msg = this._t('editor.clear_all_confirm'); + if (!confirm(msg)) return; + try { + try { await this._clearAllDataEverywhereConfirmed(); } catch {} + } catch (e) {} + }); + + // Clear local only (bottom) + qs('.clear-local-only')?.addEventListener('click', async (ev) => { + // Hard guard: do nothing if shared storage is enabled + if (this._config?.storage_enabled) { try { ev.preventDefault(); ev.stopPropagation(); } catch {} return; } + const msg = this._t('editor.clear_local_only_confirm'); + if (!confirm(msg)) return; + try { + try { localStorage.removeItem(this._localStoreKey()); } catch {} + // Clear in-memory schedules when using local storage only + this._schedules = {}; + this._profilesSelected = null; this._profilesDraft = null; this._profilesDirty = false; this._profilesEditingNew = false; + this._emit(true); + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + } catch {} + }); + // Reset onboarding (show intro again) + qs('.reset-onboard')?.addEventListener('click', async () => { + try { localStorage.removeItem('thermostat_timeline_onboard_seen_v1'); } catch {} + try { alert(this._t('onboard.reset_ok') || 'Welcome will be shown next time.'); } catch {} + }); + qs('.migrate-to-store')?.addEventListener('click', async (ev) => { + try { + // File-based storage only - no sensor entity IDs needed + const raw = localStorage.getItem(this._localStoreKey()) || ''; + if (!raw) { alert(this._t('editor.no_local_data')); return; } + const ok = confirm(this._t('editor.migrate_confirm')); + if (!ok) return; + const parsed = JSON.parse(raw || '{}'); + const schedules = parsed.schedules || parsed || {}; + const weekdays = parsed.weekdays || {}; + const profiles = parsed.profiles || {}; + let settings = parsed.settings || { time_12h: this._config.time_12h, temp_unit: this._config.temp_unit, color_ranges: this._config.color_ranges, color_global: !!this._config.color_global, row_height: Number(this._config.row_height ?? 64), default_temp: Number(this._config.default_temp||20), min_temp: Number(this._config.min_temp ?? 5), max_temp: Number(this._config.max_temp ?? 25), away: this._config.away, merges: this._config.merges, labels: this._config.labels, temp_sensors: this._config.temp_sensors, per_room_defaults: !!(this._config.per_room_defaults ?? false), apply_on_default_change: !!(this._config.apply_on_default_change ?? true), profiles_enabled: !!this._config.profiles_enabled, holidays_enabled: !!this._config.holidays_enabled, holidays_source: this._config.holidays_source || 'calendar', holidays_entity: this._config.holidays_entity || '', holidays_dates: Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : [] }; + let colors = parsed.colors || { color_ranges: settings?.color_ranges || this._config.color_ranges, color_global: typeof settings?.color_global === 'boolean' ? settings.color_global : !!this._config.color_global }; + settings = { ...settings, row_height: Number(settings?.row_height ?? (this._config.row_height ?? 64)), min_temp: Number(settings?.min_temp ?? (this._config.min_temp ?? 5)), max_temp: Number(settings?.max_temp ?? (this._config.max_temp ?? 25)), apply_on_default_change: (typeof settings?.apply_on_default_change === 'boolean') ? settings.apply_on_default_change : !!(this._config.apply_on_default_change ?? true), show_pause_button: !!(this._config.show_pause_button ?? true), pause_sensor_enabled: !!(this._config.pause_sensor_enabled ?? false), pause_sensor_entity: String(this._config.pause_sensor_entity || ''), auto_apply_enabled: !!(this._config.auto_apply ?? true), pause_indef: !!this._pauseIndef, pause_until_ms: Number(this._pauseUntilMs||0) }; + if (this._hass) { + // Use set_store service directly - file-based storage + const p = { schedules, weekdays, profiles, settings, colors }; + if (this._config?.instance_enabled) { p.instance_id = this._instanceId(); p.activate = true; } + await this._hass.callService('thermostat_timeline', 'set_store', p); + } + } catch (e) {} + }); + // Tabs behavior is handled via robust click delegation in _render() + } + + _roomUsesInputNumber(idx){ + try { + const ent = (this._config?.entities||[])[idx] || ''; + const inferred = String(ent).startsWith('input_number.'); + const arr = Array.isArray(this._config?.room_use_input_number) ? this._config.room_use_input_number : []; + const explicit = (typeof arr[idx] === 'boolean') ? arr[idx] : null; + return (explicit === null) ? inferred : explicit; + } catch { return false; } + } + + _setRoomUseInputNumber(idx, on){ + try { + const ents = [...(this._config.entities||[])]; + const modes = Array.isArray(this._config.room_use_input_number) ? [...this._config.room_use_input_number] : []; + while (modes.length < ents.length) modes.push(false); + modes[idx] = !!on; + + const oldEid = ents[idx]; + // If existing selection doesn't match new mode, clear and clean mappings + const mismatch = !!oldEid && ((!!on && !String(oldEid).startsWith('input_number.')) || (!on && !String(oldEid).startsWith('climate.'))); + if (mismatch) { + // Clean up labels/merges/temp_sensors for the old primary + try { + const labels = { ...(this._config.labels || {}) }; + if (oldEid && labels[oldEid]) delete labels[oldEid]; + this._config.labels = labels; + } catch {} + try { + const merges = { ...(this._config.merges || {}) }; + if (oldEid && merges[oldEid]) delete merges[oldEid]; + for (const k of Object.keys(merges)) { + merges[k] = (merges[k] || []).filter((e) => e !== oldEid); + if (!merges[k].length) delete merges[k]; + } + this._config.merges = merges; + } catch {} + try { + const sensors = { ...(this._config.temp_sensors || {}) }; + if (oldEid && sensors[oldEid]) delete sensors[oldEid]; + this._config.temp_sensors = sensors; + } catch {} + ents[idx] = ''; + } + + this._config.entities = ents; + this._config.room_use_input_number = modes; + this._emit(true); + try { this._pushSettingsToStoreDebounced(); } catch {} + } catch { this._emit(true); } + } + + // --- Local helpers for unit/time conversions (editor context) --- + _isF(){ try { return (this._config?.temp_unit||'C')==='F'; } catch { return false; } } + _unitSymbol(){ return this._isF() ? '°F' : '°C'; } + _toDisplayTemp(c){ + // Always round display values to avoid long decimals (e.g. whole °F becomes fractional °C internally). + try { + const v = Number(c); + if (!Number.isFinite(v)) return c; + const disp = this._isF() ? (v*9/5+32) : v; + return Math.round(disp * 10) / 10; + } catch { return c; } + } + _fromDisplayTemp(v){ try { return this._isF() ? ((Number(v)-32)*5/9) : Number(v); } catch { return Number(v); } } + + + + // Force re-hydration of entity pickers on the Rooms tab with the latest hass + // reference. This helps cases where the tab was hidden during first render + // or after a hard refresh and the picker didn't populate. + _forcePickerRefresh(){ + try { + const tab = this.shadowRoot && this.shadowRoot.querySelector('.tab-rooms'); + if (!tab) return; + const pickers = Array.from(tab.querySelectorAll('ha-entity-picker, tt-entity-picker')); + for (const p of pickers){ + try { + p.hass = this._hass; + p.style.display = 'block'; + p.style.minHeight = '48px'; + if (typeof p.requestUpdate === 'function') p.requestUpdate(); + // Fallback: if element still has no height, poke its internal input later + setTimeout(()=>{ try { if (p.offsetHeight < 8 && typeof p.requestUpdate === 'function') p.requestUpdate(); } catch {} }, 50); + } catch {} + } + } catch {} + } + + connectedCallback(){ + this._render(); + try { + // Extra-robust suspend on pointer start inside any entity picker (captures from Shadow DOM) + const capHandler = (e)=>{ + try { + const path = typeof e.composedPath === 'function' ? e.composedPath() : []; + const hit = path && path.some(n => { + const t = (n && n.tagName ? n.tagName.toLowerCase() : ''); + return t === 'ha-entity-picker' || t === 'tt-entity-picker'; + }); + if (hit) this._suspendRender = true; + } catch {} + }; + this.shadowRoot.addEventListener('pointerdown', capHandler, { capture: true }); + this.shadowRoot.addEventListener('touchstart', capHandler, { capture: true, passive: true }); + + // Suspend render while interacting with any entity picker to avoid closing dropdowns + this.shadowRoot.addEventListener('focusin', (e)=>{ + try { if (e.target?.closest && (e.target.closest('ha-entity-picker') || e.target.closest('tt-entity-picker'))) this._suspendRender = true; } catch {} + try { + // Also suspend while editing inputs in Colors tab (prevents HA hass updates from re-rendering and killing focus) + const t = e.target; + if (t && t.closest && t.closest('.tab-colors')) { + if (t.tagName === 'INPUT' || t.classList?.contains('remove-btn')) { + this._suspendRender = true; + } + } + } catch {} + }); + this.shadowRoot.addEventListener('focusout', ()=>{ + // Give a tiny delay so click into next field doesn't flicker + setTimeout(()=>{ this._suspendRender = false; }, 150); + }); + // React to explicit picker open/close signals (from tt-entity-picker) + this.shadowRoot.addEventListener('picker-opened', ()=>{ try { this._suspendRender = true; } catch {} }); + this.shadowRoot.addEventListener('picker-closed', ()=>{ try { this._suspendRender = false; } catch {} }); + this.shadowRoot.addEventListener('value-changed', (e)=>{ + try { + const tag = e.target?.tagName?.toLowerCase(); + if (tag === 'ha-entity-picker' || tag === 'tt-entity-picker') { this._suspendRender = false; } + } catch {} + }); + // After hard refresh, wait for HA to define pickers and hydrate them + try { + customElements.whenDefined('ha-entity-picker').then(()=>{ try { this._forcePickerRefresh(); } catch {} }); + } catch {} + // Extra retry shortly after mount + try { setTimeout(()=>{ try { this._forcePickerRefresh(); } catch {} }, 300); } catch {} + } catch {} + } + + // ---------- Storage helpers (editor context) ---------- + // Note: File-based storage is always used now - no sensor entities needed + // Stub function for backwards compatibility - always returns empty string + _storageEntity(kind = 'schedules') { return ''; } + + // Debounced push of settings (labels/merges/colors/min/max/away/unit/format) to shared storage + _pushSettingsToStoreDebounced(){ + clearTimeout(this._storePushTimer); + this._storePushTimer = setTimeout(()=>this._pushSettingsToStore(), 500); + } + async _pushSettingsToStore(){ + try { + if (!this._hass || !this._config?.storage_enabled) { return; } + // IMPORTANT: The backend replaces the entire settings object when provided. + // Base our write on the current stored settings to avoid wiping popup-managed keys. + const resp = await this._apiFetchState(); + const base = resp?.settings; + if (!base || typeof base !== 'object') return; + + let settings = { + ...base, + + // Editor-managed settings only (popup-managed keys are preserved from base) + away: this._config.away, + // open_window is now configured via in-card popup; preserve base.open_window + + merges: this._config.merges, + labels: this._config.labels, + temp_sensors: this._config.temp_sensors, + turn_on: this._config.turn_on, + + backup_auto_enabled: !!this._config.backup_auto_enabled, + backup_interval_min: Number(this._config.backup_interval_min||1440), + + holidays_enabled: !!this._config.holidays_enabled, + holidays_source: this._config.holidays_source || base.holidays_source || 'calendar', + holidays_entity: this._config.holidays_entity || '', + holidays_dates: Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : (Array.isArray(base.holidays_dates) ? base.holidays_dates : []), + holidays_groups: Array.isArray(this._config.holidays_groups) ? this._config.holidays_groups : (Array.isArray(base.holidays_groups) ? base.holidays_groups : []), + + sync_mode: (this._config.storage_sync_mode||base.sync_mode||'instant'), + sync_delay_min: Number(this._config.storage_sync_min ?? base.sync_delay_min ?? 0), + }; + // Prevent accidental flip of holidays_enabled=true->false on load when nothing holiday-related changed + try { + const bHE = typeof base.holidays_enabled === 'boolean' ? base.holidays_enabled : undefined; + const iHE = typeof settings.holidays_enabled === 'boolean' ? settings.holidays_enabled : undefined; + if (bHE === true && iHE === false) { + const normArr = (a)=>{ try { return Array.from(new Set((a||[]).map(String))).sort(); } catch { return []; } }; + const eqDates = JSON.stringify(normArr(settings.holidays_dates)) === JSON.stringify(normArr(base.holidays_dates)); + const eqSource = String(settings.holidays_source||'') === String(base.holidays_source||''); + const eqEntity = String(settings.holidays_entity||'') === String(base.holidays_entity||''); + const eqGroups = JSON.stringify(settings.holidays_groups||[]) === JSON.stringify(base.holidays_groups||[]); + if (eqDates && eqSource && eqEntity && eqGroups) { + settings = { ...settings, holidays_enabled: true }; + } + } + } catch {} + // If user hasn't toggled holidays explicitly, keep backend value + try { + if (!this._holidaysToggleTouched && typeof base.holidays_enabled === 'boolean') { + settings = { ...settings, holidays_enabled: base.holidays_enabled }; + } + } catch {} + const p = { settings }; + if (this._config?.instance_enabled) { p.instance_id = this._instanceId(); p.activate = true; } + await this._hass.callService('thermostat_timeline','set_store', p); + } catch (err) {} + } + + // Replace this editor element with a fresh instance (simulates closing/reopening) + _hardReloadEditor(){ + try { + const cfg = JSON.parse(JSON.stringify(this._config || {})); + const hass = this._hass; + const parent = this.parentNode || (this.getRootNode && this.getRootNode().host && this.getRootNode().host.parentNode) || null; + const next = this.nextSibling || null; + const fresh = document.createElement('thermostat-timeline-card-editor'); + try { fresh.setConfig(cfg); } catch {} + try { if (hass) fresh.hass = hass; } catch {} + if (parent) { + parent.insertBefore(fresh, next); + parent.removeChild(this); + } else { + // Fallback: force re-render + picker refresh + this._render(); + this._forcePickerRefresh && this._forcePickerRefresh(); + } + } catch (e) { + try { this._render(); this._forcePickerRefresh && this._forcePickerRefresh(); } catch {} + } + } + + // Full dashboard reload with cache-busting query param + _hardReloadPage(){ + try { + const url = new URL(window.location.href); + url.searchParams.set('ttcb', String(Date.now())); + window.location.replace(url.toString()); + } catch { + try { window.location.reload(); } catch {} + } + } + + _normalizeImportedStoreFile(obj){ + // Accept either raw data object ({schedules,...}) or full HA .storage wrapper ({key,data:{...}}). + const isObj = (v)=>!!v && typeof v === 'object' && !Array.isArray(v); + const root = isObj(obj) ? obj : null; + const data = (root && isObj(root.data)) ? root.data : root; + if (!isObj(data)) return null; + + const schedules = data.schedules; + if (!isObj(schedules)) return null; + + const out = { schedules }; + if (isObj(data.settings)) out.settings = data.settings; + if (isObj(data.weekdays)) out.weekdays = data.weekdays; + if (isObj(data.profiles)) out.profiles = data.profiles; + if (isObj(data.colors)) out.colors = data.colors; + return out; + } + + async _importThermostatTimelineJsonFile(file){ + const safeMsg = (e)=>{ try { return String(e?.message || e || ''); } catch { return 'Error'; } }; + this._importStoreError = ''; + if (!file) return; + + try { + if (!this._hass) throw new Error((this._t && (this._t('backup.import.err_no_hass') || 'Home Assistant not ready')) || 'Home Assistant not ready'); + if (!this._config?.storage_enabled) throw new Error((this._t && (this._t('backup.import.err_storage_off') || 'Shared storage is OFF. Turn it on first.')) || 'Shared storage is OFF. Turn it on first.'); + const hasService = !!(this._hass?.services && this._hass.services['thermostat_timeline']); + if (!hasService) throw new Error((this._t && (this._t('backup.import.err_no_integration') || 'Integration thermostat_timeline not available')) || 'Integration thermostat_timeline not available'); + + this._importStoreBusy = true; + try { this._render(); } catch {} + + let parsed; + try { + const text = await file.text(); + parsed = JSON.parse(text); + } catch { + throw new Error((this._t && (this._t('backup.import.err_json') || 'Invalid JSON')) || 'Invalid JSON'); + } + + const store = this._normalizeImportedStoreFile(parsed); + if (!store) throw new Error(this._t ? (this._t('backup.import.err_invalid') || 'Invalid file') : 'Invalid file'); + + // 1) Create a new backup slot of the current store + await this._hass.callService('thermostat_timeline', 'backup_now', {}); + // Give HA a moment to persist the backup + await new Promise((res)=>setTimeout(res, 600)); + + // 2) Overwrite store with imported data + const payload = { force: true, schedules: store.schedules }; + if (store.settings) payload.settings = store.settings; + if (store.weekdays) payload.weekdays = store.weekdays; + if (store.profiles) payload.profiles = store.profiles; + if (store.colors) payload.colors = store.colors; + if (this._config?.instance_enabled) { payload.instance_id = this._instanceId(); payload.activate = true; } + await this._hass.callService('thermostat_timeline', 'set_store', payload); + + // 3) Sync editor UI with imported settings (best-effort) + try { + const s = store.settings; + if (s && Array.isArray(s.entities)) { + const ents = s.entities.filter(Boolean).map(String); + this._config.entities = ents; + const rawModes = Array.isArray(s.room_use_input_number) ? s.room_use_input_number.map(v=>!!v) : []; + while (rawModes.length < ents.length) rawModes.push(false); + this._config.room_use_input_number = ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return true; + if (String(eid).startsWith('climate.')) return false; + return !!rawModes[idx]; + }); + this._emit(true); + } + } catch {} + + // 4) Update local browser cache and refresh open cards + try { + const st = await this._apiFetchState(); + if (st && typeof st === 'object') { + localStorage.setItem(this._localStoreKey(), JSON.stringify({ + schedules: st.schedules || {}, + weekdays: st.weekdays || {}, + profiles: st.profiles || {}, + settings: st.settings || {}, + colors: st.colors || {}, + })); + } + } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + + // Re-render backup tab so the new slot shows up + await new Promise((res)=>setTimeout(res, 600)); + try { this._render(); } catch {} + } catch (e) { + this._importStoreError = safeMsg(e); + try { this._render(); } catch {} + } finally { + this._importStoreBusy = false; + try { this._render(); } catch {} + } + } + + _render(){ + if(!this._hass || !this.shadowRoot || !this._config) return; + if(this._openCount > 0) return; + + // Boiler tab was migrated into the in-card settings popup + if (this._activeTab === 'boiler') this._activeTab = 'settings'; + + // Open Window Detection tab was migrated into the in-card settings popup + if (this._activeTab === 'owd') this._activeTab = 'settings'; + + // Colors tab was migrated into the in-card settings popup + if (this._activeTab === 'colors') this._activeTab = 'settings'; + + // Holidays tab was migrated into the in-card settings popup + if (this._activeTab === 'holidays') this._activeTab = 'settings'; + + // Away tab was migrated into the in-card settings popup + if (this._activeTab === 'away') this._activeTab = 'settings'; + + // Rooms tab was migrated into the in-card settings popup + if (this._activeTab === 'rooms') this._activeTab = 'settings'; + + // If HA hasn't registered core editor elements yet (after hard refresh), + // re-render once they are defined so pickers/textfields upgrade properly. + try { + if (!customElements.get('ha-entity-picker')) { + customElements.whenDefined('ha-entity-picker').then(()=>{ try { this._render(); } catch {} }); + } + if (!customElements.get('ha-textfield')) { + customElements.whenDefined('ha-textfield').then(()=>{ try { this._render(); } catch {} }); + } + } catch {} + + // Robust tab delegation (ensure any tab can be opened reliably) + if (!this._tabHandlersBound) { + this._tabHandlersBound = true; + try { + this.shadowRoot.addEventListener('click', (ev)=>{ + // Shadow DOM can yield a Text node as event.target; use composedPath() + // to find the actual button element. + const path = typeof ev.composedPath === 'function' ? ev.composedPath() : []; + let el = null; + for (const n of path) { + if (n && n.classList && n.classList.contains('tab-btn')) { el = n; break; } + } + if (!el) { + const t = (ev.target && ev.target.closest) ? ev.target : null; + el = t && t.closest ? t.closest('.tab-btn') : null; + } + if (!el) return; + if (el.classList.contains('tab-settings-btn')) { this._activeTab = 'settings'; this._render(); } + if (el.classList.contains('tab-backup-btn')) { this._activeTab = 'backup'; this._render(); } + }); + } catch {} + } + + const t = this.shadowRoot.querySelector(".title"); + // Note: storage entity pickers removed - file-based storage is always used + const seEn = this.shadowRoot.querySelector('.store-enable'); + const hol = this.shadowRoot.querySelector('.holidays-enable'); + if (t) t.value = this._config.title ?? ""; + if (seEn) { + const enabled = !!this._config.storage_enabled; + seEn.checked = enabled; + // Note: storage entity pickers removed - file-based storage is always used + // Ensure controls visibility matches toggle + try { + const ctrls = this.shadowRoot.querySelector('.store-controls'); + if (ctrls) ctrls.style.display = enabled ? 'flex' : 'none'; + } catch {} + // Sync controls removed (instant-only via Save button) + // Reflect state for bottom "Clear local only" button + try { + const clrLocal = this.shadowRoot.querySelector('.clear-local-only'); + if (clrLocal) { + clrLocal.disabled = enabled; + clrLocal.setAttribute('aria-disabled', enabled ? 'true' : 'false'); + clrLocal.title = enabled ? 'Disabled while shared storage is ON' : ''; + // Hide when shared storage is enabled + clrLocal.style.display = enabled ? 'none' : ''; + } + } catch {} + } + this._applyEditorI18n(); + if (hol){ hol.checked = !!this._config.holidays_enabled; } + // Note: storage entity pickers removed - file-based storage is always used + // Backup tab controls + try { + const bAuto = this.shadowRoot.querySelector('.backup-auto'); + const bMin = this.shadowRoot.querySelector('.backup-days'); + const bRow = this.shadowRoot.querySelector('.backup-interval-row'); + const bBtn = this.shadowRoot.querySelector('.btn-backup-now'); + const rBtn = this.shadowRoot.querySelector('.btn-restore-now'); + const dBtn = this.shadowRoot.querySelector('.btn-delete-backup'); + const impInput = this.shadowRoot.querySelector('.backup-import-file'); + const impBtn = this.shadowRoot.querySelector('.btn-import-store'); + const impErr = this.shadowRoot.querySelector('.backup-import-error'); + const slotSel = this.shadowRoot.querySelector('.backup-slot-select'); + const lastDesc = this.shadowRoot.querySelector('.last-backup-desc'); + if (bAuto) bAuto.checked = !!this._config.backup_auto_enabled; + if (bRow) bRow.style.display = (!!this._config.backup_auto_enabled) ? '' : 'none'; + if (bMin && bMin !== this.shadowRoot.activeElement) bMin.value = String(this._config.backup_interval_days || 1); + + // Import UI state + try { + const msg = String(this._importStoreError || ''); + if (impErr) { + impErr.textContent = msg; + impErr.style.display = msg ? '' : 'none'; + } + if (impBtn) impBtn.disabled = !!this._importStoreBusy; + if (impInput) impInput.disabled = !!this._importStoreBusy; + } catch {} + // No master toggle; all individual checkboxes remain enabled + const fmt = (iso)=>{ + try { + if(!iso) return ''; + const d=new Date(iso); + const lang = ttGetLangFromHass(this._hass || this._lang || 'en'); + const opts={ dateStyle:'medium', timeStyle:'short', hour12: !!this._config?.time_12h }; + if (this._config?.time_source==='ha' && this._hass?.config?.time_zone) opts.timeZone=this._hass.config.time_zone; + return new Intl.DateTimeFormat(lang, opts).format(d); + } catch { + return String(iso||''); + } + }; + // Fetch backup info from API + const fetchBackupInfo = async ()=>{ + try { + if (!this._apiSupported?.()) return null; + return await this._hass.callApi('GET', 'thermostat_timeline/state'); + } catch { return null; } + }; + (async ()=>{ + const api = await fetchBackupInfo(); + if (lastDesc && api){ + const ts = api?.backup?.last_backup_ts; + lastDesc.textContent = ts ? fmt(ts) : ''; + } + if (slotSel && api){ + const prevSel = slotSel.value; + const slots = Array.isArray(api?.backup?.slots) ? api.backup.slots : []; + const opts=[]; + slots.forEach((entry, i)=>{ + if(!entry) return; + const ts = entry.ts || entry.created_at || entry.created || null; + const labelTs = ts ? fmt(ts) : ''; + const label = labelTs || 'Backup'; + opts.push({value:String(i+1), label}); + }); + slotSel.innerHTML=''; + const newOpt = document.createElement('option'); + newOpt.value = 'new'; + newOpt.textContent = (this._t && this._t('backup.new')) ? this._t('backup.new') : 'New backup'; + slotSel.append(newOpt); + opts.forEach((o)=>{ const opt=document.createElement('option'); opt.value=o.value; opt.textContent=o.label; slotSel.append(opt); }); + + // Restore previous selection if still valid, else default to first backup (if any) otherwise new + const validValues = new Set(['new', ...opts.map(o=>o.value)]); + let targetVal = 'new'; + if (prevSel && validValues.has(prevSel)) { + targetVal = prevSel; + } else if (opts.length > 0) { + targetVal = opts[0].value; + } + slotSel.value = targetVal; + const applyStates = ()=>{ + const val = slotSel.value; + const isNew = (val === 'new' || !val); + const setState = (btn, disabled)=>{ + if (!btn) return; + btn.disabled = !!disabled; + btn.style.opacity = disabled ? '0.55' : ''; + btn.style.pointerEvents = disabled ? 'none' : ''; + btn.style.filter = disabled ? 'grayscale(0.35)' : ''; + }; + setState(rBtn, isNew); + setState(dBtn, isNew); + setState(bBtn, !isNew); + }; + applyStates(); + slotSel.onchange = applyStates; + } + })(); + bBtn && (bBtn.onclick = async ()=>{ + try { + // Always full backup + await this._hass.callService('thermostat_timeline','backup_now', {}); + // Note: sensor update removed - file-based storage is used + // give HA a moment to persist + await new Promise(res=>setTimeout(res, 800)); + setTimeout(()=>{ try { this._render(); } catch{} }, 1200); + } catch {} + }); + rBtn && (rBtn.onclick = async ()=>{ + try { + const slot = Number(this.shadowRoot.querySelector('.backup-slot-select')?.value || '0') || undefined; + if (confirm((this._t && this._t('backup.restore.confirm_overwrite')) || 'Restore from backup? This will overwrite current schedules and settings.')) { + const payload = { mode: 'replace' }; + if (slot) payload.slot = slot; + await this._hass.callService('thermostat_timeline','restore_now', payload); + setTimeout(()=>{ try { this._render(); } catch{} }, 800); + } + } catch {} + }); + dBtn && (dBtn.onclick = async ()=>{ + try { + const slot = Number(this.shadowRoot.querySelector('.backup-slot-select')?.value || '0') || undefined; + if (!slot) return; + const msg = this._t ? this._t('backup.delete.confirm') : null; + if (!confirm(msg || 'Delete selected backup?')) return; + await this._hass.callService('thermostat_timeline','delete_backup', { slot }); + setTimeout(()=>{ try { this._render(); } catch{} }, 800); + } catch {} + }); + // Import from thermostat_timeline.json file + const runImport = async ()=>{ + try { + const file = impInput?.files?.[0]; + await this._importThermostatTimelineJsonFile(file); + } finally { + try { if (impInput) impInput.value = ''; } catch {} + } + }; + if (impBtn) impBtn.onclick = runImport; + if (impInput) impInput.onchange = runImport; + bAuto && bAuto.addEventListener('change', (e)=>{ const on=!!e.target.checked; this._upd('backup_auto_enabled', on); if (bRow) bRow.style.display = on? '' : 'none'; try { this._pushSettingsToStoreDebounced(); } catch {} }); + bMin && bMin.addEventListener('change', (e)=>{ let v=Math.round(Number(e.target.value||1)); if(!Number.isFinite(v)) v=1; v=Math.max(1, Math.min(365, v)); e.target.value=String(v); this._upd('backup_interval_days', v); try { this._pushSettingsToStoreDebounced(); } catch {} }); + } catch {} + // No mode dropdown in editor + + const wrap = this.shadowRoot.querySelector(".entities"); + const want = (this._config.entities || []).length; + if (wrap) { + // Rebuild rows; preserve which are open using a stable key per row (entity id or placeholder) + wrap.innerHTML = ''; + for (let i=0;i { + try { + const id = st?.entity_id || st?.entityId || st?.entityID || ""; + if (!id) return false; + const chosenPrim = (this._config.entities || []).filter(Boolean); + const allLinked = Object.values(this._config.merges || {}).flat(); + const blocked = new Set([...chosenPrim, ...allLinked]); + const current = this._config.entities[i]; + return !blocked.has(id) || id === current; + } catch { return true; } + }; + } + + // Update header title and label input to reflect current selection/override + try { + const eid = this._config.entities[i] || ""; + const labels = this._config.labels || {}; + const titleEl = row.querySelector('.summary-title'); + const subEl = row.querySelector('.summary-sub'); + const nameInp = row.querySelector('.label-input'); + const friendly = eid ? (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid)) : ''; + const shown = labels[eid] || friendly || this._t('editor.entity_placeholder'); + if (titleEl) titleEl.textContent = shown; + if (nameInp && nameInp !== this.shadowRoot.activeElement) nameInp.value = labels[eid] || ''; + // Build merged subtitle text + if (subEl) { + const linked = (this._config.merges?.[eid] || []).map(id => ( + this._hass?.states?.[id]?.attributes?.friendly_name || (id.split('.')[1] || id) + )); + const parts = []; + if (eid) parts.push(eid); + if (linked.length) parts.push(`${this._t('editor.merged_with')}: ${linked.join(', ')}`); + subEl.textContent = parts.join(' • '); + } + } catch {} + + // Rebuild merge chips every render + try { + const eid = this._config.entities[i] || ''; + const chips = row.querySelector('.linked-chips'); + const mergePicker = row.querySelector('.merge-picker'); + if (mergePicker) mergePicker.hass = this._hass; + if (chips && eid){ + chips.innerHTML = ''; + const links = (this._config.merges?.[eid] || []); + for (const l of links){ + const chip = document.createElement('span'); + chip.className = 'pill-chip'; + const txt = document.createElement('span'); + txt.textContent = this._hass?.states?.[l]?.attributes?.friendly_name || (l.split('.')[1]||l); + const rm = document.createElement('button'); + rm.type = 'button'; rm.className = 'rm'; rm.textContent = '×'; + rm.setAttribute('aria-label', this._t('editor.remove')); + rm.addEventListener('click', ()=>{ + const merges = { ...(this._config.merges || {}) }; + merges[eid] = (merges[eid]||[]).filter(x=>x!==l); + if (!merges[eid]?.length) delete merges[eid]; + this._config.merges = merges; + this._emit(true); + }); + chip.append(txt, rm); + chips.append(chip); + } + } + + // (colors tab content and visibility now rendered once outside the entity loop) + } catch {} + } + } + + // Build colors tab content (always render once, even with zero rooms) + // Build colors tab content (always render once, even with zero rooms) + try { + const root = this.shadowRoot.querySelector('.tab-colors'); + if (root) { + root.innerHTML = ''; + + // Colors mode toggle (Per room | All rooms) + const modeWrap = document.createElement('div'); + modeWrap.style.display = 'flex'; + modeWrap.style.alignItems = 'center'; + modeWrap.style.justifyContent = 'space-between'; + modeWrap.style.gap = '10px'; + modeWrap.style.marginBottom = '6px'; + const modeText = document.createElement('div'); + modeText.style.display = 'grid'; + modeText.style.gap = '2px'; + const modeTitle = document.createElement('div'); modeTitle.className = 'colors-mode-title'; modeTitle.style.fontWeight = '600'; modeTitle.textContent = this._t('editor.colors.mode') || 'Color mode'; + const modeDesc = document.createElement('div'); modeDesc.className = 'colors-mode-desc'; modeDesc.style.fontSize = '.85rem'; modeDesc.style.color = 'var(--secondary-text-color)'; modeDesc.textContent = this._t('editor.colors.mode.desc') || 'Choose per-room colors or one set for all rooms.'; + modeText.append(modeTitle, modeDesc); + const seg = document.createElement('div'); seg.className = 'seg colors-mode'; + const bRoom = document.createElement('button'); bRoom.type = 'button'; bRoom.className = 'mode-room'; bRoom.textContent = this._t('editor.colors.mode_per_room') || 'Per room'; + const bGlob = document.createElement('button'); bGlob.type = 'button'; bGlob.className = 'mode-global'; bGlob.textContent = this._t('editor.colors.mode_global') || 'All rooms'; + const applySegUi = ()=>{ const on = !!this._config.color_global; bGlob.classList.toggle('active', on); bRoom.classList.toggle('active', !on); }; + bRoom.onclick = ()=>{ this._config.color_global = false; this._emit(true); this._pushSettingsToStoreDebounced(); }; + bGlob.onclick = ()=>{ this._config.color_global = true; this._emit(true); this._pushSettingsToStoreDebounced(); }; + seg.append(bRoom, bGlob); + applySegUi(); + modeWrap.append(modeText, seg); + root.append(modeWrap); + + const isGlobal = !!this._config.color_global; + + // If per-room mode and there are no rooms yet, show helper box + if (!isGlobal && want === 0) { + const box = document.createElement('div'); box.className = 'empty-box'; + const p = document.createElement('div'); p.textContent = this._t('editor.colors.empty') || 'No rooms added yet.'; + const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'add-entity-btn'; btn.innerHTML = '' + (this._t('editor.colors.go_add') || 'Add room') + ''; + btn.onclick = () => { + this._activeTab = 'rooms'; this._render(); + const add = this.shadowRoot.querySelector('.add-room-btn'); + if (add) { add.classList.add('pulse-attn','pulse'); setTimeout(() => add.classList.remove('pulse'), 3500); add.scrollIntoView({ behavior:'smooth', block:'center' }); } + }; + box.append(p, btn); root.append(box); + } + + // --- FIXED: GLOBAL EDITOR --- + if (isGlobal) { + const eid = '*'; + const line = document.createElement('div'); line.className = 'inline'; + const summary = document.createElement('div'); summary.className = 'summary'; + const sumLeft = document.createElement('div'); sumLeft.className = 'summary-left'; + const exp = document.createElement('button'); exp.className = 'expander'; exp.innerHTML = ''; + const txtWrap = document.createElement('div'); txtWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); titleSpan.className = 'summary-title'; titleSpan.textContent = this._t('editor.colors.global_title') || 'All rooms'; + txtWrap.append(titleSpan); sumLeft.append(exp, txtWrap); summary.append(sumLeft); + line.append(summary); + const details = document.createElement('div'); details.className = 'details'; line.append(details); + const colorsWrap = document.createElement('div'); colorsWrap.style.display = 'grid'; colorsWrap.style.gap = '0px'; details.append(colorsWrap); + const head = document.createElement('div'); head.className = 'colors-head'; + head.innerHTML = `${this._t('editor.colors.col_from')}${this._t('editor.colors.col_to')}${this._t('editor.colors.col_color')}`; + try { if (this._openColorRows.has(eid)) line.classList.add('open'); } catch {} + const renderRanges = () => { + colorsWrap.innerHTML = ''; + // Get current list (treat as read-only) + const ranges = (this._config.color_ranges?.[eid]) || []; + if (ranges.length > 0) colorsWrap.append(head); + ranges.forEach((r, ri) => { + const outer = document.createElement('div'); outer.className = 'color-item'; + const row = document.createElement('div'); row.className = 'colors-row'; + const f = document.createElement('input'); f.type = 'number'; f.step = '0.5'; f.value = String(this._toDisplayTemp(r.from ?? 0)); f.setAttribute('aria-label', this._t('editor.colors.col_from')); f.title = this._t('editor.colors.col_from'); + const t = document.createElement('input'); t.type = 'number'; t.step = '0.5'; t.value = String(this._toDisplayTemp(r.to ?? 0)); t.setAttribute('aria-label', this._t('editor.colors.col_to')); t.title = this._t('editor.colors.col_to'); + const c = document.createElement('input'); c.type = 'color'; c.value = r.color || '#ffb347'; c.setAttribute('aria-label', this._t('editor.colors.col_color')); c.title = this._t('editor.colors.col_color'); + const maxDisp = this._toDisplayTemp(this._config.max_temp ?? 25); + const minDisp = this._toDisplayTemp(this._config.min_temp ?? 5); + f.min = String(minDisp); t.min = String(minDisp); + f.max = String(maxDisp); t.max = String(maxDisp); + const rm = document.createElement('button'); rm.type = 'button'; rm.className = 'remove-btn'; rm.innerHTML = '' + (this._t('editor.remove') || 'Remove') + ''; + + const repaint = ()=>{ this._emit(true); try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} try { document.querySelectorAll('thermostat-timeline-card').forEach(el=>el?.devRefreshFromEditor?.()); } catch {} }; + + // --- SAFE IMMUTABLE UPDATES --- + f.onchange = () => { + let v=parseFloat(f.value); if (isNaN(v)) v=minDisp; v=Math.max(minDisp, Math.min(maxDisp, v)); f.value=String(v); + // Copy array + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + // Copy object and update + if(copied_config[ri]) copied_config[ri] = { ...copied_config[ri], from: this._fromDisplayTemp(v) }; + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + repaint(); + }; + t.onchange = () => { + let v=parseFloat(t.value); if (isNaN(v)) v=minDisp; v=Math.max(minDisp, Math.min(maxDisp, v)); t.value=String(v); + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + if(copied_config[ri]) copied_config[ri] = { ...copied_config[ri], to: this._fromDisplayTemp(v) }; + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + repaint(); + }; + c.onchange = () => { + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + if(copied_config[ri]) copied_config[ri] = { ...copied_config[ri], color: c.value }; + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + repaint(); + }; + rm.onclick = () => { + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + copied_config.splice(ri, 1); + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + this._emit(true); renderRanges(); + }; + // ----------------------------- + + row.append(f, t, c, rm); outer.append(row); colorsWrap.append(outer); + }); + const add = document.createElement('button'); add.className = 'add-entity-btn'; add.type = 'button'; add.innerHTML = '' + (this._t('editor.heat_colors.add') || 'Add color range') + ''; + add.onclick = () => { const arr = [...(this._config.color_ranges?.[eid] || [])]; arr.push({ from: this._config.default_temp || 20, to: this._config.default_temp || 20, color: '#ffb347' }); this._config.color_ranges = { ...(this._config.color_ranges || {}), [eid]: arr }; this._emit(true); renderRanges(); }; + colorsWrap.append(add); + }; + renderRanges(); + exp.onclick = () => { line.classList.toggle('open'); const key = eid; if (line.classList.contains('open')) this._openColorRows.add(key); else this._openColorRows.delete(key); }; + root.append(line); + } + + // --- FIXED: PER-ROOM EDITOR --- + for (let i = 0; !isGlobal && i < want; i++) { + const eid = this._config.entities[i]; if (!eid) continue; + const line = document.createElement('div'); line.className = 'inline'; + const summary = document.createElement('div'); summary.className = 'summary'; + const sumLeft = document.createElement('div'); sumLeft.className = 'summary-left'; + const exp = document.createElement('button'); exp.className = 'expander'; exp.innerHTML = ''; + const txtWrap = document.createElement('div'); txtWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); titleSpan.className = 'summary-title'; + try { + const labels = this._config.labels || {}; + const display = labels[eid] || this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid); + titleSpan.textContent = display; + } catch { titleSpan.textContent = this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid); } + txtWrap.append(titleSpan); sumLeft.append(exp, txtWrap); summary.append(sumLeft); + line.append(summary); + const details = document.createElement('div'); details.className = 'details'; line.append(details); + const colorsWrap = document.createElement('div'); colorsWrap.style.display = 'grid'; colorsWrap.style.gap = '0px'; details.append(colorsWrap); + const head = document.createElement('div'); head.className = 'colors-head'; + head.innerHTML = `${this._t('editor.colors.col_from')}${this._t('editor.colors.col_to')}${this._t('editor.colors.col_color')}`; + try { if (this._openColorRows.has(eid)) line.classList.add('open'); } catch {} + const renderRanges = () => { + colorsWrap.innerHTML = ''; + // Get current list (treat as read-only) + const ranges = (this._config.color_ranges?.[eid]) || []; + if (ranges.length > 0) colorsWrap.append(head); + ranges.forEach((r, ri) => { + const outer = document.createElement('div'); outer.className = 'color-item'; + const row = document.createElement('div'); row.className = 'colors-row'; + const f = document.createElement('input'); f.type = 'number'; f.step = '0.5'; f.value = String(this._toDisplayTemp(r.from ?? 0)); f.setAttribute('aria-label', this._t('editor.colors.col_from')); f.title = this._t('editor.colors.col_from'); + const t = document.createElement('input'); t.type = 'number'; t.step = '0.5'; t.value = String(this._toDisplayTemp(r.to ?? 0)); t.setAttribute('aria-label', this._t('editor.colors.col_to')); t.title = this._t('editor.colors.col_to'); + const c = document.createElement('input'); c.type = 'color'; c.value = r.color || '#ffb347'; c.setAttribute('aria-label', this._t('editor.colors.col_color')); c.title = this._t('editor.colors.col_color'); + const maxDisp = this._toDisplayTemp(this._config.max_temp ?? 25); + const minDisp = this._toDisplayTemp(this._config.min_temp ?? 5); + f.min = String(minDisp); t.min = String(minDisp); + f.max = String(maxDisp); t.max = String(maxDisp); + const rm = document.createElement('button'); rm.type = 'button'; rm.className = 'remove-btn'; rm.innerHTML = '' + (this._t('editor.remove') || 'Remove') + ''; + + const repaint = ()=>{ + this._emit(true); + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + try { document.querySelectorAll('thermostat-timeline-card').forEach(el=>el?.devRefreshFromEditor?.()); } catch {} + }; + + // --- SAFE IMMUTABLE UPDATES --- + f.onchange = () => { + let v=parseFloat(f.value); if (isNaN(v)) v=minDisp; v=Math.max(minDisp, Math.min(maxDisp, v)); f.value=String(v); + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + if(copied_config[ri]) copied_config[ri] = { ...copied_config[ri], from: this._fromDisplayTemp(v) }; + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + repaint(); + }; + t.onchange = () => { + let v=parseFloat(t.value); if (isNaN(v)) v=minDisp; v=Math.max(minDisp, Math.min(maxDisp, v)); t.value=String(v); + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + if(copied_config[ri]) copied_config[ri] = { ...copied_config[ri], to: this._fromDisplayTemp(v) }; + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + repaint(); + }; + c.onchange = () => { + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + if(copied_config[ri]) copied_config[ri] = { ...copied_config[ri], color: c.value }; + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + repaint(); + }; + rm.onclick = () => { + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + copied_config.splice(ri, 1); + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + this._emit(true); renderRanges(); + }; + // ----------------------------- + + row.append(f, t, c, rm); outer.append(row); colorsWrap.append(outer); + }); + const add = document.createElement('button'); add.className = 'add-entity-btn'; add.type = 'button'; add.innerHTML = '' + (this._t('editor.heat_colors.add') || 'Add color range') + ''; + add.onclick = () => { const arr = [...(this._config.color_ranges?.[eid] || [])]; arr.push({ from: this._config.default_temp || 20, to: this._config.default_temp || 20, color: '#ffb347' }); this._config.color_ranges = { ...(this._config.color_ranges || {}), [eid]: arr }; this._emit(true); renderRanges(); }; + colorsWrap.append(add); + }; + renderRanges(); + exp.onclick = () => { + line.classList.toggle('open'); + const key = eid; + if (line.classList.contains('open')) this._openColorRows.add(key); + else this._openColorRows.delete(key); + }; + root.append(line); + } + + // Add explicit refresh/apply button for colors + const refresh = document.createElement('button'); + refresh.className = 'add-entity-btn'; + refresh.type = 'button'; + refresh.innerHTML = '' + (this._t('editor.colors.refresh') || 'Update schedule') + ''; + refresh.onclick = async () => { + // Persist color settings to local/shared storage so live card instances pick them up + try { + let settings = { time_12h: this._config.time_12h, temp_unit: this._config.temp_unit, time_source: this._config.time_source, row_height: Number(this._config.row_height ?? 64), default_temp: Number(this._config.default_temp || 20), min_temp: Number(this._config.min_temp ?? 5), max_temp: Number(this._config.max_temp ?? 25), per_room_defaults: !!(this._config.per_room_defaults ?? false), apply_on_edit: !!(this._config.apply_on_edit ?? true), apply_on_default_change: !!(this._config.apply_on_default_change ?? true), color_ranges: this._config.color_ranges, color_global: !!this._config.color_global, away: this._config.away, merges: this._config.merges, labels: this._config.labels, temp_sensors: this._config.temp_sensors, profiles_enabled: !!this._config.profiles_enabled, holidays_enabled: !!this._config.holidays_enabled, holidays_source: this._config.holidays_source || 'calendar', holidays_entity: this._config.holidays_entity || '', holidays_dates: Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : [], sync_mode: (this._config.storage_sync_mode||'instant'), sync_delay_min: Number(this._config.storage_sync_min||0) }; + try { + if (!this._holidaysToggleTouched) { + const st = await this._apiFetchState?.(); + const base = st?.settings; + if (typeof base?.holidays_enabled === 'boolean') settings.holidays_enabled = base.holidays_enabled; else delete settings.holidays_enabled; + } + } catch {} + const colors = { color_ranges: this._config.color_ranges, color_global: !!this._config.color_global }; + let schedules = {}; + // Prefer current file storage schedules when enabled - use API + try { + if (this._config?.storage_enabled && this._apiSupported?.()) { + const resp = await this._apiFetchState?.(); + if (resp && typeof resp === 'object' && resp.schedules) { + schedules = JSON.parse(JSON.stringify(resp.schedules)); + } + } + } catch {} + // Fallback to localStorage copy + if (!schedules || Object.keys(schedules).length===0){ + try { + const raw = localStorage.getItem(this._localStoreKey()) || ''; + const parsed = JSON.parse(raw||'{}'); + schedules = parsed.schedules || {}; + // Unwrap if accidentally saved nested + if (schedules && schedules.schedules && typeof schedules.schedules === 'object') schedules = schedules.schedules; + } catch {} + } + // Write local copy + try { localStorage.setItem(this._localStoreKey(), JSON.stringify({ schedules, settings: { ...settings, show_pause_button: !!(this._config.show_pause_button ?? true), show_room_temp: !!(this._config.show_room_temp ?? true), pause_sensor_enabled: !!(this._config.pause_sensor_enabled ?? false), pause_sensor_entity: String(this._config.pause_sensor_entity || ''), auto_apply_enabled: !!(this._config.auto_apply ?? true), pause_indef: !!this._pauseIndef, pause_until_ms: Number(this._pauseUntilMs||0) }, colors })); } catch {} + // And update shared file storage if available + try { + if (this._config.storage_enabled) { + const setPayload = { ...settings, show_pause_button: !!(this._config.show_pause_button ?? true), show_room_temp: !!(this._config.show_room_temp ?? true), pause_sensor_enabled: !!(this._config.pause_sensor_enabled ?? false), pause_sensor_entity: String(this._config.pause_sensor_entity || ''), auto_apply_enabled: !!(this._config.auto_apply ?? true), pause_indef: !!this._pauseIndef, pause_until_ms: Number(this._pauseUntilMs||0) }; + const p = { schedules, settings: setPayload, colors }; + if (this._config?.instance_enabled) { p.instance_id = this._instanceId(); p.activate = true; } + await this._hass.callService('thermostat_timeline','set_store', p); + } + } catch {} + } catch {} + // Emit config change and force preview card re-render + this._emit(true); + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + try { document.querySelectorAll('thermostat-timeline-card').forEach(el=>el?.devRefreshFromEditor?.()); } catch {} + }; + // Append buttons stacked (each on its own line) and aligned left + // Update schedule button + root.append(refresh); + + // Single button: clear all color data (local + shared if available) + const clrAll = document.createElement('button'); + clrAll.className = 'remove-btn clr-colors-all-btn'; + clrAll.type = 'button'; + try { clrAll.style.width = 'max-content'; clrAll.style.justifySelf = 'start'; } catch {} + clrAll.innerHTML = '' + (this._t('editor.colors.clear_all') || 'Clear all color data') + ''; + clrAll.onclick = async () => { + const ok = confirm(this._t('editor.colors.clear_all_confirm') || 'This will delete all color ranges from both local and shared storage. Continue?'); + if (!ok) return; + try { + // 1) Clear in-memory config + this._config.color_ranges = {}; + // 2) Clear localStorage copy + try { + const raw = localStorage.getItem(this._localStoreKey()) || ''; + let schedules = {}; + let settings = { time_12h: this._config.time_12h, temp_unit: this._config.temp_unit, time_source: this._config.time_source, row_height: Number(this._config.row_height ?? 64), default_temp: Number(this._config.default_temp || 20), color_ranges: {}, color_global: !!this._config.color_global, min_temp: Number(this._config.min_temp ?? 5), max_temp: Number(this._config.max_temp ?? 25), per_room_defaults: !!(this._config.per_room_defaults ?? false), apply_on_edit: !!(this._config.apply_on_edit ?? true), apply_on_default_change: !!(this._config.apply_on_default_change ?? true), away: this._config.away, merges: this._config.merges, labels: this._config.labels, temp_sensors: this._config.temp_sensors, sync_mode: (this._config.storage_sync_mode||'instant'), sync_delay_min: Number(this._config.storage_sync_min||0) }; + let colors = { color_ranges: {}, color_global: !!this._config.color_global }; + if (raw) { + try { + const parsed = JSON.parse(raw || '{}'); + schedules = parsed.schedules || {}; + const prev = parsed.settings || {}; + settings = { ...prev, color_ranges: {} }; + colors = parsed.colors || colors; + } catch {} + } + localStorage.setItem(this._localStoreKey(), JSON.stringify({ schedules, settings, colors })); + } catch {} + // 3) Clear shared file storage if enabled + try { + if (this._hass && this._config?.storage_enabled) { + let schedules = {}; + let settings = { time_12h: this._config.time_12h, temp_unit: this._config.temp_unit, time_source: this._config.time_source, row_height: Number(this._config.row_height ?? 64), default_temp: Number(this._config.default_temp || 20), color_ranges: {}, color_global: !!this._config.color_global, min_temp: Number(this._config.min_temp ?? 5), max_temp: Number(this._config.max_temp ?? 25), per_room_defaults: !!(this._config.per_room_defaults ?? false), apply_on_edit: !!(this._config.apply_on_edit ?? true), apply_on_default_change: !!(this._config.apply_on_default_change ?? true), away: this._config.away, merges: this._config.merges, labels: this._config.labels, temp_sensors: this._config.temp_sensors, sync_mode: (this._config.storage_sync_mode||'instant'), sync_delay_min: Number(this._config.storage_sync_min||0) }; + let colors = { color_ranges: {}, color_global: !!this._config.color_global }; + // Fetch current schedules from API to preserve them while clearing colors + try { + const resp = await this._apiFetchState?.(); + if (resp && typeof resp === 'object') { + if (resp.schedules) schedules = JSON.parse(JSON.stringify(resp.schedules)); + if (resp.settings) settings = { ...resp.settings, color_ranges: {} }; + if (resp.colors) colors = { ...resp.colors, color_ranges: {} }; + } + } catch {} + const p = { schedules, settings, colors }; + if (this._config?.instance_enabled) { p.instance_id = this._instanceId(); p.activate = true; } + await this._hass.callService('thermostat_timeline', 'set_store', p); + } + } catch (e) {} + } catch {} + // Repaint editor + live cards + this._emit(true); + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + try { document.querySelectorAll('thermostat-timeline-card').forEach(el=>el?.devRefreshFromEditor?.()); } catch {} + }; + root.append(clrAll); + } + } catch {} + + // Build Holidays tab content + try { + const root = this.shadowRoot.querySelector('.tab-holidays'); + if (root) { + root.innerHTML = ''; + // Toggle enable inside Holidays tab + const enableCard = document.createElement('div'); enableCard.className = 'settings-card'; + const enRow = document.createElement('div'); enRow.className='setting'; + const enText = document.createElement('div'); enText.className='text'; + const enTitle = document.createElement('div'); enTitle.className='title'; enTitle.textContent = this._t('holidays.enable'); + const enDesc = document.createElement('div'); enDesc.className='desc'; enDesc.textContent = this._t('holidays.desc'); + enText.append(enTitle, enDesc); + const enSwitch = document.createElement('ha-switch'); enSwitch.className = 'holidays-enable'; enSwitch.checked = !!this._config.holidays_enabled; + enSwitch.addEventListener('change', (e)=>{ const on = !!e.target.checked; this._upd('holidays_enabled', on); this._holidaysToggleTouched = true; try { contentWrap.style.display = on ? '' : 'none'; } catch {} try { this._pushSettingsToStoreDebounced(); } catch {} }); + enRow.append(enText, enSwitch); enableCard.append(enRow); root.append(enableCard); + // Container for remaining content (shown only when enabled) + const contentWrap = document.createElement('div'); contentWrap.className='holidays-content'; contentWrap.style.display = this._config.holidays_enabled ? '' : 'none'; root.append(contentWrap); + // Enable hint if no rooms + if ((this._config.entities||[]).length === 0) { + const box = document.createElement('div'); box.className='empty-box'; + const p = document.createElement('div'); p.textContent = this._t('editor.select_entities'); + const btn = document.createElement('button'); btn.type='button'; btn.className='add-entity-btn'; btn.innerHTML = ''+(this._t('editor.add_entity')||'Add room')+''; + btn.onclick = ()=>{ this._activeTab='rooms'; this._render(); }; + box.append(p, btn); contentWrap.append(box); + } + // Source selector + const sourceWrap = document.createElement('div'); sourceWrap.className='settings-card'; + const sRow = document.createElement('div'); sRow.className='setting'; + const sText = document.createElement('div'); sText.className='text'; + const sTitle = document.createElement('div'); sTitle.className='title'; sTitle.textContent = this._t('holidays.source'); + const sDesc = document.createElement('div'); sDesc.className='desc'; sDesc.textContent = this._t('holidays.desc'); + sText.append(sTitle, sDesc); + const sSeg = document.createElement('div'); sSeg.className='seg'; + const bCal = document.createElement('button'); bCal.type='button'; bCal.className='h-src-cal'; bCal.textContent = this._t('holidays.source.calendar'); + const bMan = document.createElement('button'); bMan.type='button'; bMan.className='h-src-man'; bMan.textContent = this._t('holidays.source.manual'); + const applySegUi = ()=>{ const src = this._config.holidays_source || 'calendar'; bCal.classList.toggle('active', src==='calendar'); bMan.classList.toggle('active', src==='manual'); }; + bCal.onclick = ()=>{ this._config.holidays_source='calendar'; applySegUi(); this._emit(true); try { this._pushSettingsToStoreDebounced(); } catch {} }; + bMan.onclick = ()=>{ this._config.holidays_source='manual'; applySegUi(); this._emit(true); try { this._pushSettingsToStoreDebounced(); } catch {} }; + sSeg.append(bCal, bMan); sRow.append(sText, sSeg); sourceWrap.append(sRow); applySegUi(); + // Calendar entity picker + const rowCal = document.createElement('div'); rowCal.className='setting'; rowCal.style.display = (this._config.holidays_source||'calendar')==='calendar' ? 'grid' : 'none'; + // Override layout to single-column so description can sit under the dropdown + try { rowCal.style.gridTemplateColumns = '1fr'; } catch {} + const calTxt = document.createElement('div'); calTxt.className='text'; + const calTitle = document.createElement('div'); calTitle.className='title'; calTitle.textContent = this._t('holidays.entity'); + calTxt.append(calTitle); + const picker = document.createElement('tt-entity-picker'); picker.className='holiday-entity'; picker.setAttribute('include-domains','["calendar"]'); picker.setAttribute('label', this._t('holidays.entity')); + try { picker.hass = this._hass; picker.value = this._config.holidays_entity || ''; } catch {} + picker.addEventListener('value-changed', (e)=>{ this._upd('holidays_entity', e.detail.value||''); try { this._pushSettingsToStoreDebounced(); } catch {} }); + // Description moved below the dropdown to avoid cramped layout + const calDesc = document.createElement('div'); calDesc.className='desc'; calDesc.textContent = this._t('holidays.entity_desc'); + rowCal.append(calTxt, picker, calDesc); sourceWrap.append(rowCal); + // Manual dates list + const rowMan = document.createElement('div'); rowMan.className='setting'; rowMan.style.display = (this._config.holidays_source||'calendar')==='manual' ? 'grid' : 'none'; + const manTxt = document.createElement('div'); manTxt.className='text'; + const manTitle = document.createElement('div'); manTitle.className='title'; manTitle.textContent = this._t('holidays.dates'); + const manDesc = document.createElement('div'); manDesc.className='desc'; manDesc.textContent=''; manTxt.append(manTitle, manDesc); + const manWrap = document.createElement('div'); manWrap.style.display='grid'; manWrap.style.gap='8px'; + const addRow = document.createElement('div'); addRow.style.display='flex'; addRow.style.gap='8px'; addRow.style.alignItems='center'; addRow.style.flexWrap='wrap'; + const inp = document.createElement('input'); inp.type='date'; inp.className='tt-input'; + const addBtn = document.createElement('button'); addBtn.type='button'; addBtn.className='add-entity-btn'; addBtn.innerHTML = ''+(this._t('holidays.add_date')||'Add date')+''; + const dateMsg = document.createElement('span'); dateMsg.style.color='var(--error-color)'; dateMsg.style.fontSize='.85rem'; dateMsg.style.marginLeft='6px'; + addBtn.onclick = ()=>{ + const v=inp.value; if (!v) return; + // If date is already covered by a range, show hint and do nothing + try { + const groups = Array.isArray(this._config.holidays_groups) ? this._config.holidays_groups : []; + const covered = groups.some(g => Array.isArray(g?.dates) && g.dates.map(String).includes(String(v))); + if (covered) { dateMsg.textContent = this._t('holidays.date_in_range') || 'Date already covered by a range'; return; } + } catch {} + const arr = Array.isArray(this._config.holidays_dates)?[...this._config.holidays_dates]:[]; if (!arr.includes(v)) arr.push(v); + this._config.holidays_dates = arr.sort(); inp.value=''; dateMsg.textContent=''; this._emit(true); try { this._pushSettingsToStoreDebounced(); } catch {} + }; + addRow.append(inp, addBtn, dateMsg); manWrap.append(addRow); + + // Range add: from date .. to date + const rangeRow = document.createElement('div'); rangeRow.style.display='flex'; rangeRow.style.gap='8px'; rangeRow.style.alignItems='center'; rangeRow.style.flexWrap='wrap'; + const inpFrom = document.createElement('input'); inpFrom.type='date'; inpFrom.className='tt-input'; + const inpTo = document.createElement('input'); inpTo.type='date'; inpTo.className='tt-input'; + const addRangeBtn = document.createElement('button'); addRangeBtn.type='button'; addRangeBtn.className='add-entity-btn'; addRangeBtn.innerHTML = ''+(this._t('holidays.add_range')||'Add range')+''; + addRangeBtn.onclick = ()=>{ + try { + const f = String(inpFrom.value||''); const t = String(inpTo.value||''); + if (!f || !t) return; + let d1 = new Date(f+'T00:00:00'); let d2 = new Date(t+'T00:00:00'); + if (isNaN(d1.getTime()) || isNaN(d2.getTime())) return; + if (d1.getTime() > d2.getTime()) { const tmp=d1; d1=d2; d2=tmp; } + const fmt = (d)=>{ const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`; }; + const cur = Array.isArray(this._config.holidays_dates)? new Set(this._config.holidays_dates.map(String)) : new Set(); + const grpDates = []; + for (let d=new Date(d1); d.getTime()<=d2.getTime(); d.setDate(d.getDate()+1)) { const s=fmt(d); cur.add(s); grpDates.push(s); } + const next = Array.from(cur).sort(); + this._config.holidays_dates = next; + // Create UI range group tag for easier removal later + try { + const fromS = fmt(d1), toS = fmt(d2); + const gid = 'hg_'+Date.now()+'_'+Math.floor(Math.random()*1e6); + const groups = Array.isArray(this._config.holidays_groups) ? [...this._config.holidays_groups] : []; + groups.push({ id: gid, from: fromS, to: toS, dates: grpDates }); + this._config.holidays_groups = groups; + } catch {} + try { inpFrom.value=''; inpTo.value=''; } catch {} + this._emit(true); try { this._pushSettingsToStoreDebounced(); } catch {} + } catch {} + }; + rangeRow.append(inpFrom, inpTo, addRangeBtn); manWrap.append(rangeRow); + const singlesLbl = document.createElement('div'); singlesLbl.className='title'; singlesLbl.style.fontSize='.9rem'; singlesLbl.textContent = this._t('holidays.single_dates')||'Single dates'; + const chips = document.createElement('div'); chips.className='linked-chips'; + const arr = Array.isArray(this._config.holidays_dates)?this._config.holidays_dates:[]; + const groupDatesSet = (()=>{ try { const gs = Array.isArray(this._config.holidays_groups)?this._config.holidays_groups:[]; return new Set([].concat(...gs.map(g=>g.dates||[])).map(String)); } catch { return new Set(); } })(); + const singles = arr.filter(d=>!groupDatesSet.has(String(d))); + chips.innerHTML=''; + for (const d of singles){ const chip=document.createElement('span'); chip.className='pill-chip'; const txt=document.createElement('span'); txt.textContent=d; const rm=document.createElement('button'); rm.type='button'; rm.className='rm'; rm.textContent='×'; rm.addEventListener('click', ()=>{ const v=(this._config.holidays_dates||[]).filter(x=>x!==d); this._config.holidays_dates=v; this._emit(true); try { this._pushSettingsToStoreDebounced(); } catch {} }); chip.append(txt, rm); chips.append(chip); } + manWrap.append(singlesLbl, chips); + // Range group chips + const grpLbl = document.createElement('div'); grpLbl.className='title'; grpLbl.style.fontSize='.9rem'; grpLbl.textContent = this._t('holidays.ranges')||'Holiday ranges'; + const grpChips = document.createElement('div'); grpChips.className='linked-chips'; + const renderGrp = ()=>{ + try { + grpChips.innerHTML=''; + const groups = Array.isArray(this._config.holidays_groups) ? this._config.holidays_groups : []; + for (const g of groups){ + const chip=document.createElement('span'); chip.className='pill-chip'; + const txt=document.createElement('span'); txt.textContent = `${g.from} – ${g.to}`; + const rm=document.createElement('button'); rm.type='button'; rm.className='rm'; rm.textContent='×'; + rm.addEventListener('click', ()=>{ + try { + const all = Array.isArray(this._config.holidays_groups) ? this._config.holidays_groups : []; + const others = all.filter(x=>x!==g); + const othersDates = new Set([].concat(...others.map(o=>o.dates||[])).map(String)); + const curSet = new Set((this._config.holidays_dates||[]).map(String)); + for (const d of (g.dates||[])) { if (!othersDates.has(String(d))) curSet.delete(String(d)); } + this._config.holidays_dates = Array.from(curSet).sort(); + this._config.holidays_groups = others; + this._emit(true); try { this._pushSettingsToStoreDebounced(); } catch {} + // rerender both lists + chips.innerHTML=''; + for (const d of this._config.holidays_dates){ const c=document.createElement('span'); c.className='pill-chip'; const t=document.createElement('span'); t.textContent=d; const r=document.createElement('button'); r.type='button'; r.className='rm'; r.textContent='×'; r.addEventListener('click', ()=>{ const v=(this._config.holidays_dates||[]).filter(x=>x!==d); this._config.holidays_dates=v; this._emit(true); try { this._pushSettingsToStoreDebounced(); } catch {} }); c.append(t,r); chips.append(c); } + renderGrp(); + } catch {} + }); + chip.append(txt, rm); grpChips.append(chip); + } + } catch {} + }; + renderGrp(); + manWrap.append(grpLbl, grpChips); + rowMan.append(manTxt, manWrap); sourceWrap.append(rowMan); + contentWrap.append(sourceWrap); + // Inline Holidays editor (preview + edit) inside editor + const edWrap = document.createElement('div'); edWrap.className = 'holiday-ed'; edWrap.style.position = 'relative'; + // Room tabs + const tabs = document.createElement('div'); tabs.className='hol-tabs'; tabs.style.display='flex'; tabs.style.gap='8px'; tabs.style.flexWrap='wrap'; edWrap.append(tabs); + const note = document.createElement('div'); note.style.fontSize='.85rem'; note.style.color='var(--secondary-text-color)'; note.textContent = this._t('week.editor.preview_note'); edWrap.append(note); + // Scale + const scaleOuter = document.createElement('div'); scaleOuter.className='week-scale'; const scale = document.createElement('div'); scale.className='week-scale-inner'; scaleOuter.append(scale); edWrap.append(scaleOuter); + // Track + const track = document.createElement('div'); track.className='week-track'; edWrap.append(track); + // Toolbar + const tools = document.createElement('div'); tools.className='hol-toolbar'; + const addBlkBtn = document.createElement('button'); addBlkBtn.className='add-entity-btn'; addBlkBtn.innerHTML = ''+ (this._t('ui.add_block') || 'Add block') +''; + const saveBtn = document.createElement('button'); saveBtn.className='add-entity-btn'; saveBtn.innerHTML = ''+ (this._t('ui.save')||'Save') +''; + tools.append(addBlkBtn, saveBtn); edWrap.append(tools); + // Inline mini block editor panel + const mini = document.createElement('div'); mini.className='mini-ed'; mini.style.display='none'; mini.innerHTML = ` +
    +
    +
    +
    + + + +
    + `; + edWrap.append(mini); + contentWrap.append(edWrap); + + // Helpers + const toTime = (min)=>{ min=(Number(min)||0)%1440; const hh=String(Math.floor(min/60)).padStart(2,'0'); const mm=String(min%60).padStart(2,'0'); return `${hh}:${mm}`; }; + const fromTime = (s)=>{ const m=(/^(\d{1,2}):(\d{2})$/).exec(String(s||'')); if(!m) return 0; let h=Number(m[1]); let mi=Number(m[2]); h=Math.min(23,Math.max(0,h)); mi=Math.min(59,Math.max(0,mi)); return h*60+mi; }; + const toDisp = (c)=> this._toDisplayTemp(Number(c)); + const fromDisp = (v)=> this._fromDisplayTemp(Number(v)); + + // Color helpers (mirror of card logic) + const colorFor = (eid, tempC)=>{ + try { + const cr = this._config?.color_ranges || {}; + const list = this._config?.color_global ? (cr['*'] || []) : (cr[eid] || cr['*'] || []); + for (const r of (list||[])){ + const f = Number(r.from); const t = Number(r.to); + if (Number.isFinite(f) && Number.isFinite(t) && tempC >= f && tempC <= t) return r.color || null; + } + } catch {} + return null; + }; + const contrastText = (hex)=>{ + try { const h = String(hex||'').replace('#',''); if (h.length!==6) return '#000000'; const r=parseInt(h.slice(0,2),16), g=parseInt(h.slice(2,4),16), b=parseInt(h.slice(4,6),16); const yiq=(r*299+g*587+b*114)/1000; return yiq>=140 ? '#000000' : '#ffffff'; } catch { return '#000000'; } + }; + + const ensureHolDraft = async ()=>{ + if (this._holDraft) return; + let schedules = {}; let settings={}; + // Load from file storage via API + try { + if (this._config?.storage_enabled && this._apiSupported?.()) { + const resp = await this._apiFetchState?.(); + if (resp && typeof resp === 'object') { + schedules = resp.schedules || {}; + if (schedules.schedules) schedules = schedules.schedules; + settings = resp.settings || {}; + } + } + } catch {} + if (!Object.keys(schedules||{}).length){ + try { + const raw = localStorage.getItem(this._localStoreKey()) || ''; + const parsed = JSON.parse(raw||'{}'); + schedules = parsed.schedules||{}; + if (schedules.schedules) schedules = schedules.schedules; + settings = parsed.settings||{}; + } catch {} + } + const roomsMap={}; for (const eid of (this._config?.entities||[])){ const row=schedules[eid]||{}; const arr=(row.holiday && Array.isArray(row.holiday.blocks))?row.holiday.blocks:[]; roomsMap[eid]=JSON.parse(JSON.stringify(arr)); } + this._holDraft={ rooms: roomsMap }; this._holRoom = this._holRoom || (this._config?.entities||[])[0] || null; + }; + + const paintScale = ()=>{ scale.innerHTML=''; for(let i=0;i<=24;i+=2){ const t=document.createElement('div'); t.style.position='absolute'; t.style.left=(i/24*100)+'%'; t.style.top='4px'; t.style.bottom='4px'; t.style.borderLeft='1px solid var(--divider-color)'; const lab=document.createElement('div'); lab.style.position='absolute'; lab.style.top='-2px'; lab.style.transform='translate(-50%,0)'; lab.style.left='0'; lab.style.fontSize='.75rem'; lab.style.color='var(--secondary-text-color)'; lab.textContent=(i===24?'00:00':toTime(i*60)); t.append(lab); scale.append(t);} }; + + const paintTabs = ()=>{ tabs.innerHTML=''; const ents=this._config?.entities||[]; if (!this._holRoom || !ents.includes(this._holRoom)) this._holRoom=ents[0]||null; for (const eid of ents){ const b=document.createElement('button'); b.type='button'; b.className='wk-tab'+(eid===this._holRoom?' active':''); let lbl=this._config?.labels?.[eid] || (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1]||eid)); b.textContent=lbl; b.onclick=()=>{ this._holRoom=eid; paintTrack(); }; tabs.append(b);} }; + + const openMini = (blk)=>{ mini.style.display=''; const f=mini.querySelector('.mi-from'); const t=mini.querySelector('.mi-to'); const tmp=mini.querySelector('.mi-temp'); f.value=toTime(blk.startMin); t.value=toTime(blk.endMin%1440); tmp.value=String(toDisp(blk.temp)); this._holMini = { room: this._holRoom, blk: { id: blk.id, startMin: blk.startMin, endMin: blk.endMin, temp: blk.temp } }; + // Focus first field so global editor render guard keeps panel open + try { f.focus(); f.select && f.select(); } catch {} + mini.querySelector('.mi-save').onclick=()=>{ const s=fromTime(f.value); let e=fromTime(t.value); if (e===0 && t.value==='00:00') e=1440; const arr=(this._holDraft.rooms||{})[this._holRoom]||[]; let b=arr.find(x=>x.id===blk.id); if (!b) { b = { id: blk.id, startMin:s, endMin:e, temp: fromDisp(tmp.value) }; arr.push(b); } else { b.startMin=Math.max(0,Math.min(1439,s)); b.endMin=Math.max(1,Math.min(1440,e)); b.temp=fromDisp(tmp.value); } arr.sort((a,bx)=>a.startMin-bx.startMin||a.endMin-bx.endMin); (this._holDraft.rooms||{})[this._holRoom]=arr; this._holMini = null; mini.style.display='none'; paintTrack(); }; + mini.querySelector('.mi-cancel').onclick=()=>{ this._holMini = null; mini.style.display='none'; }; + mini.querySelector('.mi-del').onclick=()=>{ const arr=(this._holDraft.rooms||{})[this._holRoom]||[]; const i=arr.findIndex(x=>x.id===blk.id); if (i>=0) { arr.splice(i,1); (this._holDraft.rooms||{})[this._holRoom]=arr; paintTrack(); } this._holMini = null; mini.style.display='none'; }; + }; + + const paintTrack = async ()=>{ await ensureHolDraft(); paintTabs(); paintScale(); track.innerHTML=''; + // Ensure tooltip exists once + let tip = edWrap.querySelector('.ed-hol-tooltip'); + if (!tip) { tip = document.createElement('div'); tip.className='wk-tooltip ed-hol-tooltip'; tip.style.display='none'; edWrap.append(tip); } + const arr=(this._holDraft.rooms||{})[this._holRoom]||[]; + for (const b of arr){ const div=document.createElement('div'); div.className='block'; div.style.left=(b.startMin/1440*100)+'%'; div.style.width=((b.endMin-b.startMin)/1440*100)+'%'; + // Apply heat color if configured + try { const clr = colorFor(this._holRoom, Number(b.temp)); if (clr) { div.style.background = clr; div.style.borderColor = clr; const txt=contrastText(clr); if (txt) div.style.color = txt; } } catch {} + const showTip = ()=>{ + const txt = `${toTime(b.startMin)} - ${toTime(b.endMin)} • ${toDisp(b.temp)} ${this._isF()? '°F':'°C'}`; + tip.textContent = txt; tip.style.display = ''; + const box = div.getBoundingClientRect(); + const cont = edWrap.getBoundingClientRect(); + const tbox = track.getBoundingClientRect(); + // Base center: visual center of the hovered block element + let center = box.left + box.width/2 - cont.left; + const tipW = tip.offsetWidth || 0; const tipH = tip.offsetHeight || 0; const pad = 8; + const minC = (tbox.left - cont.left) + pad + tipW/2; + const maxC = (tbox.right - cont.left) - pad - tipW/2; + center = Math.max(minC, Math.min(maxC, center)); + // Prefer above the block; if not enough space, flip below + const yAbove = box.top - cont.top - 8; // anchor a bit above block + const yBelow = box.bottom - cont.top + 8; // a bit below block + if (yAbove - tipH < pad) { + tip.style.top = Math.min(cont.height - pad, yBelow) + 'px'; + tip.style.transform = 'translate(-50%,0)'; + } else { + // Place right above the block top edge + tip.style.top = Math.max(pad, box.top - cont.top + 6) + 'px'; + tip.style.transform = 'translate(-50%,-100%)'; + } + tip.style.left = center + 'px'; + }; + const hideTip = ()=>{ const delay = (window.matchMedia && window.matchMedia('(pointer:coarse)').matches) ? 3000 : 120; if (tip._hideTimer) try{ clearTimeout(tip._hideTimer);}catch{}; tip._hideTimer = setTimeout(()=>{ try { tip.style.display='none'; tip._hideTimer = null; } catch{} }, delay); }; + div.addEventListener('mouseenter', showTip); div.addEventListener('mouseleave', hideTip); + div.ondblclick=()=>openMini(b); track.append(div); + } + // Re-open mini editor if it was open before a re-render + if (this._holMini && this._holMini.room === this._holRoom) { + openMini(this._holMini.blk); + } + }; + + addBlkBtn.onclick = async ()=>{ await ensureHolDraft(); const now=new Date(); const m=now.getHours()*60+now.getMinutes(); const start=Math.max(0,Math.min(1380, Math.round(m))); const end=Math.min(1440,start+60); const blk={ id: Math.random().toString(36).slice(2,9), startMin:start, endMin:end, temp: fromDisp(this._config.default_temp||20) }; openMini(blk); }; + + saveBtn.onclick = async ()=>{ + await ensureHolDraft(); + let schedules={}; + let settings={}; + // Load current schedules from file storage via API + try { + if (this._config?.storage_enabled && this._apiSupported?.()) { + const resp = await this._apiFetchState?.(); + if (resp && typeof resp === 'object') { + schedules = resp.schedules || {}; + if (schedules.schedules) schedules = schedules.schedules; + settings = resp.settings || {}; + } + } + } catch {} + if (!Object.keys(schedules||{}).length){ + try { + const raw=localStorage.getItem(this._localStoreKey())||''; + const parsed=JSON.parse(raw||'{}'); + schedules=parsed.schedules||{}; + if (schedules.schedules) schedules=schedules.schedules; + settings=parsed.settings||{}; + } catch {} + } + for (const eid of (this._config?.entities||[])){ + const row=schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + const arr=(this._holDraft.rooms||{})[eid]||[]; + row.holiday={ blocks: JSON.parse(JSON.stringify(arr)) }; + schedules[eid]=row; + } + const wantF = this._isF(); + const schOut = wantF ? this._convertSchedulesTemps(schedules,'F') : schedules; + let settingsOut = { ...settings, holidays_enabled: !!this._config.holidays_enabled, holidays_source: this._config.holidays_source || 'calendar', holidays_entity: this._config.holidays_entity || '', holidays_dates: Array.isArray(this._config.holidays_dates)?this._config.holidays_dates:[], holidays_groups: Array.isArray(this._config.holidays_groups)?this._config.holidays_groups:[] }; + // Preserve backend holidays_enabled unless explicitly toggled in this session + try { + if (!this._holidaysToggleTouched) { + const st = await this._apiFetchState?.(); + const base = st?.settings; + if (typeof base?.holidays_enabled === 'boolean') settingsOut.holidays_enabled = base.holidays_enabled; else delete settingsOut.holidays_enabled; + } + } catch {} + try { + if (wantF) { + const toStore=(n)=>{ const v=Number(n); return Number.isFinite(v)? this._cToF(v): n; }; + if (settingsOut.default_temp!==undefined) settingsOut.default_temp = toStore(settingsOut.default_temp); + if (settingsOut.min_temp!==undefined) settingsOut.min_temp = toStore(settingsOut.min_temp); + if (settingsOut.max_temp!==undefined) settingsOut.max_temp = toStore(settingsOut.max_temp); + if (settingsOut.away && typeof settingsOut.away==='object' && settingsOut.away.target_c!==undefined) { + settingsOut.away = { ...settingsOut.away, target_c: toStore(settingsOut.away.target_c) }; + } + } + } catch {} + try { localStorage.setItem(this._localStoreKey(), JSON.stringify({ schedules: schOut, settings: settingsOut })); } catch {} + // Save to file storage via service + try { + if (this._config.storage_enabled) { + const p = { schedules: schOut, settings: settingsOut }; + if (this._config?.instance_enabled) { p.instance_id = this._instanceId(); p.activate = true; } + await this._hass.callService('thermostat_timeline','set_store', p); + } + } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + }; + + // Initial render + (async()=>{ await ensureHolDraft(); await paintTrack(); })(); + } + } catch {} + + // Apply tab visibility/class based on current state + try { + const sBtn = this.shadowRoot.querySelector('.tab-settings-btn'); + const bBtn = this.shadowRoot.querySelector('.tab-backup-btn'); + + const sSec = this.shadowRoot.querySelector('.tab-settings'); + const bSec = this.shadowRoot.querySelector('.tab-backup'); + + const setActive = (btn, on) => { + if (!btn) return; + try { btn.classList.toggle('active', !!on); } catch {} + try { btn.setAttribute('aria-selected', on ? 'true' : 'false'); } catch {} + }; + const setShow = (sec, on) => { + if (!sec) return; + try { sec.style.display = on ? '' : 'none'; } catch {} + }; + + const showBackup = !!this._config.storage_enabled; + if (!showBackup && this._activeTab === 'backup') this._activeTab = 'settings'; + if (this._activeTab === 'sync') this._activeTab = 'settings'; + if (!['settings', 'backup'].includes(this._activeTab)) this._activeTab = 'settings'; + + const isSettings = this._activeTab === 'settings'; + const isBackup = this._activeTab === 'backup'; + + setActive(sBtn, isSettings); + if (bBtn) { + setActive(bBtn, isBackup); + try { bBtn.style.display = showBackup ? '' : 'none'; } catch {} + } + + setShow(sSec, isSettings); + setShow(bSec, showBackup && isBackup); + } catch {} + + } + + // Pointer-driven Drag & Drop (works reliably inside HA editors) + _startPointerDnd(line, idx){ + const wrap = line.parentElement; + if (!wrap) return; + if (this._pointerDndActive) return; + this._pointerDndActive = true; + // Avoid any re-render while dragging which could disrupt DOM + try { this._suspendRender = true; } catch {} + + const rect = line.getBoundingClientRect(); + const ghost = line.cloneNode(true); + ghost.classList.add('drag-ghost'); + ghost.style.width = rect.width + 'px'; + ghost.style.height = rect.height + 'px'; + ghost.style.top = rect.top + 'px'; + ghost.style.left = rect.left + 'px'; + + const placeholder = document.createElement('div'); + placeholder.className = 'placeholder'; + placeholder.style.height = rect.height + 'px'; + + // place placeholder where line was, and hide line + wrap.insertBefore(placeholder, line); + line.style.display = 'none'; + document.body.appendChild(ghost); + + const readClient = (e)=>{ + if (e.touches && e.touches[0]) return { x: e.touches[0].clientX, y: e.touches[0].clientY }; + return { x: e.clientX, y: e.clientY }; + }; + + const onMove = (ev)=>{ + try { ev.preventDefault(); } catch {} + const { y, x } = readClient(ev); + ghost.style.top = (y - rect.height/2) + 'px'; + + // find element under pointer within this shadowRoot + let el = this.shadowRoot && this.shadowRoot.elementFromPoint(x, y); + // climb to row in the same wrap + while (el && el !== wrap && !el.classList?.contains('inline')) el = el.parentElement; + if (!el || el===placeholder || el===wrap) return; + const targetRect = el.getBoundingClientRect(); + const before = (y - targetRect.top) < targetRect.height/2; + if (before) wrap.insertBefore(placeholder, el); + else wrap.insertBefore(placeholder, el.nextSibling); + }; + + const endDrag = ()=>{ + window.removeEventListener('mousemove', onMove, true); + window.removeEventListener('mouseup', onUp, true); + window.removeEventListener('touchmove', onMove, true); + window.removeEventListener('touchend', onUp, true); + ghost.remove(); + // compute new order + const from = idx; + const to = Array.prototype.indexOf.call(wrap.children, placeholder); + const arr = [...(this._config.entities||[])]; + const modes = Array.isArray(this._config.room_use_input_number) ? [...this._config.room_use_input_number] : []; + while (modes.length < arr.length) modes.push(false); + const [item] = arr.splice(from,1); + const [mItem] = modes.splice(from,1); + const correctedTo = Math.max(0, Math.min(to, arr.length)); + arr.splice(correctedTo, 0, item); + modes.splice(correctedTo, 0, mItem); + placeholder.remove(); + line.style.display = ''; + this._pointerDndActive = false; + this._config.entities = arr; + this._config.room_use_input_number = modes; + try { this._suspendRender = false; } catch {} + this._emit(true); + }; + + const onUp = (ev)=>{ try { ev.preventDefault?.(); } catch {} endDrag(); }; + + window.addEventListener('mousemove', onMove, true); + window.addEventListener('mouseup', onUp, true); + window.addEventListener('touchmove', onMove, true); + window.addEventListener('touchend', onUp, true); + } + + // Async creator used with lit's `until` to delay row insertion until HA elements are ready + async _createRoomLine(ent, idx){ + try { await customElements.whenDefined('ha-entity-picker'); } catch {} + try { await customElements.whenDefined('ha-textfield'); } catch {} + const line = this._makeEntityRow(idx); + // Restore open state for this row + try { + const key = ent || `#idx:${idx}`; + if (this._openRows.has(key)) line.classList.add('open'); + } catch {} + return line; + } + + _makeEntityRow(idx){ + const line = document.createElement("div"); line.className = "inline"; + + // Summary bar (collapsed row header) + const summary = document.createElement('div'); + summary.className = 'summary'; + const sumLeft = document.createElement('div'); sumLeft.className = 'summary-left'; + const expander = document.createElement('button'); expander.className = 'expander'; expander.innerHTML = ``; sumLeft.append(expander); + // Title and subtitle container + const textWrap = document.createElement('div'); textWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); titleSpan.className = 'summary-title'; titleSpan.textContent = this._t('editor.entity_placeholder'); + const subSpan = document.createElement('span'); subSpan.className = 'summary-sub'; subSpan.textContent = ''; + textWrap.append(titleSpan, subSpan); + sumLeft.append(textWrap); + summary.append(sumLeft); + const actions = document.createElement("div"); actions.className = "actions"; + const handle = document.createElement("button"); handle.className = "drag-handle"; handle.setAttribute("draggable","true"); handle.setAttribute("title", this._t('editor.drag_reorder')); handle.innerHTML = ``; actions.append(handle); + // Start pointer-driven drag on mousedown (more reliable inside HA editors than native DnD) + handle.addEventListener('mousedown', (ev)=>{ + ev.preventDefault(); + try { + const wrap = line.parentElement; + const curIdx = wrap ? Array.prototype.indexOf.call(wrap.children, line) : idx; + this._startPointerDnd(line, curIdx); + } catch { this._startPointerDnd(line, idx); } + }); + // Touch support + handle.addEventListener('touchstart', (ev)=>{ + try { ev.preventDefault(); } catch {} + try { + const wrap = line.parentElement; + const curIdx = wrap ? Array.prototype.indexOf.call(wrap.children, line) : idx; + this._startPointerDnd(line, curIdx); + } catch { this._startPointerDnd(line, idx); } + }, { passive: false }); + const del = document.createElement("button"); del.className = "remove-btn"; del.innerHTML = `${this._t('editor.remove')}`; actions.append(del); + summary.append(actions); + + // Details (expanded content) + const details = document.createElement('div'); details.className = 'details'; + + // Left column: entity picker + optional label + merge UI + const left = document.createElement("div"); left.style.display = "grid"; left.style.gap = "6px"; + + const useInput = this._roomUsesInputNumber(idx); + + // Per-room mode toggle + const modeRow = document.createElement('div'); + modeRow.style.display = 'flex'; + modeRow.style.alignItems = 'center'; + modeRow.style.justifyContent = 'space-between'; + modeRow.style.gap = '10px'; + const modeText = document.createElement('div'); + modeText.style.display = 'grid'; + modeText.style.gap = '2px'; + const modeTitle = document.createElement('div'); + modeTitle.style.fontWeight = '600'; + modeTitle.textContent = this._t('editor.room_mode.title') || 'Use input_number instead of climate'; + const modeDesc = document.createElement('div'); + modeDesc.style.fontSize = '.85rem'; + modeDesc.style.color = 'var(--secondary-text-color)'; + modeDesc.textContent = this._t('editor.room_mode.desc') || ''; + modeText.append(modeTitle, modeDesc); + const modeSwitch = document.createElement('ha-switch'); + modeSwitch.className = 'room-mode-switch'; + modeSwitch.checked = !!useInput; + modeSwitch.addEventListener('change', (e)=>{ this._setRoomUseInputNumber(idx, !!e.target.checked); }); + modeRow.append(modeText, modeSwitch); + + const pick = document.createElement("tt-entity-picker"); + try { pick.style.display = 'block'; pick.style.minHeight = '48px'; } catch {} + try { pick.hass = this._hass; } catch {} + try { pick.value = this._config.entities[idx] || ""; } catch {} + pick.setAttribute("include-domains", useInput ? '["input_number"]' : '["climate"]'); + pick.setAttribute("label", this._t("editor.entity_placeholder")); + // Filter out already-selected entities in other pickers (but allow the current one) + pick.entityFilter = (st) => { + try { + const id = st?.entity_id || st?.entityId || st?.entityID || ""; + if (!id) return false; + const chosenPrim = (this._config.entities || []).filter(Boolean); + const allLinked = Object.values(this._config.merges || {}).flat(); + const blocked = new Set([...chosenPrim, ...allLinked]); + const current = this._config.entities[idx]; + return !blocked.has(id) || id === current; + } catch { return true; } + }; + + const nameInp = document.createElement("input"); + nameInp.type = 'text'; + nameInp.className = "label-input tt-input"; + nameInp.setAttribute("placeholder", this._t('editor.display_name_optional')); + try { + const curEid = this._config.entities[idx]; + const curLbl = (this._config.labels || {})[curEid] || ''; + nameInp.value = curLbl; + } catch {} + nameInp.addEventListener("change", (e)=>{ + const eid = this._config.entities[idx]; + if (!eid) return; + const labels = { ...(this._config.labels || {}) }; + const val = (e.target.value || "").trim(); + if (val) labels[eid] = val; else delete labels[eid]; + this._config.labels = labels; + // Repaint so the summary header updates immediately with the custom name + this._emit(true); + // Persist label to local browser storage so live preview doesn’t get + // overwritten by periodic hass() reloads when shared storage is OFF + try { + const raw = localStorage.getItem(this._localStoreKey()) || ''; + let parsed = {}; + try { parsed = JSON.parse(raw || '{}'); } catch {} + const schedules = parsed.schedules || {}; + const prevSettings = parsed.settings || {}; + const settings = { ...prevSettings, labels: { ...(prevSettings.labels||{}), ...labels } }; + localStorage.setItem(this._localStoreKey(), JSON.stringify({ schedules, settings })); + } catch {} + // Nudge any live preview card instances to refresh immediately + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + try { document.querySelectorAll('thermostat-timeline-card').forEach(el=>el?.devRefreshFromEditor?.()); } catch {} + try { this._pushSettingsToStoreDebounced(); } catch {} + }); + + // Per-room: optional turn_on behavior + const turnOnWrap = document.createElement('div'); + turnOnWrap.style.display = 'grid'; + turnOnWrap.style.gap = '6px'; + const turnOnRow = document.createElement('div'); + turnOnRow.style.display = 'flex'; + turnOnRow.style.alignItems = 'center'; + turnOnRow.style.justifyContent = 'space-between'; + turnOnRow.style.gap = '10px'; + const turnOnText = document.createElement('div'); + turnOnText.style.display = 'grid'; + turnOnText.style.gap = '2px'; + const turnOnTitle = document.createElement('div'); + turnOnTitle.style.fontWeight = '600'; + turnOnTitle.textContent = this._t('editor.turn_on.title') || 'Send turn_on command'; + const turnOnDesc = document.createElement('div'); + turnOnDesc.style.fontSize = '.85rem'; + turnOnDesc.style.color = 'var(--secondary-text-color)'; + turnOnDesc.textContent = this._t('editor.turn_on.desc') || ''; + turnOnText.append(turnOnTitle, turnOnDesc); + const turnOnSwitch = document.createElement('ha-switch'); + turnOnSwitch.className = 'turn-on-switch'; + // Dropdown (only visible when enabled) + const turnOnOrderRow = document.createElement('div'); + turnOnOrderRow.style.display = 'grid'; + turnOnOrderRow.style.gap = '2px'; + const orderLbl = document.createElement('div'); + orderLbl.className = 'slabel'; + orderLbl.textContent = this._t('editor.turn_on.order') || 'Order'; + const orderSel = document.createElement('select'); + orderSel.className = 'turn-on-order'; + try { + orderSel.style.height = '32px'; + orderSel.style.border = '1px solid var(--divider-color)'; + orderSel.style.borderRadius = '8px'; + orderSel.style.background = 'var(--card-background-color)'; + orderSel.style.color = 'var(--primary-text-color)'; + orderSel.style.padding = '0 10px'; + orderSel.style.width = '100%'; + orderSel.style.maxWidth = '440px'; + } catch {} + const optBefore = document.createElement('option'); + optBefore.value = 'before'; + optBefore.textContent = this._t('editor.turn_on.order.before') || 'Before set_temperature'; + const optAfter = document.createElement('option'); + optAfter.value = 'after'; + optAfter.textContent = this._t('editor.turn_on.order.after') || 'After set_temperature'; + orderSel.append(optBefore, optAfter); + + const readTurnOnCfg = ()=>{ + try { + const eid = this._config.entities[idx]; + const map = (this._config.turn_on && typeof this._config.turn_on === 'object') ? this._config.turn_on : {}; + const cfg = (eid && map[eid] && typeof map[eid] === 'object') ? map[eid] : {}; + const enabled = !!cfg.enabled; + const order = String(cfg.order || 'before'); + return { eid, enabled, order: (order === 'after' ? 'after' : 'before') }; + } catch { return { eid: this._config.entities[idx], enabled: false, order: 'before' }; } + }; + const writeTurnOnCfg = (eid, patch)=>{ + try { + if (!eid) return; + const prev = (this._config.turn_on && typeof this._config.turn_on === 'object') ? this._config.turn_on : {}; + const cur = (prev[eid] && typeof prev[eid] === 'object') ? prev[eid] : {}; + const next = { ...prev, [eid]: { ...cur, ...patch } }; + // If disabling, keep order but store enabled=false (so it can be re-enabled without losing selection) + this._config.turn_on = next; + } catch {} + // Persist to local browser storage for immediate live preview when Sync is OFF + try { + const raw = localStorage.getItem(this._localStoreKey()) || ''; + let parsed = {}; + try { parsed = JSON.parse(raw || '{}'); } catch { parsed = {}; } + const schedules = parsed.schedules || {}; + const prevSettings = parsed.settings || {}; + const settings = { ...prevSettings, turn_on: { ...(prevSettings.turn_on||{}), ...(this._config.turn_on||{}) } }; + localStorage.setItem(this._localStoreKey(), JSON.stringify({ schedules, settings })); + } catch {} + try { this._pushSettingsToStoreDebounced(); } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + try { document.querySelectorAll('thermostat-timeline-card').forEach(el=>el?.devRefreshFromEditor?.()); } catch {} + }; + + const applyTurnOnUi = ()=>{ + const { enabled, order } = readTurnOnCfg(); + try { turnOnSwitch.checked = !!enabled; } catch {} + try { orderSel.value = (order === 'after' ? 'after' : 'before'); } catch {} + try { turnOnOrderRow.style.display = enabled ? '' : 'none'; } catch {} + }; + + turnOnSwitch.addEventListener('change', (e)=>{ + try { + const { eid, order } = readTurnOnCfg(); + const enabled = !!e.target.checked; + writeTurnOnCfg(eid, { enabled, order }); + applyTurnOnUi(); + this._emit(true); + } catch {} + }); + orderSel.addEventListener('change', ()=>{ + try { + const { eid, enabled } = readTurnOnCfg(); + const order = String(orderSel.value || 'before'); + writeTurnOnCfg(eid, { enabled: !!enabled, order: (order === 'after' ? 'after' : 'before') }); + this._emit(true); + } catch {} + }); + // Initial values + applyTurnOnUi(); + turnOnRow.append(turnOnText, turnOnSwitch); + turnOnOrderRow.append(orderLbl, orderSel); + turnOnWrap.append(turnOnRow, turnOnOrderRow); + + // Merge/Link section + const linkWrap = document.createElement('div'); + linkWrap.style.display = 'grid'; + linkWrap.style.gap = '6px'; + // Visible label for merge/link section + const linkLabel = document.createElement('div'); + linkLabel.className = 'label'; + linkLabel.textContent = this._t('editor.merge_label') || 'Flet med (tilføj ekstra termostat)'; + const linkPicker = document.createElement('tt-entity-picker'); + try { linkPicker.style.display = 'block'; linkPicker.style.minHeight = '48px'; } catch {} + linkPicker.className = 'merge-picker'; + linkPicker.setAttribute('include-domains', '["climate"]'); + linkPicker.setAttribute('label', this._t('editor.merge_label') || 'Flet med (tilføj ekstra termostat)'); + linkPicker.hass = this._hass; + const linkFilter = (st) => { + try { + const id = st?.entity_id || st?.entityId || st?.entityID || ''; + if (!id) return false; + const prims = (this._config.entities || []).filter(Boolean); + const allLinked = Object.values(this._config.merges || {}).flat(); + const used = new Set([...prims, ...allLinked]); + // Allow selecting if not already used anywhere and not equal to current primary + const cur = this._config.entities[idx]; + return !used.has(id) && id !== cur; + } catch { return true; } + }; + linkPicker.entityFilter = linkFilter; + linkPicker.addEventListener('value-changed', (e)=>{ + const toAdd = e.detail.value; + linkPicker.value = ''; + const primary = this._config.entities[idx]; + if (!primary || !toAdd) return; + const merges = { ...(this._config.merges || {}) }; + const arr = Array.from(new Set([...(merges[primary] || []), toAdd])); + merges[primary] = arr; + this._config.merges = merges; + this._emit(true); + try { this._pushSettingsToStoreDebounced(); } catch {} + }); + + const chips = document.createElement('div'); + chips.className = 'linked-chips'; + + left.append(pick, nameInp, turnOnWrap, linkWrap, chips); + // Insert mode toggle at the top + try { left.insertBefore(modeRow, left.firstChild); } catch { left.prepend(modeRow); } + // Hide merge UI when controlling input_number + try { if (useInput) { linkWrap.style.display = 'none'; chips.style.display = 'none'; turnOnWrap.style.display = 'none'; } } catch {} + details.append(left); + + // Expand/collapse + const toggle = ()=>{ + line.classList.toggle('open'); + try { + const eid = this._config.entities[idx] || `#idx:${idx}`; + if (line.classList.contains('open')) this._openRows.add(eid); + else this._openRows.delete(eid); + } catch {} + // When a row is opened, force-hydrate the pickers so they render immediately + try { + if (line.classList.contains('open')) { + // Primary picker + try { + pick.hass = this._hass; + pick.style.display = 'block'; + pick.style.minHeight = '48px'; + if (typeof pick.requestUpdate === 'function') pick.requestUpdate(); + // Nudge again after layout for stubborn cases + setTimeout(()=>{ try { if (typeof pick.requestUpdate === 'function') pick.requestUpdate(); } catch {} }, 50); + } catch {} + // Merge picker + try { + linkPicker.hass = this._hass; + linkPicker.style.display = 'block'; + linkPicker.style.minHeight = '48px'; + if (typeof linkPicker.requestUpdate === 'function') linkPicker.requestUpdate(); + setTimeout(()=>{ try { if (typeof linkPicker.requestUpdate === 'function') linkPicker.requestUpdate(); } catch {} }, 50); + } catch {} + } + } catch {} + }; + expander.addEventListener('click', (e)=>{ e.stopPropagation(); toggle(); }); + sumLeft.addEventListener('click', (e)=>{ if (e.target === expander) return; toggle(); }); + + line.append(summary, details); + + // populate chips on render + const refreshChips = () => { + try { + linkPicker.hass = this._hass; + const primary = this._config.entities[idx]; + const links = (this._config.merges?.[primary] || []); + chips.innerHTML = ''; + for (const l of links){ + const chip = document.createElement('span'); + chip.className = 'pill-chip'; + const txt = document.createElement('span'); + txt.textContent = this._hass?.states?.[l]?.attributes?.friendly_name || (l.split('.')[1]||l); + const rm = document.createElement('button'); + rm.type = 'button'; rm.className = 'rm'; rm.textContent = '×'; + rm.setAttribute('aria-label', this._t('editor.remove')); + rm.addEventListener('click', ()=>{ + const merges = { ...(this._config.merges || {}) }; + merges[primary] = (merges[primary]||[]).filter(x=>x!==l); + if (!merges[primary]?.length) delete merges[primary]; + this._config.merges = merges; + this._emit(true); + try { this._pushSettingsToStoreDebounced(); } catch {} + }); + chip.append(txt, rm); + chips.append(chip); + } + // Update subtitle after chip changes + try { + const subEl = line.querySelector('.summary-sub'); + const linkedNames = links.map(id => this._hass?.states?.[id]?.attributes?.friendly_name || (id.split('.')[1]||id)); + const parts = []; + const eid = primary; + if (eid) parts.push(eid); + if (linkedNames.length) parts.push(`${this._t('editor.merged_with')}: ${linkedNames.join(', ')}`); + if (subEl) subEl.textContent = parts.join(' • '); + } catch {} + linkWrap.innerHTML = ''; + linkWrap.append(linkLabel, linkPicker); + } catch {} + }; + + // Initial render of merge section + if (!useInput) refreshChips(); + + // Remove this room/entity + del.addEventListener('click', () => { + try { + const arr = [...(this._config.entities || [])]; + const modes = Array.isArray(this._config.room_use_input_number) ? [...this._config.room_use_input_number] : []; + const primary = arr[idx]; + // Remove the primary from entities + arr.splice(idx, 1); + try { if (modes.length) modes.splice(idx, 1); } catch {} + // Clean up labels + const labels = { ...(this._config.labels || {}) }; + if (primary && labels[primary]) delete labels[primary]; + // Clean up turn_on + const turnOn = { ...(this._config.turn_on || {}) }; + if (primary && turnOn[primary]) delete turnOn[primary]; + // Clean up merges: remove mapping for this primary and remove it from other arrays + const merges = { ...(this._config.merges || {}) }; + if (primary && merges[primary]) delete merges[primary]; + for (const k of Object.keys(merges)) { + merges[k] = (merges[k] || []).filter((e) => e !== primary); + if (!merges[k].length) delete merges[k]; + } + this._config.entities = arr; + this._config.room_use_input_number = modes; + this._config.labels = labels; + this._config.merges = merges; + this._config.turn_on = turnOn; + this._emit(true); + } catch (e) {} + }); + + // When picker changes primary value, migrate merges + pick.addEventListener("value-changed", (e)=>{ + const oldPrimary = this._config.entities[idx]; + const newPrimary = e.detail.value; + + // Auto-sync mode based on chosen domain (defensive) + try { + const modes = Array.isArray(this._config.room_use_input_number) ? [...this._config.room_use_input_number] : []; + const ents = [...(this._config.entities || [])]; + while (modes.length < ents.length) modes.push(false); + if (typeof newPrimary === 'string') { + if (newPrimary.startsWith('input_number.')) modes[idx] = true; + else if (newPrimary.startsWith('climate.')) modes[idx] = false; + } + this._config.room_use_input_number = modes; + } catch {} + if (oldPrimary && newPrimary && oldPrimary !== newPrimary){ + const merges = { ...(this._config.merges || {}) }; + if (merges[oldPrimary] && !merges[newPrimary]) { merges[newPrimary] = merges[oldPrimary]; delete merges[oldPrimary]; } + else if (merges[oldPrimary] && merges[newPrimary]) { merges[newPrimary] = Array.from(new Set([...(merges[newPrimary]||[]), ...(merges[oldPrimary]||[])])); delete merges[oldPrimary]; } + this._config.merges = merges; + // migrate temp sensor mapping to new primary key + try { + const sensors = { ...(this._config.temp_sensors || {}) }; + if (sensors[oldPrimary] && !sensors[newPrimary]) sensors[newPrimary] = sensors[oldPrimary]; + if (sensors[oldPrimary]) delete sensors[oldPrimary]; + this._config.temp_sensors = sensors; + } catch {} + // migrate per-room turn_on mapping to new primary key + try { + const to = { ...(this._config.turn_on || {}) }; + if (to[oldPrimary] && !to[newPrimary]) to[newPrimary] = to[oldPrimary]; + if (to[oldPrimary]) delete to[oldPrimary]; + this._config.turn_on = to; + } catch {} + } + // Avoid mutating a potentially frozen array (some browsers/HA paths freeze config) + try { + const ents = [...(this._config.entities || [])]; + ents[idx] = newPrimary; + this._config.entities = ents; + } catch { + // Fallback (should not happen, but keep previous behavior if cloning fails) + try { this._config.entities[idx] = newPrimary; } catch {} + } + // Move open-state key from old -> new entity id + try { + const oldKey = oldPrimary || `#idx:${idx}`; + const newKey = newPrimary || `#idx:${idx}`; + if (this._openRows.has(oldKey)) { this._openRows.delete(oldKey); this._openRows.add(newKey); } + } catch {} + try { nameInp.value = (this._config.labels || {})[newPrimary] || ""; if (nameInp && typeof nameInp.requestUpdate === 'function') nameInp.requestUpdate(); } catch {} + if (this._openCount > 0) this._pendingEmit = true; else this._emit(true); + try { this._pushSettingsToStoreDebounced(); } catch {} + if (!this._roomUsesInputNumber(idx)) refreshChips(); + // refresh per-room turn_on UI after primary change + try { applyTurnOnUi(); } catch {} + // Also update subtitle on primary change + try { + const subEl = line.querySelector('.summary-sub'); + const eid = this._config.entities[idx]; + const linkedNames = (this._config.merges?.[eid] || []).map(id => this._hass?.states?.[id]?.attributes?.friendly_name || (id.split('.')[1]||id)); + const parts = []; + if (eid) parts.push(eid); + if (linkedNames.length) parts.push(`${this._t('editor.merged_with')}: ${linkedNames.join(', ')}`); + if (subEl) subEl.textContent = parts.join(' • '); + } catch {} + }); + + return line; + } + + // (Removed unused pointer-driven drag & drop prototype) + + _addEntity(){ + this._config.entities = [...(this._config.entities||[]), ""]; + try { + const modes = Array.isArray(this._config.room_use_input_number) ? [...this._config.room_use_input_number] : []; + modes.push(false); + this._config.room_use_input_number = modes; + } catch {} + this._emit(true); + } + _upd(key,val){ this._config[key] = val; this._emit(); } + _emit(repaint=false){ + if (this._openCount > 0) { this._pendingEmit = true; return; } + // Emit a fresh cloned config to ensure HA editor detects changes immediately + try { this._config.respect_storage_weekdays = false; } catch {} + let cfg; + try { cfg = JSON.parse(JSON.stringify(this._config)); } + catch { cfg = { ...this._config }; } + // These settings are now edited in the in-card popup + stored in shared/local storage, + // so they should not be emitted into YAML config. + try { + for (const k of ['row_height','default_temp','min_temp','max_temp','auto_apply','apply_on_edit','apply_on_default_change','per_room_defaults','show_pause_button','show_room_temp','pause_sensor_enabled','pause_sensor_entity','presence_sensor_enabled','presence_sensors','presence_sensor_temps','presence_sensor_delays','presence_sensor_delay_units','time_12h','time_source','temp_unit','profiles_enabled','weekdays_enabled','weekdays_view','weekdays_selected_room','boiler_enabled','boiler_switch','boiler_switch_domain','boiler_temp_sensor','boiler_min_temp','boiler_max_temp','open_window','color_ranges','color_global','respect_storage_weekdays']) { + if (k in cfg) delete cfg[k]; + } + } catch {} + // Always keep instance config in YAML (per-card) + try { + cfg.instance_enabled = !!this._config?.instance_enabled; + cfg.instance_id = String(this._config?.instance_id || ''); + cfg.instance_uid = String(this._config?.instance_uid || ''); + } catch {} + this.dispatchEvent(new CustomEvent("config-changed", { detail:{ config: cfg }, bubbles:true, composed:true })); + if (repaint) this._render(); + } + + _t(k){ return ttLocalize(k, this._hass || this._lang || 'en'); } + + _applyEditorI18n(){ + try { + + const t = (k)=> this._t(k); + const root = this.shadowRoot; + if (!root) return; + // Title row + const titleRow = root.querySelector('.row .title')?.closest('.row'); + const titleLabel = titleRow?.querySelector('.label') || root.querySelector('.row .label'); + if (titleLabel) titleLabel.textContent = t('editor.title_label'); + const titleField = root.querySelector('ha-textfield.title'); + if (titleField) { + titleField.setAttribute('label', t('editor.title_label')); + // Show localized preset when empty, and keep it updated with language + try { titleField.setAttribute('placeholder', this._t('card.title_default')); } catch {} + } + + // Storage entity (use the .storage picker to find its label robustly) + const storagePicker = root.querySelector('.storage'); + if (storagePicker) { + const storageLabel = storagePicker.closest('.row')?.querySelector('.label'); + // Hide the label text for the storage section + if (storageLabel) storageLabel.textContent = ''; + // toggle texts + const stTitle = root.querySelector('.store-title'); + const stDesc = root.querySelector('.store-desc'); + // Title + info icon (click to open popover) + const stTitleText = root.querySelector('.store-title .store-title-text'); + if (stTitleText) stTitleText.textContent = t('editor.store_enable.title'); + if (stDesc) { + const shortTxt = this._t('editor.store_enable.short'); + const icon = root.querySelector('.info-icon'); + const textSpan = root.querySelector('.store-desc-text'); + if (textSpan) textSpan.textContent = shortTxt !== 'editor.store_enable.short' ? shortTxt : (this._t('editor.store_enable.title') || ''); + + // Build popover once + let pop = root.querySelector('.info-pop'); + if (!pop) { + pop = document.createElement('div'); + pop.className = 'info-pop hidden'; + pop.setAttribute('role','dialog'); + root.append(pop); + } + const openPop = () => { + const desc = this._t('editor.store_enable.desc'); + pop.textContent = desc; + const r = icon.getBoundingClientRect(); + const maxW = Math.min(520, Math.max(240, Math.floor(window.innerWidth*0.9))); + pop.style.maxWidth = maxW + 'px'; + let left = Math.max(8, Math.min(window.innerWidth - maxW - 8, r.left)); + let top = r.bottom + 8; + pop.style.left = left + 'px'; + pop.style.top = top + 'px'; + pop.classList.remove('hidden'); + }; + const closePop = () => { if (pop) pop.classList.add('hidden'); }; + + // Bind click/tap once + if (icon && !icon.dataset.bound) { + icon.dataset.bound = '1'; + icon.addEventListener('click', (e) => { e.stopPropagation(); if (pop.classList.contains('hidden')) openPop(); else closePop(); }); + icon.addEventListener('touchend', (e) => { try { e.preventDefault(); } catch{}; e.stopPropagation(); if (pop.classList.contains('hidden')) openPop(); else closePop(); }, { passive:false }); + // Close on outside click or ESC + const onOutside = (ev)=>{ + const path = ev.composedPath ? ev.composedPath() : []; + if (path.includes(icon) || path.includes(pop)) return; closePop(); + }; + window.addEventListener('click', onOutside); + window.addEventListener('keydown', (ev)=>{ if (ev.key==='Escape') closePop(); }); + } + } + // Check that the thermostat_timeline integration/service exists in Home Assistant + try { + const hasService = !!(this._hass && this._hass.services && this._hass.services['thermostat_timeline']); + let warn = root.querySelector('.store-missing'); + const warnText = this._t ? this._t('editor.store_missing') : null; + const msg = warnText && warnText !== 'editor.store_missing' + ? warnText + : 'Integration "Thermostat Pro Timeline Sync" is not installed. Shared storage is unavailable.'; + var githubLink = ' [Åbn i HACS]'; + if (!warn) { + warn = document.createElement('div'); + warn.className = 'store-missing'; + warn.style.color = 'var(--error-color)'; + warn.style.fontSize = '.9rem'; + warn.style.marginTop = '6px'; + warn.style.display = 'none'; + const controls = root.querySelector('.store-controls'); + if (controls && controls.parentNode) controls.parentNode.insertBefore(warn, controls); + } + // Always update warning content + // Sæt tekst og link som separate child nodes + // Fjern alle eksisterende child nodes + while (warn.firstChild) warn.removeChild(warn.firstChild); + // Tilføj tekst og link igen + warn.appendChild(document.createTextNode(msg.replace(/\.$/, '') + '. ')); + let link = document.createElement('a'); + link.href = 'https://my.home-assistant.io/redirect/hacs_repository/?owner=qlerup&repository=thermostat-pro-timeline-sync'; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + link.style.color = 'var(--error-color)'; + link.style.textDecoration = 'underline'; + link.style.fontWeight = '500'; + link.textContent = this._t('editor.open_in_hacs'); + warn.appendChild(link); + if (!hasService) { + warn.style.display = 'block'; + const sw = root.querySelector('.store-enable'); if (sw) sw.disabled = true; + const picker = root.querySelector('.storage'); if (picker) { try { picker.disabled = true; picker.setAttribute('disabled',''); } catch {} } + } else { + warn.style.display = 'none'; + const sw = root.querySelector('.store-enable'); if (sw) sw.disabled = false; + const picker = root.querySelector('.storage'); if (picker) { try { picker.disabled = false; picker.removeAttribute('disabled'); } catch {} } + } + // Update small status chip in tabs + try { + const chip = root.querySelector('.int-status'); + const icon = chip?.querySelector('ha-icon'); + const txt = chip?.querySelector('.text'); + if (chip && icon && txt) { + if (hasService) { + chip.classList.remove('bad'); chip.classList.add('ok'); + icon.setAttribute('icon', 'mdi:cloud-check-outline'); + txt.textContent = t('editor.integration.connected') || 'Connected to integration'; + } else { + chip.classList.remove('ok'); chip.classList.add('bad'); + icon.setAttribute('icon', 'mdi:cloud-off-outline'); + txt.textContent = t('editor.integration.disconnected') || 'Not connected to integration'; + } + // Make the chip clickable to jump to Settings (Sync is inside Settings) + chip.style.userSelect = 'none'; + chip.addEventListener('click', ()=>{ try { this._activeTab = 'settings'; this._render(); } catch {} }); + } + } catch {} + } catch {} + // (clear storage only button removed) + const clrAllBtn = root.querySelector('.clear-all span'); + if (clrAllBtn) clrAllBtn.textContent = t('editor.clear_all'); + const migBtn = root.querySelector('.migrate-to-store span'); + if (migBtn) migBtn.textContent = t('editor.migrate_to_store'); + } + const clrLocalBtn = root.querySelector('.clear-local-only span'); + if (clrLocalBtn) clrLocalBtn.textContent = t('editor.clear_local_only'); + const resetOnbBtn = root.querySelector('.reset-onboard span'); + if (resetOnbBtn) resetOnbBtn.textContent = t('onboard.reset'); + + // Entities section label (use the .entities container to find its label robustly) + const entitiesWrap = root.querySelector('.entities'); + if (entitiesWrap) { + const entitiesLabel = entitiesWrap.closest('.row')?.querySelector('.label'); + if (entitiesLabel) entitiesLabel.textContent = t('editor.select_entities'); + } + + // Add room button text (target only the actual add-room control) + const addBtn = root.querySelector('.add-room-btn span'); + if (addBtn) addBtn.textContent = t('editor.add_entity'); + + const bMinLbl = root.querySelector('.boiler-min-temp-label'); + if (bMinLbl) bMinLbl.textContent = (t('boiler.min_temp') || 'Min boiler temp (°C)').replace('°C', this._unitSymbol()); + const bMaxLbl = root.querySelector('.boiler-max-temp-label'); + if (bMaxLbl) bMaxLbl.textContent = (t('boiler.max_temp') || 'Max boiler temp (°C)').replace('°C', this._unitSymbol()); + const bEnTitle = root.querySelector('.boiler-enable-title'); + if (bEnTitle) bEnTitle.textContent = t('boiler.enable') || 'Enable boiler controls'; + const bEnDesc = root.querySelector('.boiler-enable-desc'); + if (bEnDesc) bEnDesc.textContent = t('boiler.enable.desc') || ''; + const bSensTitle = root.querySelector('.boiler-sensor-title'); + if (bSensTitle) bSensTitle.textContent = t('boiler.temp_sensor') || 'Boiler temperature sensor'; + const bSensDesc = root.querySelector('.boiler-sensor-desc'); + if (bSensDesc) bSensDesc.textContent = t('boiler.temp_sensor.desc') || ''; + // Tabs labels + try { + const sBtn = root.querySelector('.tab-settings-btn'); if (sBtn) sBtn.textContent = t('editor.tabs.settings'); + const rBtn = root.querySelector('.tab-rooms-btn'); if (rBtn) rBtn.textContent = t('editor.tabs.rooms'); + const oBtn = root.querySelector('.tab-boiler-btn'); if (oBtn) oBtn.textContent = t('editor.tabs.boiler') || 'Boiler'; + const cBtn = root.querySelector('.tab-colors-btn'); if (cBtn) cBtn.textContent = t('editor.tabs.colors'); + const owBtn = root.querySelector('.tab-owd-btn'); if (owBtn) owBtn.textContent = t('editor.tabs.owd') || 'Open Window Detection'; + const bBtn = root.querySelector('.tab-backup-btn'); if (bBtn) bBtn.textContent = t('editor.tabs.backup') || 'Backup'; + const hBtn = root.querySelector('.tab-holidays-btn'); if (hBtn) hBtn.textContent = t('editor.tabs.holidays'); + const boTitle = root.querySelector('.boiler-switch-title'); if (boTitle) boTitle.textContent = t('boiler.switch') || 'Boiler switch'; + // Holidays tab inner labels + try { + const srcTitle = root.querySelector('.tab-holidays .settings-card .setting .title'); if (srcTitle) srcTitle.textContent = t('holidays.source'); + const srcDesc = root.querySelector('.tab-holidays .settings-card .setting .desc'); if (srcDesc) srcDesc.textContent = t('holidays.desc'); + const calTitle = root.querySelector('.tab-holidays .settings-card .setting:nth-child(2) .title'); if (calTitle) calTitle.textContent = t('holidays.entity'); + const manTitle = root.querySelector('.tab-holidays .settings-card .setting:nth-child(3) .title'); if (manTitle) manTitle.textContent = t('holidays.dates'); + const editBtns = root.querySelectorAll('.tab-holidays .inline .actions .btn'); editBtns.forEach(b=>{ try { b.textContent = t('holidays.edit'); } catch{} }); + const segCal = root.querySelector('.tab-holidays .h-src-cal'); if (segCal) segCal.textContent = t('holidays.source.calendar'); + const segMan = root.querySelector('.tab-holidays .h-src-man'); if (segMan) segMan.textContent = t('holidays.source.manual'); + const addDate = root.querySelector('.tab-holidays .add-entity-btn span'); if (addDate) addDate.textContent = t('holidays.add_date'); + } catch {} + // (reload buttons removed) + // Colors mode labels + const cmTitle = root.querySelector('.colors-mode-title'); if (cmTitle) cmTitle.textContent = t('editor.colors.mode'); + const cmDesc = root.querySelector('.colors-mode-desc'); if (cmDesc) cmDesc.textContent = t('editor.colors.mode.desc'); + const cmRoom = root.querySelector('.colors-mode .mode-room'); if (cmRoom) cmRoom.textContent = t('editor.colors.mode_per_room'); + const cmGlob = root.querySelector('.colors-mode .mode-global'); if (cmGlob) cmGlob.textContent = t('editor.colors.mode_global'); + // Colors tab action label (single clear-all button) + const clrAllBtn = root.querySelector('.clr-colors-all-btn span'); if (clrAllBtn) clrAllBtn.textContent = t('editor.colors.clear_all'); + // Away tab labels + const awayTitle = root.querySelector('.away-enable-title'); if (awayTitle) awayTitle.textContent = t('away.enable'); + const awayDesc = root.querySelector('.away-desc'); if (awayDesc) awayDesc.textContent = t('away.desc'); + const awayTemp = root.querySelector('.away-temp-title'); if (awayTemp) awayTemp.textContent = t('away.temp'); + const awayPersons = root.querySelector('.away-persons-title'); if (awayPersons) awayPersons.textContent = t('away.persons'); + // Presence labels in Away tab + const advTitle = root.querySelector('.away-adv-title'); if (advTitle) advTitle.textContent = t('presence.enable_advanced'); + const advDesc = root.querySelector('.away-adv-desc'); if (advDesc) advDesc.textContent = t('presence.enable_advanced.desc'); + const pcTitle = root.querySelector('.presence-combos-title'); if (pcTitle) pcTitle.textContent = t('presence.combos'); + const plTitle = root.querySelector('.presence-live-title'); if (plTitle) plTitle.textContent = t('presence.live_header'); + const plDesc = root.querySelector('.presence-live-desc'); if (plDesc) plDesc.textContent = t('presence.live_header.desc'); + const pcDesc = root.querySelector('.presence-combos-desc'); if (pcDesc) pcDesc.textContent = ''; + } catch {} + + // Sync delay labels removed (controls removed) + + // Backup tab labels + try { + const bt = root.querySelector('.backup-title'); if (bt) bt.textContent = t('backup.title') || 'Backup'; + const bd = root.querySelector('.backup-desc'); if (bd) bd.textContent = t('backup.desc') || 'Copy schedules/settings to backup and restore when needed.'; + // Selection UI removed; no per-section labels to set + const bn = root.querySelector('.backup-now-label'); if (bn) bn.textContent = t('backup.now') || 'Backup now'; + const brl = root.querySelector('.backup-restore-label'); if (brl) brl.textContent = t('backup.restore') || 'Restore'; + const bdl = root.querySelector('.backup-delete-label'); if (bdl) bdl.textContent = t('backup.delete') || 'Delete'; + const bitl = root.querySelector('.backup-import-title'); if (bitl) bitl.textContent = t('backup.import.title') || 'Import thermostat_timeline.json'; + const bidl = root.querySelector('.backup-import-desc'); if (bidl) bidl.textContent = t('backup.import.desc') || ''; + const bibl = root.querySelector('.backup-import-btn-label'); if (bibl) bibl.textContent = t('backup.import.btn') || 'Import file'; + const bat = root.querySelector('.backup-auto-title'); if (bat) bat.textContent = t('backup.auto'); + const bad = root.querySelector('.backup-auto-desc'); if (bad) bad.textContent = t('backup.auto.desc'); + const bit = root.querySelector('.backup-int-title'); if (bit) bit.textContent = t('backup.interval'); + const bid = root.querySelector('.backup-int-desc'); if (bid) bid.textContent = t('backup.interval.desc'); + const bst = root.querySelector('.backup-sensor-title'); if (bst) bst.textContent = t('backup.sensor'); + const blt = root.querySelector('.backup-last-title'); if (blt) blt.textContent = t('backup.last'); + } catch {} + + } catch(e){ /* ignore */ } + } + + // Detect whether a given title equals any built-in localized default + _isDefaultTitle(txt){ + try { + const s = String(txt || '').trim(); + for (const k of Object.keys(TT_I18N || {})){ + const v = TT_I18N[k] && TT_I18N[k]['card.title_default']; + if (v && String(v).trim() === s) return true; + } + } catch {} + return false; + } +} + +class UiHelper { + // BinarydustFF - fix + // Block's pills (showing temperature) were rendered differently between modes + // Creating a unique helper to unify the rendering method + /** + * Generate a pill element inside a parent block + * @param {ThermostatTimelineCard} ttcInst Current `ThermostatTimelineCard` instance + * @param {HTMLDivElement} parentBlock + * @param {string} content The string content + * @returns {HTMLSpanElement} The generated pill + */ + static genBlockPill(ttcInst, parentBlock, content) { + // Pill creation + const pill = document.createElement('span'); + pill.className = 'pill'; + pill.textContent = content; + parentBlock.append(pill); + + // Pill styling + try { + const blcBgClr = parentBlock.style.backgroundColor || ''; + if (!blcBgClr) { // possible? + // console.warn("ThermostatTimelineCard: Block without background color", parentBlock) + // Skip the styling part + return pill + } + const pillTxtClr = parentBlock.style.color || ttcInst._contrastTextColor(blcBgClr) || ''; + pill.style.backgroundColor = pillTxtClr === '#ffffff' ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.06)'; + pill.style.backgroundColor = pillTxtClr === '#ffffff' ? 'rgba(255,255,255,0.28)' : 'rgba(0,0,0,0.12)'; + } catch {} + return pill; + } +} + +// Avoid hard failures if the editor gets registered elsewhere/earlier. +try { + if (!customElements.get("thermostat-timeline-card-editor")) { + customElements.define("thermostat-timeline-card-editor", ThermostatTimelineCardEditor); + } +} catch {} + +// Registrér i “Custom cards” +window.customCards = window.customCards || []; +window.customCards.push({ type: "thermostat-timeline-card", name: "Thermostat Pro Timeline Card", description: "24h tidslinje – transition-baseret set_temperature + smart replan & apply-on-change", version: TT_CARD_VERSION }); + +function loadCard() {} +loadCard(); diff --git a/www/community/lovelace-thermostat-pro-timeline/thermostat-pro-timeline.js.gz b/www/community/lovelace-thermostat-pro-timeline/thermostat-pro-timeline.js.gz new file mode 100644 index 0000000..4c6f539 Binary files /dev/null and b/www/community/lovelace-thermostat-pro-timeline/thermostat-pro-timeline.js.gz differ diff --git a/www/community/lovelace-wallpanel/wallpanel.js b/www/community/lovelace-wallpanel/wallpanel.js new file mode 100644 index 0000000..84282d6 --- /dev/null +++ b/www/community/lovelace-wallpanel/wallpanel.js @@ -0,0 +1 @@ +!function(){"use strict";function e(e,t,n){return(t=function(e){var t=function(e,t){if("object"!=typeof e||!e)return e;var n=e[Symbol.toPrimitive];if(void 0!==n){var r=n.call(e,t||"default");if("object"!=typeof r)return r;throw new TypeError("@@toPrimitive must return a primitive value.")}return("string"===t?String:Number)(e)}(e,"string");return"symbol"==typeof t?t:t+""}(t))in e?Object.defineProperty(e,t,{value:n,enumerable:!0,configurable:!0,writable:!0}):e[t]=n,e}function t(e,t){var n=Object.keys(e);if(Object.getOwnPropertySymbols){var r=Object.getOwnPropertySymbols(e);t&&(r=r.filter(function(t){return Object.getOwnPropertyDescriptor(e,t).enumerable})),n.push.apply(n,r)}return n}function n(n){for(var r=1;r0&&e[0]<4?1:+(e[0]+e[1])),!t&&r&&(!(e=r.match(/Edge\/(\d+)/))||e[1]>=74)&&(e=r.match(/Chrome\/(\d+)/))&&(t=+e[1]),Y=t}function Ee(){if($)return X;$=1;var e=we(),t=v(),n=s().String;return X=!!Object.getOwnPropertySymbols&&!t(function(){var t=Symbol("symbol detection");return!n(t)||!(Object(t)instanceof Symbol)||!Symbol.sham&&e&&e<41})}function xe(){if(Q)return J;Q=1;var e=Ee();return J=e&&!Symbol.sham&&"symbol"==typeof Symbol.iterator}function Se(){if(K)return Z;K=1;var e=Ae(),t=ve(),n=_e(),r=xe(),i=Object;return Z=r?function(e){return"symbol"==typeof e}:function(r){var o=e("Symbol");return t(o)&&n(o.prototype,i(r))}}function Ce(){if(te)return ee;te=1;var e=String;return ee=function(t){try{return e(t)}catch(e){return"Object"}}}function Ie(){if(re)return ne;re=1;var e=ve(),t=Ce(),n=TypeError;return ne=function(r){if(e(r))return r;throw new n(t(r)+" is not a function")}}function Te(){if(oe)return ie;oe=1;var e=Ie(),t=me();return ie=function(n,r){var i=n[r];return t(i)?void 0:e(i)}}function Re(){if(se)return ae;se=1;var e=_(),t=ve(),n=ye(),r=TypeError;return ae=function(i,o){var a,s;if("string"===o&&t(a=i.toString)&&!n(s=e(a,i)))return s;if(t(a=i.valueOf)&&!n(s=e(a,i)))return s;if("string"!==o&&t(a=i.toString)&&!n(s=e(a,i)))return s;throw new r("Can't convert object to primitive value")}}var ke,Oe,Pe,Me,Le,Be,Ue,De,Fe,Ne,We,je,ze,He,Ge,qe,Ye,Ve,Xe,$e,Je,Qe,Ze,Ke,et={exports:{}};function tt(){return Oe?ke:(Oe=1,ke=!1)}function nt(){if(Me)return Pe;Me=1;var e=s(),t=Object.defineProperty;return Pe=function(n,r){try{t(e,n,{value:r,configurable:!0,writable:!0})}catch(t){e[n]=r}return r}}function rt(){if(Le)return et.exports;Le=1;var e=tt(),t=s(),n=nt(),r="__core-js_shared__",i=et.exports=t[r]||n(r,{});return(i.versions||(i.versions=[])).push({version:"3.43.0",mode:e?"pure":"global",copyright:"© 2014-2025 Denis Pushkarev (zloirock.ru)",license:"https://github.com/zloirock/core-js/blob/v3.43.0/LICENSE",source:"https://github.com/zloirock/core-js"}),et.exports}function it(){if(Ue)return Be;Ue=1;var e=rt();return Be=function(t,n){return e[t]||(e[t]=n||{})}}function ot(){if(Fe)return De;Fe=1;var e=ge(),t=Object;return De=function(n){return t(e(n))}}function at(){if(We)return Ne;We=1;var e=he(),t=ot(),n=e({}.hasOwnProperty);return Ne=Object.hasOwn||function(e,r){return n(t(e),r)}}function st(){if(ze)return je;ze=1;var e=he(),t=0,n=Math.random(),r=e(1.1.toString);return je=function(e){return"Symbol("+(void 0===e?"":e)+")_"+r(++t+n,36)}}function ct(){if(Ge)return He;Ge=1;var e=s(),t=it(),n=at(),r=st(),i=Ee(),o=xe(),a=e.Symbol,c=t("wks"),l=o?a.for||a:a&&a.withoutSetter||r;return He=function(e){return n(c,e)||(c[e]=i&&n(a,e)?a[e]:l("Symbol."+e)),c[e]}}function lt(){if(Ye)return qe;Ye=1;var e=_(),t=ye(),n=Se(),r=Te(),i=Re(),o=ct(),a=TypeError,s=o("toPrimitive");return qe=function(o,c){if(!t(o)||n(o))return o;var l,u=r(o,s);if(u){if(void 0===c&&(c="default"),l=e(u,o,c),!t(l)||n(l))return l;throw new a("Can't convert object to primitive value")}return void 0===c&&(c="number"),i(o,c)}}function ut(){if(Xe)return Ve;Xe=1;var e=lt(),t=Se();return Ve=function(n){var r=e(n,"string");return t(r)?r:r+""}}function ht(){if(Je)return $e;Je=1;var e=s(),t=ye(),n=e.document,r=t(n)&&t(n.createElement);return $e=function(e){return r?n.createElement(e):{}}}function ft(){if(Ze)return Qe;Ze=1;var e=y(),t=v(),n=ht();return Qe=!e&&!t(function(){return 7!==Object.defineProperty(n("div"),"a",{get:function(){return 7}}).a})}function dt(){if(Ke)return p;Ke=1;var e=y(),t=_(),n=le(),r=ue(),i=pe(),o=ut(),a=at(),s=ft(),c=Object.getOwnPropertyDescriptor;return p.f=e?c:function(e,l){if(e=i(e),l=o(l),s)try{return c(e,l)}catch(e){}if(a(e,l))return r(!t(n.f,e,l),e[l])},p}var mt,gt,pt,vt,yt,At,_t,bt={};function wt(){if(gt)return mt;gt=1;var e=y(),t=v();return mt=e&&t(function(){return 42!==Object.defineProperty(function(){},"prototype",{value:42,writable:!1}).prototype})}function Et(){if(vt)return pt;vt=1;var e=ye(),t=String,n=TypeError;return pt=function(r){if(e(r))return r;throw new n(t(r)+" is not an object")}}function xt(){if(yt)return bt;yt=1;var e=y(),t=ft(),n=wt(),r=Et(),i=ut(),o=TypeError,a=Object.defineProperty,s=Object.getOwnPropertyDescriptor,c="enumerable",l="configurable",u="writable";return bt.f=e?n?function(e,t,n){if(r(e),t=i(t),r(n),"function"==typeof e&&"prototype"===t&&"value"in n&&u in n&&!n[u]){var o=s(e,t);o&&o[u]&&(e[t]=n.value,n={configurable:l in n?n[l]:o[l],enumerable:c in n?n[c]:o[c],writable:!1})}return a(e,t,n)}:a:function(e,n,s){if(r(e),n=i(n),r(s),t)try{return a(e,n,s)}catch(e){}if("get"in s||"set"in s)throw new o("Accessors not supported");return"value"in s&&(e[n]=s.value),e},bt}function St(){if(_t)return At;_t=1;var e=y(),t=xt(),n=ue();return At=e?function(e,r,i){return t.f(e,r,n(1,i))}:function(e,t,n){return e[t]=n,e}}var Ct,It,Tt,Rt,kt,Ot,Pt,Mt,Lt,Bt,Ut,Dt,Ft,Nt,Wt,jt={exports:{}};function zt(){if(It)return Ct;It=1;var e=y(),t=at(),n=Function.prototype,r=e&&Object.getOwnPropertyDescriptor,i=t(n,"name"),o=i&&"something"===function(){}.name,a=i&&(!e||e&&r(n,"name").configurable);return Ct={EXISTS:i,PROPER:o,CONFIGURABLE:a}}function Ht(){if(Rt)return Tt;Rt=1;var e=he(),t=ve(),n=rt(),r=e(Function.toString);return t(n.inspectSource)||(n.inspectSource=function(e){return r(e)}),Tt=n.inspectSource}function Gt(){if(Mt)return Pt;Mt=1;var e=it(),t=st(),n=e("keys");return Pt=function(e){return n[e]||(n[e]=t(e))}}function qt(){return Bt?Lt:(Bt=1,Lt={})}function Yt(){if(Dt)return Ut;Dt=1;var e,t,n,r=function(){if(Ot)return kt;Ot=1;var e=s(),t=ve(),n=e.WeakMap;return kt=t(n)&&/native code/.test(String(n))}(),i=s(),o=ye(),a=St(),c=at(),l=rt(),u=Gt(),h=qt(),f="Object already initialized",d=i.TypeError,m=i.WeakMap;if(r||l.state){var g=l.state||(l.state=new m);g.get=g.get,g.has=g.has,g.set=g.set,e=function(e,t){if(g.has(e))throw new d(f);return t.facade=e,g.set(e,t),t},t=function(e){return g.get(e)||{}},n=function(e){return g.has(e)}}else{var p=u("state");h[p]=!0,e=function(e,t){if(c(e,p))throw new d(f);return t.facade=e,a(e,p,t),t},t=function(e){return c(e,p)?e[p]:{}},n=function(e){return c(e,p)}}return Ut={set:e,get:t,has:n,enforce:function(r){return n(r)?t(r):e(r,{})},getterFor:function(e){return function(n){var r;if(!o(n)||(r=t(n)).type!==e)throw new d("Incompatible receiver, "+e+" required");return r}}}}function Vt(){if(Ft)return jt.exports;Ft=1;var e=he(),t=v(),n=ve(),r=at(),i=y(),o=zt().CONFIGURABLE,a=Ht(),s=Yt(),c=s.enforce,l=s.get,u=String,h=Object.defineProperty,f=e("".slice),d=e("".replace),m=e([].join),g=i&&!t(function(){return 8!==h(function(){},"length",{value:8}).length}),p=String(String).split("String"),A=jt.exports=function(e,t,n){"Symbol("===f(u(t),0,7)&&(t="["+d(u(t),/^Symbol\(([^)]*)\).*$/,"$1")+"]"),n&&n.getter&&(t="get "+t),n&&n.setter&&(t="set "+t),(!r(e,"name")||o&&e.name!==t)&&(i?h(e,"name",{value:t,configurable:!0}):e.name=t),g&&n&&r(n,"arity")&&e.length!==n.arity&&h(e,"length",{value:n.arity});try{n&&r(n,"constructor")&&n.constructor?i&&h(e,"prototype",{writable:!1}):e.prototype&&(e.prototype=void 0)}catch(e){}var a=c(e);return r(a,"source")||(a.source=m(p,"string"==typeof t?t:"")),e};return Function.prototype.toString=A(function(){return n(this)&&l(this).source||a(this)},"toString"),jt.exports}function Xt(){if(Wt)return Nt;Wt=1;var e=ve(),t=xt(),n=Vt(),r=nt();return Nt=function(i,o,a,s){s||(s={});var c=s.enumerable,l=void 0!==s.name?s.name:o;if(e(a)&&n(a,l,s),s.global)c?i[o]=a:r(o,a);else{try{s.unsafe?i[o]&&(c=!0):delete i[o]}catch(e){}c?i[o]=a:t.f(i,o,{value:a,enumerable:!1,configurable:!s.nonConfigurable,writable:!s.nonWritable})}return i}}var $t,Jt,Qt,Zt,Kt,en,tn,nn,rn,on,an,sn,cn,ln,un,hn,fn,dn={};function mn(){if(Zt)return Qt;Zt=1;var e=function(){if(Jt)return $t;Jt=1;var e=Math.ceil,t=Math.floor;return $t=Math.trunc||function(n){var r=+n;return(r>0?t:e)(r)}}();return Qt=function(t){var n=+t;return n!=n||0===n?0:e(n)}}function gn(){if(en)return Kt;en=1;var e=mn(),t=Math.max,n=Math.min;return Kt=function(r,i){var o=e(r);return o<0?t(o+i,0):n(o,i)}}function pn(){if(nn)return tn;nn=1;var e=mn(),t=Math.min;return tn=function(n){var r=e(n);return r>0?t(r,9007199254740991):0}}function vn(){if(on)return rn;on=1;var e=pn();return rn=function(t){return e(t.length)}}function yn(){if(ln)return cn;ln=1;var e=he(),t=at(),n=pe(),r=function(){if(sn)return an;sn=1;var e=pe(),t=gn(),n=vn(),r=function(r){return function(i,o,a){var s=e(i),c=n(s);if(0===c)return!r&&-1;var l,u=t(a,c);if(r&&o!=o){for(;c>u;)if((l=s[u++])!=l)return!0}else for(;c>u;u++)if((r||u in s)&&s[u]===o)return r||u||0;return!r&&-1}};return an={includes:r(!0),indexOf:r(!1)}}().indexOf,i=qt(),o=e([].push);return cn=function(e,a){var s,c=n(e),l=0,u=[];for(s in c)!t(i,s)&&t(c,s)&&o(u,s);for(;a.length>l;)t(c,s=a[l++])&&(~r(u,s)||o(u,s));return u}}function An(){return hn?un:(hn=1,un=["constructor","hasOwnProperty","isPrototypeOf","propertyIsEnumerable","toLocaleString","toString","valueOf"])}function _n(){if(fn)return dn;fn=1;var e=yn(),t=An().concat("length","prototype");return dn.f=Object.getOwnPropertyNames||function(n){return e(n,t)},dn}var bn,wn,En,xn,Sn,Cn,In,Tn,Rn,kn,On,Pn,Mn,Ln,Bn,Un,Dn,Fn,Nn,Wn,jn,zn,Hn,Gn,qn,Yn,Vn,Xn,$n,Jn,Qn,Zn,Kn,er,tr,nr,rr,ir,or,ar,sr,cr,lr={};function ur(){return bn||(bn=1,lr.f=Object.getOwnPropertySymbols),lr}function hr(){if(En)return wn;En=1;var e=Ae(),t=he(),n=_n(),r=ur(),i=Et(),o=t([].concat);return wn=e("Reflect","ownKeys")||function(e){var t=n.f(i(e)),a=r.f;return a?o(t,a(e)):t}}function fr(){if(Sn)return xn;Sn=1;var e=at(),t=hr(),n=dt(),r=xt();return xn=function(i,o,a){for(var s=t(o),c=r.f,l=n.f,u=0;uy&&l(o,arguments[y]),o});if(x.prototype=w,"Error"!==_?i?i(x,E):o(x,E,{name:!0}):h&&v in b&&(a(x,b,v),a(x,b,"prepareStackTrace")),o(x,b),!f)try{w.name!==_&&n(w,"name",_),w.constructor=x}catch(e){}return x}},ar}!function(){if(cr)return a;cr=1;var e=mr(),t=s(),n=gr(),r=Tr(),i="WebAssembly",o=t[i],c=7!==new Error("e",{cause:7}).cause,l=function(t,n){var i={};i[t]=r(t,n,c),e({global:!0,constructor:!0,arity:1,forced:c},i)},u=function(t,n){if(o&&o[t]){var a={};a[t]=r(i+"."+t,n,c),e({target:i,stat:!0,constructor:!0,arity:1,forced:c},a)}};l("Error",function(e){return function(t){return n(e,this,arguments)}}),l("EvalError",function(e){return function(t){return n(e,this,arguments)}}),l("RangeError",function(e){return function(t){return n(e,this,arguments)}}),l("ReferenceError",function(e){return function(t){return n(e,this,arguments)}}),l("SyntaxError",function(e){return function(t){return n(e,this,arguments)}}),l("TypeError",function(e){return function(t){return n(e,this,arguments)}}),l("URIError",function(e){return function(t){return n(e,this,arguments)}}),u("CompileError",function(e){return function(t){return n(e,this,arguments)}}),u("LinkError",function(e){return function(t){return n(e,this,arguments)}}),u("RuntimeError",function(e){return function(t){return n(e,this,arguments)}})}();var Rr,kr,Or,Pr,Mr,Lr,Br,Ur,Dr,Fr,Nr,Wr,jr,zr,Hr,Gr,qr,Yr={};function Vr(){if(kr)return Rr;kr=1;var e=fe();return Rr=Array.isArray||function(t){return"Array"===e(t)}}function Xr(){if(Pr)return Or;Pr=1;var e=TypeError;return Or=function(t){if(t>9007199254740991)throw e("Maximum allowed index exceeded");return t}}function $r(){if(Lr)return Mr;Lr=1;var e=fe(),t=he();return Mr=function(n){if("Function"===e(n))return t(n)}}function Jr(){if(Ur)return Br;Ur=1;var e=$r(),t=Ie(),n=A(),r=e(e.bind);return Br=function(e,i){return t(e),void 0===i?e:n?r(e,i):function(){return e.apply(i,arguments)}},Br}function Qr(){if(Wr)return Nr;Wr=1;var e=he(),t=v(),n=ve(),r=wr(),i=Ae(),o=Ht(),a=function(){},s=i("Reflect","construct"),c=/^\s*(?:class|function)\b/,l=e(c.exec),u=!c.test(a),h=function(e){if(!n(e))return!1;try{return s(a,[],e),!0}catch(e){return!1}},f=function(e){if(!n(e))return!1;switch(r(e)){case"AsyncFunction":case"GeneratorFunction":case"AsyncGeneratorFunction":return!1}try{return u||!!l(c,o(e))}catch(e){return!0}};return f.sham=!0,Nr=!s||t(function(){var e;return h(h.call)||!h(Object)||!h(function(){e=!0})||e})?f:h}function Zr(){if(zr)return jr;zr=1;var e=Vr(),t=Qr(),n=ye(),r=ct()("species"),i=Array;return jr=function(o){var a;return e(o)&&(a=o.constructor,(t(a)&&(a===i||e(a.prototype))||n(a)&&null===(a=a[r]))&&(a=void 0)),void 0===a?i:a}}function Kr(){if(Gr)return Hr;Gr=1;var e=Zr();return Hr=function(t,n){return new(e(t))(0===n?0:n)}}!function(){if(qr)return Yr;qr=1;var e=mr(),t=function(){if(Fr)return Dr;Fr=1;var e=Vr(),t=vn(),n=Xr(),r=Jr(),i=function(o,a,s,c,l,u,h,f){for(var d,m,g=l,p=0,v=!!h&&r(h,f);p0&&e(d)?(m=t(d),g=i(o,a,d,m,g,u-1)-1):(n(g+1),o[g]=d),g++),p++;return g};return Dr=i}(),n=ot(),r=vn(),i=mn(),o=Kr();e({target:"Array",proto:!0},{flat:function(){var e=arguments.length?arguments[0]:void 0,a=n(this),s=r(a),c=o(a,0);return c.length=t(c,a,a,s,0,void 0===e?1:i(e)),c}})}();var ei,ti,ni,ri={};!function(){if(ni)return ri;ni=1;var e=mr(),t=ot(),n=vn(),r=function(){if(ti)return ei;ti=1;var e=y(),t=Vr(),n=TypeError,r=Object.getOwnPropertyDescriptor,i=e&&!function(){if(void 0!==this)return!0;try{Object.defineProperty([],"length",{writable:!1}).length=1}catch(e){return e instanceof TypeError}}();return ei=i?function(e,i){if(t(e)&&!r(e,"length").writable)throw new n("Cannot set read only .length");return e.length=i}:function(e,t){return e.length=t}}(),i=Xr();e({target:"Array",proto:!0,arity:1,forced:v()(function(){return 4294967297!==[].push.call({length:4294967296},1)})||!function(){try{Object.defineProperty([],"length",{writable:!1}).push()}catch(e){return e instanceof TypeError}}()},{push:function(e){var o=t(this),a=n(o),s=arguments.length;i(a+s);for(var c=0;c0;)r[s]=r[--s];s!==c++&&(r[s]=a)}else for(var l=t(o/2),u=n(e(r,0,l),i),h=n(e(r,l),i),f=u.length,d=h.length,m=0,g=0;m3)){if(h)return!0;if(d)return d<603;var e,t,n,r,i="";for(e=65;e<76;e++){switch(t=String.fromCharCode(e),e){case 66:case 69:case 70:case 72:n=3;break;case 68:case 71:n=4;break;default:n=2}for(r=0;r<47;r++)m.push({k:t+r,v:n})}for(m.sort(function(e,t){return t.v-e.v}),r=0;ra(n)?1:-1}}(e)),s=i(u),l=0;lu;)n.f(e,a=c[u++],s[a]);return e},Di}(),r=An(),i=qt(),o=Ni(),a=ht(),s=Gt(),c="prototype",l="script",u=s("IE_PROTO"),h=function(){},f=function(e){return"<"+l+">"+e+""},d=function(e){e.write(f("")),e.close();var t=e.parentWindow.Object;return e=null,t},m=function(){try{e=new ActiveXObject("htmlfile")}catch(e){}var t,n,i;m="undefined"!=typeof document?document.domain&&e?d(e):(n=a("iframe"),i="java"+l+":",n.style.display="none",o.appendChild(n),n.src=String(i),(t=n.contentWindow.document).open(),t.write(f("document.F=Object")),t.close(),t.F):d(e);for(var s=r.length;s--;)delete m[c][r[s]];return m()};return i[u]=!0,Pi=Object.create||function(e,r){var i;return null!==e?(h[c]=t(e),i=new h,h[c]=null,i[u]=e):i=m(),void 0===r?i:n.f(i,r)}}function ji(){if(Bi)return Li;Bi=1;var e=ct(),t=Wi(),n=xt().f,r=e("unscopables"),i=Array.prototype;return void 0===i[r]&&n(i,r,{configurable:!0,value:t(null)}),Li=function(e){i[r][e]=!0}}Ui||(Ui=1,ji()("flat"));var zi,Hi,Gi,qi,Yi,Vi,Xi,$i,Ji,Qi,Zi,Ki,eo,to,no,ro,io,oo,ao,so,co,lo,uo,ho,fo,mo,go,po,vo,yo,Ao,_o,bo,wo={};function Eo(){return Hi?zi:(Hi=1,zi="undefined"!=typeof ArrayBuffer&&"undefined"!=typeof DataView)}function xo(){if(qi)return Gi;qi=1;var e=Vt(),t=xt();return Gi=function(n,r,i){return i.get&&e(i.get,r,{getter:!0}),i.set&&e(i.set,r,{setter:!0}),t.f(n,r,i)}}function So(){if(Vi)return Yi;Vi=1;var e=Xt();return Yi=function(t,n,r){for(var i in n)e(t,i,n[i],r);return t}}function Co(){if($i)return Xi;$i=1;var e=_e(),t=TypeError;return Xi=function(n,r){if(e(r,n))return n;throw new t("Incorrect invocation")}}function Io(){if(Qi)return Ji;Qi=1;var e=mn(),t=pn(),n=RangeError;return Ji=function(r){if(void 0===r)return 0;var i=e(r),o=t(i);if(i!==o)throw new n("Wrong length or index");return o}}function To(){if(ro)return no;ro=1;var e=Ki?Zi:(Ki=1,Zi=Math.sign||function(e){var t=+e;return 0===t||t!=t?t:t<0?-1:1}),t=function(){if(to)return eo;to=1;var e=4503599627370496;return eo=function(t){return t+e-e}}(),n=Math.abs;return no=function(r,i,o,a){var s=+r,c=n(s),l=e(s);if(co||h!=h?l*(1/0):l*h}}function Ro(){if(so)return ao;so=1;var e=Array,t=Math.abs,n=Math.pow,r=Math.floor,i=Math.log,o=Math.LN2;return ao={pack:function(a,s,c){var l,u,h,f=e(c),d=8*c-s-1,m=(1<>1,p=23===s?n(2,-24)-n(2,-77):0,v=a<0||0===a&&1/a<0?1:0,y=0;for((a=t(a))!=a||a===1/0?(u=a!=a?1:0,l=m):(l=r(i(a)/o),a*(h=n(2,-l))<1&&(l--,h*=2),(a+=l+g>=1?p/h:p*n(2,1-g))*h>=2&&(l++,h/=2),l+g>=m?(u=0,l=m):l+g>=1?(u=(a*h-1)*n(2,s),l+=g):(u=a*n(2,g-1)*n(2,s),l=0));s>=8;)f[y++]=255&u,u/=256,s-=8;for(l=l<0;)f[y++]=255&l,l/=256,d-=8;return f[y-1]|=128*v,f},unpack:function(e,t){var r,i=e.length,o=8*i-t-1,a=(1<>1,c=o-7,l=i-1,u=e[l--],h=127&u;for(u>>=7;c>0;)h=256*h+e[l--],c-=8;for(r=h&(1<<-c)-1,h>>=-c,c+=t;c>0;)r=256*r+e[l--],c-=8;if(0===h)h=1-s;else{if(h===a)return r?NaN:u?-1/0:1/0;r+=n(2,t),h-=s}return(u?-1:1)*r*n(2,h-t)}}}function ko(){if(ho)return uo;ho=1;var e=at(),t=ve(),n=ot(),r=Gt(),i=function(){if(lo)return co;lo=1;var e=v();return co=!e(function(){function e(){}return e.prototype.constructor=null,Object.getPrototypeOf(new e)!==e.prototype})}(),o=r("IE_PROTO"),a=Object,s=a.prototype;return uo=i?a.getPrototypeOf:function(r){var i=n(r);if(e(i,o))return i[o];var c=i.constructor;return t(c)&&i instanceof c?c.prototype:i instanceof a?s:null}}function Oo(){if(mo)return fo;mo=1;var e=ot(),t=gn(),n=vn();return fo=function(r){for(var i=e(this),o=n(i),a=arguments.length,s=t(a>1?arguments[1]:void 0,o),c=a>2?arguments[2]:void 0,l=void 0===c?o:t(c,o);l>s;)i[s++]=r;return i},fo}function Po(){if(po)return go;po=1;var e=xt().f,t=at(),n=ct()("toStringTag");return go=function(r,i,o){r&&!o&&(r=r.prototype),r&&!t(r,n)&&e(r,n,{configurable:!0,value:i})}}function Mo(){if(yo)return vo;yo=1;var e=s(),t=he(),n=y(),r=Eo(),i=zt(),o=St(),a=xo(),c=So(),l=v(),u=Co(),h=mn(),f=pn(),d=Io(),m=function(){if(oo)return io;oo=1;var e=To();return io=Math.fround||function(t){return e(t,1.1920928955078125e-7,34028234663852886e22,11754943508222875e-54)}}(),g=Ro(),p=ko(),A=Ar(),_=Oo(),b=bi(),w=br(),E=fr(),x=Po(),S=Yt(),C=i.PROPER,I=i.CONFIGURABLE,T="ArrayBuffer",R="DataView",k="prototype",O="Wrong index",P=S.getterFor(T),M=S.getterFor(R),L=S.set,B=e[T],U=B,D=U&&U[k],F=e[R],N=F&&F[k],W=Object.prototype,j=e.Array,z=e.RangeError,H=t(_),G=t([].reverse),q=g.pack,Y=g.unpack,V=function(e){return[255&e]},X=function(e){return[255&e,e>>8&255]},$=function(e){return[255&e,e>>8&255,e>>16&255,e>>24&255]},J=function(e){return e[3]<<24|e[2]<<16|e[1]<<8|e[0]},Q=function(e){return q(m(e),23,4)},Z=function(e){return q(e,52,8)},K=function(e,t,n){a(e[k],t,{configurable:!0,get:function(){return n(this)[t]}})},ee=function(e,t,n,r){var i=M(e),o=d(n),a=!!r;if(o+t>i.byteLength)throw new z(O);var s=i.bytes,c=o+i.byteOffset,l=b(s,c,c+t);return a?l:G(l)},te=function(e,t,n,r,i,o){var a=M(e),s=d(n),c=r(+i),l=!!o;if(s+t>a.byteLength)throw new z(O);for(var u=a.bytes,h=s+a.byteOffset,f=0;f>24)},setUint8:function(e,t){ie(this,e,t<<24>>24)}},{unsafe:!0})}else D=(U=function(e){u(this,D);var t=d(e);L(this,{type:T,bytes:H(j(t),0),byteLength:t}),n||(this.byteLength=t,this.detached=!1)})[k],N=(F=function(e,t,r){u(this,N),u(e,D);var i=P(e),o=i.byteLength,a=h(t);if(a<0||a>o)throw new z("Wrong offset");if(a+(r=void 0===r?o-a:f(r))>o)throw new z("Wrong length");L(this,{type:R,buffer:e,byteLength:r,byteOffset:a,bytes:i.bytes}),n||(this.buffer=e,this.byteLength=r,this.byteOffset=a)})[k],n&&(K(U,"byteLength",P),K(F,"buffer",M),K(F,"byteLength",M),K(F,"byteOffset",M)),c(N,{getInt8:function(e){return ee(this,1,e)[0]<<24>>24},getUint8:function(e){return ee(this,1,e)[0]},getInt16:function(e){var t=ee(this,2,e,arguments.length>1&&arguments[1]);return(t[1]<<8|t[0])<<16>>16},getUint16:function(e){var t=ee(this,2,e,arguments.length>1&&arguments[1]);return t[1]<<8|t[0]},getInt32:function(e){return J(ee(this,4,e,arguments.length>1&&arguments[1]))},getUint32:function(e){return J(ee(this,4,e,arguments.length>1&&arguments[1]))>>>0},getFloat32:function(e){return Y(ee(this,4,e,arguments.length>1&&arguments[1]),23)},getFloat64:function(e){return Y(ee(this,8,e,arguments.length>1&&arguments[1]),52)},setInt8:function(e,t){te(this,1,e,V,t)},setUint8:function(e,t){te(this,1,e,V,t)},setInt16:function(e,t){te(this,2,e,X,t,arguments.length>2&&arguments[2])},setUint16:function(e,t){te(this,2,e,X,t,arguments.length>2&&arguments[2])},setInt32:function(e,t){te(this,4,e,$,t,arguments.length>2&&arguments[2])},setUint32:function(e,t){te(this,4,e,$,t,arguments.length>2&&arguments[2])},setFloat32:function(e,t){te(this,4,e,Q,t,arguments.length>2&&arguments[2])},setFloat64:function(e,t){te(this,8,e,Z,t,arguments.length>2&&arguments[2])}});return x(U,T),x(F,R),vo={ArrayBuffer:U,DataView:F}}function Lo(){if(_o)return Ao;_o=1;var e=Ae(),t=xo(),n=ct(),r=y(),i=n("species");return Ao=function(n){var o=e(n);r&&o&&!o[i]&&t(o,i,{configurable:!0,get:function(){return this}})}}!function(){if(bo)return wo;bo=1;var e=mr(),t=s(),n=Mo(),r=Lo(),i="ArrayBuffer",o=n[i];e({global:!0,constructor:!0,forced:t[i]!==o},{ArrayBuffer:o}),r(i)}();var Bo,Uo={};!function(){if(Bo)return Uo;Bo=1;var e=mr(),t=$r(),n=v(),r=Mo(),i=Et(),o=gn(),a=pn(),s=r.ArrayBuffer,c=r.DataView,l=c.prototype,u=t(s.prototype.slice),h=t(l.getUint8),f=t(l.setUint8);e({target:"ArrayBuffer",proto:!0,unsafe:!0,forced:n(function(){return!new s(2).slice(1,void 0).byteLength})},{slice:function(e,t){if(u&&void 0===t)return u(i(this),e);for(var n=i(this).byteLength,r=o(e,n),l=o(void 0===t?n:t,n),d=new s(a(l-r)),m=new c(this),g=new c(d),p=0;r92||"NODE"===r&&n>94||"BROWSER"===r&&n>97)return!1;var e=new ArrayBuffer(8),t=i(e,{transfer:[e]});return 0!==e.byteLength||8!==t.byteLength})}function ua(){if(na)return ta;na=1;var e,t,n,r,i=s(),o=ca(),a=la(),c=i.structuredClone,l=i.ArrayBuffer,u=i.MessageChannel,h=!1;if(a)h=function(e){c(e,{transfer:[e]})};else if(l)try{u||(e=o("worker_threads"))&&(u=e.MessageChannel),u&&(t=new u,n=new l(2),r=function(e){t.port1.postMessage(null,[e])},2===n.byteLength&&(r(n),0===n.byteLength&&(h=r)))}catch(e){}return ta=h}function ha(){if(ia)return ra;ia=1;var e=s(),t=he(),n=pr(),r=Io(),i=function(){if(Yo)return qo;Yo=1;var e=Go(),t=TypeError;return qo=function(n){if(e(n))throw new t("ArrayBuffer is detached");return n}}(),o=Ho(),a=ua(),c=la(),l=e.structuredClone,u=e.ArrayBuffer,h=e.DataView,f=Math.min,d=u.prototype,m=h.prototype,g=t(d.slice),p=n(d,"resizable","get"),v=n(d,"maxByteLength","get"),y=t(m.getInt8),A=t(m.setInt8);return ra=(c||a)&&function(e,t,n){var s,d=o(e),m=void 0===t?d:r(t),_=!p||!p(e);if(i(e),c&&(e=l(e,{transfer:[e]}),d===m&&(n||_)))return e;if(d>=m&&(!n||_))s=g(e,0,m);else{var b=n&&!_&&v?{maxByteLength:v(e)}:void 0;s=new u(m,b);for(var w=new h(e),E=new h(s),x=f(m,d),S=0;S=0;i--)if(void 0!==t[i])try{r=e(t[i].iterator,n,r)}catch(e){n="throw",r=e}if("throw"===n)throw r;return r}}function Ga(){if(Oa)return ka;Oa=1;var e=_(),t=Wi(),n=St(),r=So(),i=ct(),o=Yt(),a=Te(),s=ba().IteratorPrototype,c=ja(),l=za(),u=Ha(),h=i("toStringTag"),f="IteratorHelper",d="WrapForValidIterator",m="normal",g="throw",p=o.set,v=function(n){var i=o.getterFor(n?d:f);return r(t(s),{next:function(){var e=i(this);if(n)return e.nextHandler();if(e.done)return c(void 0,!0);try{var t=e.nextHandler();return e.returnHandlerResult?t:c(t,e.done)}catch(t){throw e.done=!0,t}},return:function(){var t=i(this),r=t.iterator;if(t.done=!0,n){var o=a(r,"return");return o?e(o,r):c(void 0,!0)}if(t.inner)try{l(t.inner.iterator,m)}catch(e){return l(r,g,e)}if(t.openIters)try{u(t.openIters,m)}catch(e){return l(r,g,e)}return r&&l(r,m),c(void 0,!0)}})},y=v(!0),A=v(!1);return n(A,h,"Iterator Helper"),ka=function(e,t,n){var r=function(r,i){i?(i.iterator=r.iterator,i.next=r.next):i=r,i.type=t?d:f,i.returnHandlerResult=!!n,i.nextHandler=e,i.counter=0,i.done=!1,p(this,i)};return r.prototype=t?y:A,r}}function qa(){if(Ma)return Pa;Ma=1;var e=Et(),t=za();return Pa=function(n,r,i,o){try{return o?r(e(i)[0],i[1]):r(i)}catch(e){t(n,"throw",e)}}}function Ya(){return Ba?La:(Ba=1,La=function(e,t){var n="function"==typeof Iterator&&Iterator.prototype[e];if(n)try{n.call({next:null},t).next()}catch(e){return!0}})}function Va(){if(Da)return Ua;Da=1;var e=s();return Ua=function(t,n){var r=e.Iterator,i=r&&r.prototype,o=i&&i[t],a=!1;if(o)try{o.call({next:function(){return{done:!0}},return:function(){a=!0}},-1)}catch(e){e instanceof n||(a=!1)}if(!a)return o}}!function(){if(Fa)return Na;Fa=1;var e=mr(),t=_(),n=Ie(),r=Et(),i=Wa(),o=Ga(),a=qa(),s=tt(),c=za(),l=Ya(),u=Va(),h=!s&&!l("filter",function(){}),f=!s&&!h&&u("filter",TypeError),d=s||h||f,m=o(function(){for(var e,n,i=this.iterator,o=this.predicate,s=this.next;;){if(e=r(t(s,i)),this.done=!!e.done)return;if(n=e.value,a(i,o,[n,this.counter++],!0))return n}});e({target:"Iterator",proto:!0,real:!0,forced:d},{filter:function(e){r(this);try{n(e)}catch(e){c(this,"throw",e)}return f?t(f,this,e):new m(i(this),{predicate:e})}})}();var Xa,$a,Ja,Qa,Za,Ka,es,ts,ns,rs,is,os={};function as(){return $a?Xa:($a=1,Xa={})}function ss(){if(Qa)return Ja;Qa=1;var e=ct(),t=as(),n=e("iterator"),r=Array.prototype;return Ja=function(e){return void 0!==e&&(t.Array===e||r[n]===e)}}function cs(){if(Ka)return Za;Ka=1;var e=wr(),t=Te(),n=me(),r=as(),i=ct()("iterator");return Za=function(o){if(!n(o))return t(o,i)||t(o,"@@iterator")||r[e(o)]}}function ls(){if(ts)return es;ts=1;var e=_(),t=Ie(),n=Et(),r=Ce(),i=cs(),o=TypeError;return es=function(a,s){var c=arguments.length<2?i(a):s;if(t(c))return n(e(c,a));throw new o(r(a)+" is not iterable")},es}function us(){if(rs)return ns;rs=1;var e=Jr(),t=_(),n=Et(),r=Ce(),i=ss(),o=vn(),a=_e(),s=ls(),c=cs(),l=za(),u=TypeError,h=function(e,t){this.stopped=e,this.result=t},f=h.prototype;return ns=function(d,m,g){var p,v,y,A,_,b,w,E=g&&g.that,x=!(!g||!g.AS_ENTRIES),S=!(!g||!g.IS_RECORD),C=!(!g||!g.IS_ITERATOR),I=!(!g||!g.INTERRUPTED),T=e(m,E),R=function(e){return p&&l(p,"normal"),new h(!0,e)},k=function(e){return x?(n(e),I?T(e[0],e[1],R):T(e[0],e[1])):I?T(e,R):T(e)};if(S)p=d.iterator;else if(C)p=d;else{if(!(v=c(d)))throw new u(r(d)+" is not iterable");if(i(v)){for(y=0,A=o(d);A>y;y++)if((_=k(d[y]))&&a(f,_))return _;return new h(!1)}p=s(d,v)}for(b=S?d.next:p.next;!(w=t(b,p)).done;){try{_=k(w.value)}catch(e){l(p,"throw",e)}if("object"==typeof _&&_&&a(f,_))return _}return new h(!1)}}!function(){if(is)return os;is=1;var e=mr(),t=_(),n=us(),r=Ie(),i=Et(),o=Wa(),a=za(),s=Va()("forEach",TypeError);e({target:"Iterator",proto:!0,real:!0,forced:s},{forEach:function(e){i(this);try{r(e)}catch(e){a(this,"throw",e)}if(s)return t(s,this,e);var c=o(this),l=0;n(c,function(t){e(t,l++)},{IS_RECORD:!0})}})}();var hs,fs={};!function(){if(hs)return fs;hs=1;var e=mr(),t=_(),n=Ie(),r=Et(),i=Wa(),o=Ga(),a=qa(),s=za(),c=Ya(),l=Va(),u=tt(),h=!u&&!c("map",function(){}),f=!u&&!h&&l("map",TypeError),d=u||h||f,m=o(function(){var e=this.iterator,n=r(t(this.next,e));if(!(this.done=!!n.done))return a(e,this.mapper,[n.value,this.counter++],!0)});e({target:"Iterator",proto:!0,real:!0,forced:d},{map:function(e){r(this);try{n(e)}catch(e){s(this,"throw",e)}return f?t(f,this,e):new m(i(this),{mapper:e})}})}();var ds,ms,gs,ps={};!function(){if(gs)return ps;gs=1;var e=mr(),t=Ae(),n=gr(),r=_(),i=he(),o=v(),a=ve(),s=Se(),c=bi(),l=function(){if(ms)return ds;ms=1;var e=he(),t=Vr(),n=ve(),r=fe(),i=Er(),o=e([].push);return ds=function(e){if(n(e))return e;if(t(e)){for(var a=e.length,s=[],c=0;cb)","g");return"b"!==e.exec("b").groups.a||"bc"!=="b".replace(e,"$c")})}!function(){if(Os)return Ps;Os=1;var e=y(),t=s(),n=he(),r=dr(),i=br(),o=St(),a=Wi(),c=_n().f,l=_e(),u=function(){if(ys)return vs;ys=1;var e=ye(),t=fe(),n=ct()("match");return vs=function(r){var i;return e(r)&&(void 0!==(i=r[n])?!!i:"RegExp"===t(r))}}(),h=Er(),f=Ls(),d=Bs(),m=_r(),g=Xt(),p=v(),A=at(),_=Yt().enforce,b=Lo(),w=ct(),E=Us(),x=Ds(),S=w("match"),C=t.RegExp,I=C.prototype,T=t.SyntaxError,R=n(I.exec),k=n("".charAt),O=n("".replace),P=n("".indexOf),M=n("".slice),L=/^\?<[^\s\d!#%&*+<=>@^][^\s!#%&*+<=>@^]*>/,B=/a/g,U=/a/g,D=new C(B)!==B,F=d.MISSED_STICKY,N=d.UNSUPPORTED_Y,W=e&&(!D||F||E||x||p(function(){return U[S]=!1,C(B)!==B||C(U)===U||"/a/i"!==String(C(B,"i"))}));if(r("RegExp",W)){for(var j=function(e,t){var n,r,s,c,d,m,g=l(I,this),p=u(e),v=void 0===t,y=[],b=e;if(!g&&p&&v&&e.constructor===j)return e;if((p||l(I,e))&&(e=e.source,v&&(t=f(b))),e=void 0===e?"":h(e),t=void 0===t?"":h(t),b=e,E&&"dotAll"in B&&(r=!!t&&P(t,"s")>-1)&&(t=O(t,/s/g,"")),n=t,F&&"sticky"in B&&(s=!!t&&P(t,"y")>-1)&&N&&(t=O(t,/y/g,"")),x&&(c=function(e){for(var t,n=e.length,r=0,i="",o=[],s=a(null),c=!1,l=!1,u=0,h="";r<=n;r++){if("\\"===(t=k(e,r)))t+=k(e,++r);else if("]"===t)c=!1;else if(!c)switch(!0){case"["===t:c=!0;break;case"("===t:if(i+=t,"?:"===M(e,r+1,r+3))continue;R(L,M(e,r+1))&&(r+=2,l=!0),u++;continue;case">"===t&&l:if(""===h||A(s,h))throw new T("Invalid capture group name");s[h]=!0,o[o.length]=[h,u],l=!1,h="";continue}l?h+=t:i+=t}return[i,o]}(e),e=c[0],y=c[1]),d=i(C(e,t),g?this:I,j),(r||s||y.length)&&(m=_(d),r&&(m.dotAll=!0,m.raw=j(function(e){for(var t,n=e.length,r=0,i="",o=!1;r<=n;r++)"\\"!==(t=k(e,r))?o||"."!==t?("["===t?o=!0:"]"===t&&(o=!1),i+=t):i+="[\\s\\S]":i+=t+k(e,++r);return i}(e),n)),s&&(m.sticky=!0),y.length&&(m.groups=y)),e!==b)try{o(d,"source",""===b?"(?:)":b)}catch(e){}return d},z=c(C),H=0;z.length>H;)m(j,C,z[H++]);I.constructor=j,j.prototype=I,g(t,"RegExp",j,{constructor:!0})}b("RegExp")}();var Fs,Ns={};!function(){if(Fs)return Ns;Fs=1;var e=y(),t=Us(),n=fe(),r=xo(),i=Yt().get,o=RegExp.prototype,a=TypeError;e&&t&&r(o,"dotAll",{configurable:!0,get:function(){if(this!==o){if("RegExp"===n(this))return!!i(this).dotAll;throw new a("Incompatible receiver, RegExp required")}}})}();var Ws,js,zs,Hs={};function Gs(){if(js)return Ws;js=1;var e,t,n=_(),r=he(),i=Er(),o=Ms(),a=Bs(),s=it(),c=Wi(),l=Yt().get,u=Us(),h=Ds(),f=s("native-string-replace",String.prototype.replace),d=RegExp.prototype.exec,m=d,g=r("".charAt),p=r("".indexOf),v=r("".replace),y=r("".slice),A=(t=/b*/g,n(d,e=/a/,"a"),n(d,t,"a"),0!==e.lastIndex||0!==t.lastIndex),b=a.BROKEN_CARET,w=void 0!==/()??/.exec("")[1];return(A||w||b||u||h)&&(m=function(e){var t,r,a,s,u,h,_,E=this,x=l(E),S=i(e),C=x.raw;if(C)return C.lastIndex=E.lastIndex,t=n(m,C,S),E.lastIndex=C.lastIndex,t;var I=x.groups,T=b&&E.sticky,R=n(o,E),k=E.source,O=0,P=S;if(T&&(R=v(R,"y",""),-1===p(R,"g")&&(R+="g"),P=y(S,E.lastIndex),E.lastIndex>0&&(!E.multiline||E.multiline&&"\n"!==g(S,E.lastIndex-1))&&(k="(?: "+k+")",P=" "+P,O++),r=new RegExp("^(?:"+k+")",R)),w&&(r=new RegExp("^"+k+"$(?!\\s)",R)),A&&(a=E.lastIndex),s=n(d,T?r:E,P),T?s?(s.input=y(s.input,O),s[0]=y(s[0],O),s.index=E.lastIndex,E.lastIndex+=s[0].length):E.lastIndex=0:A&&s&&(E.lastIndex=E.global?s.index+s[0].length:a),w&&s&&s.length>1&&n(f,s[0],r,function(){for(u=1;u=d?e?"":void 0:(l=o(h,f))<55296||l>56319||f+1===d||(u=o(h,f+1))<56320||u>57343?e?i(h,f):l:e?a(h,f,f+2):u-56320+(l-55296<<10)+65536}};return Xs={codeAt:s(!1),charAt:s(!0)}}function oc(){if(Qs)return Js;Qs=1;var e=ic().charAt;return Js=function(t,n,r){return n+(r?e(t,n).length:1)}}function ac(){if(Ks)return Zs;Ks=1;var e=he(),t=ot(),n=Math.floor,r=e("".charAt),i=e("".replace),o=e("".slice),a=/\$([$&'`]|\d{1,2}|<[^>]*>)/g,s=/\$([$&'`]|\d{1,2})/g;return Zs=function(e,c,l,u,h,f){var d=l+e.length,m=u.length,g=s;return void 0!==h&&(h=t(h),g=a),i(f,g,function(t,i){var a;switch(r(i,0)){case"$":return"$";case"&":return e;case"`":return o(c,0,l);case"'":return o(c,d);case"<":a=h[o(i,1,-1)];break;default:var s=+i;if(0===s)return t;if(s>m){var f=n(s/10);return 0===f?t:f<=m?void 0===u[f-1]?r(i,1):u[f-1]+r(i,1):t}a=u[s-1]}return void 0===a?"":a})}}!function(){if(nc)return rc;nc=1;var e=gr(),t=_(),n=he(),r=function(){if(Vs)return Ys;Vs=1,qs();var e=_(),t=Xt(),n=Gs(),r=v(),i=ct(),o=St(),a=i("species"),s=RegExp.prototype;return Ys=function(c,l,u,h){var f=i(c),d=!r(function(){var e={};return e[f]=function(){return 7},7!==""[c](e)}),m=d&&!r(function(){var e=!1,t=/a/;return"split"===c&&((t={}).constructor={},t.constructor[a]=function(){return t},t.flags="",t[f]=/./[f]),t.exec=function(){return e=!0,null},t[f](""),!e});if(!d||!m||u){var g=/./[f],p=l(f,""[c],function(t,r,i,o,a){var c=r.exec;return c===n||c===s.exec?d&&!a?{done:!0,value:e(g,r,i,o)}:{done:!0,value:e(t,i,r,o)}:{done:!1}});t(String.prototype,c,p[0]),t(s,f,p[1])}h&&o(s[f],"sham",!0)}}(),i=v(),o=Et(),a=ve(),s=ye(),c=mn(),l=pn(),u=Er(),h=ge(),f=oc(),d=Te(),m=ac(),g=Ls(),p=function(){if(tc)return ec;tc=1;var e=_(),t=Et(),n=ve(),r=fe(),i=Gs(),o=TypeError;return ec=function(a,s){var c=a.exec;if(n(c)){var l=e(c,a,s);return null!==l&&t(l),l}if("RegExp"===r(a))return e(i,a,s);throw new o("RegExp#exec called on incompatible receiver")}}(),y=ct()("replace"),A=Math.max,b=Math.min,w=n([].concat),E=n([].push),x=n("".indexOf),S=n("".slice),C=function(e){return void 0===e?e:String(e)},I="$0"==="a".replace(/./,"$0"),T=!!/./[y]&&""===/./[y]("a","$0");r("replace",function(n,r,i){var v=T?"$":"$0";return[function(e,n){var i=h(this),o=s(e)?d(e,y):void 0;return o?t(o,e,i,n):t(r,u(i),e,n)},function(t,n){var s=o(this),h=u(t);if("string"==typeof n&&-1===x(n,v)&&-1===x(n,"$<")){var d=i(r,s,h,n);if(d.done)return d.value}var y=a(n);y||(n=u(n));var _,I=u(g(s)),T=-1!==x(I,"g");T&&(_=-1!==x(I,"u"),s.lastIndex=0);for(var R,k=[];null!==(R=p(s,h))&&(E(k,R),T);){""===u(R[0])&&(s.lastIndex=f(h,l(s.lastIndex),_))}for(var O="",P=0,M=0;M=P&&(O+=S(h,P,U)+L,P=U+B.length)}return O+S(h,P)}]},!!i(function(){var e=/./;return e.exec=function(){var e=[];return e.groups={a:"7"},e},"7"!=="".replace(e,"$")})||!I||T)}();var sc,cc,lc,uc,hc,fc,dc,mc={};function gc(){return cc?sc:(cc=1,sc="\t\n\v\f\r                 \u2028\u2029\ufeff")}!function(){if(dc)return mc;dc=1;var e=mr(),t=function(){if(uc)return lc;uc=1;var e=he(),t=ge(),n=Er(),r=gc(),i=e("".replace),o=RegExp("^["+r+"]+"),a=RegExp("(^|[^"+r+"])["+r+"]+$"),s=function(e){return function(r){var s=n(t(r));return 1&e&&(s=i(s,o,"")),2&e&&(s=i(s,a,"$1")),s}};return lc={start:s(1),end:s(2),trim:s(3)}}().trim,n=function(){if(fc)return hc;fc=1;var e=zt().PROPER,t=v(),n=gc();return hc=function(r){return t(function(){return!!n[r]()||"​…᠎"!=="​…᠎"[r]()||e&&n[r].name!==r})}}();e({target:"String",proto:!0,forced:n("trim")},{trim:function(){return t(this)}})}();var pc,vc,yc,Ac,_c,bc,wc,Ec,xc,Sc,Cc,Ic,Tc,Rc,kc,Oc,Pc,Mc,Lc,Bc,Uc,Dc,Fc,Nc,Wc,jc,zc,Hc,Gc={exports:{}};function qc(){if(vc)return pc;vc=1;var e=ct()("iterator"),t=!1;try{var n=0,r={next:function(){return{done:!!n++}},return:function(){t=!0}};r[e]=function(){return this},Array.from(r,function(){throw 2})}catch(e){}return pc=function(n,r){try{if(!r&&!t)return!1}catch(e){return!1}var i=!1;try{var o={};o[e]=function(){return{next:function(){return{done:i=!0}}}},n(o)}catch(e){}return i}}function Yc(){if(Ac)return yc;Ac=1;var e,t,n,r=Eo(),i=y(),o=s(),a=ve(),c=ye(),l=at(),u=wr(),h=Ce(),f=St(),d=Xt(),m=xo(),g=_e(),p=ko(),v=Ar(),A=ct(),_=st(),b=Yt(),w=b.enforce,E=b.get,x=o.Int8Array,S=x&&x.prototype,C=o.Uint8ClampedArray,I=C&&C.prototype,T=x&&p(x),R=S&&p(S),k=Object.prototype,O=o.TypeError,P=A("toStringTag"),M=_("TYPED_ARRAY_TAG"),L="TypedArrayConstructor",B=r&&!!v&&"Opera"!==u(o.opera),U=!1,D={Int8Array:1,Uint8Array:1,Uint8ClampedArray:1,Int16Array:2,Uint16Array:2,Int32Array:4,Uint32Array:4,Float32Array:4,Float64Array:8},F={BigInt64Array:8,BigUint64Array:8},N=function(e){var t=p(e);if(c(t)){var n=E(t);return n&&l(n,L)?n[L]:N(t)}},W=function(e){if(!c(e))return!1;var t=u(e);return l(D,t)||l(F,t)};for(e in D)(n=(t=o[e])&&t.prototype)?w(n)[L]=t:B=!1;for(e in F)(n=(t=o[e])&&t.prototype)&&(w(n)[L]=t);if((!B||!a(T)||T===Function.prototype)&&(T=function(){throw new O("Incorrect invocation")},B))for(e in D)o[e]&&v(o[e],T);if((!B||!R||R===k)&&(R=T.prototype,B))for(e in D)o[e]&&v(o[e].prototype,R);if(B&&p(I)!==R&&v(I,R),i&&!l(R,P))for(e in U=!0,m(R,P,{configurable:!0,get:function(){return c(this)?this[M]:void 0}}),D)o[e]&&f(o[e],M,e);return yc={NATIVE_ARRAY_BUFFER_VIEWS:B,TYPED_ARRAY_TAG:U&&M,aTypedArray:function(e){if(W(e))return e;throw new O("Target is not a typed array")},aTypedArrayConstructor:function(e){if(a(e)&&(!v||g(T,e)))return e;throw new O(h(e)+" is not a typed array constructor")},exportTypedArrayMethod:function(e,t,n,r){if(i){if(n)for(var a in D){var s=o[a];if(s&&l(s.prototype,e))try{delete s.prototype[e]}catch(n){try{s.prototype[e]=t}catch(e){}}}R[e]&&!n||d(R,e,n?t:B&&S[e]||t,r)}},exportTypedArrayStaticMethod:function(e,t,n){var r,a;if(i){if(v){if(n)for(r in D)if((a=o[r])&&l(a,e))try{delete a[e]}catch(e){}if(T[e]&&!n)return;try{return d(T,e,n?t:B&&T[e]||t)}catch(e){}}for(r in D)!(a=o[r])||a[e]&&!n||d(a,e,t)}},getTypedArrayConstructor:N,isView:function(e){if(!c(e))return!1;var t=u(e);return"DataView"===t||l(D,t)||l(F,t)},isTypedArray:W,TypedArray:T,TypedArrayPrototype:R}}function Vc(){if(Ec)return wc;Ec=1;var e=ye(),t=Math.floor;return wc=Number.isInteger||function(n){return!e(n)&&isFinite(n)&&t(n)===n}}function Xc(){if(Sc)return xc;Sc=1;var e=mn(),t=RangeError;return xc=function(n){var r=e(n);if(r<0)throw new t("The argument can't be less than 0");return r}}function $c(){if(Ic)return Cc;Ic=1;var e=Xc(),t=RangeError;return Cc=function(n,r){var i=e(n);if(i%r)throw new t("Wrong offset");return i}}function Jc(){if(Rc)return Tc;Rc=1;var e=Math.round;return Tc=function(t){var n=e(t);return n<0?0:n>255?255:255&n}}function Qc(){if(Oc)return kc;Oc=1;var e=Qr(),t=Ce(),n=TypeError;return kc=function(r){if(e(r))return r;throw new n(t(r)+" is not a constructor")}}function Zc(){if(Mc)return Pc;Mc=1;var e=wr();return Pc=function(t){var n=e(t);return"BigInt64Array"===n||"BigUint64Array"===n}}function Kc(){if(Bc)return Lc;Bc=1;var e=lt(),t=TypeError;return Lc=function(n){var r=e(n,"number");if("number"==typeof r)throw new t("Can't convert number to bigint");return BigInt(r)}}function el(){if(Dc)return Uc;Dc=1;var e=Jr(),t=_(),n=Qc(),r=ot(),i=vn(),o=ls(),a=cs(),s=ss(),c=Zc(),l=Yc().aTypedArrayConstructor,u=Kc();return Uc=function(h){var f,d,m,g,p,v,y,A,_=n(this),b=r(h),w=arguments.length,E=w>1?arguments[1]:void 0,x=void 0!==E,S=a(b);if(S&&!s(S))for(A=(y=o(b,S)).next,b=[];!(v=t(A,y)).done;)b.push(v.value);for(x&&w>2&&(E=e(E,arguments[2])),d=i(b),m=new(l(_))(d),g=c(m),f=0;d>f;f++)p=x?E(b[f],f):b[f],m[f]=g?u(p):+p;return m},Uc}function tl(){if(jc)return Wc;jc=1;var e=vn();return Wc=function(t,n,r){for(var i=0,o=arguments.length>2?r:e(n),a=new t(o);o>i;)a[i]=n[i++];return a},Wc}function nl(){if(zc)return Gc.exports;zc=1;var e=mr(),t=s(),n=_(),r=y(),i=function(){if(bc)return _c;bc=1;var e=s(),t=v(),n=qc(),r=Yc().NATIVE_ARRAY_BUFFER_VIEWS,i=e.ArrayBuffer,o=e.Int8Array;return _c=!r||!t(function(){o(1)})||!t(function(){new o(-1)})||!n(function(e){new o,new o(null),new o(1.5),new o(e)},!0)||t(function(){return 1!==new o(new i(2),1,void 0).length})}(),o=Yc(),a=Mo(),c=Co(),l=ue(),u=St(),h=Vc(),f=pn(),d=Io(),m=$c(),g=Jc(),p=ut(),A=at(),b=wr(),w=ye(),E=Se(),x=Wi(),S=_e(),C=Ar(),I=_n().f,T=el(),R=function(){if(Nc)return Fc;Nc=1;var e=Jr(),t=he(),n=de(),r=ot(),i=vn(),o=Kr(),a=t([].push),s=function(t){var s=1===t,c=2===t,l=3===t,u=4===t,h=6===t,f=7===t,d=5===t||h;return function(m,g,p,v){for(var y,A,_=r(m),b=n(_),w=i(b),E=e(g,p),x=0,S=v||o,C=s?S(m,w):c||f?S(m,0):void 0;w>x;x++)if((d||x in b)&&(A=E(y=b[x],x,_),t))if(s)C[x]=A;else if(A)switch(t){case 3:return!0;case 5:return y;case 6:return x;case 2:a(C,y)}else switch(t){case 4:return!1;case 7:a(C,y)}return h?-1:l||u?u:C}};return Fc={forEach:s(0),map:s(1),filter:s(2),some:s(3),every:s(4),find:s(5),findIndex:s(6),filterReject:s(7)}}().forEach,k=Lo(),O=xo(),P=xt(),M=dt(),L=tl(),B=Yt(),U=br(),D=B.get,F=B.set,N=B.enforce,W=P.f,j=M.f,z=t.RangeError,H=a.ArrayBuffer,G=H.prototype,q=a.DataView,Y=o.NATIVE_ARRAY_BUFFER_VIEWS,V=o.TYPED_ARRAY_TAG,X=o.TypedArray,$=o.TypedArrayPrototype,J=o.isTypedArray,Q="BYTES_PER_ELEMENT",Z="Wrong length",K=function(e,t){O(e,t,{configurable:!0,get:function(){return D(this)[t]}})},ee=function(e){var t;return S(G,e)||"ArrayBuffer"===(t=b(e))||"SharedArrayBuffer"===t},te=function(e,t){return J(e)&&!E(t)&&t in e&&h(+t)&&t>=0},ne=function(e,t){return t=p(t),te(e,t)?l(2,e[t]):j(e,t)},re=function(e,t,n){return t=p(t),!(te(e,t)&&w(n)&&A(n,"value"))||A(n,"get")||A(n,"set")||n.configurable||A(n,"writable")&&!n.writable||A(n,"enumerable")&&!n.enumerable?W(e,t,n):(e[t]=n.value,e)};return r?(Y||(M.f=ne,P.f=re,K($,"buffer"),K($,"byteOffset"),K($,"byteLength"),K($,"length")),e({target:"Object",stat:!0,forced:!Y},{getOwnPropertyDescriptor:ne,defineProperty:re}),Gc.exports=function(r,o,a){var s=r.match(/\d+/)[0]/8,l=r+(a?"Clamped":"")+"Array",h="get"+r,p="set"+r,v=t[l],y=v,A=y&&y.prototype,_={},b=function(e,t){W(e,t,{get:function(){return function(e,t){var n=D(e);return n.view[h](t*s+n.byteOffset,!0)}(this,t)},set:function(e){return function(e,t,n){var r=D(e);r.view[p](t*s+r.byteOffset,a?g(n):n,!0)}(this,t,e)},enumerable:!0})};Y?i&&(y=o(function(e,t,r,i){return c(e,A),U(w(t)?ee(t)?void 0!==i?new v(t,m(r,s),i):void 0!==r?new v(t,m(r,s)):new v(t):J(t)?L(y,t):n(T,y,t):new v(d(t)),e,y)}),C&&C(y,X),R(I(v),function(e){e in y||u(y,e,v[e])}),y.prototype=A):(y=o(function(e,t,r,i){c(e,A);var o,a,l,u=0,h=0;if(w(t)){if(!ee(t))return J(t)?L(y,t):n(T,y,t);o=t,h=m(r,s);var g=t.byteLength;if(void 0===i){if(g%s)throw new z(Z);if((a=g-h)<0)throw new z(Z)}else if((a=f(i)*s)+h>g)throw new z(Z);l=a/s}else l=d(t),o=new H(a=l*s);for(F(e,{buffer:o,byteOffset:h,byteLength:a,length:l,view:new q(o)});u=0?a:o+a;return s<0||s>=o?void 0:i[s]})}();var ol,al={};!function(){if(ol)return al;ol=1;var e=Yc(),t=Oo(),n=Kc(),r=wr(),i=_(),o=he(),a=v(),s=e.aTypedArray,c=e.exportTypedArrayMethod,l=o("".slice);c("fill",function(e){var o=arguments.length;s(this);var a="Big"===l(r(this),0,3)?n(e):+e;return i(t,this,a,o>1?arguments[1]:void 0,o>2?arguments[2]:void 0)},a(function(){var e=0;return new Int8Array(2).fill({valueOf:function(){return e++}}),1!==e}))}();var sl,cl,ll,ul={};function hl(){if(cl)return sl;cl=1;var e=Jr(),t=de(),n=ot(),r=vn(),i=function(i){var o=1===i;return function(a,s,c){for(var l,u=n(a),h=t(u),f=r(h),d=e(s,c);f-- >0;)if(d(l=h[f],f,u))switch(i){case 0:return l;case 1:return f}return o?-1:void 0}};return sl={findLast:i(0),findLastIndex:i(1)}}!function(){if(ll)return ul;ll=1;var e=Yc(),t=hl().findLast,n=e.aTypedArray;(0,e.exportTypedArrayMethod)("findLast",function(e){return t(n(this),e,arguments.length>1?arguments[1]:void 0)})}();var fl,dl={};!function(){if(fl)return dl;fl=1;var e=Yc(),t=hl().findLastIndex,n=e.aTypedArray;(0,e.exportTypedArrayMethod)("findLastIndex",function(e){return t(n(this),e,arguments.length>1?arguments[1]:void 0)})}();var ml,gl={};!function(){if(ml)return gl;ml=1;var e=s(),t=_(),n=Yc(),r=vn(),i=$c(),o=ot(),a=v(),c=e.RangeError,l=e.Int8Array,u=l&&l.prototype,h=u&&u.set,f=n.aTypedArray,d=n.exportTypedArrayMethod,m=!a(function(){var e=new Uint8ClampedArray(2);return t(h,e,{length:1,0:3},1),3!==e[1]}),g=m&&n.NATIVE_ARRAY_BUFFER_VIEWS&&a(function(){var e=new l(2);return e.set(1),e.set("2",1),0!==e[0]||2!==e[1]});d("set",function(e){f(this);var n=i(arguments.length>1?arguments[1]:void 0,1),a=o(e);if(m)return t(h,this,a,n);var s=this.length,l=r(a),u=0;if(l+n>s)throw new c("Wrong length");for(;u0&&1/n<0?1:-1:t>n}}(e))},!p||g)}();var yl,Al,_l,bl={};function wl(){if(Al)return yl;Al=1;var e=vn();return yl=function(t,n){for(var r=e(t),i=new n(r),o=0;o=s||l<0)throw new n("Incorrect index");for(var u=new i(s),h=0;h=t.length)return e.target=null,a(void 0,!0);switch(e.kind){case"keys":return a(n,!1);case"values":return a(t[n],!1)}return a([n,t[n]],!1)},"values");var f=n.Arguments=n.Array;if(t("keys"),t("values"),t("entries"),!s&&c&&"values"!==f.name)try{i(f,"name",{value:"values"})}catch(e){}return Fl}!function(){if(Wl)return jl;Wl=1;var e=s(),t=Ol?kl:(Ol=1,kl={CSSRuleList:0,CSSStyleDeclaration:0,CSSValueList:0,ClientRectList:0,DOMRectList:0,DOMStringList:0,DOMTokenList:1,DataTransferItemList:0,FileList:0,HTMLAllCollection:0,HTMLCollection:0,HTMLFormElement:0,HTMLSelectElement:0,MediaList:0,MimeTypeArray:0,NamedNodeMap:0,NodeList:1,PaintRequestList:0,Plugin:0,PluginArray:0,SVGLengthList:0,SVGNumberList:0,SVGPathSegList:0,SVGPointList:0,SVGStringList:0,SVGTransformList:0,SourceBufferList:0,StyleSheetList:0,TextTrackCueList:0,TextTrackList:0,TouchList:0}),n=function(){if(Ml)return Pl;Ml=1;var e=ht()("span").classList,t=e&&e.constructor&&e.constructor.prototype;return Pl=t===Object.prototype?void 0:t}(),r=Gl(),i=St(),o=Po(),a=ct()("iterator"),c=r.values,l=function(e,n){if(e){if(e[a]!==c)try{i(e,a,c)}catch(t){e[a]=c}if(o(e,n,!0),t[n])for(var s in r)if(e[s]!==r[s])try{i(e,s,r[s])}catch(t){e[s]=r[s]}}};for(var u in t)l(e[u]&&e[u].prototype,u);l(n,"DOMTokenList")}();var ql,Yl,Vl,Xl,$l,Jl={};function Ql(){if(Yl)return ql;Yl=1;var e=y(),t=v(),n=Et(),r=xr(),i=Error.prototype.toString,o=t(function(){if(e){var t=Object.create(Object.defineProperty({},"name",{get:function(){return this===t}}));if("true"!==i.call(t))return!0}return"2: 1"!==i.call({message:1,name:2})||"Error"!==i.call({})});return ql=o?function(){var e=n(this),t=r(e.name,"Error"),i=r(e.message);return t?i?t+": "+i:t:i}:i}function Zl(){return Xl?Vl:(Xl=1,Vl={IndexSizeError:{s:"INDEX_SIZE_ERR",c:1,m:1},DOMStringSizeError:{s:"DOMSTRING_SIZE_ERR",c:2,m:0},HierarchyRequestError:{s:"HIERARCHY_REQUEST_ERR",c:3,m:1},WrongDocumentError:{s:"WRONG_DOCUMENT_ERR",c:4,m:1},InvalidCharacterError:{s:"INVALID_CHARACTER_ERR",c:5,m:1},NoDataAllowedError:{s:"NO_DATA_ALLOWED_ERR",c:6,m:0},NoModificationAllowedError:{s:"NO_MODIFICATION_ALLOWED_ERR",c:7,m:1},NotFoundError:{s:"NOT_FOUND_ERR",c:8,m:1},NotSupportedError:{s:"NOT_SUPPORTED_ERR",c:9,m:1},InUseAttributeError:{s:"INUSE_ATTRIBUTE_ERR",c:10,m:1},InvalidStateError:{s:"INVALID_STATE_ERR",c:11,m:1},SyntaxError:{s:"SYNTAX_ERR",c:12,m:1},InvalidModificationError:{s:"INVALID_MODIFICATION_ERR",c:13,m:1},NamespaceError:{s:"NAMESPACE_ERR",c:14,m:1},InvalidAccessError:{s:"INVALID_ACCESS_ERR",c:15,m:1},ValidationError:{s:"VALIDATION_ERR",c:16,m:0},TypeMismatchError:{s:"TYPE_MISMATCH_ERR",c:17,m:1},SecurityError:{s:"SECURITY_ERR",c:18,m:1},NetworkError:{s:"NETWORK_ERR",c:19,m:1},AbortError:{s:"ABORT_ERR",c:20,m:1},URLMismatchError:{s:"URL_MISMATCH_ERR",c:21,m:1},QuotaExceededError:{s:"QUOTA_EXCEEDED_ERR",c:22,m:1},TimeoutError:{s:"TIMEOUT_ERR",c:23,m:1},InvalidNodeTypeError:{s:"INVALID_NODE_TYPE_ERR",c:24,m:1},DataCloneError:{s:"DATA_CLONE_ERR",c:25,m:1}})}!function(){if($l)return Jl;$l=1;var e=mr(),t=Ae(),n=ca(),r=v(),i=Wi(),o=ue(),a=xt().f,s=Xt(),c=xo(),l=at(),u=Co(),h=Et(),f=Ql(),d=xr(),m=Zl(),g=Cr(),p=Yt(),A=y(),_=tt(),b="DOMException",w="DATA_CLONE_ERR",E=t("Error"),x=t(b)||function(){try{(new(t("MessageChannel")||n("worker_threads").MessageChannel)).port1.postMessage(new WeakMap)}catch(e){if(e.name===w&&25===e.code)return e.constructor}}(),S=x&&x.prototype,C=E.prototype,I=p.set,T=p.getterFor(b),R="stack"in new E(b),k=function(e){return l(m,e)&&m[e].m?m[e].c:0},O=function(){u(this,P);var e=arguments.length,t=d(e<1?void 0:arguments[0]),n=d(e<2?void 0:arguments[1],"Error"),r=k(n);if(I(this,{type:b,name:n,message:t,code:r}),A||(this.name=n,this.message=t,this.code=r),R){var i=new E(t);i.name=b,a(this,"stack",o(1,g(i.stack,1)))}},P=O.prototype=i(C),M=function(e){return{enumerable:!0,configurable:!0,get:e}},L=function(e){return M(function(){return T(this)[e]})};A&&(c(P,"code",L("code")),c(P,"message",L("message")),c(P,"name",L("name"))),a(P,"constructor",o(1,O));var B=r(function(){return!(new x instanceof E)}),U=B||r(function(){return C.toString!==f||"2: 1"!==String(new x(1,2))}),D=B||r(function(){return 25!==new x(1,"DataCloneError").code}),F=B||25!==x[w]||25!==S[w],N=_?U||D||F:B;e({global:!0,constructor:!0,forced:N},{DOMException:N?O:x});var W=t(b),j=W.prototype;for(var z in U&&(_||x===W)&&s(j,"toString",f),D&&A&&x===W&&c(j,"code",M(function(){return k(h(this).name)})),m)if(l(m,z)){var H=m[z],G=H.s,q=o(6,H.c);l(W,G)||a(W,G,q),l(j,G)||a(j,G,q)}}();var Kl,eu={};!function(){if(Kl)return eu;Kl=1;var e=mr(),t=s(),n=Ae(),r=ue(),i=xt().f,o=at(),a=Co(),c=br(),l=xr(),u=Zl(),h=Cr(),f=y(),d=tt(),m="DOMException",g=n("Error"),p=n(m),v=function(){a(this,A);var e=arguments.length,t=l(e<1?void 0:arguments[0]),n=l(e<2?void 0:arguments[1],"Error"),o=new p(t,n),s=new g(t);return s.name=m,i(o,"stack",r(1,h(s.stack,1))),c(o,this,v),o},A=v.prototype=p.prototype,_="stack"in new g(m),b="stack"in new p(1,2),w=p&&f&&Object.getOwnPropertyDescriptor(t,m),E=!(!w||w.writable&&w.configurable),x=_&&!E&&!b;e({global:!0,constructor:!0,forced:d||x},{DOMException:x?v:p});var S=n(m),C=S.prototype;if(C.constructor!==S)for(var I in d||i(C,"constructor",r(1,S)),u)if(o(u,I)){var T=u[I],R=T.s;o(S,R)||i(S,R,r(6,T.c))}}();var tu,nu={};!function(){if(tu)return nu;tu=1;var e=Ae(),t="DOMException";Po()(e(t),t)}();var ru,iu={};!function(){if(ru)return iu;ru=1;var e=mr(),t=s(),n=xo(),r=y(),i=TypeError,o=Object.defineProperty,a=t.self!==t;try{if(r){var c=Object.getOwnPropertyDescriptor(t,"self");!a&&c&&c.get&&c.enumerable||n(t,"self",{get:function(){return t},set:function(e){if(this!==t)throw new i("Illegal invocation");o(t,"self",{value:e,writable:!0,configurable:!0,enumerable:!0})},configurable:!0,enumerable:!0})}else e({global:!0,simple:!0,forced:a},{self:t})}catch(e){}}();var ou,au,su,cu,lu,uu,hu,fu,du,mu,gu,pu={},vu={};function yu(){if(su)return au;su=1;var e=v(),t=ct(),n=y(),r=tt(),i=t("iterator");return au=!e(function(){var e=new URL("b?a=1&b=2&c=3","https://a"),t=e.searchParams,o=new URLSearchParams("a=1&a=2&b=3"),a="";return e.pathname="c%20d",t.forEach(function(e,n){t.delete("b"),a+=n+e}),o.delete("a",2),o.delete("b",void 0),r&&(!e.toJSON||!o.has("a",1)||o.has("a",2)||!o.has("a",void 0)||o.has("b"))||!t.size&&(r||!n)||!t.sort||"https://a/c%20d?a=1&c=3"!==e.href||"3"!==t.get("c")||"a=1"!==String(new URLSearchParams("?a=1"))||!t[i]||"a"!==new URL("https://a@b").username||"b"!==new URLSearchParams(new URLSearchParams("a=b")).get("a")||"xn--e1aybc"!==new URL("https://тест").host||"#%D0%B1"!==new URL("https://a#б").hash||"a1c3"!==a||"x"!==new URL("https://x",void 0).host})}function Au(){if(hu)return uu;hu=1;var e=Jr(),t=_(),n=ot(),r=qa(),i=ss(),o=Qr(),a=vn(),s=_a(),c=ls(),l=cs(),u=Array;return uu=function(h){var f=n(h),d=o(this),m=arguments.length,g=m>1?arguments[1]:void 0,p=void 0!==g;p&&(g=e(g,m>2?arguments[2]:void 0));var v,y,A,_,b,w,E=l(f),x=0;if(!E||this===u&&i(E))for(v=a(f),y=d?new this(v):u(v);v>x;x++)w=p?g(f[x],x):f[x],s(y,x,w);else for(y=d?new this:[],b=(_=c(f,E)).next;!(A=t(b,_)).done;x++)w=p?r(_,g,[A.value,x],!0):A.value,s(y,x,w);return y.length=x,y},uu}function _u(){if(du)return fu;du=1;var e=he(),t=2147483647,n=/[^\0-\u007E]/,r=/[.\u3002\uFF0E\uFF61]/g,i="Overflow: input needs wider integers to process",o=RangeError,a=e(r.exec),s=Math.floor,c=String.fromCharCode,l=e("".charCodeAt),u=e([].join),h=e([].push),f=e("".replace),d=e("".split),m=e("".toLowerCase),g=function(e){return e+22+75*(e<26)},p=function(e,t,n){var r=0;for(e=n?s(e/700):e>>1,e+=s(e/t);e>455;)e=s(e/35),r+=36;return s(r+36*e/(e+38))},v=function(e){var n=[];e=function(e){for(var t=[],n=0,r=e.length;n=55296&&i<=56319&&n=d&&a<_&&(_=a);var b=A+1;if(_-d>s((t-m)/b))throw new o(i);for(m+=(_-d)*b,d=_,r=0;rt)throw new o(i);if(a===d){for(var w=m,E=36;;){var x=E<=v?1:E>=v+26?26:E-v;if(wc;){if(t=+arguments[c++],n(t,1114111)!==t)throw new r(t+" is not a valid code point");o[c]=t<65536?i(t):i(55296+((t-=65536)>>10),t%1024+56320)}return a(o,"")}})}();var e=mr(),t=s(),n=ku(),r=Ae(),i=_(),o=he(),a=y(),c=yu(),l=Xt(),u=xo(),h=So(),f=Po(),d=zl(),m=Yt(),g=Co(),p=ve(),v=at(),A=Jr(),b=wr(),w=Et(),E=ye(),x=Er(),S=Wi(),C=ue(),I=ls(),T=cs(),R=ja(),k=bu(),O=ct(),P=wi(),M=O("iterator"),L="URLSearchParams",B=L+"Iterator",U=m.set,D=m.getterFor(L),F=m.getterFor(B),N=n("fetch"),W=n("Request"),j=n("Headers"),z=W&&W.prototype,H=j&&j.prototype,G=t.TypeError,q=t.encodeURIComponent,Y=String.fromCharCode,V=r("String","fromCodePoint"),X=parseInt,$=o("".charAt),J=o([].join),Q=o([].push),Z=o("".replace),K=o([].shift),ee=o([].splice),te=o("".split),ne=o("".slice),re=o(/./.exec),ie=/\+/g,oe=/^[0-9a-f]+$/i,ae=function(e,t){var n=ne(e,t,t+2);return re(oe,n)?X(n,16):NaN},se=function(e){for(var t=0,n=128;n>0&&0!==(e&n);n>>=1)t++;return t},ce=function(e){var t=null;switch(e.length){case 1:t=e[0];break;case 2:t=(31&e[0])<<6|63&e[1];break;case 3:t=(15&e[0])<<12|(63&e[1])<<6|63&e[2];break;case 4:t=(7&e[0])<<18|(63&e[1])<<12|(63&e[2])<<6|63&e[3]}return t>1114111?null:t},le=function(e){for(var t=(e=Z(e,ie," ")).length,n="",r=0;rt){n+="%",r++;continue}var o=ae(e,r+1);if(o!=o){n+=i,r++;continue}r+=2;var a=se(o);if(0===a)i=Y(o);else{if(1===a||a>4){n+="�",r++;continue}for(var s=[o],c=1;ct||"%"!==$(e,r));){var l=ae(e,r+1);if(l!=l){r+=3;break}if(l>191||l<128)break;Q(s,l),r+=2,c++}if(s.length!==a){n+="�";continue}var u=ce(s);null===u?n+="�":i=V(u)}}n+=i,r++}return n},fe=/[!'()~]|%20/g,de={"!":"%21","'":"%27","(":"%28",")":"%29","~":"%7E","%20":"+"},me=function(e){return de[e]},ge=function(e){return Z(q(e),fe,me)},pe=d(function(e,t){U(this,{type:B,target:D(e).entries,index:0,kind:t})},L,function(){var e=F(this),t=e.target,n=e.index++;if(!t||n>=t.length)return e.target=null,R(void 0,!0);var r=t[n];switch(e.kind){case"keys":return R(r.key,!1);case"values":return R(r.value,!1)}return R([r.key,r.value],!1)},!0),_e=function(e){this.entries=[],this.url=null,void 0!==e&&(E(e)?this.parseObject(e):this.parseQuery("string"==typeof e?"?"===$(e,0)?ne(e,1):e:x(e)))};_e.prototype={type:L,bindURL:function(e){this.url=e,this.update()},parseObject:function(e){var t,n,r,o,a,s,c,l=this.entries,u=T(e);if(u)for(n=(t=I(e,u)).next;!(r=i(n,t)).done;){if(a=(o=I(w(r.value))).next,(s=i(a,o)).done||(c=i(a,o)).done||!i(a,o).done)throw new G("Expected sequence with length 2");Q(l,{key:x(s.value),value:x(c.value)})}else for(var h in e)v(e,h)&&Q(l,{key:h,value:x(e[h])})},parseQuery:function(e){if(e)for(var t,n,r=this.entries,i=te(e,"&"),o=0;o0?arguments[0]:void 0));a||(this.size=e.entries.length)},we=be.prototype;if(h(we,{append:function(e,t){var n=D(this);k(arguments.length,2),Q(n.entries,{key:x(e),value:x(t)}),a||this.length++,n.updateURL()},delete:function(e){for(var t=D(this),n=k(arguments.length,1),r=t.entries,i=x(e),o=n<2?void 0:arguments[1],s=void 0===o?o:x(o),c=0;ct.key?1:-1}),e.updateURL()},forEach:function(e){for(var t,n=D(this).entries,r=A(e,arguments.length>1?arguments[1]:void 0),i=0;i1?Se(arguments[1]):{})}}),p(W)){var Ce=function(e){return g(this,z),new W(e,arguments.length>1?Se(arguments[1]):{})};z.constructor=Ce,Ce.prototype=z,e({global:!0,constructor:!0,dontCallGetSet:!0,forced:!0},{Request:Ce})}}return Su={URLSearchParams:be,getState:D}}function Pu(){if(Iu)return pu;Iu=1,function(){if(ou)return vu;ou=1;var e=ic().charAt,t=Er(),n=Yt(),r=Hl(),i=ja(),o="String Iterator",a=n.set,s=n.getterFor(o);r(String,"String",function(e){a(this,{type:o,string:t(e),index:0})},function(){var t,n=s(this),r=n.string,o=n.index;return o>=r.length?i(void 0,!0):(t=e(r,o),n.index+=t.length,i(t,!1))})}();var e,t=mr(),n=y(),r=yu(),i=s(),o=Jr(),a=he(),c=Xt(),l=xo(),u=Co(),h=at(),f=function(){if(lu)return cu;lu=1;var e=y(),t=he(),n=_(),r=v(),i=Fi(),o=ur(),a=le(),s=ot(),c=de(),l=Object.assign,u=Object.defineProperty,h=t([].concat);return cu=!l||r(function(){if(e&&1!==l({b:1},l(u({},"a",{enumerable:!0,get:function(){u(this,"b",{value:3,enumerable:!1})}}),{b:2})).b)return!0;var t={},n={},r=Symbol("assign detection"),o="abcdefghijklmnopqrst";return t[r]=7,o.split("").forEach(function(e){n[e]=e}),7!==l({},t)[r]||i(l({},n)).join("")!==o})?function(t,r){for(var l=s(t),u=arguments.length,f=1,d=o.f,m=a.f;u>f;)for(var g,p=c(arguments[f++]),v=d?h(i(p),d(p)):i(p),y=v.length,A=0;y>A;)g=v[A++],e&&!n(m,p,g)||(l[g]=p[g]);return l}:l,cu}(),d=Au(),m=bi(),g=ic().codeAt,p=_u(),A=Er(),b=Po(),w=bu(),E=Ou(),x=Yt(),S=x.set,C=x.getterFor("URL"),I=E.URLSearchParams,T=E.getState,R=i.URL,k=i.TypeError,O=i.parseInt,P=Math.floor,M=Math.pow,L=a("".charAt),B=a(/./.exec),U=a([].join),D=a(1.1.toString),F=a([].pop),N=a([].push),W=a("".replace),j=a([].shift),z=a("".split),H=a("".slice),G=a("".toLowerCase),q=a([].unshift),Y="Invalid scheme",V="Invalid host",X="Invalid port",$=/[a-z]/i,J=/[\d+-.a-z]/i,Q=/\d/,Z=/^0x/i,K=/^[0-7]+$/,ee=/^\d+$/,te=/^[\da-f]+$/i,ne=/[\0\t\n\r #%/:<>?@[\\\]^|]/,re=/[\0\t\n\r #/:<>?@[\\\]^|]/,ie=/^[\u0000-\u0020]+/,oe=/(^|[^\u0000-\u0020])[\u0000-\u0020]+$/,ae=/[\t\n\r]/g,se=function(e){var t,n,r,i;if("number"==typeof e){for(t=[],n=0;n<4;n++)q(t,e%256),e=P(e/256);return U(t,".")}if("object"==typeof e){for(t="",r=function(e){for(var t=null,n=1,r=null,i=0,o=0;o<8;o++)0!==e[o]?(i>n&&(t=r,n=i),r=null,i=0):(null===r&&(r=o),++i);return i>n?r:t}(e),n=0;n<8;n++)i&&0===e[n]||(i&&(i=!1),r===n?(t+=n?":":"::",i=!0):(t+=D(e[n],16),n<7&&(t+=":")));return"["+t+"]"}return e},ce={},ue=f({},ce,{" ":1,'"':1,"<":1,">":1,"`":1}),fe=f({},ue,{"#":1,"?":1,"{":1,"}":1}),me=f({},fe,{"/":1,":":1,";":1,"=":1,"@":1,"[":1,"\\":1,"]":1,"^":1,"|":1}),ge=function(e,t){var n=g(e,0);return n>32&&n<127&&!h(t,e)?e:encodeURIComponent(e)},pe={ftp:21,file:null,http:80,https:443,ws:80,wss:443},ve=function(e,t){var n;return 2===e.length&&B($,L(e,0))&&(":"===(n=L(e,1))||!t&&"|"===n)},ye=function(e){var t;return e.length>1&&ve(H(e,0,2))&&(2===e.length||"/"===(t=L(e,2))||"\\"===t||"?"===t||"#"===t)},Ae=function(e){return"."===e||"%2e"===G(e)},_e=function(e){return".."===(e=G(e))||"%2e."===e||".%2e"===e||"%2e%2e"===e},be={},we={},Ee={},xe={},Se={},Ce={},Ie={},Te={},Re={},ke={},Oe={},Pe={},Me={},Le={},Be={},Ue={},De={},Fe={},Ne={},We={},je={},ze=function(e,t,n){var r,i,o,a=A(e);if(t){if(i=this.parse(a))throw new k(i);this.searchParams=null}else{if(void 0!==n&&(r=new ze(n,!0)),i=this.parse(a,null,r))throw new k(i);(o=T(new I)).bindURL(this),this.searchParams=o}};ze.prototype={type:"URL",parse:function(t,n,r){var i,o,a,s,c=this,l=n||be,u=0,f="",g=!1,p=!1,v=!1;for(t=A(t),n||(c.scheme="",c.username="",c.password="",c.host=null,c.port=null,c.path=[],c.query=null,c.fragment=null,c.cannotBeABaseURL=!1,t=W(t,ie,""),t=W(t,oe,"$1")),t=W(t,ae,""),i=d(t);u<=i.length;){switch(o=i[u],l){case be:if(!o||!B($,o)){if(n)return Y;l=Ee;continue}f+=G(o),l=we;break;case we:if(o&&(B(J,o)||"+"===o||"-"===o||"."===o))f+=G(o);else{if(":"!==o){if(n)return Y;f="",l=Ee,u=0;continue}if(n&&(c.isSpecial()!==h(pe,f)||"file"===f&&(c.includesCredentials()||null!==c.port)||"file"===c.scheme&&!c.host))return;if(c.scheme=f,n)return void(c.isSpecial()&&pe[c.scheme]===c.port&&(c.port=null));f="","file"===c.scheme?l=Le:c.isSpecial()&&r&&r.scheme===c.scheme?l=xe:c.isSpecial()?l=Te:"/"===i[u+1]?(l=Se,u++):(c.cannotBeABaseURL=!0,N(c.path,""),l=Ne)}break;case Ee:if(!r||r.cannotBeABaseURL&&"#"!==o)return Y;if(r.cannotBeABaseURL&&"#"===o){c.scheme=r.scheme,c.path=m(r.path),c.query=r.query,c.fragment="",c.cannotBeABaseURL=!0,l=je;break}l="file"===r.scheme?Le:Ce;continue;case xe:if("/"!==o||"/"!==i[u+1]){l=Ce;continue}l=Re,u++;break;case Se:if("/"===o){l=ke;break}l=Fe;continue;case Ce:if(c.scheme=r.scheme,o===e)c.username=r.username,c.password=r.password,c.host=r.host,c.port=r.port,c.path=m(r.path),c.query=r.query;else if("/"===o||"\\"===o&&c.isSpecial())l=Ie;else if("?"===o)c.username=r.username,c.password=r.password,c.host=r.host,c.port=r.port,c.path=m(r.path),c.query="",l=We;else{if("#"!==o){c.username=r.username,c.password=r.password,c.host=r.host,c.port=r.port,c.path=m(r.path),c.path.length--,l=Fe;continue}c.username=r.username,c.password=r.password,c.host=r.host,c.port=r.port,c.path=m(r.path),c.query=r.query,c.fragment="",l=je}break;case Ie:if(!c.isSpecial()||"/"!==o&&"\\"!==o){if("/"!==o){c.username=r.username,c.password=r.password,c.host=r.host,c.port=r.port,l=Fe;continue}l=ke}else l=Re;break;case Te:if(l=Re,"/"!==o||"/"!==L(f,u+1))continue;u++;break;case Re:if("/"!==o&&"\\"!==o){l=ke;continue}break;case ke:if("@"===o){g&&(f="%40"+f),g=!0,a=d(f);for(var y=0;y65535)return X;c.port=c.isSpecial()&&w===pe[c.scheme]?null:w,f=""}if(n)return;l=De;continue}return X}f+=o;break;case Le:if(c.scheme="file","/"===o||"\\"===o)l=Be;else{if(!r||"file"!==r.scheme){l=Fe;continue}switch(o){case e:c.host=r.host,c.path=m(r.path),c.query=r.query;break;case"?":c.host=r.host,c.path=m(r.path),c.query="",l=We;break;case"#":c.host=r.host,c.path=m(r.path),c.query=r.query,c.fragment="",l=je;break;default:ye(U(m(i,u),""))||(c.host=r.host,c.path=m(r.path),c.shortenPath()),l=Fe;continue}}break;case Be:if("/"===o||"\\"===o){l=Ue;break}r&&"file"===r.scheme&&!ye(U(m(i,u),""))&&(ve(r.path[0],!0)?N(c.path,r.path[0]):c.host=r.host),l=Fe;continue;case Ue:if(o===e||"/"===o||"\\"===o||"?"===o||"#"===o){if(!n&&ve(f))l=Fe;else if(""===f){if(c.host="",n)return;l=De}else{if(s=c.parseHost(f))return s;if("localhost"===c.host&&(c.host=""),n)return;f="",l=De}continue}f+=o;break;case De:if(c.isSpecial()){if(l=Fe,"/"!==o&&"\\"!==o)continue}else if(n||"?"!==o)if(n||"#"!==o){if(o!==e&&(l=Fe,"/"!==o))continue}else c.fragment="",l=je;else c.query="",l=We;break;case Fe:if(o===e||"/"===o||"\\"===o&&c.isSpecial()||!n&&("?"===o||"#"===o)){if(_e(f)?(c.shortenPath(),"/"===o||"\\"===o&&c.isSpecial()||N(c.path,"")):Ae(f)?"/"===o||"\\"===o&&c.isSpecial()||N(c.path,""):("file"===c.scheme&&!c.path.length&&ve(f)&&(c.host&&(c.host=""),f=L(f,0)+":"),N(c.path,f)),f="","file"===c.scheme&&(o===e||"?"===o||"#"===o))for(;c.path.length>1&&""===c.path[0];)j(c.path);"?"===o?(c.query="",l=We):"#"===o&&(c.fragment="",l=je)}else f+=ge(o,fe);break;case Ne:"?"===o?(c.query="",l=We):"#"===o?(c.fragment="",l=je):o!==e&&(c.path[0]+=ge(o,ce));break;case We:n||"#"!==o?o!==e&&("'"===o&&c.isSpecial()?c.query+="%27":c.query+="#"===o?"%23":ge(o,ce)):(c.fragment="",l=je);break;case je:o!==e&&(c.fragment+=ge(o,ue))}u++}},parseHost:function(e){var t,n,r;if("["===L(e,0)){if("]"!==L(e,e.length-1))return V;if(t=function(e){var t,n,r,i,o,a,s,c=[0,0,0,0,0,0,0,0],l=0,u=null,h=0,f=function(){return L(e,h)};if(":"===f()){if(":"!==L(e,1))return;h+=2,u=++l}for(;f();){if(8===l)return;if(":"!==f()){for(t=n=0;n<4&&B(te,f());)t=16*t+O(f(),16),h++,n++;if("."===f()){if(0===n)return;if(h-=n,l>6)return;for(r=0;f();){if(i=null,r>0){if(!("."===f()&&r<4))return;h++}if(!B(Q,f()))return;for(;B(Q,f());){if(o=O(f(),10),null===i)i=o;else{if(0===i)return;i=10*i+o}if(i>255)return;h++}c[l]=256*c[l]+i,2!==++r&&4!==r||l++}if(4!==r)return;break}if(":"===f()){if(h++,!f())return}else if(f())return;c[l++]=t}else{if(null!==u)return;h++,u=++l}}if(null!==u)for(a=l-u,l=7;0!==l&&a>0;)s=c[l],c[l--]=c[u+a-1],c[u+--a]=s;else if(8!==l)return;return c}(H(e,1,-1)),!t)return V;this.host=t}else if(this.isSpecial()){if(e=p(e),B(ne,e))return V;if(t=function(e){var t,n,r,i,o,a,s,c=z(e,".");if(c.length&&""===c[c.length-1]&&c.length--,(t=c.length)>4)return e;for(n=[],r=0;r1&&"0"===L(i,0)&&(o=B(Z,i)?16:8,i=H(i,8===o?1:2)),""===i)a=0;else{if(!B(10===o?ee:8===o?K:te,i))return e;a=O(i,o)}N(n,a)}for(r=0;r=M(256,5-t))return null}else if(a>255)return null;for(s=F(n),r=0;r1?arguments[1]:void 0,i=S(t,new ze(e,!1,r));n||(t.href=i.serialize(),t.origin=i.getOrigin(),t.protocol=i.getProtocol(),t.username=i.getUsername(),t.password=i.getPassword(),t.host=i.getHost(),t.hostname=i.getHostname(),t.port=i.getPort(),t.pathname=i.getPathname(),t.search=i.getSearch(),t.searchParams=i.getSearchParams(),t.hash=i.getHash())},Ge=He.prototype,qe=function(e,t){return{get:function(){return C(this)[e]()},set:t&&function(e){return C(this)[t](e)},configurable:!0,enumerable:!0}};if(n&&(l(Ge,"href",qe("serialize","setHref")),l(Ge,"origin",qe("getOrigin")),l(Ge,"protocol",qe("getProtocol","setProtocol")),l(Ge,"username",qe("getUsername","setUsername")),l(Ge,"password",qe("getPassword","setPassword")),l(Ge,"host",qe("getHost","setHost")),l(Ge,"hostname",qe("getHostname","setHostname")),l(Ge,"port",qe("getPort","setPort")),l(Ge,"pathname",qe("getPathname","setPathname")),l(Ge,"search",qe("getSearch","setSearch")),l(Ge,"searchParams",qe("getSearchParams")),l(Ge,"hash",qe("getHash","setHash"))),c(Ge,"toJSON",function(){return C(this).serialize()},{enumerable:!0}),c(Ge,"toString",function(){return C(this).serialize()},{enumerable:!0}),R){var Ye=R.createObjectURL,Ve=R.revokeObjectURL;Ye&&c(He,"createObjectURL",o(Ye,R)),Ve&&c(He,"revokeObjectURL",o(Ve,R))}return b(He,"URL"),t({global:!0,constructor:!0,forced:!r,sham:!n},{URL:He}),pu}Tu||(Tu=1,Pu());var Mu,Lu={};!function(){if(Mu)return Lu;Mu=1;var e=mr(),t=_();e({target:"URL",proto:!0,enumerable:!0},{toJSON:function(){return t(URL.prototype.toString,this)}})}();var Bu;Bu||(Bu=1,Ou());var Uu,Du={};!function(){if(Uu)return Du;Uu=1;var e=Xt(),t=he(),n=Er(),r=bu(),i=URLSearchParams,o=i.prototype,a=t(o.append),s=t(o.delete),c=t(o.forEach),l=t([].push),u=new i("a=1&a=2&b=3");u.delete("a",1),u.delete("b",void 0),u+""!="a=2"&&e(o,"delete",function(e){var t=arguments.length,i=t<2?void 0:arguments[1];if(t&&void 0===i)return s(this,e);var o=[];c(this,function(e,t){l(o,{key:t,value:e})}),r(t,1);for(var u,h=n(e),f=n(i),d=0,m=0,g=!1,p=o.length;d=n;){const e=eh.keys().next().value;eh.delete(e)}eh.set(e,t)}}const nh={};let rh=null,ih=null,oh=null,ah=null,sh=null,ch=null,lh=null;function uh(e){return e&&"object"==typeof e&&!Array.isArray(e)}function hh(e){const t=[];return JSON.stringify(e,function(e,n){if("object"==typeof n&&null!==n){if(-1!==t.indexOf(n))return;t.push(n)}return n})}const fh={messages:[],logLevelConsole:"warning",logLevelSystem:"error",systemTargetLogLevel:"",addMessage:function(e,t){if(!Yu.debug)return;const n={level:e,date:(new Date).toISOString(),text:"",objs:[],stack:""},r=new Error;r.stack&&(n.stack=r.stack.toString().replace(/^Error\r?\n/,""));for(let e=0;e1e3&&fh.messages.shift()},systemLog(e,t){ih&&ih.hass&&ih.hass.callService("system_log","write",{logger:"frontend.wallpanel".concat(ah?"."+ah:""),message:t,level:fh.systemTargetLogLevel||e},void 0,!1).catch(()=>{})},downloadMessages:function(){const e=new Blob([hh(fh.messages)],{type:"text/plain"}),t=URL.createObjectURL(e),n=document.createElement("a");n.href=t,n.target="_blank",n.download="wallpanel_log.txt",document.body.appendChild(n),n.click(),n.remove(),setTimeout(()=>URL.revokeObjectURL(t),0)},purgeMessages:function(){fh.messages=[]},debug:function(){["debug"].includes(fh.logLevelConsole)&&console.debug.apply(this,arguments),["debug"].includes(fh.logLevelSystem)&&fh.systemLog("debug",hh(arguments)),fh.addMessage("debug",arguments)},info:function(){["debug","info"].includes(fh.logLevelConsole)&&console.info.apply(this,arguments),["debug","info"].includes(fh.logLevelSystem)&&fh.systemLog("info",hh(arguments)),fh.addMessage("info",arguments)},log:function(){fh.info.apply(this,arguments)},warning:function(){["debug","info","warning"].includes(fh.logLevelConsole)&&console.warn.apply(this,arguments),["debug","info","warning"].includes(fh.logLevelSystem)&&fh.systemLog("warning",hh(arguments)),fh.addMessage("warning",arguments)},warn:function(){fh.warning.apply(this,arguments)},error:function(){if(["debug","info","warning","error"].includes(fh.logLevelConsole)&&console.error.apply(this,arguments),["debug","info","warning","error"].includes(fh.logLevelSystem)&&fh.systemLog("error",hh(arguments)),fh.addMessage("error",arguments),Yu.alert_errors){const e="Wallpanel error: ".concat(hh(arguments));Qu?Qu.showMessage("error","Error",e,1e4):alert(e)}},err:function(){fh.error.apply(this,arguments)}};class dh{constructor(){this.enabled=!1,this.error=null,this.nativeWakeLockSupported="wakeLock"in navigator,this._lock=null,this._player=null,this._isPlaying=!1,this._onWakeLockRelease=null;const e=()=>{fh.debug("handleVisibilityChange"),this.enabled&&!document.hidden&&this.enable()};if(document.addEventListener("visibilitychange",e),document.addEventListener("fullscreenchange",e),!this.nativeWakeLockSupported){const e="data:video/mp4;base64,AAAAIGZ0eXBpc29tAAACAGlzb21pc28yYXZjMW1wNDEAAAAIZnJlZQAAA1NtZGF0AAACrwYF//+r3EXpvebZSLeWLNgg2SPu73gyNjQgLSBjb3JlIDE2NCByMzA5NSBiYWVlNDAwIC0gSC4yNjQvTVBFRy00IEFWQyBjb2RlYyAtIENvcHlsZWZ0IDIwMDMtMjAyMiAtIGh0dHA6Ly93d3cudmlkZW9sYW4ub3JnL3gyNjQuaHRtbCAtIG9wdGlvbnM6IGNhYmFjPTEgcmVmPTMgZGVibG9jaz0xOi0zOi0zIGFuYWx5c2U9MHgzOjB4MTEzIG1lPWhleCBzdWJtZT03IHBzeT0xIHBzeV9yZD0yLjAwOjAuNzAgbWl4ZWRfcmVmPTEgbWVfcmFuZ2U9MTYgY2hyb21hX21lPTEgdHJlbGxpcz0xIDh4OGRjdD0xIGNxbT0wIGRlYWR6b25lPTIxLDExIGZhc3RfcHNraXA9MSBjaHJvbWFfcXBfb2Zmc2V0PS00IHRocmVhZHM9MSBsb29rYWhlYWRfdGhyZWFkcz0xIHNsaWNlZF90aHJlYWRzPTAgbnI9MCBkZWNpbWF0ZT0xIGludGVybGFjZWQ9MCBibHVyYXlfY29tcGF0PTAgY29uc3RyYWluZWRfaW50cmE9MCBiZnJhbWVzPTMgYl9weXJhbWlkPTIgYl9hZGFwdD0xIGJfYmlhcz0wIGRpcmVjdD0xIHdlaWdodGI9MSBvcGVuX2dvcD0wIHdlaWdodHA9MiBrZXlpbnQ9MjUwIGtleWludF9taW49MSBzY2VuZWN1dD00MCBpbnRyYV9yZWZyZXNoPTAgcmNfbG9va2FoZWFkPTQwIHJjPWNyZiBtYnRyZWU9MSBjcmY9MjMuMCBxY29tcD0wLjYwIHFwbWluPTAgcXBtYXg9NjkgcXBzdGVwPTQgaXBfcmF0aW89MS40MCBhcT0xOjEuMjAAgAAAABFliIQAF85//vfUt8yy7VNwgQAAAAlBmiRsQXzn/vAAAAAJQZ5CeIL5z4aBAAAACQGeYXRBfOeGgAAAAAkBnmNqQXznhoEAAAAPQZpoSahBaJlMCC+c//7xAAAAC0GehkURLBfOf4aBAAAACQGepXRBfOeGgQAAAAkBnqdqQXznhoAAAAAPQZqpSahBbJlMCC+c//7wAAADs21vb3YAAABsbXZoZAAAAAAAAAAAAAAAAAAAA+gAACcQAAEAAAEAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAIAAALddHJhawAAAFx0a2hkAAAAAwAAAAAAAAAAAAAAAQAAAAAAACcQAAAAAAAAAAAAAAAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAQAAAAAAIAAAACAAAAAAAJGVkdHMAAAAcZWxzdAAAAAAAAAABAAAnEAAAgAAAAQAAAAACVW1kaWEAAAAgbWRoZAAAAAAAAAAAAAAAAAAAQAAAAoAAVcQAAAAAAC1oZGxyAAAAAAAAAAB2aWRlAAAAAAAAAAAAAAAAVmlkZW9IYW5kbGVyAAAAAgBtaW5mAAAAFHZtaGQAAAABAAAAAAAAAAAAAAAkZGluZgAAABxkcmVmAAAAAAAAAAEAAAAMdXJsIAAAAAEAAAHAc3RibAAAAMBzdHNkAAAAAAAAAAEAAACwYXZjMQAAAAAAAAABAAAAAAAAAAAAAAAAAAAAAAAIAAgASAAAAEgAAAAAAAAAARVMYXZjNTkuMzcuMTAwIGxpYngyNjQAAAAAAAAAAAAAABj//wAAADZhdmNDAWQACv/hABlnZAAKrNlfllwEQAAAAwBAAAADAIPEiWWAAQAGaOvjxMhM/fj4AAAAABBwYXNwAAAAAQAAAAEAAAAUYnRydAAAAAAAAAKiAAACogAAABhzdHRzAAAAAAAAAAEAAAAKAABAAAAAABRzdHNzAAAAAAAAAAEAAAABAAAAYGN0dHMAAAAAAAAACgAAAAEAAIAAAAAAAQABQAAAAAABAACAAAAAAAEAAAAAAAAAAQAAQAAAAAABAAFAAAAAAAEAAIAAAAAAAQAAAAAAAAABAABAAAAAAAEAAIAAAAAAHHN0c2MAAAAAAAAAAQAAAAEAAAAKAAAAAQAAADxzdHN6AAAAAAAAAAAAAAAKAAACyAAAAA0AAAANAAAADQAAAA0AAAATAAAADwAAAA0AAAANAAAAEwAAABRzdGNvAAAAAAAAAAEAAAAwAAAAYnVkdGEAAABabWV0YQAAAAAAAAAhaGRscgAAAAAAAAAAbWRpcmFwcGwAAAAAAAAAAAAAAAAtaWxzdAAAACWpdG9vAAAAHWRhdGEAAAABAAAAAExhdmY1OS4yNy4xMDA=";this._player=document.createElement("video"),this._player.setAttribute("id","ScreenWakeLockVideo"),this._player.setAttribute("src",e),this._player.setAttribute("playsinline",""),this._player.setAttribute("muted",""),this._player.style.display="none",document.getElementById("ScreenWakeLockVideo")||document.body.appendChild(this._player),this._player.addEventListener("ended",()=>{fh.debug("Video ended"),this.enabled&&this.enable()}),this._player.addEventListener("playing",()=>{fh.debug("Video playing"),this._isPlaying=!0}),this._player.addEventListener("pause",()=>{fh.debug("Video pause"),this._isPlaying=!1})}}enable(){if(this.nativeWakeLockSupported){if(fh.debug("Requesting native screen wakelock"),this._lock)return void fh.debug("Screen wakelock already active");navigator.wakeLock.request("screen").then(e=>{fh.debug("Request screen wakelock successful"),this._lock=e,this._onWakeLockRelease=()=>{fh.debug("Screen wakelock released"),this.enabled=!1,this._lock=null},e.addEventListener("release",this._onWakeLockRelease),this.enabled=!0,this.error=null}).catch(e=>{this.enabled=!1,this.error=e,fh.warning("Failed to request screen wakeLock: ".concat(e))})}else{fh.debug("Starting video player"),!this._player.paused&&this._isPlaying&&this._player.pause();const e=this._player.play();e&&e.then(()=>{this.enabled=!0,this.error=null,fh.debug("Video play successful")}).catch(e=>{this.enabled=!1,this.error=e,fh.warning("Failed to play video: ".concat(e))})}}disable(){this.nativeWakeLockSupported?(fh.debug("Releasing native screen wakelock"),this._lock&&(this._onWakeLockRelease&&(this._lock.removeEventListener("release",this._onWakeLockRelease),this._onWakeLockRelease=null),this._lock.release()),this._lock=null):(fh.debug("Stopping video player"),!this._player.paused&&this._isPlaying&&this._player.pause()),this.enabled=!1}}class mh{constructor(){this.enabled=!1,this.error=!1,this.width=64,this.height=48,this.thresholdRatio=.05,this.threshold=this.width*this.height*this.thresholdRatio,this.captureInterval=300,this.motionActive=!1,this.motionStartDelay=0,this.motionStopDelay=2e3,this.motionDetectionRequiredRatio=.8,this.roomSettledRequiredRatio=1,this.motionStopTimeout=null,this.captureTimer=null,this.referenceImageData=null,this.previousImageData=null,this.referenceChangeRatios=[],this.previousChangeRatios=[],this.maxRatioHistorySize=Math.ceil(6e4/this.captureInterval),this.videoElement=document.createElement("video"),this.videoElement.setAttribute("id","wallpanelMotionDetectionVideo"),this.videoElement.style.display="none",document.body.appendChild(this.videoElement),this.canvasElement=document.createElement("canvas"),this.canvasElement.setAttribute("id","wallpanelMotionDetectionCanvas"),this.canvasElement.style.display="none",document.body.appendChild(this.canvasElement),this.context=this.canvasElement.getContext("2d",{willReadFrequently:!0}),this._elementsAppended=!0}compareImageData(e,t){if(!e||!t)return 0;let n=0;const r=e.data,i=t.data;for(let e=0;e=256&&n++}return n/(this.width*this.height)}storeRatio(e,t){for(e.push(t);e.length>this.maxRatioHistorySize;)e.shift()}getRatiosForInterval(e,t){const n=Math.max(1,Math.ceil(t/this.captureInterval));return e.length0&&e.filter(e=>e>=this.thresholdRatio).length/e.length>=t}hasRequiredRatioBelowThreshold(e,t){return e.length>0&&e.filter(e=>e=t}resetMotionState(){this.motionActive=!1,this.motionStopTimeout&&(clearTimeout(this.motionStopTimeout),this.motionStopTimeout=null),this.referenceImageData=null,this.previousImageData=null,this.referenceChangeRatios=[],this.previousChangeRatios=[]}capture(){this.context.globalCompositeOperation="source-over",this.context.drawImage(this.videoElement,0,0,this.width,this.height);const e=this.context.getImageData(0,0,this.width,this.height);if(!this.referenceImageData)return this.referenceImageData=e,void(this.previousImageData=e);const t=this.compareImageData(e,this.referenceImageData),n=this.compareImageData(e,this.previousImageData);this.previousImageData=e,this.storeRatio(this.referenceChangeRatios,t),this.storeRatio(this.previousChangeRatios,n);const r=this.getRatiosForInterval(this.referenceChangeRatios,this.motionStartDelay),i=this.hasRequiredRatioAboveThreshold(r,this.motionDetectionRequiredRatio),o=this.previousChangeRatios.length>=this.maxRatioHistorySize&&this.hasRequiredRatioBelowThreshold(this.previousChangeRatios,this.roomSettledRequiredRatio);i?(this.motionActive||(fh.debug("Motion started"),this.motionActive=!0,Qu.motionDetected()),this.motionStopTimeout&&(clearTimeout(this.motionStopTimeout),this.motionStopTimeout=null)):this.motionActive&&!this.motionStopTimeout&&(this.motionStopTimeout=setTimeout(()=>{fh.debug("Motion stopped"),this.motionActive=!1,this.motionStopTimeout=null,Qu.motionStopped()},this.motionStopDelay)),o&&(fh.info("Room settled, new reference image stored"),this.referenceImageData=e,this.referenceChangeRatios=[])}start(){if(!this.enabled&&!this.error){if(!navigator.mediaDevices)return this.error=!0,void fh.error("No media devices found");this._elementsAppended||(document.body.appendChild(this.videoElement),document.body.appendChild(this.canvasElement),this._elementsAppended=!0),this.enabled=!0,this.width=Yu.camera_motion_detection_capture_width,this.height=Yu.camera_motion_detection_capture_height,this.thresholdRatio=.01*Yu.camera_motion_detection_threshold,this.threshold=this.width*this.height*this.thresholdRatio,this.captureInterval=1e3*Yu.camera_motion_detection_capture_interval,this.motionStartDelay=1e3*Yu.camera_motion_detection_motion_start_delay,this.motionStopDelay=1e3*Yu.camera_motion_detection_motion_stop_delay,this.maxRatioHistorySize=Math.ceil(6e4/this.captureInterval),this.resetMotionState(),this.videoElement.width=this.width,this.videoElement.height=this.height,this.canvasElement.width=this.width,this.canvasElement.height=this.height,Yu.camera_motion_detection_capture_visible?(this.canvasElement.style.position="fixed",this.canvasElement.style.top=0,this.canvasElement.style.left=0,this.canvasElement.style.zIndex=1e4,this.canvasElement.style.border="1px solid black",this.canvasElement.style.display="block"):this.canvasElement.style.display="none",navigator.mediaDevices.getUserMedia({audio:!1,video:{facingMode:{ideal:Yu.camera_motion_detection_facing_mode},width:this.width,height:this.height}}).then(e=>{this.videoElement.srcObject=e;const t=this.videoElement.play();t&&t.catch(e=>{fh.warning("Camera motion detection video play failed: ".concat(e))}),this.enabled&&(this.captureTimer=setInterval(this.capture.bind(this),this.captureInterval))}).catch(e=>{this.enabled=!1,fh.error("Camera motion detection error:",e),this._elementsAppended&&(this.videoElement.remove(),this.canvasElement.remove(),this._elementsAppended=!1)})}}stop(){if(!this.enabled)return;this.enabled=!1,this.resetMotionState(),this.captureTimer&&(clearInterval(this.captureTimer),this.captureTimer=null),this.videoElement.pause();const e=this.videoElement.srcObject;e&&"function"==typeof e.getTracks&&e.getTracks().forEach(e=>{e.stop()}),this.videoElement.srcObject=null,this._elementsAppended&&(this.videoElement.remove(),this.canvasElement.remove(),this._elementsAppended=!1)}}function gh(e){const t=e.slice();for(let e=t.length-1;e>0;e--){const n=Math.floor(Math.random()*(e+1));[t[e],t[n]]=[t[n],t[e]]}return t}function ph(e){if(!e.shadowRoot)return[null,null];const t=e.shadowRoot.querySelector("ha-web-rtc-player")||e.shadowRoot.querySelector("ha-hls-player");if(!t||!t.shadowRoot)return[t,null];const n=t.shadowRoot.querySelector("video");return[t,n]}function vh(e){for(var t=arguments.length,n=new Array(t>1?t-1:0),r=1;rc(e)):c(a),Array.isArray(e[o])&&"string"==typeof a&&(a=a.split(",").map(e=>e.trim())),Object.assign(e,{[o]:a})}}return vh(e,...n)}function yh(){const e=new URLSearchParams(window.location.search),t=Yu;Yu={},vh(Yu,Hu),0===Object.keys(qu).length&&(qu=xh(),0===Object.keys(qu).length&&fh.debug("No wallpanel config found in dashboard config")),vh(Yu,qu);const n={};for(let[t,r]of e)if(t.startsWith("wp_")&&(t=t.substring(3),Gu[t]&&(fh.warn("The configuration option '".concat(t,"' has been renamed to '").concat(Gu[t],"'. Please update your wallpanel configuration accordingly.")),t=Gu[t]),t in Hu&&r)){try{r=JSON.parse(r)}catch(e){}const e=Hu[t];if(Array.isArray(e))Array.isArray(r)?n[t]=r:n[t]="string"==typeof r?r.split(",").map(e=>e.trim()):[r];else if("boolean"==typeof e)n[t]=["true","on","yes","1"].includes(String(r));else if("number"==typeof e){const e=Number(r);Number.isFinite(e)?n[t]=e:fh.warn("Invalid numeric wp_ param '".concat(t,"': ").concat(r))}else n[t]=r}Yu=vh(Yu,n);const r=Yu.profile;if(Yu.profiles&&r&&Yu.profiles[r]&&(Yu=vh(Yu,Yu.profiles[r]),fh.debug("Profile set from config: ".concat(r))),Yu.profiles&&ah&&Yu.profiles["device.".concat(ah)]){const e="device.".concat(ah);Yu=vh(Yu,Yu.profiles[e]),fh.debug("Profile set from device: ".concat(e))}if(Yu.profiles){const e=[sh,ch,lh];for(let t=0;t{t.tagName.toLowerCase().startsWith("browser-mod-popup")&&t.shadowRoot&&t.shadowRoot.querySelector("ha-dialog")&&e.push(t)}),e):e}function _h(){const e=Ah();return e&&0!=e.length?e[0]:null}function bh(){if("1"==new URLSearchParams(window.location.search).get("edit"))return fh.debug("Edit mode active"),!1;if(!Yu.enabled)return fh.debug("Wallpanel not enabled in config"),!1;if(Yu.enabled_on_views&&Yu.enabled_on_views.length>0&&$u&&!Yu.enabled_on_views.includes($u))return fh.debug("Wallpanel not enabled on current tab ".concat($u)),!1;if(Yu.disable_screensaver_when_assist_active){const e=ih.shadowRoot.querySelector("ha-voice-command-dialog");if(e&&e.shadowRoot&&e.shadowRoot.querySelector("ha-dialog"))return fh.debug("Assist is active, wallpanel disabled"),!1}return Qu&&Qu.disable_screensaver_on_browser_mod_popup_function&&_h()&&Qu.disable_screensaver_on_browser_mod_popup_function(_h())?(fh.debug("Browser mod popup function returned true, wallpanel disabled"),!1):!Yu.disable_screensaver_on_browser_mod_popup||!_h()||(fh.debug("Browser mod popup active, wallpanel disabled"),!1)}function wh(){return Yu.show_images&&Yu.image_url?Yu.image_url.startsWith("media-entity-video://")?"media-entity-video":Yu.image_url.startsWith("media-entity-image://")?"media-entity-image":Yu.image_url.startsWith("media-source://")?"media-source":Yu.image_url.startsWith("https://api.unsplash")?"unsplash-api":Yu.image_url.startsWith("immich+")?"immich-api":Yu.image_url.startsWith("iframe+")?"iframe":Yu.image_url.startsWith("embed+")?"embed":"url":""}function Eh(){try{return oh.shadowRoot.querySelector("ha-panel-lovelace")}catch(e){fh.error("Failed to get ha-panel-lovelace:",e)}}function xh(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:[];const t=Eh(),n={};if(t&&t.lovelace){let r;t.lovelace.config&&t.lovelace.config.wallpanel?r=t.lovelace.config.wallpanel:t.lovelace.rawConfig&&t.lovelace.rawConfig.wallpanel&&(r=t.lovelace.rawConfig.wallpanel),r&&(0===e.length&&(e=Object.keys(r)),e.forEach(e=>{(e in Hu||e in Gu)&&(n[e]=r[e])}))}return n}function Sh(e){fh.debug("setSidebarVisibility: hidden=".concat(e));try{const t=oh.shadowRoot.querySelector("ha-panel-lovelace");if(t){const n=t.shadowRoot.querySelector("hui-root");if(n){const t=n.shadowRoot.querySelector("ha-menu-button");t&&(e?t.style.display="none":t.style.removeProperty("display"))}}}catch(e){fh.warn(e)}try{const t=oh.shadowRoot.querySelector("ha-drawer");if(t){const n=t.shadowRoot.querySelector(".sidebar-shell")||t.shadowRoot.querySelector("aside"),r=n&&n.classList.contains("sidebar-shell")?"--ha-sidebar-width":"--mdc-drawer-width";n&&(e?(n.style.opacity=0,n.style.maxWidth="0px",oh.style.setProperty(r,"env(safe-area-inset-left)")):(n.style.opacity=1,n.style.maxWidth="",oh.style.removeProperty(r)),window.dispatchEvent(new Event("resize")))}}catch(e){fh.warn(e)}}function Ch(e,t){fh.debug("setToolbarVisibility: hideToolbar=".concat(e,", hideActionItems=").concat(t));try{const n=oh.shadowRoot.querySelector("ha-panel-lovelace");if(!n)return;let r=n.shadowRoot.querySelector("hui-root");if(!r)return;r=r.shadowRoot;const i=r.querySelector("#view");let o=r.querySelector("app-toolbar");if(o||(o=r.querySelector("div.toolbar")),e)o.style.setProperty("display","none"),Yu.keep_toolbar_space||(i.style.minHeight="100vh",i.style.marginTop="0",i.style.paddingTop="0");else{o.style.removeProperty("display"),i.style.removeProperty("min-height"),i.style.removeProperty("margin-top"),i.style.removeProperty("padding-top");const e=o.querySelector("div.action-items");t?e.style.setProperty("display","none"):e.style.setProperty("display","flex")}window.dispatchEvent(new Event("resize"))}catch(e){fh.warn(e)}}function Ih(){return void 0!==document.fullscreenElement?Boolean(document.fullscreenElement):void 0!==document.webkitCurrentFullScreenElement&&Boolean(document.webkitCurrentFullScreenElement)}function Th(){fh.debug("Enter fullscreen");const e=document.documentElement;e.requestFullscreen?e.requestFullscreen().then(()=>{fh.debug("Successfully requested fullscreen")},e=>{fh.debug("Failed to enter fullscreen:",e)}):e.mozRequestFullScreen?e.mozRequestFullScreen():e.msRequestFullscreen?e.msRequestFullscreen():e.webkitRequestFullscreen&&e.webkitRequestFullscreen()}function Rh(){const e=customElements.get("hui-view-container"),t=customElements.get("hui-view");if(!e||!t){throw new Error("Failed to get hui-view-container / hui-view from customElements")}class r extends e{}class i extends t{constructor(){super(),this.mediaList=[],this.mediaIndex=-1,this.mediaListDirection="forwards",this.lastMediaListUpdate,this.updatingMediaList=!1,this.updatingMedia=!1,this.lastMediaUpdate=0,this.isPaused=!1,this.mediaTimeElapsedBeforePause=0,this.lastImageUrlEntityValue=null,this.blockEventsUntil=0,this.screensaverStartedAt,this.screensaverStoppedAt=new Date,this.infoBoxContentCreatedDate,this.idleSince=Date.now(),this.lastProfileSet=Yu.profile,this.lastMove=null,this.lastCorner=0,this.translateTimeout=null,this.lastClickTime=0,this.clickCount=0,this.touchStartX=-1,this.currentWidth=0,this.currentHeight=0,this.energyCollectionUpdateEnabled=!1,this.energyCollectionUpdateInterval=60,this.lastEnergyCollectionUpdate=0,this.screensaverStopNavigationPathTimeout=null,this.disable_screensaver_on_browser_mod_popup_function=null,this.moreInfoDialogOpenedAt=0,this.timerInterval=null,this.windowEventHandlers=[],this.infoBoxResizeObserver=null,this.screenWakeLock=new dh,this.cameraMotionDetection=new mh,this.lovelace=null,this.__hass=ih.hass||ih.__hass,this.__cards=[],this.__badges=[],this.__views=[],ih.provideHass(this),this.timerInterval=setInterval(this.timer.bind(this),1e3),Ju.hass=this.__hass}set hass(e){fh.debug("Update hass"),this.__hass=e;let t=!1;for(const e in nh){const n=this.__hass.states[e];n&&n.state!=nh[e]&&(nh[e]=n.state,t=!0)}if(!this.updateProfile()&&t&&yh(),!bh())return;const n=Yu.screensaver_entity;if(n&&this.__hass.states[n]){const e=new Date(this.__hass.states[n].last_changed),t=this.__hass.states[n].state;"off"==t&&this.screensaverStartedAt&&e.getTime()-this.screensaverStartedAt>0?this.stopScreensaver(Yu.fade_out_time_screensaver_entity):"on"==t&&this.screensaverStoppedAt&&e.getTime()-this.screensaverStoppedAt>0&&this.startScreensaver()}this.screensaverRunning()&&(this.__cards.forEach(e=>{e.hass=this.hass}),this.__badges.forEach(e=>{e.hass=this.hass}),this.__views.forEach(e=>{e.hass=this.hass}),"media-entity-image"==wh()&&this.switchActiveMedia("entity_update"))}get hass(){return this.__hass}setScreensaverEntityState(){const e=Yu.screensaver_entity;if(!e||!this.__hass.states[e])return;if(this.screensaverRunning()&&"on"==this.__hass.states[e].state)return;if(!this.screensaverRunning()&&"off"==this.__hass.states[e].state)return;const t=this.screensaverRunning()?"turn_on":"turn_off";fh.debug("Updating screensaver_entity",e,t),this.__hass.callService("input_boolean",t,{entity_id:e}).then(e=>{fh.debug(e)},e=>{fh.error("Failed to set screensaver entity state:",e)})}setImageURLEntityState(){const e=Yu.image_url_entity;if(!e||!this.__hass.states[e])return;const t=this.getActiveMediaElement();if(!t||!t.mediaUrl)return;const n=t.mediaUrl.substring(0,255);this.lastImageUrlEntityValue!==n&&(fh.debug("Updating image_url_entity",e,n),this.__hass.callService("input_text","set_value",{entity_id:e,value:n}).then(e=>{fh.debug(e),this.lastImageUrlEntityValue=n},e=>{fh.error("Failed to set image url entity state:",e)}))}updateProfile(){const e=Yu.profile_entity;if(e&&this.__hass.states[e]){const t=this.__hass.states[e].state;if(t&&t!=this.lastProfileSet||!t&&this.lastProfileSet)return fh.debug("Set profile to ".concat(t)),this.lastProfileSet=t,yh(),!0}return!1}timer(){Yu.enabled&&Xu&&(this.screensaverRunning()?Yu.disable_screensaver_on_browser_mod_popup&&_h()?this.stopScreensaver(Yu.fade_out_time_browser_mod_popup):this.updateScreensaver():bh()&&Yu.idle_time>0&&Date.now()-this.idleSince>=1e3*Yu.idle_time&&(Yu.camera_motion_detection_stop_screensaver&&this.cameraMotionDetection&&this.cameraMotionDetection.motionActive||this.startScreensaver()))}setDefaultStyle(){this.messageContainer.removeAttribute("style"),this.messageContainer.style.position="fixed",this.messageContainer.style.top="1rem",this.messageContainer.style.left="1rem",this.messageContainer.style.bottom="1rem",this.messageContainer.style.right="1rem",this.messageContainer.style.alignItems="flex-end",this.messageContainer.style.display="flex",this.messageContainer.style.flexDirection="column",this.messageContainer.style.gap="0.5rem",this.messageContainer.style.zIndex=this.style.zIndex+1,this.messageContainer.style.pointerEvents="none",this.messageContainer.style.visibility="hidden",this.debugBox.removeAttribute("style"),this.debugBox.style.position="fixed",this.debugBox.style.pointerEvents="none",this.debugBox.style.top="0%",this.debugBox.style.left="0%",this.debugBox.style.width="100%",this.debugBox.style.height="100%",this.debugBox.style.background="#00000099",this.debugBox.style.color="#ffffff",this.debugBox.style.zIndex=this.style.zIndex+2,this.debugBox.style.fontFamily="monospace",this.debugBox.style.fontSize="12px",this.debugBox.style.overflowWrap="break-word",this.debugBox.style.overflowY="auto",this.debugBox.style.visibility="hidden",this.screensaverContainer.removeAttribute("style"),this.screensaverContainer.style.position="fixed",this.screensaverContainer.style.pointerEvents="auto",this.screensaverContainer.style.top=0,this.screensaverContainer.style.right=0,this.screensaverContainer.style.bottom=0,this.screensaverContainer.style.left=0,this.screensaverContainer.style.background="#000000",this.screensaverContainer.style.overflow="hidden",this.imageOneContainer.removeAttribute("style"),this.imageOneContainer.style.opacity=0,this.imageOneContainer.style.position="absolute",this.imageOneContainer.style.pointerEvents="none",this.imageOneContainer.style.top=0,this.imageOneContainer.style.left=0,this.imageOneContainer.style.width="100%",this.imageOneContainer.style.height="100%",this.imageOneContainer.style.border="none",this.imageOneBackground.style.position="absolute",this.imageOneBackground.style.pointerEvents="none",this.imageOneBackground.style.top=0,this.imageOneBackground.style.left=0,this.imageOneBackground.style.width="100%",this.imageOneBackground.style.height="100%",this.imageOneBackground.style.border="none",this.imageOne.removeAttribute("style"),this.imageOne.style.position="relative",this.imageOne.style.pointerEvents="none",this.imageOne.style.width="100%",this.imageOne.style.height="100%",this.imageOne.style.border="none",this.imageOneInfoContainer.removeAttribute("style"),this.imageOneInfoContainer.style.position="absolute",this.imageOneInfoContainer.style.pointerEvents="none",this.imageOneInfoContainer.style.top=0,this.imageOneInfoContainer.style.left=0,this.imageOneInfoContainer.style.width="100%",this.imageOneInfoContainer.style.height="100%",this.imageOneInfoContainer.style.border="none",this.imageOneInfo.style.overflowY="auto",this.imageTwoContainer.removeAttribute("style"),this.imageTwoContainer.style.opacity=0,this.imageTwoContainer.style.position="absolute",this.imageTwoContainer.style.pointerEvents="none",this.imageTwoContainer.style.top=0,this.imageTwoContainer.style.left=0,this.imageTwoContainer.style.width="100%",this.imageTwoContainer.style.height="100%",this.imageTwoContainer.style.border="none",this.imageTwoBackground.style.position="absolute",this.imageTwoBackground.style.pointerEvents="none",this.imageTwoBackground.style.top=0,this.imageTwoBackground.style.left=0,this.imageTwoBackground.style.width="100%",this.imageTwoBackground.style.height="100%",this.imageTwoBackground.style.border="none",this.imageTwo.removeAttribute("style"),this.imageTwo.style.position="relative",this.imageTwo.style.pointerEvents="none",this.imageTwo.style.width="100%",this.imageTwo.style.height="100%",this.imageTwo.style.border="none",this.imageTwoInfoContainer.removeAttribute("style"),this.imageTwoInfoContainer.style.position="absolute",this.imageTwoInfoContainer.style.pointerEvents="none",this.imageTwoInfoContainer.style.top=0,this.imageTwoInfoContainer.style.left=0,this.imageTwoInfoContainer.style.width="100%",this.imageTwoInfoContainer.style.height="100%",this.imageTwoInfoContainer.style.border="none",this.imageTwoInfo.style.overflowY="auto",this.screensaverImageOverlay.removeAttribute("style"),this.screensaverImageOverlay.style.position="absolute",this.screensaverImageOverlay.style.top=0,this.screensaverImageOverlay.style.left=0,this.screensaverImageOverlay.style.width="100%",this.screensaverImageOverlay.style.height="100%",this.screensaverImageOverlay.style.background="#00000000",this.infoContainer.removeAttribute("style"),this.infoContainer.style.position="absolute",this.infoContainer.style.pointerEvents="none",this.infoContainer.style.top=0,this.infoContainer.style.left=0,this.infoContainer.style.width="100%",this.infoContainer.style.height="100%",this.infoContainer.style.transition="opacity 2000ms ease-in-out",this.infoContainer.style.padding="25px",this.infoContainer.style.boxSizing="border-box",this.infoBox.removeAttribute("style"),this.infoBox.style.pointerEvents="none",this.infoBox.style.width="fit-content",this.infoBox.style.maxHeight="100%",this.infoBox.style.borderRadius="10px",this.infoBox.style.overflowY="auto",this.infoBox.style.scrollbarWidth="none",this.infoBox.style.setProperty("--wp-card-width","500px"),this.infoBox.style.setProperty("--wp-card-padding","0"),this.infoBox.style.setProperty("--wp-card-margin","5px"),this.infoBox.style.setProperty("--wp-card-backdrop-filter","none"),this.infoBox.style.setProperty("--wp-badges-minwidth","200px"),this.infoBoxPosX.style.pointerEvents="none",this.infoBoxPosX.style.height="100%",this.infoBoxPosX.style.width="100%",this.infoBoxPosY.style.pointerEvents="none",this.infoBoxPosY.style.height="100%",this.infoBoxPosY.style.width="100%",this.infoBoxContent.style.pointerEvents="none",this.infoBoxContent.style.width="fit-content",this.infoBoxContent.style.height="100%",this.infoBoxContent.style.display="grid",this.fixedInfoContainer.removeAttribute("style"),this.fixedInfoContainer.style.position="fixed",this.fixedInfoContainer.style.pointerEvents="none",this.fixedInfoContainer.style.top=0,this.fixedInfoContainer.style.left=0,this.fixedInfoContainer.style.width="100%",this.fixedInfoContainer.style.height="100%",this.fixedInfoBox.style.pointerEvents="none",this.fixedInfoBox.style.cssText=this.infoBox.style.cssText,this.screensaverOverlay.removeAttribute("style"),this.screensaverOverlay.style.position="absolute",this.screensaverOverlay.style.pointerEvents="none",this.screensaverOverlay.style.top=0,this.screensaverOverlay.style.left=0,this.screensaverOverlay.style.width="100%",this.screensaverOverlay.style.height="100%",this.screensaverOverlay.style.background="#00000000"}updateStyle(){this.screensaverOverlay.style.background="#00000000",this.debugBox.style.visibility=Yu.debug?"visible":"hidden",this.debugBox.style.pointerEvents=Yu.debug?"auto":"none",this.style.transition=Yu.fade_in_time>0?"opacity ".concat(Math.round(1e3*Yu.fade_in_time),"ms ease-in-out"):"",this.imageOneContainer.style.transition=Yu.crossfade_time>0?"opacity ".concat(Math.round(1e3*Yu.crossfade_time),"ms ease-in-out"):"",this.imageTwoContainer.style.transition=Yu.crossfade_time>0?"opacity ".concat(Math.round(1e3*Yu.crossfade_time),"ms ease-in-out"):"",this.messageContainer.style.visibility=this.screensaverRunning()?"visible":"hidden",this.screensaverImageOverlay.style.pointerEvents=Yu.content_interaction?"none":"auto";const e=Yu.show_image_info&&Boolean(Yu.image_info_template);if(this.imageOneInfoContainer.style.display=e?"block":"none",this.imageTwoInfoContainer.style.display=e?"block":"none",Yu.info_animation_duration_x?this.infoBoxPosX.style.animation="moveX ".concat(Yu.info_animation_duration_x,"s ").concat(Yu.info_animation_timing_function_x," infinite alternate"):this.infoBoxPosX.style.animation="",Yu.info_animation_duration_y?this.infoBoxPosY.style.animation="moveY ".concat(Yu.info_animation_duration_y,"s ").concat(Yu.info_animation_timing_function_y," infinite alternate"):this.infoBoxPosY.style.animation="",Yu.style)for(const e in Yu.style)if(e.startsWith("wallpanel-")&&"wallpanel-shadow-host"!=e&&"wallpanel-screensaver-info-box-badges"!=e&&"wallpanel-screensaver-info-box-views"!=e&&!Ku[e]){const t=this.shadowRoot.getElementById(e);if(t){fh.debug("Setting style attributes for element #".concat(e));for(const n in Yu.style[e])fh.debug("Setting style attribute ".concat(n," to ").concat(Yu.style[e][n])),t.style.setProperty(n,Yu.style[e][n]);t==this.infoBox?this.fixedInfoBox.style.cssText=this.infoBox.style.cssText:t==this.infoBoxContent&&(this.fixedInfoBoxContent.style.cssText=this.infoBoxContent.style.cssText)}else fh.error("Element #".concat(e," not found"))}}updateShadowStyle(){const e=getComputedStyle(this.infoContainer),t=this.infoContainer.offsetWidth-parseInt(e.paddingLeft)-parseInt(e.paddingRight)-this.infoBox.offsetWidth,n=this.infoContainer.offsetHeight-parseInt(e.paddingTop)-parseInt(e.paddingBottom)-this.infoBox.offsetHeight;let r="";if(Yu.style){if(Yu.style["wallpanel-shadow-host"])for(const e in Yu.style["wallpanel-shadow-host"])r+="".concat(e,": ").concat(Yu.style["wallpanel-shadow-host"][e],";\n");for(const e in Ku)Yu.style[e]&&vh(Ku[e],Yu.style[e])}let i="";for(const e in Ku){i+=".".concat(e," {\n");for(const t in Ku[e])i+="".concat(t,": ").concat(Ku[e][t],";\n");i+="}\n"}this.shadowStyle.innerHTML="\n\t\t\t\t:host {\n\t\t\t\t\t".concat(r,"\n\t\t\t\t}\n\t\t\t\t@keyframes moveX {\n\t\t\t\t\t100% {\n\t\t\t\t\t\ttransform: translate3d(").concat(t,"px, 0, 0);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t@keyframes moveY {\n\t\t\t\t\t100% {\n\t\t\t\t\t\ttransform: translate3d(0, ").concat(n,"px, 0);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t@keyframes horizontalProgress {\n\t\t\t\t\t0% {\n\t\t\t\t\t\twidth: 0%;\n\t\t\t\t\t}\n\t\t\t\t\t100% {\n\t\t\t\t\t\twidth: 100%;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t@keyframes kenBurnsEffect-experimental {\n\t\t\t\t\t0% {\n\t\t\t\t\t\ttransform: scale(1.0) translateX(calc(var(--hidden-width) / -2 * 1px)) translateY(calc(var(--hidden-height) / -2 * 1px));\n\t\t\t\t\t}\n\t\t\t\t\t50% {\n\t\t\t\t\t\ttransform: scale(var(--ken-burns-zoom));\n\t\t\t\t\t}\n\t\t\t\t\t90% {\n\t\t\t\t\t\ttransform: scale(calc(1.0 + ((var(--ken-burns-zoom) - 1.0)/2))) translateX(calc(var(--hidden-width) / 2 * 1px)) translateY(calc(var(--hidden-height) / 2 * 1px));\n\t\t\t\t\t}\n\t\t\t\t\t100% {\n\t\t\t\t\t\ttransform: scale(1.0);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t@keyframes kenBurnsEffect-experimental2 {\n\t\t\t\t\t0% {\n\t\t\t\t\t\ttransform: scale(1.0);\n\t\t\t\t\t}\n\t\t\t\t\t25% {\n\t\t\t\t\t\ttransform: scale(calc(1.0 + ((var(--ken-burns-zoom) - 1.0)/2))) translateX(calc(var(--hidden-width) / 2 * 1px)) translateY(calc(var(--hidden-height) / 2 * 1px));\n\t\t\t\t\t}\n\t\t\t\t\t50% {\n\t\t\t\t\t\ttransform: scale(var(--ken-burns-zoom));\n\t\t\t\t\t}\n\t\t\t\t\t75% {\n\t\t\t\t\t\ttransform: scale(calc(1.0 + ((var(--ken-burns-zoom) - 1.0)/2))) translateX(calc(var(--hidden-width) / -2 * 1px)) translateY(calc(var(--hidden-height) / -2 * 1px));\n\t\t\t\t\t}\n\t\t\t\t\t100% {\n\t\t\t\t\t\ttransform: scale(1.0);\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t@keyframes kenBurnsEffect-simple {\n\t\t\t\t\t0% {\n\t\t\t\t\t\ttransform-origin: bottom left;\n\t\t\t\t\t\ttransform: scale(1.0);\n\t\t\t\t\t}\n\t\t\t\t\t100% {\n\t\t\t\t\t\ttransform: scale(var(--ken-burns-zoom));\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t\t").concat(i,"\n\t\t\t\t").concat(Yu.custom_css,"\n\t\t\t")}randomMove(){const e=getComputedStyle(this.infoContainer),t=this.infoContainer.offsetWidth-parseInt(e.paddingLeft)-parseInt(e.paddingRight)-this.infoBox.offsetWidth,n=this.infoContainer.offsetHeight-parseInt(e.paddingTop)-parseInt(e.paddingBottom)-this.infoBox.offsetHeight,r=Math.floor(Math.random()*t),i=Math.floor(Math.random()*n);this.moveInfoBox(r,i)}moveAroundCorners(){let e=null;arguments.length>0&&void 0!==arguments[0]&&arguments[0]?e=0:this.lastCorner=(this.lastCorner+1)%4;const t=getComputedStyle(this.infoContainer),n=[2,3].includes(this.lastCorner)?this.infoContainer.offsetWidth-parseInt(t.paddingLeft)-parseInt(t.paddingRight)-this.infoBox.offsetWidth:0,r=[1,2].includes(this.lastCorner)?this.infoContainer.offsetHeight-parseInt(t.paddingTop)-parseInt(t.paddingBottom)-this.infoBox.offsetHeight:0;this.moveInfoBox(n,r,e)}moveInfoBox(e,t){let n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;if(this.lastMove=Date.now(),null===n&&(n=Yu.info_move_fade_duration),n>0)if(this.infoBox.animate){const e=[{opacity:1},{opacity:0,offset:.5},{opacity:1}];this.infoBox.animate(e,{duration:Math.round(1e3*n),iterations:1})}else fh.warn("This browser does not support the animate() method, please set info_move_fade_duration to 0");const r=this;let i=Math.round(500*n);i<0&&(i=0),r.translateTimeout&&clearTimeout(r.translateTimeout),r.translateTimeout=setTimeout(function(){r.infoBoxPosX.style.transform="translate3d(".concat(e,"px, 0, 0)"),r.infoBoxPosY.style.transform="translate3d(0, ".concat(t,"px, 0)")},i)}createInfoBoxContent(){fh.debug("Creating info box content");const e=Eh();if(e){if(this.lovelace=e.lovelace||e.__lovelace,this.infoBoxContentCreatedDate=new Date,this.infoBoxContent.innerHTML="",this.__badges=[],this.__cards=[],this.__views=[],this.energyCollectionUpdateEnabled=!1,this.shadowRoot.querySelectorAll(".wp-card").forEach(e=>{e.parentElement.removeChild(e)}),Yu.badges&&Yu.badges.length>0){const e=document.createElement("div");if(e.id="wallpanel-screensaver-info-box-badges",e.classList.add("wp-badges"),e.style.padding="var(--wp-card-padding)",e.style.margin="var(--wp-card-margin)",e.style.textAlign="center",e.style.display="flex",e.style.alignItems="flex-start",e.style.flexWrap="wrap",e.style.justifyContent="center",e.style.gap="8px",e.style.margin="0px",e.style.minWidth="var(--wp-badges-minwidth)",Yu.style[e.id])for(const t in Yu.style[e.id])fh.debug("Setting style attribute ".concat(t," to ").concat(Yu.style[e.id][t])),e.style.setProperty(t,Yu.style[e.id][t]);Yu.badges.forEach(t=>{const n=JSON.parse(JSON.stringify(t));fh.debug("Creating badge:",n);let r={};n.wp_style&&(r=n.wp_style,delete n.wp_style);const i=(this._createBadgeElement?this._createBadgeElement:this.createBadgeElement).bind(this)(n);i.hass=this.hass;for(const e in r)i.style.setProperty(e,r[e]);this.__badges.push(i),e.append(i)}),this.infoBoxContent.appendChild(e)}if(Yu.views&&Yu.views.length>0){const e=document.createElement("div");if(e.id="wallpanel-screensaver-info-box-views",e.classList.add("wp-views"),Yu.style[e.id])for(const t in Yu.style[e.id])fh.debug("Setting style attribute ".concat(t," to ").concat(Yu.style[e.id][t])),e.style.setProperty(t,Yu.style[e.id][t]);const t=this.lovelace.config.views;Yu.views.forEach(n=>{let r=-1;const i=JSON.parse(JSON.stringify(n));for(var o=0;o0&&Yu.cards.forEach(e=>{const t=JSON.parse(JSON.stringify(e));fh.debug("Creating card:",t);let n={};t.wp_style&&(n=t.wp_style,delete t.wp_style),t.type&&t.type.includes("energy")&&(t.collection_key="energy_wallpanel",this.energyCollectionUpdateEnabled=!0);const r=(this._createCardElement?this._createCardElement:this.createCardElement).bind(this)(t);r.hass=this.hass,this.__cards.push(r);let i=this.infoBoxContent;const o=document.createElement("div");o.classList.add("wp-card"),o.style.width="var(--wp-card-width)",o.style.padding="var(--wp-card-padding)",o.style.margin="var(--wp-card-margin)",o.style.backdropFilter="var(--wp-card-backdrop-filter)",Yu.content_interaction?o.style.pointerEvents="auto":o.style.pointerEvents="none";for(const e in n)if("parent"==e){const t=this.shadowRoot.getElementById(n[e]);t&&(i=t)}else o.style.setProperty(e,n[e]);o.append(r),i.appendChild(o)}),setTimeout(this.updateShadowStyle.bind(this),500)}}restartProgressBarAnimation(){if(!this.progressBarContainer)return;if(this.progressBar.style.animation="none",!Yu.show_progress_bar)return;const e=this;setTimeout(function(){e.progressBar.style.animation="horizontalProgress ".concat(Yu.display_time,"s linear"),e.progressBar.style.animationPlayState=e.isPaused?"paused":"running"},25)}restartKenBurnsEffect(){if(!Yu.image_animation_ken_burns||!Yu.image_animation_ken_burns_animations.length)return;const e=this.getActiveMediaElement();e.style.animation="none",e.style.setProperty("--ken-burns-zoom",Yu.image_animation_ken_burns_zoom);let t=Math.floor(1e3*Yu.image_animation_ken_burns_delay);t<50&&(t=50);const n=Math.ceil(Yu.image_animation_ken_burns_duration||1.2*(Yu.display_time+2*Yu.crossfade_time)),r=Yu.image_animation_ken_burns_animations[Math.floor(Math.random()*Yu.image_animation_ken_burns_animations.length)];this.kenburnsDelayStartTimer&&clearTimeout(this.kenburnsDelayStartTimer),e.animationIterationCount=1,this.kenburnsDelayStartTimer=setTimeout(function(){e.style.animation="kenBurnsEffect-".concat(r," ").concat(n,"s linear forwards")},t)}getMediaElement(){let e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0],t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=this.imageOneContainer.style.opacity==(e?1:0)?this.imageOne:this.imageTwo;if(t&&"ha-camera-stream"==n.tagName.toLowerCase()){const e=ph(n)[1];e&&(n=e)}return n}getActiveMediaElement(){let e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return this.getMediaElement(!0,e)}getInactiveMediaElement(){let e=arguments.length>0&&void 0!==arguments[0]&&arguments[0];return this.getMediaElement(!1,e)}replaceMediaElement(e,t){if("string"==typeof e.src&&e.src.startsWith("blob:")&&URL.revokeObjectURL(e.src),"function"==typeof e.pause&&e.pause(),"ha-camera-stream"==e.tagName.toLowerCase()){const[t,n]=ph(e);n&&n.pause(),t&&"function"==typeof t.stop&&t.stop()}t=t.toLowerCase();const n=document.createElement(t);return[...e.attributes].filter(e=>"src"!=e.name).forEach(e=>n.setAttribute(e.name,e.value)),n.mediaUrl=e.mediaUrl,n.infoCacheUrl=e.infoCacheUrl,"video"===t&&Object.assign(n,{preload:"auto",muted:!1,volume:Yu.video_volume}),e.replaceWith(n),e===this.imageOne?this.imageOne=n:this.imageTwo=n,e.remove(),n}loadBackgroundImage(e){let t=e.src;const n=e.tagName.toLowerCase();if("ha-camera-stream"===n){const n=ph(e)[1];if(!n||!n.poster)return;t=n.poster}else if("video"===n){const n=document.createElement("canvas");n.width=e.videoWidth,n.height=e.videoHeight;n.getContext("2d").drawImage(e,0,0,n.width,n.height);try{t=n.toDataURL("image/png")}catch(e){t=null,fh.error("Error extracting canvas image:",e)}}let r=this.imageOneBackground;e==this.imageTwo&&(r=this.imageTwoBackground),r.style.backgroundImage=t?"url('".concat(t.replace(/'/g,"%27"),"')"):""}connectedCallback(){this.style.zIndex=Yu.z_index,this.style.visibility="hidden",this.style.opacity=0,this.style.position="fixed",this.timerInterval||(this.timerInterval=setInterval(this.timer.bind(this),1e3)),this.messageContainer=document.createElement("div"),this.messageContainer.id="wallpanel-message-container",this.debugBox=document.createElement("div"),this.debugBox.id="wallpanel-debug-box",this.screensaverContainer=document.createElement("div"),this.screensaverContainer.id="wallpanel-screensaver-container",this.imageOneContainer=document.createElement("div"),this.imageOneContainer.id="wallpanel-screensaver-image-one-container",this.imageOneBackground=document.createElement("div"),this.imageOneBackground.className="wallpanel-screensaver-image-background",this.imageOneBackground.id="wallpanel-screensaver-image-one-background",this.imageOne=document.createElement("img"),this.imageOne.id="wallpanel-screensaver-image-one",this.imageOneInfoContainer=document.createElement("div"),this.imageOneInfoContainer.className="wallpanel-screensaver-image-info-container",this.imageOneInfoContainer.id="wallpanel-screensaver-image-one-info-container",this.imageOneInfo=document.createElement("div"),this.imageOneInfo.className="wallpanel-screensaver-image-info",this.imageOneInfo.id="wallpanel-screensaver-image-one-info",this.imageOneInfoContainer.appendChild(this.imageOneInfo),this.imageOneContainer.appendChild(this.imageOneBackground),this.imageOneContainer.appendChild(this.imageOne),this.imageOneContainer.appendChild(this.imageOneInfoContainer),this.screensaverContainer.appendChild(this.imageOneContainer),this.imageTwoContainer=document.createElement("div"),this.imageTwoContainer.id="wallpanel-screensaver-image-two-container",this.imageTwoBackground=document.createElement("div"),this.imageTwoBackground.className="wallpanel-screensaver-image-background",this.imageTwoBackground.id="wallpanel-screensaver-image-two-background",this.imageTwo=document.createElement("img"),this.imageTwo.id="wallpanel-screensaver-image-two",this.imageTwoInfoContainer=document.createElement("div"),this.imageTwoInfoContainer.className="wallpanel-screensaver-image-info-container",this.imageTwoInfoContainer.id="wallpanel-screensaver-image-two-info-container",this.imageTwoInfo=document.createElement("div"),this.imageTwoInfo.className="wallpanel-screensaver-image-info",this.imageTwoInfo.id="wallpanel-screensaver-image-two-info",this.imageTwoInfoContainer.appendChild(this.imageTwoInfo),this.imageTwoContainer.appendChild(this.imageTwoBackground),this.imageTwoContainer.appendChild(this.imageTwo),this.imageTwoContainer.appendChild(this.imageTwoInfoContainer),this.screensaverContainer.appendChild(this.imageTwoContainer),this.screensaverImageOverlay=document.createElement("div"),this.screensaverImageOverlay.id="wallpanel-screensaver-image-overlay",this.screensaverContainer.appendChild(this.screensaverImageOverlay),this.progressBarContainer=document.createElement("div"),this.progressBarContainer.className="wallpanel-progress",this.progressBar=document.createElement("div"),this.progressBar.className="wallpanel-progress-inner",this.progressBar.id="wallpanel-progress-inner",this.progressBarContainer.appendChild(this.progressBar),Yu.show_progress_bar&&this.screensaverContainer.appendChild(this.progressBarContainer),this.pauseIndicator=document.createElement("div"),this.pauseIndicator.id="wp-pause-indicator",this.pauseIndicator.innerHTML="⏸️",this.pauseIndicator.style.position="absolute",this.pauseIndicator.style.top="1rem",this.pauseIndicator.style.left="1rem",this.pauseIndicator.style.fontSize="4rem",this.pauseIndicator.style.lineHeight="1",this.pauseIndicator.style.color="white",this.pauseIndicator.style.filter="drop-shadow(0 0 8px rgba(0,0,0,0.8))",this.pauseIndicator.style.zIndex="9999",this.pauseIndicator.style.pointerEvents="none",this.pauseIndicator.style.display="none",this.screensaverContainer.appendChild(this.pauseIndicator),this.infoContainer=document.createElement("div"),this.infoContainer.id="wallpanel-screensaver-info-container",this.fixedInfoContainer=document.createElement("div"),this.fixedInfoContainer.id="wallpanel-screensaver-fixed-info-container",this.fixedInfoBox=document.createElement("div"),this.fixedInfoBox.id="wallpanel-screensaver-fixed-info-box",this.fixedInfoBoxContent=document.createElement("div"),this.fixedInfoBoxContent.id="wallpanel-screensaver-fixed-info-box-content",this.screensaverContainer.appendChild(this.infoContainer),this.infoBoxPosX=document.createElement("div"),this.infoBoxPosX.id="wallpanel-screensaver-info-box-pos-x",this.infoBoxPosX.x="0",this.infoBoxPosY=document.createElement("div"),this.infoBoxPosY.id="wallpanel-screensaver-info-box-pos-y",this.infoBoxPosY.y="0",this.infoBox=document.createElement("div"),this.infoBox.id="wallpanel-screensaver-info-box",this.infoBoxContent=document.createElement("div"),this.infoBoxContent.id="wallpanel-screensaver-info-box-content",this.infoBoxContent.style.display="grid",this.infoBox.appendChild(this.infoBoxContent),this.infoBoxPosX.appendChild(this.infoBox),this.infoBoxPosY.appendChild(this.infoBoxPosX),this.infoContainer.appendChild(this.infoBoxPosY),this.fixedInfoBox.appendChild(this.fixedInfoBoxContent),this.fixedInfoContainer.appendChild(this.fixedInfoBox),this.infoContainer.appendChild(this.fixedInfoContainer),this.screensaverOverlay=document.createElement("div"),this.screensaverOverlay.id="wallpanel-screensaver-overlay",this.screensaverContainer.appendChild(this.screensaverOverlay),this.shadowStyle=document.createElement("style");const e=this.attachShadow({mode:"open"});e.appendChild(this.shadowStyle),e.appendChild(this.screensaverContainer),e.appendChild(this.messageContainer),e.appendChild(this.debugBox);const t=this,n=["click","touchstart","touchend","wheel"];Yu.stop_screensaver_on_key_down&&n.push("keydown"),Yu.stop_screensaver_on_mouse_move&&n.push("mousemove");const r=e=>{try{t.handleInteractionEvent(e)}catch(e){fh.error(e.stack)}};n.forEach(function(e){window.addEventListener(e,r,!0),t.windowEventHandlers.push({eventName:e,handler:r,options:!0})});const i=e=>{Yu.disable_context_menu&&t.screensaverRunning()&&e.preventDefault()};window.addEventListener("contextmenu",i),this.windowEventHandlers.push({eventName:"contextmenu",handler:i});const o=()=>{try{const e=this.screensaverContainer.clientWidth,n=this.screensaverContainer.clientHeight;!t.screensaverRunning()||t.currentWidth==e&&t.currentHeight==n||(fh.debug("Size changed from ".concat(t.currentWidth,"x").concat(t.currentHeight," to ").concat(e,"x").concat(n)),t.currentWidth=e,t.currentHeight=n,t.updateShadowStyle(),t.setMediaDimensions())}catch(e){fh.error(e.stack)}};window.addEventListener("resize",o),this.windowEventHandlers.push({eventName:"resize",handler:o});const a=()=>{try{t.screensaverRunning()&&t.moreInfoDialogToForeground()}catch(e){fh.error(e.stack)}};window.addEventListener("hass-more-info",a),this.windowEventHandlers.push({eventName:"hass-more-info",handler:a}),this.infoBoxResizeObserver=new ResizeObserver(()=>{"corners"===Yu.info_move_pattern&&this.moveAroundCorners(!0)}),this.infoBoxResizeObserver.observe(this.infoBoxContent),this.setScreensaverEntityState()}disconnectedCallback(){this.timerInterval&&(clearInterval(this.timerInterval),this.timerInterval=null),this.translateTimeout&&(clearTimeout(this.translateTimeout),this.translateTimeout=null),this.windowEventHandlers.length&&(this.windowEventHandlers.forEach(e=>{let{eventName:t,handler:n,options:r}=e;window.removeEventListener(t,n,r)}),this.windowEventHandlers=[]),this.infoBoxResizeObserver&&(this.infoBoxResizeObserver.disconnect(),this.infoBoxResizeObserver=null)}reconfigure(e){const t=e&&Object.keys(e).length>0;!t||e.hide_toolbar==Yu.hide_toolbar&&e.hide_toolbar_action_icons==Yu.hide_toolbar_action_icons&&e.hide_toolbar_on_subviews==Yu.hide_toolbar_on_subviews&&e.hide_sidebar==Yu.hide_sidebar&&e.fullscreen==Yu.fullscreen||kh(),Yu.theme&&(fh.info("Apply theme",Yu.theme),Ju.theme=Yu.theme,Ju._applyTheme()),this.updateStyle(),this.screensaverRunning()&&this.createInfoBoxContent(),!Yu.info_move_interval&&t&&e.info_move_interval&&Qu.moveInfoBox(0,0);const n=e.image_url!=Yu.image_url||"immich-api"==wh()&&(Yu.immich_shared_albums!=e.immich_shared_albums||Yu.immich_memories!=e.immich_memories||Yu.immich_favorites!=e.immich_favorites||JSON.stringify(Yu.immich_album_names)!=JSON.stringify(e.immich_album_names)||JSON.stringify(Yu.immich_tag_names)!=JSON.stringify(e.immich_tag_names)||JSON.stringify(Yu.immich_persons)!=JSON.stringify(e.immich_persons));if(Yu.show_images&&(!this.mediaList||!this.mediaList.length||!t||!e.show_images||n)){const e=this,r=this.screensaverRunning()&&t;n&&(this.mediaList=[],this.mediaIndex=-1),this.updateMediaList(function(){r&&e.switchActiveMedia("reconfigure")},n)}if(Yu.disable_screensaver_on_browser_mod_popup_func)try{this.disable_screensaver_on_browser_mod_popup_function=new Function("bmp",Yu.disable_screensaver_on_browser_mod_popup_func)}catch(e){this.disable_screensaver_on_browser_mod_popup_function=null,fh.error("Invalid disable_screensaver_on_browser_mod_popup_func:",e)}bh()&&Yu.camera_motion_detection_enabled?this.cameraMotionDetection.start():this.cameraMotionDetection.stop()}getMoreInfoDialog(){const e=ih.shadowRoot.querySelector("ha-more-info-dialog");if(!e)return;const t=e.shadowRoot.querySelector("ha-dialog");return t||void 0}moreInfoDialogToForeground(){const e=this;!function t(){let n=arguments.length>0&&void 0!==arguments[0]?arguments[0]:1;const r=e.getMoreInfoDialog();if(r)return r.style.position="absolute",void(r.style.zIndex=e.style.zIndex+1);n<10&&setTimeout(t,50,n+1)}()}fetchEXIFInfo(e){const t=this;if(eh.get(e.infoCacheUrl))return;const n=document.createElement("img");n.src=e.src,n.mediaUrl=e.mediaUrl,n.infoCacheUrl=e.infoCacheUrl,Hh(n,function(){fh.debug("EXIF data:",n.exifdata),th(n.infoCacheUrl,n.exifdata),t.setMediaDataInfo(n);const e=n.exifdata.GPSLongitude,r=n.exifdata.GPSLatitude;if(Yu.fetch_address_data&&e&&!isNaN(e[0])&&r&&!isNaN(r[0])){let i="S"==n.exifdata.GPSLatitudeRef?-1:1;const o=r[0]*i+(r[1]*i*60+r[2]*i)/3600;i="W"==n.exifdata.GPSLongitudeRef?-1:1;const a=e[0]*i+(e[1]*i*60+e[2]*i)/3600;fh.debug("Fetching nominatim data for lat=".concat(o," lon=").concat(a));const s=new XMLHttpRequest;s.onload=function(){if(200==this.status||0===this.status){const e=JSON.parse(s.responseText);if(fh.debug("nominatim data:",e),e&&e.address){const r=eh.get(n.infoCacheUrl);r?(r.address=e.address,t.setMediaDataInfo(n)):fh.warn("URL not in cache:",n.infoCacheUrl)}}else fh.error("nominatim error:",this.status,s.status,s.responseText)},s.onerror=function(e){fh.error("nominatim error:",e)},s.ontimeout=function(e){fh.error("nominatim timeout:",e)},s.open("GET","https://nominatim.openstreetmap.org/reverse?lat=".concat(o,"&lon=").concat(a,"&format=json")),s.timeout=15e3,s.send()}})}setMediaDataInfo(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null;const t=Yu.show_image_info&&Boolean(Yu.image_info_template);if(this.imageOneInfoContainer.style.display=t?"block":"none",this.imageTwoInfoContainer.style.display=t?"block":"none",!t)return this.imageOneInfo.innerHTML="",void(this.imageTwoInfo.innerHTML="");e||(e=this.getActiveMediaElement());const n=e.infoCacheUrl;let r=e.mediaUrl;if(!n)return void fh.debug("infoCacheUrl missing:",e);if(!r)return void fh.debug("mediaUrl missing:",e);r=decodeURI(r);const i=[];if(this.imageOne.infoCacheUrl==n&&i.push(this.imageOneInfo),this.imageTwo.infoCacheUrl==n&&i.push(this.imageTwoInfo),0==i.length)return;let o=eh.get(n);o||(o={}),o.image||(o.image={}),void 0===o.image.url&&(o.image.url=r);const a=r.replace(/\?[^?]*$/,"").replace(/\/+$/,"");if(void 0===o.image.path&&(o.image.path=a.replace(/^[^:]+:\/\/[^/]+/,"")),void 0===o.image.relativePath){const c="media-source"===wh()?n.replace(/\?[^?]*$/,"").replace(/\/+$/,""):a;o.image.relativePath=c.replace(Yu.image_url,"").replace(/^\/+/,"")}if(void 0===o.image.filename&&(o.image.filename=a.replace(/^.*[\\/]/,"")),void 0===o.image.folderName){o.image.folderName="";const l=a.split("/");l.length>=2&&(o.image.folderName=l[l.length-2])}o.mediaPosition=this.mediaIndex+1,o.mediaCount=this.mediaList.length,fh.debug("Media info:",o);let s=Yu.image_info_template||"";if("analyze"==s){function u(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"";const n=Object.keys(e);n.sort(),n.forEach(n=>{const r=e[n];"object"==typeof r&&null!==r?u(r,n+"."):s+="".concat(t).concat(n,": ").concat(hh(r),"
    ")})}s="",u(o),this.imageOneInfo.style.pointerEvents="none",this.imageTwoInfo.style.pointerEvents="none",i.forEach(e=>{e.style.pointerEvents="auto"})}else s=s.replace(/\${([^}]+)}/g,(e,t)=>{const n=t.split("||");for(let e=0;e{const t=e.split(":",2);t[0]&&t[1]&&(a[t[0].replace(/\s/g,"")]=t[1].replace(/\s/g,""))}))}}let s="";const c=t.split("|");let l="";for(let e=0;e{s&&(s=s[e])}),s)break}if(s){if(/DateTime/i.test(l)){const e=new Date(s.replace(/(\d\d\d\d):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)/,"$1-$2-$3T$4:$5:$6"));if(isNaN(e))continue;a||(a={year:"numeric",month:"2-digit",day:"2-digit"}),s=e.toLocaleDateString((ih.hass||ih.__hass).locale.language,a)}return"object"==typeof s&&(s=JSON.stringify(s)),r+s+i}}return""});i.forEach(e=>{e.innerHTML=s,e.style.display="block"})}async updateMediaList(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:null,t=arguments.length>1&&void 0!==arguments[1]&&arguments[1],n=arguments.length>2&&void 0!==arguments[2]?arguments[2]:0;if(!Yu.image_url)return;if(this.updatingMediaList)return;if(!t&&(new Date).getTime()-this.lastMediaListUpdate<1e3*Yu.media_list_update_interval)return;const r=this;let i=null;const o=wh();if("unsplash-api"==o)i=r.updateMediaListFromUnsplashAPI;else if("immich-api"==o)i=r.updateMediaListFromImmichAPI;else{if("media-source"!=o){let t=Yu.image_url;return"iframe"==o?t=t.replace(/^iframe\+/,""):"embed"==o&&(t=t.replace(/^embed\+/,"")),this.mediaList=[t],void(e&&e())}i=r.updateMediaListFromMediaSource}this.updatingMediaList=!0,this.lastMediaListUpdate=Date.now();try{await i.bind(r)(),fh.debug("Media list from ".concat(o," is now:"),r.mediaList),e&&e()}catch(t){const i=3,a=3e3;if(fh.warn("Failed to update media list from ".concat(o,":"),t),nr.updateMediaList(e,!0,n+1),a);else{const e="Failed to update media list from ".concat(Yu.image_url," after ").concat(i," retries: ").concat(t.message||hh(t));fh.error(e)}}this.updatingMediaList=!1}async findMedias(e){const t=this;fh.debug("findMedias: ".concat(e));const n=[];if(Yu.exclude_filenames)for(const e of Yu.exclude_filenames)n.push(new RegExp(e));try{const r=await t.hass.callWS({type:"media_source/browse_media",media_content_id:e});fh.debug("Found media entry",r);const i=r.children.map(async e=>{const r=e.media_content_id.replace(/^media-source:\/\/[^/]+/,"");for(const e of n)if(e.test(r))return null;return["image","video"].includes(e.media_class)?Yu.exclude_media_types&&Yu.exclude_media_types.includes(e.media_class)?null:e.media_content_id:"directory"==e.media_class?await t.findMedias(e.media_content_id):null});return(await Promise.all(i)).flat().filter(e=>null!==e)}catch(t){throw fh.warn("Error browsing media ".concat(e,":"),t),t}}async updateMediaListFromMediaSource(){const e=Yu.image_url,t=this;try{let n=await t.findMedias(e);n="random"==Yu.media_order?gh(n):n.sort(),n.length>Yu.media_list_max_size&&(fh.info("Using only ".concat(Yu.media_list_max_size," of ").concat(n.length," media items")),n=n.slice(0,Yu.media_list_max_size)),t.mediaList=n}catch(e){throw new Error("Failed to update image list from ".concat(Yu.image_url,": ").concat(e.message||hh(e)))}}async updateMediaListFromUnsplashAPI(){const e=[],t="".concat(Yu.image_url,"&count=30");fh.debug("Unsplash API request: ".concat(t));try{const n={method:"GET",headers:{Accept:"application/json"}};"undefined"!=typeof AbortSignal&&void 0!==AbortSignal.timeout&&(fh.debug("Using AbortSignal"),n.signal=AbortSignal.timeout(1e3*Yu.immich_api_request_timeout));const r=await fetch(t,n);if(!r.ok){const e=await r.text();throw new Error("Unsplash API request failed: ".concat(r.status," ").concat(r.statusText," - ").concat(e))}const i=await r.json();fh.debug("Got Unsplash API response"),i.forEach(t=>{fh.debug(t);const n="".concat(t.urls.raw,"&w=${width}&h=${height}&auto=format");e.push(n),th(n,t)}),this.mediaList=e}catch(e){if("AbortError"===e.name)throw new Error("Unsplash API request timed out: ".concat(t));throw e}}async _immichFetch(e,t){let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:{};const i={headers:{"x-api-key":t,"Content-Type":"application/json",Accept:"application/json"}},o=n(n({},i),r);o.headers=n(n({},i.headers),r.headers),"undefined"!=typeof AbortSignal&&void 0!==AbortSignal.timeout&&(fh.debug("Using AbortSignal"),o.signal=AbortSignal.timeout(1e4)),fh.debug("Immich API request: ".concat(e));try{const t=await fetch(e,o);if(!t.ok){const e=await t.text();throw new Error("Immich API request failed: ".concat(t.status," ").concat(t.statusText," - ").concat(e))}return await t.json()}catch(t){if("AbortError"===t.name)throw new Error("Immich API request timed out: ".concat(e));throw t}}async updateMediaListFromImmichAPI(){if(!Yu.immich_api_keys||!Yu.immich_api_keys.length)throw new Error("No immich_api_key configured");const e=this,t=this.screensaverContainer.clientWidth>=this.screensaverContainer.clientHeight?"landscape":"portrait";let n=Yu.exclude_media_orientation;"auto"==n&&(n="landscape"==t?"portrait":"landscape"),fh.debug("config.exclude_media_orientation=".concat(Yu.exclude_media_orientation,", screenOrientation=").concat(t,", exclude_media_orientation=").concat(n));let r=[];const i={},o=Yu.image_url.replace(/^immich\+/,""),a=[];if(Yu.exclude_filenames)for(const e of Yu.exclude_filenames)a.push(new RegExp(e));async function s(t,s){let c=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;await async function(t,n){const r=Yu.immich_exclude_tag_names&&Yu.immich_exclude_tag_names.length;await Promise.all(t.map(async t=>{if(!t.exifInfo||r&&!t.tags){fh.debug("Fetching asset info for ".concat(t.id));const r=await e._immichFetch("".concat(o,"/assets/").concat(t.id),n);t.exifInfo=r.exifInfo,t.tags=r.tags.map(e=>e.value)}}))}(t,s);for(const e of t){fh.debug("Processing immich asset",e);const t=e.type.toLowerCase();if(!["image","video"].includes(t)){fh.debug("Neither image nor video, skipping");continue}if(Yu.exclude_media_types&&Yu.exclude_media_types.includes(t)){fh.debug('Media type "'.concat(t,'" excluded'));continue}let l=!1;for(const t of a)t.test(e.originalFileName)&&(fh.debug('Media item excluded by regex "'.concat(t,'"')),l=!0);if(l)continue;if(Yu.immich_exclude_tag_names&&Yu.immich_exclude_tag_names.length){const t=e.tags.filter(e=>Yu.immich_exclude_tag_names.includes(e));if(t.length>0){fh.debug("Media item excluded due to tag(s): ".concat(t.join(", ")));continue}}if(n&&e.exifInfo&&e.exifInfo.exifImageWidth&&e.exifInfo.exifImageHeight){let t=e.exifInfo.exifImageWidth>=e.exifInfo.exifImageHeight?"landscape":"portrait";if(e.exifInfo.orientation&&[5,6,7,8].includes(parseInt(e.exifInfo.orientation))&&(t="landscape"==t?"portrait":"landscape"),t===n){fh.debug('Media item with orientation "'.concat(t,'" excluded'));continue}}let u="original";"preview"==Yu.immich_resolution&&(u="video"==t?"video/playback":"thumbnail?size=preview");const h="".concat(o,"/assets/").concat(e.id,"/").concat(u);if(r.indexOf(h)>=0)continue;r.push(h);const f=e.exifInfo||{};f.immichApiKey=s,f.mediaType=t,f.image={filename:e.originalFileName,folderName:c},i[h]=f}}try{for(const t of Yu.immich_api_keys)if(fh.debug("Using immich API key ".concat(t)),Yu.immich_persons&&Yu.immich_persons.length){fh.debug("Searching for assets based on persons");const n=Yu.immich_persons.map(e=>(Array.isArray(e)?e:[e]).map(e=>e.toLowerCase())),r={};let i=[],a=1,c=!0;for(;c;){const n=await e._immichFetch("".concat(o,"/people?size=1000&page=").concat(a),t);i=i.concat(n.people),c=n.hasNextPage,a++}i.forEach(e=>{e.name&&e.id&&(r[e.name.toLowerCase()]=e.id)});for(const i of n){const n=[];if(i.forEach(e=>{const t=r[e.toLowerCase()];t?n.push(t):fh.error("Person not found in immich: ".concat(e))}),n.length>0){fh.debug("Searching asset metadata for persons: ",n);let r=1;for(;;){fh.debug("Fetching asset metadata page ".concat(r));const a=await e._immichFetch("".concat(o,"/search/metadata"),t,{method:"POST",body:JSON.stringify({personIds:n,withExif:!0,page:r,size:1e3})});if(fh.debug("Got immich API response",a),!a.assets.count){if(1==r){const e="No media items found in immich that contain all these people: ".concat(i);fh.error(e)}break}if(await s(a.assets.items,t),!a.assets.nextPage)break;r=a.assets.nextPage}}}}else if(Yu.immich_memories){fh.debug("Fetching immich memories (on_this_day)");const n=await e._immichFetch("".concat(o,"/memories?type=on_this_day"),t);fh.debug("Got immich API response",n);const r=new Date,i=n.filter(e=>{const t=new Date(e.showAt),n=new Date(e.hideAt);return r>=t&&r<=n});fh.debug("Found ".concat(i.length," visible memories")),await Promise.all(i.map(async e=>{fh.debug("Processing memory:",e),await s(e.assets,t)}))}else if(Yu.immich_favorites){fh.debug("Search for favorites in asset metadata");let n=1;for(;;){fh.debug("Fetching asset metadata page ".concat(n));const r=await e._immichFetch("".concat(o,"/search/metadata"),t,{method:"POST",body:JSON.stringify({isFavorite:!0,withExif:!0,page:n,size:1e3})});if(fh.debug("Got immich API response",r),!r.assets.count){if(1==n){const e="No favorite media items found in immich";fh.error(e)}break}if(await s(r.assets.items,t),!r.assets.nextPage)break;n=r.assets.nextPage}}else if(Yu.immich_tag_names&&Yu.immich_tag_names.length){fh.debug("Searching for assets based on tag names");const n=Yu.immich_tag_names.map(e=>e.toLowerCase());fh.debug("Fetching immich tags");const r=await e._immichFetch("".concat(o,"/tags"),t);fh.debug("Got immich API response",r);const i=r.filter(e=>{const t=n.includes(e.name.toLowerCase());return fh.debug("".concat(t?"Adding":"Skipping"," tag: ").concat(e.name)),t}).map(e=>e.id);if(i.length>0){fh.debug("Searching asset metadata for tags: ",i);let r=1;for(;;){fh.debug("Fetching asset metadata page ".concat(r));const a=await e._immichFetch("".concat(o,"/search/metadata"),t,{method:"POST",body:JSON.stringify({tagIds:i,withExif:!0,page:r,size:1e3})});if(fh.debug("Got immich API response",a),!a.assets.count){if(1==r){const e="No media items found in immich that contain these tags: ".concat(n);fh.error(e)}break}if(await s(a.assets.items,t),!a.assets.nextPage)break;r=a.assets.nextPage}}else{const e="No matching immich tags found or selected.";fh.error(e)}}else{fh.debug("Searching for assets based on albums");const n=(Yu.immich_album_names||[]).map(e=>e.toLowerCase());fh.debug("Fetching immich albums (shared=".concat(Yu.immich_shared_albums,")"));const r=await e._immichFetch("".concat(o,"/albums?shared=").concat(Yu.immich_shared_albums),t);fh.debug("Got immich API response",r);const i=r.filter(e=>{const t=!n.length||n.includes(e.albumName.toLowerCase());return fh.debug("".concat(t?"Adding":"Skipping"," album: ").concat(e.albumName)),t}).map(e=>e.id);if(i.length>0){const n=i.map(n=>(fh.debug("Fetching album metadata: ",n),e._immichFetch("".concat(o,"/albums/").concat(n),t))),r=await Promise.all(n);for(const e of r)fh.debug("Got immich album details",e),await s(e.assets,t,e.albumName)}else fh.debug("No matching immich albums found or selected.")}!function(){if(0==r.length){const e="No matching media assets found";fh.error(e)}r="random"==Yu.media_order?gh(r):r.sort(),r.length>Yu.media_list_max_size&&(fh.info("Using only ".concat(Yu.media_list_max_size," of ").concat(r.length," media items")),r=r.slice(0,Yu.media_list_max_size)),r.forEach(e=>{th(e,i[e])}),e.mediaList=r}()}catch(e){throw fh.error("Immich API processing failed:",e),e}}fillPlaceholders(e){const t=this.screensaverContainer.clientWidth,n=this.screensaverContainer.clientHeight,r=Date.now(),i=Math.floor(r/1e3);return e=(e=(e=(e=e.replace(/\${width}/g,t)).replace(/\${height}/g,n)).replace(/\${timestamp_ms}/g,r)).replace(/\${timestamp}/g,i)}async updateMediaFromUrl(e,t){var n=this;let r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null,i=arguments.length>3&&void 0!==arguments[3]?arguments[3]:null,o=arguments.length>4&&void 0!==arguments[4]&&arguments[4];Yu.force_load_media_with_fetch&&(o=!0);const a=async e=>{const n={img:"load",video:"loadeddata",iframe:"load",embed:"load"}[e.tagName.toLowerCase()];if(!n)throw new Error('Unsupported element tag "'.concat(e.tagName,'"'));const r=new Promise((r,i)=>{const o=()=>{e.onerror=null,e.removeEventListener(n,a)},a=()=>{o(),r()};e.addEventListener(n,a),e.onerror=()=>{o(),i(new Error("Failed to load ".concat(e.tagName,' "').concat(t,'"')))}});if(o)if(Yu.stream_load_media)fetch(t,{headers:i}).then(n=>{if(!n.ok)throw new Error("Failed to load ".concat(e.tagName,' "').concat(t,'": ').concat(n));if(!n.body)throw new Error("Failed to load ".concat(e.tagName,' "').concat(t,'": empty body'));const r=n.body.getReader();return new ReadableStream({start:e=>function t(){return r.read().then(n=>{let{done:r,value:i}=n;if(!r)return e.enqueue(i),t();e.close()})}()})}).then(e=>new Response(e)).then(e=>e.blob()).then(t=>{"string"==typeof e.src&&e.src.startsWith("blob:")&&URL.revokeObjectURL(e.src),e.src=URL.createObjectURL(t)});else{i=i||{};const n=await fetch(t,{headers:i});if(fh.debug("Got respone",n),!n.ok)throw new Error("Failed to load ".concat(e.tagName,' "').concat(t,'": ').concat(n));"string"==typeof e.src&&e.src.startsWith("blob:")&&URL.revokeObjectURL(e.src);const r=await n.blob();e.src=URL.createObjectURL(r)}else e.src=t;return r},s=async function(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null,r=arguments.length>2&&void 0!==arguments[2]?arguments[2]:null;t||(t="img"===e.tagName.toLowerCase()?"video":"img");const i=n.replaceMediaElement(e,t);try{return await a(i),i}catch(e){throw r||e}},c=async(e,t)=>{try{return await a(e),e}catch(n){if(t)return await s(e,null,n);throw n}};return r?r===e.tagName.toLowerCase()?await c(e,!1):await s(e,r):await c(e,!0)}getNextMediaURL(){let e=!(arguments.length>0&&void 0!==arguments[0])||arguments[0];if(!this.mediaList.length)return null;let t=this.mediaIndex;return"forwards"==this.mediaListDirection?t++:t--,t>=this.mediaList.length?t=0:t<0&&(t=this.mediaList.length-1),e&&(this.mediaIndex=t),this.mediaList[t]}async updateMediaFromMediaSource(e){var t;const n=await this.hass.callWS({type:"media_source/resolve_media",media_content_id:e.mediaUrl}),r=null===(t=n.mime_type)||void 0===t?void 0:t.match(/^(image|video)\//),i={image:"img",video:"video"}[null==r?void 0:r[1]]||null;let o=n.url;return o.startsWith("http://")||o.startsWith("https://")||(o="".concat(document.location.origin).concat(o)),fh.debug("Setting image src: ".concat(o)),e.mediaUrl=o,await this.updateMediaFromUrl(e,e.mediaUrl,i)}async updateMediaFromImmichAPI(e){const t=eh.get(e.mediaUrl)||{},n="video"==t.mediaType?"video":"img";return await this.updateMediaFromUrl(e,e.mediaUrl,n,{"x-api-key":t.immichApiKey},!0)}async updateMediaFromMediaEntity(e){const t=e.mediaUrl.match(/^media-entity-(image|video):\/\/(.*)/);if(!t)throw new Error('Invalid URL "'.concat(e.mediaUrl,'"'));const n=t[1],r=t[2],i=this.hass.states[r];if(!i)throw new Error('Entity "'.concat(r,'" not available'));function o(){"media_exif"in i.attributes?th(e.infoCacheUrl,i.attributes.media_exif):th(e.infoCacheUrl,i.attributes)}if(rh=i.state,"video"==n)return e.infoCacheUrl=e.mediaUrl,o(),(e=this.replaceMediaElement(e,"ha-camera-stream")).style.visibility="hidden",Object.assign(e,{hass:this.__hass,stateObj:this.__hass.states[r],"allow-exoplayer":!1,controls:!1}),new Promise((t,n)=>{let r=0;e.addEventListener("load",async function e(i){const o=i.currentTarget;o.removeEventListener("load",e),await o.updateComplete;const[a,s]=ph(o);if(!a||!s)return r++,r<=20?void setTimeout(()=>e(i),100):void n(new Error("Failed to initialize camera stream player"));if(a.style.height="100%",s.autoplay=!1,s.muted=!1,s.volume=Yu.video_volume,s.style.maxHeight="100%",s.style.height="100%",s.readyState>=s.HAVE_ENOUGH_DATA)t(o);else{const e=()=>{s.removeEventListener("canplay",e),t(o)};s.addEventListener("canplay",e)}})});{if(!i.attributes||!i.attributes.entity_picture)throw new Error('Entity "'.concat(r,'" has no entity_picture attribute'));const t=i.attributes.entity_picture;let n=t.indexOf("?")>0?"&":"?";return n+=this.fillPlaceholders("width=${width}&height=${height}&ts=${timestamp_ms}"),e.mediaUrl=t+n,e.infoCacheUrl=e.mediaUrl,o(),await this.updateMediaFromUrl(e,e.mediaUrl,"img",null,!0)}}async updateMediaFromUnsplashAPI(e){const t=eh.get(e.mediaUrl);return e.mediaUrl=this.fillPlaceholders(e.mediaUrl),t&&th(e.mediaUrl,t),await this.updateMediaFromUrl(e,e.mediaUrl,"img")}async updateMediaFromMediaIframe(e){return await this.updateMediaFromUrl(e,e.mediaUrl,"iframe")}async updateMediaFromMediaEmbed(e){return await this.updateMediaFromUrl(e,e.mediaUrl,"embed")}async updateMediaFromOtherSrc(e){return e.mediaUrl=this.fillPlaceholders(e.mediaUrl),await this.updateMediaFromUrl(e,e.mediaUrl)}async updateMedia(e){if(Yu.show_images){this.updatingMedia=!0;try{if(e==this.getActiveMediaElement()){const e=this.getInactiveMediaElement();if("video"===e.tagName.toLowerCase())try{e.pause()}catch(e){fh.debug(e)}else if("ha-camera-stream"===e.tagName.toLowerCase())try{const[t,n]=ph(e);n&&n.pause(),t&&"function"==typeof t.stop&&t.stop()}catch(e){fh.debug(e)}}if(e.mediaUrl=this.getNextMediaURL(),!e.mediaUrl)return;if(e.infoCacheUrl=e.mediaUrl,e="media-source"==wh()?await this.updateMediaFromMediaSource(e):"unsplash-api"==wh()?await this.updateMediaFromUnsplashAPI(e):"immich-api"==wh()?await this.updateMediaFromImmichAPI(e):wh().startsWith("media-entity")?await this.updateMediaFromMediaEntity(e):"iframe"==wh()?await this.updateMediaFromMediaIframe(e):"embed"==wh()?await this.updateMediaFromMediaEmbed(e):await this.updateMediaFromOtherSrc(e)){e.style.visibility="visible";const t="video"===e.tagName.toLowerCase();t&&await new Promise((t,n)=>{if(e.readyState>=e.HAVE_ENOUGH_DATA)t();else{const r=()=>{e.removeEventListener("canplay",r),e.removeEventListener("error",i),t()},i=()=>{e.removeEventListener("canplay",r),e.removeEventListener("error",i),n(new Error("Video failed to load"))};e.addEventListener("canplay",r),e.addEventListener("error",i)}}),"image"===Yu.image_background&&this.loadBackgroundImage(e),!t&&Yu.show_image_info&&/.*\.jpe?g$/i.test(e.mediaUrl.split("?")[0].replace(/\/*$/,""))&&this.fetchEXIFInfo(e)}}catch(t){fh.error("Failed to update media from ".concat(e.mediaUrl,":"),t)}finally{this.updatingMedia=!1}return e}}setMediaDimensions(){if(!Yu.calculate_media_size)return;const e=this.getActiveMediaElement(),t=this.getActiveMediaElement(!0);if(fh.debug("Setting dimensions for media element",e),!e.mediaUrl)return;const n=t.tagName.toLowerCase();let r=0,i=0;"video"===n?(r=t.videoWidth,i=t.videoHeight):"img"===n&&(r=t.naturalWidth,i=t.naturalHeight);const o=!r||!i||r>=i?Yu.image_fit_landscape:Yu.image_fit_portrait;e.style.position="absolute",e.style.left="0px",e.style.top="0px",e.style.objectFit=o;const a=this.screensaverContainer.clientWidth,s=this.screensaverContainer.clientHeight;let c=s,l=a,u=0,h=0,f=0,d=0;if(fh.debug("Available size is ".concat(a,"x").concat(s,", media size is ").concat(r,"x").concat(i)),r&&i){const e=a/r,t=s/i,n=a-r*t,m=s-i*e;fh.debug("Diff is ".concat(n,"x").concat(m)),"contain"==o&&n=m?(fh.debug("Using available width"),l=a,c=Math.floor(i*e),f="top"==Yu.media_vertical_align?0:"bottom"==Yu.media_vertical_align?s-c:Math.floor((c-s)/-2),u=Math.max(c-s,0)):(fh.debug("Using available height"),c=s,l=Math.floor(r*t),d="left"==Yu.media_horizontal_align?0:"right"==Yu.media_horizontal_align?a-l:Math.floor((l-a)/-2),h=Math.max(l-a,0))}else"iframe"!==n&&"embed"!==n&&fh.warn("Size not available for media element",t);fh.debug("Setting dimensions: size=".concat(l,"x").concat(c," - position=").concat(d,"x").concat(f," - hidden=").concat(h,"x").concat(u)),e.style.width="".concat(l,"px"),e.style.height="".concat(c,"px"),e.style.top="".concat(f,"px"),e.style.left="".concat(d,"px"),e.style.setProperty("--hidden-width",h),e.style.setProperty("--hidden-height",u)}startPlayingActiveMedia(){const e=this.getActiveMediaElement(),t=this.getActiveMediaElement(!0);if("function"!=typeof t.play)return;const n=()=>{t._wp_video_playback_listeners&&(Object.entries(t._wp_video_playback_listeners).forEach(e=>{let[n,r]=e;t.removeEventListener(n,r)}),t._wp_video_playback_listeners=null)};if(t.loop=Yu.video_loop,!Yu.video_loop&&!t._wp_video_playback_listeners){const e=()=>{if(this.getActiveMediaElement()===t){if(t.currentTime>Yu.crossfade_time){t.duration-t.currentTime<=Yu.crossfade_time&&(this.switchActiveMedia("display_time_elapsed"),n())}}else n()},r=()=>{this.getActiveMediaElement()===t&&this.switchActiveMedia("media_end"),n()},i=()=>{n()};t._wp_video_playback_listeners={timeupdate:e,ended:r,pause:i},Object.entries(t._wp_video_playback_listeners).forEach(e=>{let[n,r]=e;t.addEventListener(n,r)})}t.play().catch(r=>{n(),e===this.getActiveMediaElement()&&fh.error('Failed to play media "'.concat(e.mediaUrl,'" (src=').concat(t.src,"):"),r)})}async switchActiveMedia(e){this.afterFadeoutTimer&&clearTimeout(this.afterFadeoutTimer);const t=wh();if(t.startsWith("media-entity")){const t=Yu.image_url.replace(/^media-entity-(image|video):\/\//,""),n=this.hass.states[t];if(!n)return void fh.error('Media entity "'.concat(t,'" not available'));if(rh!=n.state)fh.debug("Media entity ".concat(t," state has changed"));else{if("entity_update"==e)return;if("start"==e||"user_action"==e)fh.debug("Media entity ".concat(t," state unchanged, but eventType = ").concat(e));else{if(!Yu.media_entity_load_unchanged)return this.lastMediaUpdate=Date.now(),this.isPaused&&(this.mediaTimeElapsedBeforePause=0),void this.restartProgressBarAnimation();fh.debug("Media entity ".concat(t," state unchanged, but media_entity_load_unchanged = true"))}}rh=n.state}this.lastMediaUpdate=Date.now(),this.isPaused&&(this.mediaTimeElapsedBeforePause=0);const n=this.getActiveMediaElement();if(("iframe"===t||"embed"===t)&&!Yu.iframe_load_unchanged&&this.getNextMediaURL(!1)==n.mediaUrl)return;let r=0;"start"!=e&&(Yu.crossfade_time>0&&(r=Math.round(1e3*Yu.crossfade_time)),"user_action"==e&&r>250&&(r=250));const i=this.getInactiveMediaElement(),o=await this.updateMedia(i);o&&this._switchActiveMedia(o,r)}_switchActiveMedia(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;this.lastMediaUpdate=Date.now(),this.isPaused&&(this.mediaTimeElapsedBeforePause=0),this.imageOneContainer.style.transition="opacity ".concat(t,"ms ease-in-out"),this.imageTwoContainer.style.transition="opacity ".concat(t,"ms ease-in-out");let n=this.imageOneContainer,r=this.imageTwoContainer,i=this.imageOne,o=this.imageTwo;e==this.imageOne&&(n=this.imageTwoContainer,r=this.imageOneContainer,i=this.imageTwo,o=this.imageOne),fh.debug("Switching active media to '".concat(r.id,"'")),1!=r.style.opacity&&(r.style.opacity=1),0!=n.style.opacity&&(n.style.opacity=0),"iframe"==o.tagName.toLowerCase()&&Yu.iframe_interaction?o.style.pointerEvents="auto":o.style.pointerEvents="none",this.setMediaDataInfo(),this.setMediaDimensions(),this.setImageURLEntityState(),this.startPlayingActiveMedia(),this.restartProgressBarAnimation(),this.restartKenBurnsEffect(),"video"===i.tagName.toLowerCase()&&(this.afterFadeoutTimer=setTimeout(function(){if("video"===i.tagName.toLowerCase())try{i.pause()}catch(e){fh.debug(e)}},t))}showMessage(e,t,n){let r=arguments.length>3&&void 0!==arguments[3]?arguments[3]:5e3;if(!this.messageContainer)return;const i=document.createElement("div");i.className="wallpanel-message ".concat(e);const o=document.createElement("div");o.className="wallpanel-message-title",o.textContent=t,i.appendChild(o);const a=document.createElement("div");a.className="wallpanel-message-text",a.textContent=n,i.appendChild(a),this.messageContainer.appendChild(i),requestAnimationFrame(()=>i.classList.add("show"));const s=this;setTimeout(()=>s.hideMessage(i),r)}hideMessage(e){e.classList.remove("show"),e.addEventListener("transitionend",()=>e.remove())}hideAllMessages(){const e=this.messageContainer.querySelectorAll(".wallpanel-message");if(!e.length)return!1;const t=this;return e.forEach(e=>t.hideMessage(e)),!0}setupScreensaver(){fh.debug("Setup screensaver"),Yu.keep_screen_on_time>0&&!this.screenWakeLock.enabled&&this.screenWakeLock.enable(),Yu.fullscreen&&!Ih()&&Th()}async startScreensaver(){if(fh.debug("Start screensaver"),this.screensaverStartedAt=Date.now(),this.screensaverStoppedAt=null,this.currentWidth=this.screensaverContainer.clientWidth,this.currentHeight=this.screensaverContainer.clientHeight,this.setDefaultStyle(),yh(),!bh())return fh.debug("Wallpanel not active, not starting screensaver"),this.screensaverStartedAt=null,void(this.screensaverStoppedAt=Date.now());if(this.getActiveMediaElement()==this.imageOne?(this.imageOneContainer.style.opacity=1,this.imageTwoContainer.style.opacity=0):(this.imageOneContainer.style.opacity=0,this.imageTwoContainer.style.opacity=1),await this.switchActiveMedia("start"),this.setupScreensaver(),Yu.keep_screen_on_time>0){const e=this;setTimeout(function(){e.screensaverRunning()&&!e.screenWakeLock.enabled&&(fh.warning("Keep screen on will not work because the user didn't interact with the document first. https://goo.gl/xX8pDD"),e.showMessage("info","Keep screen on","Please tap the screen for a moment to keep it awake and prevent it from turning off.",15e3))},2e3)}this.lastMove=Date.now(),this.lastMediaUpdate=Date.now(),document.documentElement.style.overflow="hidden",this.isPaused=!1,this.mediaTimeElapsedBeforePause=0,this.pauseIndicator.style.display="none",this.createInfoBoxContent(),this.style.visibility="visible",this.style.opacity=1,this.style.pointerEvents="auto",this.setScreensaverEntityState(),(Yu.screensaver_start_navigation_path||Yu.screensaver_stop_close_browser_mod_popup)&&(this.screensaverStopNavigationPathTimeout=setTimeout(()=>{Yu.screensaver_start_navigation_path&&(Zu=!0,function(e){(!(arguments.length>1&&void 0!==arguments[1])||arguments[1])&&!e.includes("?")&&(e+=window.location.search),history.pushState(null,"",e),ih.dispatchEvent(new Event("location-changed",{bubbles:!0,cancelable:!1,composed:!0}))}(Yu.screensaver_start_navigation_path),setTimeout(()=>{Zu=!1},5e3)),Yu.screensaver_stop_close_browser_mod_popup&&Ah().forEach(e=>{e.closeDialog()})},1e3*(Yu.fade_in_time+1)))}screensaverRunning(){return Boolean(this.screensaverStartedAt)&&this.screensaverStartedAt>0}stopScreensaver(){let e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0;fh.debug("Stop screensaver"),this.screensaverStartedAt=null,this.screensaverStoppedAt=Date.now(),document.documentElement.style.removeProperty("overflow"),this.screensaverStopNavigationPathTimeout&&clearTimeout(this.screensaverStopNavigationPathTimeout),this.hideAllMessages(),this.debugBox.style.pointerEvents="none",this.style.transition=e>0?"opacity ".concat(Math.round(1e3*e),"ms ease-in-out"):"",this.style.opacity=0,this.style.visibility="hidden",this.style.pointerEvents="none",this.imageOneInfo.style.pointerEvents="none",this.imageTwoInfo.style.pointerEvents="none",this.infoBoxPosX.style.animation="",this.infoBoxPosY.style.animation="",this.idleSince=Date.now(),this.screenWakeLock.enabled&&this.screenWakeLock.disable(),setTimeout(this.setScreensaverEntityState.bind(this),25)}updateScreensaver(){const e=new Date,t=e.getTime();if(this.energyCollectionUpdateEnabled&&t-this.lastEnergyCollectionUpdate>=1e3*this.energyCollectionUpdateInterval&&(this.hass.connection._energy_wallpanel&&this.hass.connection._energy_wallpanel.refresh(),this.lastEnergyCollectionUpdate=t),this.infoBoxContentCreatedDate&&this.infoBoxContentCreatedDate.getDate()!=e.getDate()&&this.createInfoBoxContent(),Yu.info_move_interval>0&&t-this.lastMove>=1e3*Yu.info_move_interval&&("random"===Yu.info_move_pattern?this.randomMove():"corners"===Yu.info_move_pattern?this.moveAroundCorners():fh.error("Unknown info move type ".concat(Yu.info_move_pattern))),Yu.black_screen_after_time>0&&t-this.screensaverStartedAt>=1e3*Yu.black_screen_after_time?(fh.debug("Setting screen to black"),this.screensaverOverlay.style.background="#000000"):Yu.show_images?(!this.isPaused&&t-this.lastMediaUpdate>=1e3*Yu.display_time&&this.switchActiveMedia("display_time_elapsed"),t-this.lastMediaListUpdate>=1e3*Yu.media_list_update_interval&&this.updateMediaList(null,!0),"visible"!=this.imageOneContainer.style.visibility&&(this.imageOneContainer.style.visibility="visible"),"visible"!=this.imageOne.style.visibility&&(this.imageOne.style.visibility="visible"),"visible"!=this.imageTwoContainer.style.visibility&&(this.imageTwoContainer.style.visibility="visible"),"visible"!=this.imageTwo.style.visibility&&(this.imageTwo.style.visibility="visible")):("hidden"!=this.imageOneContainer.style.visibility&&(this.imageOneContainer.style.visibility="hidden"),"hidden"!=this.imageOne.style.visibility&&(this.imageOne.style.visibility="hidden"),"hidden"!=this.imageTwoContainer.style.visibility&&(this.imageTwoContainer.style.visibility="hidden"),"hidden"!=this.imageTwo.style.visibility&&(this.imageTwo.style.visibility="hidden")),Yu.close_more_info_dialog_time>0){const e=this.getMoreInfoDialog();if(e){const t=new Date;this.moreInfoDialogOpenedAt?t-this.moreInfoDialogOpenedAt>=1e3*Yu.close_more_info_dialog_time&&e.close():this.moreInfoDialogOpenedAt=t}else this.moreInfoDialogOpenedAt=0}if(Yu.debug){let e="";const t={};for(const e in Yu)["profiles"].includes(e)?t[e]="...":t[e]=Yu[e];if(e+='
    Download log
    ',e+="Version: ".concat(zu,"
    "),e+="User-Agent: ".concat(navigator.userAgent,"
    "),e+="Config: ".concat(JSON.stringify(t),"
    "),e+="Fullscreen: ".concat(Ih(),"
    "),e+="Screensaver started at: ".concat(Qu.screensaverStartedAt,"
    "),e+="Screen wake lock: enabled=".concat(this.screenWakeLock.enabled," native=").concat(this.screenWakeLock.nativeWakeLockSupported," lock=").concat(this.screenWakeLock._lock," player=").concat(this.screenWakeLock._player," error=").concat(this.screenWakeLock.error,"
    "),this.screenWakeLock._player){const t=this.screenWakeLock._player;e+="Screen wake lock video: readyState=".concat(t.readyState," currentTime=").concat(t.currentTime," paused=").concat(t.paused," ended=").concat(t.ended,"
    ")}e+="Media list size: ".concat(this.mediaList.length,"
    ");const n=this.getActiveMediaElement();if(n){e+="Current media: ".concat(n.mediaUrl,"
    ");const t=eh.get(n.infoCacheUrl);t&&(e+="Media info: ".concat(JSON.stringify(t),"
    "))}this.debugBox.innerHTML!==e&&(this.debugBox.innerHTML=e,this.debugBox.querySelector("#download_log").addEventListener("click",function(e){fh.downloadMessages(),e.preventDefault()})),this.debugBox.scrollTop=this.debugBox.scrollHeight}this.screenWakeLock.enabled&&Yu.keep_screen_on_time>0&&t-this.screensaverStartedAt>=1e3*Yu.keep_screen_on_time&&(fh.info("Disable wake lock after ".concat(Yu.keep_screen_on_time," seconds")),this.screenWakeLock.disable())}switchMediaDirection(e){this.mediaListDirection=e,this.switchActiveMedia("user_action")}async nextImage(){if(this.updatingMedia)return void fh.debug("Already switching media");fh.debug("Next image");const e=this.mediaListDirection;this.mediaListDirection="forwards";try{await this.switchActiveMedia("user_action")}finally{this.mediaListDirection=e}}async previousImage(){if(this.updatingMedia)return void fh.debug("Already switching media");fh.debug("Previous image");const e=this.mediaListDirection;this.mediaListDirection="backwards";try{await this.switchActiveMedia("user_action")}finally{this.mediaListDirection=e}}togglePlayPause(){if(fh.debug("Toggle play/pause"),this.screensaverRunning())if(this.isPaused)this.isPaused=!1,this.lastMediaUpdate=Date.now()-(this.mediaTimeElapsedBeforePause||0),this.pauseIndicator.style.display="none",this.progressBar.style.animationPlayState="running",this.startPlayingActiveMedia();else{this.isPaused=!0,this.mediaTimeElapsedBeforePause=Date.now()-this.lastMediaUpdate,this.lastMediaUpdate=Date.now(),this.pauseIndicator.style.display="block",this.progressBar.style.animationPlayState="paused";const e=this.getActiveMediaElement(!0);e&&"function"==typeof e.pause&&e.pause()}}setCameraMotionDetectionEntityState(e){const t=Yu.camera_motion_detection_set_entity;t&&this.__hass.states[t]&&(fh.debug("Updating camera motion detection entity",t,e),this.__hass.callService("input_boolean",e?"turn_on":"turn_off",{entity_id:t}).then(e=>{fh.debug(e)},e=>{fh.error("Failed to set camera motion detection entity state:",e)}))}motionDetected(){Yu.camera_motion_detection_stop_screensaver&&this.screensaverRunning()&&this.stopScreensaver(Yu.fade_out_time_motion_detected),this.setCameraMotionDetectionEntityState(!0)}motionStopped(){this.setCameraMotionDetectionEntityState(!1)}handleInteractionEvent(e){const t=["click","touchend"].includes(e.type),n=Date.now();this.idleSince=n;let r="";if("touchstart"==e.type)return void(e.touches&&e.touches[0]&&(this.touchStartX=e.touches[0].clientX));if("touchend"==e.type&&this.touchStartX>=0&&e.changedTouches&&e.changedTouches[0]){const t=e.changedTouches[0].clientX-this.touchStartX;t>=5?r="right":t<=-5&&(r="left"),this.touchStartX=-1}if(!this.screensaverRunning())return this.blockEventsUntil>n&&(t&&e.preventDefault(),e.stopImmediatePropagation()),void(t&&this.setupScreensaver());if(t&&this.hideAllMessages())return void(this.blockEventsUntil=n+1e3);let i=e.clientX,o=e.clientY;if(!i&&e.changedTouches&&e.changedTouches[0]&&(i=e.changedTouches[0].clientX),!o&&e.changedTouches&&e.changedTouches[0]&&(o=e.changedTouches[0].clientY),Yu.debug&&i&&i<100&&o&&o<100)return;const a=_h();if(a){const e=[a.shadowRoot.querySelector(".content"),a.shadowRoot.querySelector("ha-dialog-header")];for(let t=0;t=n.left&&i<=n.right&&o>=n.top&&o<=n.bottom)return void fh.debug("Event on browser mod popup:",e[t])}}if(Yu.iframe_interaction&&"iframe"==wh())return;if(Yu.content_interaction){if(this.getMoreInfoDialog())return;let e=[];e=e.concat(this.__cards||[]),e=e.concat(this.__badges||[]),e=e.concat(this.__views||[]),e.push(this.shadowRoot.getElementById("wallpanel-screensaver-info-box-content")),e.push(this.shadowRoot.getElementById("wallpanel-screensaver-fixed-info-box-content")),"analyze"==Yu.image_info_template&&(e.push(this.imageOneInfo),e.push(this.imageTwoInfo));for(let t=0;t=n.left&&i<=n.right&&o>=n.top&&o<=n.bottom)return void fh.debug("Event on:",e[t])}}let s="";if(r)s="left"==r?"forwards":"backwards",e.stopImmediatePropagation();else if(e instanceof MouseEvent||"TouchEvent"in window&&e instanceof TouchEvent){let n=0,r=0;const a=this.screensaverContainer.getBoundingClientRect();if(i&&(n=(a.right-i)/this.screensaverContainer.clientWidth),o&&(r=(a.bottom-o)/this.screensaverContainer.clientHeight),fh.debug("Event position screensaver:",i,o,a.left,a.right,a.top,a.bottom),r>1||r<0||n>1||n<0)return;if(t&&e.preventDefault(),e.stopImmediatePropagation(),Yu.touch_zone_size_next_image>0&&n<=Yu.touch_zone_size_next_image/100){if(!t)return;s="forwards"}else if(Yu.touch_zone_size_previous_image>0&&n>=(100-Yu.touch_zone_size_previous_image)/100){if(!t)return;s="backwards"}else if(n>=.4&&n<=.6&&r<=.1){const e=new Date;return t&&e-this.lastClickTime<500?(this.clickCount+=1,3==this.clickCount&&(fh.purgeMessages(),Yu.debug=!Yu.debug,this.debugBox.style.visibility=Yu.debug?"visible":"hidden",this.debugBox.style.pointerEvents=Yu.debug?"auto":"none")):this.clickCount=1,void(this.lastClickTime=e)}}s?this.updatingMedia?fh.debug("Already switching media"):(fh.debug("Switching media, direction ".concat(s)),this.mediaListDirection!=s?this.switchMediaDirection(s):this.switchActiveMedia("user_action")):t&&!Yu.stop_screensaver_on_mouse_click||(this.blockEventsUntil=n+1e3*Yu.control_reactivation_time,this.stopScreensaver(Yu.fade_out_time_interaction))}}customElements.get("wallpanel-view-container")||customElements.define("wallpanel-view-container",r),customElements.get("wallpanel-view")||customElements.define("wallpanel-view",i),Ju=document.createElement("wallpanel-view-container"),Qu=document.createElement("wallpanel-view"),Ju.appendChild(Qu),oh.shadowRoot.appendChild(Ju)}function kh(){fh.debug("activateWallpanel");let e=Yu.hide_toolbar,t=Yu.hide_toolbar_action_icons;if(e&&!Yu.hide_toolbar_on_subviews&&$u){const n=Eh();if(n&&n.lovelace&&n.lovelace.rawConfig&&n.lovelace.rawConfig.views)for(let r=0;r{fh.debug("Successfully exited fullscreen")},e=>{fh.debug("Failed to exit fullscreen:",e)}):document.mozCancelFullScreen?document.mozCancelFullScreen():document.msRequestFullscreen?document.msRequestFullscreen():document.webkitExitFullscreen&&document.webkitExitFullscreen())}function Ph(){Xu&&$u?(yh(),bh()?kh():Oh()):Oh()}function Mh(){if(fh.debug("Location changed from '".concat(Vu,"' to '").concat(window.location.href,"'")),window.location.href==Vu)return;Vu=window.location.href,Qu&&Qu.screensaverRunning&&Qu.screensaverRunning()&&Yu.stop_screensaver_on_location_change&&(Zu?(Zu=!1,Qu.screensaverStopNavigationPathTimeout&&clearTimeout(Qu.screensaverStopNavigationPathTimeout)):Qu.stopScreensaver());let e=null,t=null;const n=window.location.pathname.split("/");n.length>1&&(e=n[1],n.length>2&&(t=n[2]));const r=Xu&&e!=Xu;Xu=e,$u=t,r?(fh.debug("Reset dashboard config"),qu={},Lh(Ph)):Ph()}function Lh(e){let t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:null;const n=Date.now();null===t&&(t=n);const r=(n-t)/1e3;if(ih=document.querySelector("body > home-assistant"),ih&&ih.shadowRoot&&(oh=ih.shadowRoot.querySelector("home-assistant-main")),!ih||!ih.shadowRoot||!oh)return r>=5?void fh.error("Wallpanel startup failed after ".concat(r," seconds, element home-assistant / home-assistant-main not found.")):void setTimeout(Lh,100,e,t);const i=Eh();if(!(i&&i.lovelace&&i.lovelace.config&&i.lovelace.config))return r>=5?void fh.info("No lovelace config found after ".concat(r," seconds")):void setTimeout(Lh,100,e,t);if(!customElements.get("hui-view"))return r>=5?void fh.error("Wallpanel startup failed after ".concat(r," seconds, hui-view not found.")):void setTimeout(Lh,100,e,t);if(!window.browser_mod){let n=xh(["wait_for_browser_mod_time"]).wait_for_browser_mod_time;if(void 0===n&&(n=Hu.wait_for_browser_mod_time),r{let t=null;try{t=e.target.location.href}catch(e){fh.debug(e)}fh.debug("location-changed",t,e),setTimeout(Mh,20)}),window.navigation&&window.navigation.addEventListener("navigate",e=>{let t=null;try{t=e.destination.url}catch(e){fh.debug(e)}fh.debug("navigate",t,e),setTimeout(Mh,30)}),(ih.hass||ih.__hass).connection.subscribeEvents(function(e){fh.debug("lovelace_updated",e),(e.data.url_path?e.data.url_path:"lovelace")==Xu&&(ih.hass||ih.__hass).connection.sendMessagePromise({type:"lovelace/config",url_path:e.data.url_path}).then(e=>{if(qu={},e.wallpanel)for(const t in e.wallpanel)(t in Hu||t in Gu)&&(qu[t]=e.wallpanel[t]);Ph()})},"lovelace_updated"),setTimeout(Mh,0)})}catch(e){fh.error(e.stack)}var Bh=!1,Uh=function(e){return e instanceof Uh?e:this instanceof Uh?void(this.EXIFwrapped=e):new Uh(e)},Dh=Uh.Tags={36864:"ExifVersion",40960:"FlashpixVersion",40961:"ColorSpace",40962:"PixelXDimension",40963:"PixelYDimension",37121:"ComponentsConfiguration",37122:"CompressedBitsPerPixel",37500:"MakerNote",37510:"UserComment",40964:"RelatedSoundFile",36867:"DateTimeOriginal",36868:"DateTimeDigitized",37520:"SubsecTime",37521:"SubsecTimeOriginal",37522:"SubsecTimeDigitized",33434:"ExposureTime",33437:"FNumber",34850:"ExposureProgram",34852:"SpectralSensitivity",34855:"ISOSpeedRatings",34856:"OECF",37377:"ShutterSpeedValue",37378:"ApertureValue",37379:"BrightnessValue",37380:"ExposureBias",37381:"MaxApertureValue",37382:"SubjectDistance",37383:"MeteringMode",37384:"LightSource",37385:"Flash",37396:"SubjectArea",37386:"FocalLength",41483:"FlashEnergy",41484:"SpatialFrequencyResponse",41486:"FocalPlaneXResolution",41487:"FocalPlaneYResolution",41488:"FocalPlaneResolutionUnit",41492:"SubjectLocation",41493:"ExposureIndex",41495:"SensingMethod",41728:"FileSource",41729:"SceneType",41730:"CFAPattern",41985:"CustomRendered",41986:"ExposureMode",41987:"WhiteBalance",41988:"DigitalZoomRation",41989:"FocalLengthIn35mmFilm",41990:"SceneCaptureType",41991:"GainControl",41992:"Contrast",41993:"Saturation",41994:"Sharpness",41995:"DeviceSettingDescription",41996:"SubjectDistanceRange",40965:"InteroperabilityIFDPointer",42016:"ImageUniqueID"},Fh=Uh.TiffTags={256:"ImageWidth",257:"ImageHeight",34665:"ExifIFDPointer",34853:"GPSInfoIFDPointer",40965:"InteroperabilityIFDPointer",258:"BitsPerSample",259:"Compression",262:"PhotometricInterpretation",274:"Orientation",277:"SamplesPerPixel",284:"PlanarConfiguration",530:"YCbCrSubSampling",531:"YCbCrPositioning",282:"XResolution",283:"YResolution",296:"ResolutionUnit",273:"StripOffsets",278:"RowsPerStrip",279:"StripByteCounts",513:"JPEGInterchangeFormat",514:"JPEGInterchangeFormatLength",301:"TransferFunction",318:"WhitePoint",319:"PrimaryChromaticities",529:"YCbCrCoefficients",532:"ReferenceBlackWhite",306:"DateTime",270:"ImageDescription",271:"Make",272:"Model",305:"Software",315:"Artist",33432:"Copyright"},Nh=Uh.GPSTags={0:"GPSVersionID",1:"GPSLatitudeRef",2:"GPSLatitude",3:"GPSLongitudeRef",4:"GPSLongitude",5:"GPSAltitudeRef",6:"GPSAltitude",7:"GPSTimeStamp",8:"GPSSatellites",9:"GPSStatus",10:"GPSMeasureMode",11:"GPSDOP",12:"GPSSpeedRef",13:"GPSSpeed",14:"GPSTrackRef",15:"GPSTrack",16:"GPSImgDirectionRef",17:"GPSImgDirection",18:"GPSMapDatum",19:"GPSDestLatitudeRef",20:"GPSDestLatitude",21:"GPSDestLongitudeRef",22:"GPSDestLongitude",23:"GPSDestBearingRef",24:"GPSDestBearing",25:"GPSDestDistanceRef",26:"GPSDestDistance",27:"GPSProcessingMethod",28:"GPSAreaInformation",29:"GPSDateStamp",30:"GPSDifferential"},Wh=Uh.IFD1Tags={256:"ImageWidth",257:"ImageHeight",258:"BitsPerSample",259:"Compression",262:"PhotometricInterpretation",273:"StripOffsets",274:"Orientation",277:"SamplesPerPixel",278:"RowsPerStrip",279:"StripByteCounts",282:"XResolution",283:"YResolution",284:"PlanarConfiguration",296:"ResolutionUnit",513:"JpegIFOffset",514:"JpegIFByteCount",529:"YCbCrCoefficients",530:"YCbCrSubSampling",531:"YCbCrPositioning",532:"ReferenceBlackWhite"},jh=Uh.StringValues={ExposureProgram:{0:"Not defined",1:"Manual",2:"Normal program",3:"Aperture priority",4:"Shutter priority",5:"Creative program",6:"Action program",7:"Portrait mode",8:"Landscape mode"},MeteringMode:{0:"Unknown",1:"Average",2:"CenterWeightedAverage",3:"Spot",4:"MultiSpot",5:"Pattern",6:"Partial",255:"Other"},LightSource:{0:"Unknown",1:"Daylight",2:"Fluorescent",3:"Tungsten (incandescent light)",4:"Flash",9:"Fine weather",10:"Cloudy weather",11:"Shade",12:"Daylight fluorescent (D 5700 - 7100K)",13:"Day white fluorescent (N 4600 - 5400K)",14:"Cool white fluorescent (W 3900 - 4500K)",15:"White fluorescent (WW 3200 - 3700K)",17:"Standard light A",18:"Standard light B",19:"Standard light C",20:"D55",21:"D65",22:"D75",23:"D50",24:"ISO studio tungsten",255:"Other"},Flash:{0:"Flash did not fire",1:"Flash fired",5:"Strobe return light not detected",7:"Strobe return light detected",9:"Flash fired, compulsory flash mode",13:"Flash fired, compulsory flash mode, return light not detected",15:"Flash fired, compulsory flash mode, return light detected",16:"Flash did not fire, compulsory flash mode",24:"Flash did not fire, auto mode",25:"Flash fired, auto mode",29:"Flash fired, auto mode, return light not detected",31:"Flash fired, auto mode, return light detected",32:"No flash function",65:"Flash fired, red-eye reduction mode",69:"Flash fired, red-eye reduction mode, return light not detected",71:"Flash fired, red-eye reduction mode, return light detected",73:"Flash fired, compulsory flash mode, red-eye reduction mode",77:"Flash fired, compulsory flash mode, red-eye reduction mode, return light not detected",79:"Flash fired, compulsory flash mode, red-eye reduction mode, return light detected",89:"Flash fired, auto mode, red-eye reduction mode",93:"Flash fired, auto mode, return light not detected, red-eye reduction mode",95:"Flash fired, auto mode, return light detected, red-eye reduction mode"},SensingMethod:{1:"Not defined",2:"One-chip color area sensor",3:"Two-chip color area sensor",4:"Three-chip color area sensor",5:"Color sequential area sensor",7:"Trilinear sensor",8:"Color sequential linear sensor"},SceneCaptureType:{0:"Standard",1:"Landscape",2:"Portrait",3:"Night scene"},SceneType:{1:"Directly photographed"},CustomRendered:{0:"Normal process",1:"Custom process"},WhiteBalance:{0:"Auto white balance",1:"Manual white balance"},GainControl:{0:"None",1:"Low gain up",2:"High gain up",3:"Low gain down",4:"High gain down"},Contrast:{0:"Normal",1:"Soft",2:"Hard"},Saturation:{0:"Normal",1:"Low saturation",2:"High saturation"},Sharpness:{0:"Normal",1:"Soft",2:"Hard"},SubjectDistanceRange:{0:"Unknown",1:"Macro",2:"Close view",3:"Distant view"},FileSource:{3:"DSC"},Components:{0:"",1:"Y",2:"Cb",3:"Cr",4:"R",5:"G",6:"B"}};function zh(e){return!!e.exifdata}function Hh(e,t){function n(n){var r=Gh(n);e.exifdata=r||{};var i=function(e){var t=new DataView(e);if(255!=t.getUint8(0)||216!=t.getUint8(1))return!1;var n=2,r=e.byteLength,i=function(e,t){return 56===e.getUint8(t)&&66===e.getUint8(t+1)&&73===e.getUint8(t+2)&&77===e.getUint8(t+3)&&4===e.getUint8(t+4)&&4===e.getUint8(t+5)};for(;n")+8,l=(s=s.substring(s.indexOf("4?d:t+8,a=[],c=0;c4?d:t+8,f-1);case 3:if(1==f)return e.getUint16(t+8,!i);for(o=f>2?d:t+8,a=[],c=0;c>4){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:t+=String.fromCharCode(i);break;case 12:case 13:o=e[n++],t+=String.fromCharCode((31&i)<<6|63&o);break;case 14:o=e[n++],a=e[n++],t+=String.fromCharCode((15&i)<<12|(63&o)<<6|63&a)}return t}(r)}function Jh(e,t){if("Exif"!=$h(e,t,4))return!1;var n,r,i,o,a,s=t+6;if(18761==e.getUint16(s))n=!1;else{if(19789!=e.getUint16(s))return!1;n=!0}if(42!=e.getUint16(s+2,!n))return!1;var c=e.getUint32(s+4,!n);if(c<8)return!1;if((r=Vh(e,s,s+c,Fh,n)).ExifIFDPointer)for(i in o=Vh(e,s,s+r.ExifIFDPointer,Dh,n)){switch(i){case"LightSource":case"Flash":case"MeteringMode":case"ExposureProgram":case"SensingMethod":case"SceneCaptureType":case"SceneType":case"CustomRendered":case"WhiteBalance":case"GainControl":case"Contrast":case"Saturation":case"Sharpness":case"SubjectDistanceRange":case"FileSource":o[i]=jh[i][o[i]];break;case"ExifVersion":case"FlashpixVersion":o[i]=String.fromCharCode(o[i][0],o[i][1],o[i][2],o[i][3]);break;case"ComponentsConfiguration":o[i]=jh.Components[o[i][0]]+jh.Components[o[i][1]]+jh.Components[o[i][2]]+jh.Components[o[i][3]]}r[i]=o[i]}if(r.GPSInfoIFDPointer)for(i in a=Vh(e,s,s+r.GPSInfoIFDPointer,Nh,n)){if("GPSVersionID"==i)try{a[i]=a[i][0]+"."+a[i][1]+"."+a[i][2]+"."+a[i][3]}catch(e){}r[i]=a[i]}return r.thumbnail=function(e,t,n,r){var i=function(e,t,n){var r=e.getUint16(t,!n);return e.getUint32(t+2+12*r,!n)}(e,t+n,r);if(!i)return{};if(i>e.byteLength)return{};var o=Vh(e,t,t+i,Wh,r);if(o.Compression)switch(o.Compression){case 6:if(o.JpegIFOffset&&o.JpegIFByteCount){var a=t+o.JpegIFOffset,s=o.JpegIFByteCount;o.blob=new Blob([new Uint8Array(e.buffer,a,s)],{type:"image/jpeg"})}break;case 1:fh.log("Thumbnail image format is TIFF, which is not implemented.");break;default:fh.log("Unknown thumbnail image format '%s'",o.Compression)}else 2==o.PhotometricInterpretation&&fh.log("Thumbnail image format is RGB, which is not implemented.");return o}(e,s,c,n),r}function Qh(e){var t={};if(1==e.nodeType){if(e.attributes.length>0){t["@attributes"]={};for(var n=0;n0)for(var n=0;n\n ","\n \n "," ","\n \n \n ","\n \n \n "]);return b=function(){return a},a}function c(){var a=S(["\n
    \n ","\n
    \n "]);return c=function(){return a},a}function d(){var a=S(["\n
    \n ","\n ","\n
    \n "]);return d=function(){return a},a}function e(){var a=S(["\n
    \n ","\n ","\n
    \n "]);return e=function(){return a},a}function f(){var a=S(["\n \n \n \n ","\n \n ","\n ","\n ","\n ","\n ","\n \n ","\n "]);return f=function(){return a},a}function g(){var a=S(["",""]);return g=function(){return a},a}function h(){var a=S(["\n \n ","\n "]);return h=function(){return a},a}function j(){var a=S(["\n \n "]);return j=function(){return a},a}function k(){var a=S(["\n "]);return k=function(){return a},a}function l(){var a=S(["\n "]);return l=function(){return a},a}function m(){var a=S(["",""]);return m=function(){return a},a}function n(){var a=S(["\n \n "]);return n=function(){return a},a}function o(){var a=S(["\n \n ","\n "]);return o=function(){return a},a}function p(){var a=S(["\n \n ","\n "]);return p=function(){return a},a}function q(){var a=S(["\n \n "]);return q=function(){return a},a}function r(){var a=S(["\n \n ","\n \n "]);return r=function(){return a},a}function s(){var a=S(["\n "]);return s=function(){return a},a}function t(){var a=S(["\n \n \n \n \n \n \n \n \n \n \n \n "]);return t=function(){return a},a}function u(){var a=S(["\n \n \n \n "]);return u=function(){return a},a}function v(){var a=S(["\n
    \n ","\n ","\n
    \n "]);return v=function(){return a},a}function w(){var a=S(["\n
    \n ","\n
    \n "]);return w=function(){return a},a}function x(){var a=S([""]);return x=function(){return a},a}function y(){var a=S(["\n
    \n ","\n ","\n
    \n ","\n
    \n
    \n ","\n "]);return y=function(){return a},a}function z(){var a=S(["\n
    \n ","\n
    "]);return z=function(){return a},a}function A(){var a=S(["\n "," -\n ","\n "]);return A=function(){return a},a}function B(){var a=S(["\n ","\n "]);return B=function(){return a},a}function C(){var a=S(["\n
    \n ","\n
    \n "]);return C=function(){return a},a}function D(){var a=S(["\n \n ","\n \n ","\n \n \n ","\n \n ","\n \n "]);return D=function(){return a},a}function E(){var a=S(["\n
    \n ","\n
    ","
    \n ","\n
    \n "]);return E=function(){return a},a}function F(){var a=S(["\n
    \n ","\n
    \n "]);return F=function(){return a},a}function G(){var a=S(["\n
    \n \n
    \n "]);return G=function(){return a},a}function H(){var a=S(["\n
    \n \n
    \n "]);return H=function(){return a},a}function I(){var a=S(["\n
    \n "," ","\n
    \n "]);return I=function(){return a},a}function J(){var a=S([""]);return J=function(){return a},a}function K(){var a=S(["\n
    \n Entity not available: ","\n
    \n "]);return K=function(){return a},a}function L(){var a=S(["\n \n
    mini-graph-card
    \n ","\n
    \n "]);return L=function(){return a},a}function M(){var a=S(["\n \n "," "," "," ","\n \n "]);return M=function(){return a},a}function N(){var a=S([""]);return N=function(){return a},a}function O(a,b){var c=Object.keys(a);if(Object.getOwnPropertySymbols){var d=Object.getOwnPropertySymbols(a);b&&(d=d.filter(function(b){return Object.getOwnPropertyDescriptor(a,b).enumerable})),c.push.apply(c,d)}return c}function P(a){for(var b,c=1;c div {\n padding: 0px 16px 16px 16px;\n }\n ha-card > div:last-child {\n padding-bottom: 0;\n }\n ha-card .graph {\n padding: 0;\n order: 10;\n }\n ha-card[points] .line--points,\n ha-card[labels] .graph__labels.--primary {\n opacity: 0;\n transition: opacity .25s;\n animation: none;\n }\n ha-card[labels-secondary] .graph__labels.--secondary {\n opacity: 0;\n transition: opacity .25s;\n animation: none;\n }\n ha-card[points]:hover .line--points,\n ha-card:hover .graph__labels.--primary,\n ha-card:hover .graph__labels.--secondary {\n opacity: 1;\n }\n ha-card[fill] path {\n stroke-linecap: initial;\n stroke-linejoin: initial;\n }\n ha-card .graph__legend {\n order: -1;\n padding: 0 16px 8px 16px;\n }\n ha-card[group] {\n box-shadow: none;\n border: none;\n padding: 0;\n }\n ha-card[group] > div {\n padding-left: 0;\n padding-right: 0;\n }\n ha-card[group] .graph__legend {\n padding-left: 0;\n padding-right: 0;\n }\n ha-card[hover] {\n cursor: pointer;\n }\n ha-spinner {\n margin: 4px auto;\n }\n .flex {\n display: flex;\n display: -webkit-flex;\n min-width: 0;\n }\n .header {\n justify-content: space-between;\n }\n .header[loc=\"center\"] {\n justify-content: space-around;\n }\n .header[loc=\"left\"] {\n align-self: flex-start;\n }\n .header[loc=\"right\"] {\n align-self: flex-end;\n }\n .name {\n align-items: center;\n min-width: 0;\n letter-spacing: var(--mcg-title-letter-spacing, normal);\n }\n .name > span {\n font-size: 1.2em;\n font-weight: var(--mcg-title-font-weight, 500);\n max-height: 1.4em;\n min-height: 1.4em;\n opacity: .65;\n }\n .icon {\n color: var(--state-icon-color, #44739e);\n display: inline-block;\n flex: 0 0 1.7em;\n text-align: center;\n }\n .icon > ha-icon {\n height: 1.7em;\n width: 1.7em;\n }\n .icon[loc=\"left\"] {\n order: -1;\n margin-right: .6em;\n margin-left: 0;\n }\n .icon[loc=\"state\"] {\n align-self: center;\n }\n .states {\n align-items: flex-start;\n font-weight: 300;\n justify-content: space-between;\n flex-wrap: nowrap;\n }\n .states .icon {\n align-self: center;\n margin-left: 0;\n }\n .states[loc=\"center\"] {\n justify-content: space-evenly;\n }\n .states[loc=\"right\"] > .state {\n margin-left: auto;\n order: 2;\n }\n .states[loc=\"center\"] .states--secondary,\n .states[loc=\"right\"] .states--secondary {\n margin-left: 0;\n }\n .states[loc=\"center\"] .states--secondary {\n align-items: center;\n }\n .states[loc=\"right\"] .states--secondary {\n align-items: flex-start;\n }\n .states[loc=\"center\"] .state__time {\n left: 50%;\n transform: translateX(-50%);\n }\n .states > .icon > ha-icon {\n height: 2em !important;\n width: 2em !important;\n }\n .states--secondary {\n display: flex;\n flex-flow: column;\n flex-wrap: wrap;\n align-items: flex-end;\n margin-left: 1rem;\n min-width: 0;\n margin-left: 1.4em;\n }\n .states--secondary:empty {\n display: none;\n }\n .state {\n position: relative;\n display: flex;\n flex-wrap: nowrap;\n max-width: 100%;\n min-width: 0;\n }\n .state > svg {\n align-self: center;\n border-radius: 100%;\n }\n .state--small {\n font-size: .6em;\n margin-bottom: .6rem;\n flex-wrap: nowrap;\n }\n .state--small > svg {\n position: absolute;\n left: -1.6em;\n align-self: center;\n height: 1em;\n width: 1em;\n border-radius: 100%;\n margin-right: 1em;\n }\n .state--small:last-child {\n margin-bottom: 0;\n }\n .states--secondary > :only-child {\n font-size: 1em;\n margin-bottom: 0;\n }\n .states--secondary > :only-child svg {\n display: none;\n }\n .state__value {\n display: inline-block;\n font-size: 2.4em;\n margin-right: .25rem;\n line-height: 1.2em;\n }\n .state__uom {\n flex: 1;\n align-self: flex-end;\n display: inline-block;\n font-size: 1.4em;\n font-weight: 400;\n line-height: 1.6em;\n margin-top: .1em;\n opacity: .6;\n vertical-align: bottom;\n }\n .state--small .state__uom {\n flex: 1;\n }\n .state__time {\n font-size: .95rem;\n font-weight: 500;\n bottom: -1.1rem;\n left: 0;\n opacity: .75;\n position: absolute;\n white-space: nowrap;\n animation: fade .15s cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n .states[loc=\"right\"] .state__time {\n left: initial;\n right: 0;\n }\n .graph {\n align-self: flex-end;\n box-sizing: border-box;\n display: flex;\n flex-direction: column;\n margin-top: auto;\n width: 100%;\n }\n .graph__container {\n display: flex;\n flex-direction: row;\n position: relative;\n }\n .graph__container__svg {\n cursor: default;\n flex: 1;\n }\n svg {\n overflow: hidden;\n display: block;\n }\n path {\n stroke-linecap: round;\n stroke-linejoin: round;\n }\n .fill[anim=\"false\"] {\n animation: reveal .25s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;\n }\n .fill[anim=\"false\"][type=\"fade\"] {\n animation: reveal-2 .25s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;\n }\n .line--points[anim=\"false\"],\n .line[anim=\"false\"] {\n animation: pop .25s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;\n }\n .line--points[inactive],\n .line--rect[inactive],\n .fill--rect[inactive] {\n opacity: 0 !important;\n animation: none !important;\n transition: all .15s !important;\n }\n .line--points[tooltip] .line--point[inactive] {\n opacity: 0;\n }\n .line--point {\n cursor: pointer;\n fill: var(--primary-background-color, white);\n stroke-width: inherit;\n }\n .line--point:hover {\n fill: var(--mcg-hover, inherit) !important;\n }\n .bars {\n animation: pop .25s cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n .bars[anim] {\n animation: bars .5s cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n .bar {\n transition: opacity .25s cubic-bezier(0.215, 0.61, 0.355, 1);\n }\n .bar:hover {\n opacity: .5;\n cursor: pointer;\n }\n ha-card[gradient] .line--point:hover {\n fill: var(--primary-text-color, white);\n }\n path,\n .line--points,\n .fill {\n opacity: 0;\n }\n .line--points[anim=\"true\"][init] {\n animation: pop .5s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;\n }\n .fill[anim=\"true\"][init] {\n animation: reveal .5s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;\n }\n .fill[anim=\"true\"][init][type=\"fade\"] {\n animation: reveal-2 .5s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;\n }\n .line[anim=\"true\"][init] {\n animation: dash 1s cubic-bezier(0.215, 0.61, 0.355, 1) forwards;\n }\n .graph__labels.--secondary {\n right: 0;\n margin-right: 0px;\n align-items: flex-end;\n }\n .graph__labels {\n align-items: flex-start;\n flex-direction: column;\n font-size: calc(.15em + 8.5px);\n font-weight: 400;\n justify-content: space-between;\n margin-right: 10px;\n padding: .6em;\n position: absolute;\n pointer-events: none;\n top: 0; bottom: 0;\n opacity: .75;\n }\n .graph__labels > span {\n cursor: pointer;\n background: var(--primary-background-color, white);\n border-radius: 1em;\n padding: .2em .6em;\n box-shadow: 0 1px 3px rgba(0,0,0,.12), 0 1px 2px rgba(0,0,0,.24);\n }\n .graph__legend {\n display: flex;\n flex-direction: row;\n justify-content: space-evenly;\n padding-top: 16px;\n flex-wrap: wrap;\n }\n .graph__legend__item {\n cursor: pointer;\n display: flex;\n min-width: 0;\n margin: .4em;\n align-items: center\n }\n .graph__legend__item span {\n opacity: .75;\n margin-left: .4em;\n }\n .graph__legend__item svg {\n border-radius: 100%;\n min-width: 10px;\n }\n .info {\n justify-content: space-between;\n align-items: middle;\n }\n .info__item {\n display: flex;\n flex-flow: column;\n text-align: center;\n }\n .info__item:last-child {\n align-items: flex-end;\n text-align: right;\n }\n .info__item:first-child {\n align-items: flex-start;\n text-align: left;\n }\n .info__item__type {\n text-transform: capitalize;\n font-weight: 500;\n opacity: .9;\n }\n .info__item__time,\n .info__item__value {\n opacity: .75;\n }\n .ellipsis {\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n }\n @keyframes fade {\n 0% { opacity: 0; }\n }\n @keyframes reveal {\n 0% { opacity: 0; }\n 100% { opacity: .15; }\n }\n @keyframes reveal-2 {\n 0% { opacity: 0; }\n 100% { opacity: .4; }\n }\n @keyframes pop {\n 0% { opacity: 0; }\n 100% { opacity: 1; }\n }\n @keyframes bars {\n 0% { opacity: 0; }\n 50% { opacity: 0; }\n 100% { opacity: 1; }\n }\n @keyframes dash {\n 0% {\n opacity: 0;\n }\n 25% {\n opacity: 1;\n }\n 100% {\n opacity: 1;\n stroke-dashoffset: 0;\n }\n }"]);return R=function(){return a},a}function S(a,b){return b||(b=a.slice(0)),Object.freeze(Object.defineProperties(a,{raw:{value:Object.freeze(b)}}))}function T(a,b,c,d,e,f,g){try{var h=a[f](g),i=h.value}catch(a){return void c(a)}h.done?b(i):Promise.resolve(i).then(d,e)}function U(a){return function(){var b=this,c=arguments;return new Promise(function(d,e){function f(a){T(h,d,e,f,g,"next",a)}function g(a){T(h,d,e,f,g,"throw",a)}var h=a.apply(b,c);f(void 0)})}}function W(a){return $(a)||Z(a)||Y(a)||X()}function X(){throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function Y(a,b){if(a){if("string"==typeof a)return _(a,b);var c=Object.prototype.toString.call(a).slice(8,-1);return"Object"===c&&a.constructor&&(c=a.constructor.name),"Map"===c||"Set"===c?Array.from(a):"Arguments"===c||/^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(c)?_(a,b):void 0}}function Z(a){if("undefined"!=typeof Symbol&&Symbol.iterator in Object(a))return Array.from(a)}function $(a){if(Array.isArray(a))return _(a)}function _(a,b){(null==b||b>a.length)&&(b=a.length);for(var c=0,d=Array(b);ca.parentNode.removeChild(a))}function S(a,b){var c=2=b.oldVersion&&c.createObjectStore("local-forage-detect-blob-support")}catch(c){if("ConstraintError"===c.name)console.warn("The database \""+a.name+"\" has been upgraded from version "+b.oldVersion+" to version "+b.newVersion+", but the storage \""+a.storeName+"\" already exists.");else throw c}}),f.onerror=function(a){a.preventDefault(),d(f.error)},f.onsuccess=function(){c(f.result),ga(a)}})}function ja(a){return ia(a,!1)}function ka(a){return ia(a,!0)}function la(a,b){if(!a.db)return!0;var c=!a.db.objectStoreNames.contains(a.storeName),d=a.versiona.db.version;if(d&&(a.version!==b&&console.warn("The database \""+a.name+"\" can't be downgraded from version "+a.db.version+" to version "+a.version+"."),a.version=a.db.version),e||c){if(c){var f=a.db.version+1;f>a.version&&(a.version=f)}return!0}return!1}function ma(a){return new Vc(function(b,c){var d=new FileReader;d.onerror=c,d.onloadend=function(c){var d=btoa(c.target.result||"");b({__local_forage_encoded_blob:!0,data:d,type:a.type})},d.readAsBinaryString(a)})}function na(a){var b=ca(atob(a.data));return Z([b],{type:a.type})}function oa(a){return a&&a.__local_forage_encoded_blob}function pa(a){var b=this,c=b._initReady().then(function(){var a=Wc[b._dbInfo.name];if(a&&a.dbReady)return a.dbReady});return _(c,a,a),c}function qa(a){fa(a);for(var b,c=Wc[a.name],d=c.forages,e=0;e(a.db=b,la(a)?ka(a):b)).then(b=>{a.db=c.db=b;for(var e=0;e{throw ha(a,b),b})}function ra(a,b,c,d){d===void 0&&(d=1);try{var e=a.db.transaction(a.storeName,b);c(null,e)}catch(e){if(0{if(!a.db||"NotFoundError"===e.name&&!a.db.objectStoreNames.contains(a.storeName)&&a.version<=a.db.version)return a.db&&(a.version=a.db.version+1),ka(a)}).then(()=>qa(a).then(function(){ra(a,b,c,d-1)})).catch(c);c(e)}}function sa(){return{forages:[],db:null,dbReady:null,deferredOperations:[]}}function ta(a){var b,c,d,e,f,g=.75*a.length,h=a.length,j=0;"="===a[a.length-1]&&(g--,"="===a[a.length-2]&&g--);var k=new ArrayBuffer(g),l=new Uint8Array(k);for(b=0;b>4,l[j++]=(15&d)<<4|e>>2,l[j++]=(3&e)<<6|63&f;return k}function ua(a){var b,c=new Uint8Array(a),d="";for(b=0;b>2],d+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(3&c[b])<<4|c[b+1]>>4],d+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[(15&c[b+1])<<2|c[b+2]>>6],d+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[63&c[b+2]];return 2==c.length%3?d=d.substring(0,d.length-1)+"=":1==c.length%3&&(d=d.substring(0,d.length-2)+"=="),d}function va(a,b,c,d){a.executeSql("CREATE TABLE IF NOT EXISTS ".concat(b.storeName," ")+"(id INTEGER PRIMARY KEY, key unique, value)",[],c,d)}function wa(a,b,c,d,e,f){a.executeSql(c,d,e,function(a,g){g.code===g.SYNTAX_ERR?a.executeSql("SELECT name FROM sqlite_master WHERE type='table' AND name = ?",[b.storeName],function(a,h){h.rows.length?f(a,g):va(a,b,function(){a.executeSql(c,d,e,f)},f)},f):f(a,g)},f)}function xa(a,b,c,d){var e=this;a=aa(a);var f=new Vc(function(f,g){e.ready().then(function(){void 0===b&&(b=null);var h=b,i=e._dbInfo;i.serializer.serialize(b,function(b,j){j?g(j):i.db.transaction(function(c){wa(c,i,"INSERT OR REPLACE INTO ".concat(i.storeName," ")+"(key, value) VALUES (?, ?)",[a,b],function(){f(h)},function(a,b){g(b)})},function(b){if(b.code===b.QUOTA_ERR){if(0 '__WebKitDatabaseInfoTable__'",[],function(c,d){for(var e=[],f=0;f>8|240&b>>4,15&b>>4|240&b,(15&b)<<4|15&b,1):8===c?Qa(255&b>>24,255&b>>16,255&b>>8,(255&b)/255):4===c?Qa(15&b>>12|240&b>>8,15&b>>8|240&b>>4,15&b>>4|240&b,((15&b)<<4|15&b)/255):null):(b=ae.exec(a))?new Ta(b[1],b[2],b[3],1):(b=be.exec(a))?new Ta(255*b[1]/100,255*b[2]/100,255*b[3]/100,1):(b=ce.exec(a))?Qa(b[1],b[2],b[3],b[4]):(b=de.exec(a))?Qa(255*b[1]/100,255*b[2]/100,255*b[3]/100,b[4]):(b=ee.exec(a))?Za(b[1],b[2]/100,b[3]/100,1):(b=fe.exec(a))?Za(b[1],b[2]/100,b[3]/100,b[4]):ge.hasOwnProperty(a)?Pa(ge[a]):"transparent"===a?new Ta(NaN,NaN,NaN,0):null}function Pa(a){return new Ta(255&a>>16,255&a>>8,255&a,1)}function Qa(c,d,e,f){return 0>=f&&(c=d=e=NaN),new Ta(c,d,e,f)}function Ra(a){return(a instanceof La||(a=Oa(a)),!a)?new Ta:(a=a.rgb(),new Ta(a.r,a.g,a.b,a.opacity))}function Sa(a,c,d,b){return 1===arguments.length?Ra(a):new Ta(a,c,d,null==b?1:b)}function Ta(a,c,d,b){this.r=+a,this.g=+c,this.b=+d,this.opacity=+b}function Ua(){return"#".concat(Ya(this.r)).concat(Ya(this.g)).concat(Ya(this.b))}function Va(){var b=Wa(this.opacity);return"".concat(1===b?"rgb(":"rgba(").concat(Xa(this.r),", ").concat(Xa(this.g),", ").concat(Xa(this.b)).concat(1===b?")":", ".concat(b,")"))}function Wa(a){return isNaN(a)?1:Gb(0,Hb(1,a))}function Xa(a){return Gb(0,Hb(255,Fb(a)||0))}function Ya(a){return a=Xa(a),(16>a?"0":"")+a.toString(16)}function Za(b,c,d,e){return 0>=e?b=c=d=NaN:0>=d||1<=d?b=c=NaN:0>=c&&(b=NaN),new ab(b,c,d,e)}function $a(a){if(a instanceof ab)return new ab(a.h,a.s,a.l,a.opacity);if(a instanceof La||(a=Oa(a)),!a)return new ab;if(a instanceof ab)return a;a=a.rgb();var c=a.r/255,d=a.g/255,e=a.b/255,b=Hb(c,d,e),f=Gb(c,d,e),g=NaN,i=f-b,j=(f+b)/2;return i?(g=c===f?(d-e)/i+6*(dj?f+b:2-f-b,g*=60):i=0j?0:g,new ab(g,i,j,a.opacity)}function ab(a,b,c,d){this.h=+a,this.s=+b,this.l=+c,this.opacity=+d}function bb(a){return a=(a||0)%360,0>a?a+360:a}function cb(a){return Gb(0,Hb(1,a||0))}function db(a,b,c){return 255*(60>a?b+(c-b)*a/60:180>a?c:240>a?b+(c-b)*(240-a)/60:b)}function eb(a){if(a instanceof fb)return new fb(a.l,a.a,a.b,a.opacity);if(a instanceof lb)return mb(a);a instanceof Ta||(a=Ra(a));var c,d,e=jb(a.r),f=jb(a.g),g=jb(a.b),b=gb((.2225045*e+.7168786*f+.0606169*g)/1);return e===f&&f===g?c=d=b:(c=gb((.4360747*e+.3850649*f+.1430804*g)/ke),d=gb((.0139322*e+.0971045*f+.7141733*g)/le)),new fb(116*b-16,500*(c-b),200*(b-d),a.opacity)}function fb(c,d,a,b){this.l=+c,this.a=+d,this.b=+a,this.opacity=+b}function gb(a){return a>pe?Eb(a,1/3):a/oe+me}function hb(a){return a>ne?a*a*a:oe*(a-me)}function ib(a){return 255*(.0031308>=a?12.92*a:1.055*Eb(a,1/2.4)-.055)}function jb(a){return .04045>=(a/=255)?a/12.92:Eb((a+.055)/1.055,2.4)}function kb(a){if(a instanceof lb)return new lb(a.h,a.c,a.l,a.opacity);if(a instanceof fb||(a=eb(a)),0===a.a&&0===a.b)return new lb(NaN,0a.l?0:NaN,a.l,a.opacity);var b=Db(a.b,a.a)*ie;return new lb(0>b?b+360:b,Cb(a.a*a.a+a.b*a.b),a.l,a.opacity)}function lb(a,b,c,d){this.h=+a,this.c=+b,this.l=+c,this.opacity=+d}function mb(a){if(isNaN(a.h))return new fb(a.l,0,0,a.opacity);var b=a.h*he;return new fb(a.l,Bb(b)*a.c,Ab(b)*a.c,a.opacity)}function nb(a){if(a instanceof ob)return new ob(a.h,a.s,a.l,a.opacity);a instanceof Ta||(a=Ra(a));var c=a.r/255,d=a.g/255,e=a.b/255,b=(xe*e+ve*c-we*d)/(xe+ve-we),f=e-b,g=(ue*(d-b)-se*f)/te,i=Cb(g*g+f*f)/(ue*b*(1-b)),j=i?Db(g,f)*ie-120:NaN;return new ob(0>j?j+360:j,i,b,a.opacity)}function ob(a,b,c,d){this.h=+a,this.s=+b,this.l=+c,this.opacity=+d}function pb(b,a){return function(c){return b+c*a}}function qb(c,d,e){return c=Eb(c,e),d=Eb(d,e)-c,e=1/e,function(a){return Eb(c+a*d,e)}}function rb(c){return 1==(c=+c)?sb:function(d,a){return a-d?qb(d,a,c):ye(isNaN(d)?a:d)}}function sb(c,a){var b=a-c;return b?pb(c,b):ye(isNaN(c)?a:c)}var Ib="undefined"!=typeof window&&null!=window.customElements&&window.customElements.polyfillWrapFlushCallback!==void 0,Jb=function(a,b){for(var c=2"),Nb=new RegExp("".concat(Lb,"|").concat(Mb)),Ob="$lit$";class Pb{constructor(a,b){this.parts=[],this.element=b;for(var c,d=[],e=[],f=document.createTreeWalker(b.content,133,null,!1),g=0,h=-1,j=0,{strings:k,values:{length:l}}=a;j{var c=a.length-b.length;return 0<=c&&a.slice(c)===b},Rb=a=>-1!==a.index,Sb=()=>document.createComment(""),Tb=/([ \x09\x0a\x0c\x0d])([^\0-\x1F\x7F-\x9F "'>=/]+)([ \x09\x0a\x0c\x0d]*=[ \x09\x0a\x0c\x0d]*(?:[^ \x09\x0a\x0c\x0d"'`<>=]*|"[^"]*|'[^']*))$/,Ub=a=>{for(var b=11===a.nodeType?0:1,c=document.createTreeWalker(a,133,null,!1);c.nextNode();)b++;return b},Vb=function(a){for(var b,c=1"function"==typeof a&&Wb.has(a),Yb={},Zb={};class $b{constructor(a,b,c){this.__parts=[],this.template=a,this.processor=b,this.options=c}update(a){var b=0;for(var c of this.__parts)void 0!==c&&c.setValue(a[b]),b++;for(var d of this.__parts)void 0!==d&&d.commit()}_clone(){for(var a,b=Ib?this.template.element.content.cloneNode(!0):document.importNode(this.template.element.content,!0),c=[],d=this.template.parts,e=document.createTreeWalker(b,133,null,!1),f=0,g=0,h=e.nextNode();fa}),ac=" ".concat(Lb," ");class bc{constructor(a,b,c,d){this.strings=a,this.values=b,this.type=c,this.processor=d}getHTML(){for(var a=this.strings.length-1,b="",c=!1,d=0;d",f+1);var g=Tb.exec(e);b+=null===g?e+(c?ac:Mb):e.substr(0,g.index)+g[1]+g[2]+Ob+g[3]+Lb}return b+=this.strings[a],b}getTemplateElement(){var a=document.createElement("template"),b=this.getHTML();return void 0!==_b&&(b=_b.createHTML(b)),a.innerHTML=b,a}}class cc extends bc{getHTML(){return"".concat(super.getHTML(),"")}getTemplateElement(){var a=super.getTemplateElement(),b=a.content,c=b.firstChild;return b.removeChild(c),Jb(b,c.firstChild),a}}var dc=a=>null===a||"object"!=typeof a&&"function"!=typeof a,ec=a=>Array.isArray(a)||!!(a&&a[Symbol.iterator]);class fc{constructor(a,b,c){this.dirty=!0,this.element=a,this.name=b,this.strings=c,this.parts=[];for(var d=0;d{try{var a={get capture(){return lc=!0,!1}};window.addEventListener("test",a,a),window.removeEventListener("test",a,a)}catch(a){}})();class mc{constructor(a,b,c){this.value=void 0,this.__pendingValue=void 0,this.element=a,this.eventName=b,this.eventContext=c,this.__boundHandleEvent=a=>this.handleEvent(a)}setValue(a){this.__pendingValue=a}commit(){for(;Xb(this.__pendingValue);){var d=this.__pendingValue;this.__pendingValue=Yb,d(this)}if(this.__pendingValue!==Yb){var a=this.__pendingValue,b=this.value,c=null==a||null!=b&&(a.capture!==b.capture||a.once!==b.once||a.passive!==b.passive);c&&this.element.removeEventListener(this.eventName,this.__boundHandleEvent,this.__options),null!=a&&(null==b||c)&&(this.__options=nc(a),this.element.addEventListener(this.eventName,this.__boundHandleEvent,this.__options)),this.value=a,this.__pendingValue=Yb}}handleEvent(a){"function"==typeof this.value?this.value.call(this.eventContext||this.element,a):this.value.handleEvent(a)}}var nc=a=>a&&(lc?{capture:a.capture,passive:a.passive,once:a.once}:a.capture),oc=new Map,pc=new WeakMap,qc=(a,b,c)=>{var d=pc.get(b);d===void 0&&(Kb(b,b.firstChild),pc.set(b,d=new hc(Object.assign({templateFactory:T},c))),d.appendInto(b)),d.setValue(a),d.commit()};var rc=new class a{handleAttributeExpressions(a,b,c,d){var e=b[0];if("."===e){var g=new jc(a,b.slice(1),c);return g.parts}if("@"===e)return[new mc(a,b.slice(1),d.eventContext)];if("?"===e)return[new ic(a,b.slice(1),c)];var f=new fc(a,b,c);return f.parts}handleTextExpression(a){return new hc(a)}};"undefined"!=typeof window&&(window.litHtmlVersions||(window.litHtmlVersions=[])).push("1.3.0");var sc=function(a){for(var b=arguments.length,c=Array(1"".concat(a,"--").concat(b),vc=!0;"undefined"==typeof window.ShadyCSS?vc=!1:"undefined"==typeof window.ShadyCSS.prepareTemplateDom&&(console.warn("Incompatible ShadyCSS version detected. Please update to at least @webcomponents/webcomponentsjs@2.0.2 and @webcomponents/shadycss@1.3.1."),vc=!1);var wc,xc=a=>b=>{var c=uc(b.type,a),d=oc.get(c);void 0===d&&(d={stringsArray:new WeakMap,keyString:new Map},oc.set(c,d));var e=d.stringsArray.get(b.strings);if(void 0!==e)return e;var f=b.strings.join(Lb);if(e=d.keyString.get(f),void 0===e){var g=b.getTemplateElement();vc&&window.ShadyCSS.prepareTemplateDom(g,a),e=new Pb(b,g),d.keyString.set(f,e)}return d.stringsArray.set(b.strings,e),e},yc=["html","svg"],zc=a=>{yc.forEach(b=>{var c=oc.get(uc(b,a));c!==void 0&&c.keyString.forEach(a=>{var{element:{content:b}}=a,c=new Set;Array.from(b.querySelectorAll("style")).forEach(a=>{c.add(a)}),i(a,c)})})},Ac=new Set,Bc=(a,b,c)=>{Ac.add(a);var d=!c?document.createElement("template"):c.element,e=b.querySelectorAll("style"),{length:f}=e;if(0===f)return void window.ShadyCSS.prepareTemplateStyles(d,a);for(var g,h=document.createElement("style"),j=0;j{if(!c||"object"!=typeof c||!c.scopeName)throw new Error("The `scopeName` option is required.");var d=c.scopeName,e=pc.has(b),f=vc&&11===b.nodeType&&!!b.host,g=f&&!Ac.has(d),h=g?document.createDocumentFragment():b;if(qc(a,h,Object.assign({templateFactory:xc(d)},c)),g){var i=pc.get(h);pc.delete(h);var j=i.value instanceof $b?i.value.template:void 0;Bc(d,h,j),Kb(b,b.firstChild),b.appendChild(h),pc.set(b,i)}!e&&f&&window.ShadyCSS.styleElement(b.host)};window.JSCompiler_renameProperty=a=>a;var Dc={toAttribute(a,b){return b===Boolean?a?"":null:b===Object||b===Array?null==a?a:JSON.stringify(a):a},fromAttribute(a,b){return b===Boolean?null!==a:b===Number?null===a?null:+a:b===Object||b===Array?JSON.parse(a):a}},Ec=(a,b)=>b!==a&&(b===b||a===a),Fc={attribute:!0,type:String,converter:Dc,reflect:!1,hasChanged:Ec},Gc=1,Hc=4,Ic=8,Jc=16,Kc="finalized";class Lc extends HTMLElement{constructor(){super(),this.initialize()}static get observedAttributes(){this.finalize();var a=[];return this._classProperties.forEach((b,c)=>{var d=this._attributeNameForProperty(c,b);void 0!==d&&(this._attributeToPropertyMap.set(d,c),a.push(d))}),a}static _ensureClassProperties(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_classProperties",this))){this._classProperties=new Map;var a=Object.getPrototypeOf(this)._classProperties;a!==void 0&&a.forEach((a,b)=>this._classProperties.set(b,a))}}static createProperty(a){var b=1this._enableUpdatingResolver=a),this._changedProperties=new Map,this._saveInstanceProperties(),this.requestUpdateInternal()}_saveInstanceProperties(){this.constructor._classProperties.forEach((a,b)=>{if(this.hasOwnProperty(b)){var c=this[b];delete this[b],this._instanceProperties||(this._instanceProperties=new Map),this._instanceProperties.set(b,c)}})}_applyInstanceProperties(){this._instanceProperties.forEach((a,b)=>this[b]=a),this._instanceProperties=void 0}connectedCallback(){this.enableUpdating()}enableUpdating(){this._enableUpdatingResolver!==void 0&&(this._enableUpdatingResolver(),this._enableUpdatingResolver=void 0)}disconnectedCallback(){}attributeChangedCallback(a,b,c){b!==c&&this._attributeToProperty(a,c)}_propertyToAttribute(a,b){var c=2this._propertyToAttribute(b,this[b],a)),this._reflectingProperties=void 0),this._markUpdated()}updated(){}firstUpdated(){}}wc=Kc,Lc[wc]=!0;var Mc=window.ShadowRoot&&(window.ShadyCSS===void 0||window.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,Nc=Symbol();class Oc{constructor(a,b){if(b!==Nc)throw new Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=a}get styleSheet(){return void 0===this._styleSheet&&(Mc?(this._styleSheet=new CSSStyleSheet,this._styleSheet.replaceSync(this.cssText)):this._styleSheet=null),this._styleSheet}toString(){return this.cssText}}var Pc=a=>new Oc(a+"",Nc),Qc=a=>{if(a instanceof Oc)return a.cssText;if("number"==typeof a)return a;throw new Error("Value passed to 'css' function must be a 'css' function result: ".concat(a,". Use 'unsafeCSS' to pass non-literal values, but\n take care to ensure page security."))};(window.litElementVersions||(window.litElementVersions=[])).push("2.4.0");var Rc={};class Sc extends Lc{static getStyles(){return this.styles}static _getUniqueStyles(){if(!this.hasOwnProperty(JSCompiler_renameProperty("_styles",this))){var a=this.getStyles();if(Array.isArray(a)){var b=(a,c)=>a.reduceRight((a,c)=>Array.isArray(c)?b(c,a):(a.add(c),a),c),c=b(a,new Set),d=[];c.forEach(a=>d.unshift(a)),this._styles=d}else this._styles=void 0===a?[]:[a];this._styles=this._styles.map(a=>{if(a instanceof CSSStyleSheet&&!Mc){var b=Array.prototype.slice.call(a.cssRules).reduce((a,b)=>a+b.cssText,"");return Pc(b)}return a})}}initialize(){super.initialize(),this.constructor._getUniqueStyles(),this.renderRoot=this.createRenderRoot(),window.ShadowRoot&&this.renderRoot instanceof window.ShadowRoot&&this.adoptStyles()}createRenderRoot(){return this.attachShadow({mode:"open"})}adoptStyles(){var a=this.constructor._styles;0===a.length||(window.ShadyCSS===void 0||window.ShadyCSS.nativeShadow?Mc?this.renderRoot.adoptedStyleSheets=a.map(a=>a instanceof CSSStyleSheet?a:a.styleSheet):this._needsShimAdoptedStyleSheets=!0:window.ShadyCSS.ScopingShim.prepareAdoptedCssText(a.map(a=>a.cssText),this.localName))}connectedCallback(){super.connectedCallback(),this.hasUpdated&&window.ShadyCSS!==void 0&&window.ShadyCSS.styleElement(this)}update(a){var b=this.render();super.update(a),b!==Rc&&this.constructor.render(b,this.renderRoot,{scopeName:this.localName,eventContext:this}),this._needsShimAdoptedStyleSheets&&(this._needsShimAdoptedStyleSheets=!1,this.constructor._styles.forEach(a=>{var b=document.createElement("style");b.textContent=a.cssText,this.renderRoot.appendChild(b)}))}render(){return Rc}}Sc.finalized=!0,Sc.render=Cc;var Tc=function(){try{if("undefined"!=typeof indexedDB)return indexedDB;if("undefined"!=typeof webkitIndexedDB)return webkitIndexedDB;if("undefined"!=typeof mozIndexedDB)return mozIndexedDB;if("undefined"!=typeof OIndexedDB)return OIndexedDB;if("undefined"!=typeof msIndexedDB)return msIndexedDB}catch(a){}}();"undefined"==typeof Promise&&require("lie/polyfill");var Uc,Vc=Promise,Wc={},Xc=Object.prototype.toString,Yc={_driver:"asyncStorage",_initStorage:function(a){function b(){return Vc.resolve()}var c=this,d={db:null};if(a)for(var e in a)d[e]=a[e];var f=Wc[d.name];f||(f=sa(),Wc[d.name]=f),f.forages.push(c),c._initReady||(c._initReady=c.ready,c.ready=pa);for(var g,h=[],i=0;ia?void b(null):void c.ready().then(function(){ra(c._dbInfo,"readonly",function(e,f){if(e)return d(e);try{var g=f.objectStore(c._dbInfo.storeName),h=!1,i=g.openKeyCursor();i.onsuccess=function(){var c=i.result;return c?void(0===a?b(c.key):h?b(c.key):(h=!0,c.advance(a))):void b(null)},i.onerror=function(){d(i.error)}}catch(a){d(a)}})}).catch(d)});return $(d,b),d},keys:function(a){var b=this,c=new Vc(function(a,c){b.ready().then(function(){ra(b._dbInfo,"readonly",function(d,e){if(d)return c(d);try{var f=e.objectStore(b._dbInfo.storeName),g=f.openKeyCursor(),h=[];g.onsuccess=function(){var b=g.result;return b?void(h.push(b.key),b.continue()):void a(h)},g.onerror=function(){c(g.error)}}catch(a){c(a)}})}).catch(c)});return $(c,a),c},dropInstance:function(a,b){b=ba.apply(this,arguments);var c=this.config();a="function"!=typeof a&&a||{},a.name||(a.name=a.name||c.name,a.storeName=a.storeName||c.storeName);var d,e=this;if(!a.name)d=Vc.reject("Invalid arguments");else{var f=a.name===c.name&&e._dbInfo.db,g=f?Vc.resolve(e._dbInfo.db):ja(a).then(b=>{var c=Wc[a.name],d=c.forages;c.db=b;for(var e=0;e{if(b.objectStoreNames.contains(a.storeName)){var c=b.version+1;fa(a);var d=Wc[a.name],e=d.forages;b.close();for(var g,h=0;h{var e=Tc.open(a.name,c);e.onerror=a=>{var b=e.result;b.close(),d(a)},e.onupgradeneeded=()=>{var b=e.result;b.deleteObjectStore(a.storeName)},e.onsuccess=()=>{var a=e.result;a.close(),b(a)}});return f.then(a=>{d.db=a;for(var b,c=0;c{throw(ha(a,b)||Vc.resolve()).catch(()=>{}),b})}}):g.then(b=>{fa(a);var c=Wc[a.name],d=c.forages;b.close();for(var e,f=0;f{var d=Tc.deleteDatabase(a.name);d.onerror=d.onblocked=a=>{var b=d.result;b&&b.close(),c(a)},d.onsuccess=()=>{var a=d.result;a&&a.close(),b(a)}});return g.then(a=>{c.db=a;for(var b,e=0;e{throw(ha(a,b)||Vc.resolve()).catch(()=>{}),b})})}return $(d,b),d}},Zc=/^~~local_forage_type~([^~]+)~/,$c="__lfsc__:".length,_c=$c+"arbf".length,ad=Object.prototype.toString,bd={serialize:function(a,b){var c="";if(a&&(c=ad.call(a)),a&&("[object ArrayBuffer]"===c||a.buffer&&"[object ArrayBuffer]"===ad.call(a.buffer))){var d,e="__lfsc__:";a instanceof ArrayBuffer?(d=a,e+="arbf"):(d=a.buffer,"[object Int8Array]"===c?e+="si08":"[object Uint8Array]"===c?e+="ui08":"[object Uint8ClampedArray]"===c?e+="uic8":"[object Int16Array]"===c?e+="si16":"[object Uint16Array]"===c?e+="ur16":"[object Int32Array]"===c?e+="si32":"[object Uint32Array]"===c?e+="ui32":"[object Float32Array]"===c?e+="fl32":"[object Float64Array]"===c?e+="fl64":b(new Error("Failed to get type for BinaryArray"))),b(e+ua(d))}else if("[object Blob]"===c){var f=new FileReader;f.onload=function(){var c="~~local_forage_type~"+a.type+"~"+ua(this.result);b("__lfsc__:blob"+c)},f.readAsArrayBuffer(a)}else try{b(JSON.stringify(a))}catch(c){console.error("Couldn't convert value into a JSON string: ",a),b(null,c)}},deserialize:function(a){if(a.substring(0,$c)!=="__lfsc__:")return JSON.parse(a);var b,c=a.substring(_c),d=a.substring($c,_c);if(d==="blob"&&Zc.test(c)){var e=c.match(Zc);b=e[1],c=c.substring(e[0].length)}var f=ta(c);switch(d){case"arbf":return f;case"blob":return Z([f],{type:b});case"si08":return new Int8Array(f);case"ui08":return new Uint8Array(f);case"uic8":return new Uint8ClampedArray(f);case"si16":return new Int16Array(f);case"ur16":return new Uint16Array(f);case"si32":return new Int32Array(f);case"ui32":return new Uint32Array(f);case"fl32":return new Float32Array(f);case"fl64":return new Float64Array(f);default:throw new Error("Unkown type: "+d);}},stringToBuffer:ta,bufferToString:ua},cd={_driver:"webSQLStorage",_initStorage:function(a){var b=this,c={db:null};if(a)for(var d in a)c[d]="string"==typeof a[d]?a[d]:a[d].toString();var e=new Vc(function(a,d){try{c.db=openDatabase(c.name,c.version+"",c.description,c.size)}catch(a){return d(a)}c.db.transaction(function(e){va(e,c,function(){b._dbInfo=c,a()},function(a,b){d(b)})},d)});return c.serializer=bd,e},_support:function(){return"function"==typeof openDatabase}(),iterate:function(a,b){var c=this,d=new Vc(function(b,d){c.ready().then(function(){var e=c._dbInfo;e.db.transaction(function(c){wa(c,e,"SELECT * FROM ".concat(e.storeName),[],function(c,d){for(var f=d.rows,g=f.length,h=0;ha===b||"number"==typeof a&&"number"==typeof b&&isNaN(a)&&isNaN(b),fd=(a,b)=>{for(var c=a.length,d=0;d{})}config(a){if("object"==typeof a){if(this._ready)return new Error("Can't call config() after localforage has been used.");for(var b in a){if("storeName"==b&&(a[b]=a[b].replace(/\W/g,"_")),"version"==b&&"number"!=typeof a[b])return new Error("Database version must be a number.");this._config[b]=a[b]}return!("driver"in a&&a.driver)||this.setDriver(this._config.driver)}return"string"==typeof a?this._config[a]:this._config}defineDriver(a,b,c){var d=new Vc(function(b,c){try{var d=a._driver,e=new Error("Custom driver not compliant; see https://mozilla.github.io/localForage/#definedriver");if(!a._driver)return void c(e);for(var f=md.concat("_initStorage"),g=0,h=f.length;g(null===b._ready&&(b._ready=b._initDriver()),b._ready));return _(c,a,a),c}setDriver(a,b,c){function d(){g._config.driver=g.driver()}function e(a){return g._extend(a),d(),g._ready=g._initStorage(g._config),g._ready}function f(a){return function(){function b(){for(;cVc.resolve());return this._driverSet=i.then(()=>{var a=h[0];return g._dbInfo=null,g._ready=null,g.getDriver(a).then(a=>{g._driver=a._driver,d(),g._wrapLibraryMethodsWithReady(),g._initDriver=f(h)})}).catch(()=>{d();var a=new Error("No available storage method found.");return g._driverSet=Vc.reject(a),g._driverSet}),_(this._driverSet,b,c),this._driverSet}supports(a){return!!id[a]}_extend(a){Ea(this,a)}_getSupportedDrivers(a){for(var b,c=[],d=0,e=a.length;da.getHours()?b.amPm[0]:b.amPm[1]},A:function(a,b){return 12>a.getHours()?b.amPm[0].toUpperCase():b.amPm[1].toUpperCase()},ZZ:function(a){var b=a.getTimezoneOffset();return(0=a?"hass:battery-alert":"hass:battery-"+a}var e=b.attributes.unit_of_measurement;return"\xB0C"===e||"\xB0F"===e?"hass:thermometer":Ja("sensor")},input_datetime:function(a){return a.attributes.has_date?a.attributes.has_time?Ja("input_datetime"):"hass:calendar":"hass:clock"}},Vd=function(a){if(!a)return"hass:bookmark";if(a.attributes.icon)return a.attributes.icon;var b=Ia(a.entity_id);return b in Ud?Ud[b](a):Ja(b,a.state)},Q=function(a){var b={exports:{}};return a(b,b.exports),b.exports}(function(a){(function(b){a.exports=b()})(function(a){function b(e,f){var g=e[0],h=e[1],i=e[2],j=e[3];g+=0|(h&i|~h&j)+f[0]-680876936,g=0|(g<<7|g>>>25)+h,j+=0|(g&h|~g&i)+f[1]-389564586,j=0|(j<<12|j>>>20)+g,i+=0|(j&g|~j&h)+f[2]+606105819,i=0|(i<<17|i>>>15)+j,h+=0|(i&j|~i&g)+f[3]-1044525330,h=0|(h<<22|h>>>10)+i,g+=0|(h&i|~h&j)+f[4]-176418897,g=0|(g<<7|g>>>25)+h,j+=0|(g&h|~g&i)+f[5]+1200080426,j=0|(j<<12|j>>>20)+g,i+=0|(j&g|~j&h)+f[6]-1473231341,i=0|(i<<17|i>>>15)+j,h+=0|(i&j|~i&g)+f[7]-45705983,h=0|(h<<22|h>>>10)+i,g+=0|(h&i|~h&j)+f[8]+1770035416,g=0|(g<<7|g>>>25)+h,j+=0|(g&h|~g&i)+f[9]-1958414417,j=0|(j<<12|j>>>20)+g,i+=0|(j&g|~j&h)+f[10]-42063,i=0|(i<<17|i>>>15)+j,h+=0|(i&j|~i&g)+f[11]-1990404162,h=0|(h<<22|h>>>10)+i,g+=0|(h&i|~h&j)+f[12]+1804603682,g=0|(g<<7|g>>>25)+h,j+=0|(g&h|~g&i)+f[13]-40341101,j=0|(j<<12|j>>>20)+g,i+=0|(j&g|~j&h)+f[14]-1502002290,i=0|(i<<17|i>>>15)+j,h+=0|(i&j|~i&g)+f[15]+1236535329,h=0|(h<<22|h>>>10)+i,g+=0|(h&j|i&~j)+f[1]-165796510,g=0|(g<<5|g>>>27)+h,j+=0|(g&i|h&~i)+f[6]-1069501632,j=0|(j<<9|j>>>23)+g,i+=0|(j&h|g&~h)+f[11]+643717713,i=0|(i<<14|i>>>18)+j,h+=0|(i&g|j&~g)+f[0]-373897302,h=0|(h<<20|h>>>12)+i,g+=0|(h&j|i&~j)+f[5]-701558691,g=0|(g<<5|g>>>27)+h,j+=0|(g&i|h&~i)+f[10]+38016083,j=0|(j<<9|j>>>23)+g,i+=0|(j&h|g&~h)+f[15]-660478335,i=0|(i<<14|i>>>18)+j,h+=0|(i&g|j&~g)+f[4]-405537848,h=0|(h<<20|h>>>12)+i,g+=0|(h&j|i&~j)+f[9]+568446438,g=0|(g<<5|g>>>27)+h,j+=0|(g&i|h&~i)+f[14]-1019803690,j=0|(j<<9|j>>>23)+g,i+=0|(j&h|g&~h)+f[3]-187363961,i=0|(i<<14|i>>>18)+j,h+=0|(i&g|j&~g)+f[8]+1163531501,h=0|(h<<20|h>>>12)+i,g+=0|(h&j|i&~j)+f[13]-1444681467,g=0|(g<<5|g>>>27)+h,j+=0|(g&i|h&~i)+f[2]-51403784,j=0|(j<<9|j>>>23)+g,i+=0|(j&h|g&~h)+f[7]+1735328473,i=0|(i<<14|i>>>18)+j,h+=0|(i&g|j&~g)+f[12]-1926607734,h=0|(h<<20|h>>>12)+i,g+=0|(h^i^j)+f[5]-378558,g=0|(g<<4|g>>>28)+h,j+=0|(g^h^i)+f[8]-2022574463,j=0|(j<<11|j>>>21)+g,i+=0|(j^g^h)+f[11]+1839030562,i=0|(i<<16|i>>>16)+j,h+=0|(i^j^g)+f[14]-35309556,h=0|(h<<23|h>>>9)+i,g+=0|(h^i^j)+f[1]-1530992060,g=0|(g<<4|g>>>28)+h,j+=0|(g^h^i)+f[4]+1272893353,j=0|(j<<11|j>>>21)+g,i+=0|(j^g^h)+f[7]-155497632,i=0|(i<<16|i>>>16)+j,h+=0|(i^j^g)+f[10]-1094730640,h=0|(h<<23|h>>>9)+i,g+=0|(h^i^j)+f[13]+681279174,g=0|(g<<4|g>>>28)+h,j+=0|(g^h^i)+f[0]-358537222,j=0|(j<<11|j>>>21)+g,i+=0|(j^g^h)+f[3]-722521979,i=0|(i<<16|i>>>16)+j,h+=0|(i^j^g)+f[6]+76029189,h=0|(h<<23|h>>>9)+i,g+=0|(h^i^j)+f[9]-640364487,g=0|(g<<4|g>>>28)+h,j+=0|(g^h^i)+f[12]-421815835,j=0|(j<<11|j>>>21)+g,i+=0|(j^g^h)+f[15]+530742520,i=0|(i<<16|i>>>16)+j,h+=0|(i^j^g)+f[2]-995338651,h=0|(h<<23|h>>>9)+i,g+=0|(i^(h|~j))+f[0]-198630844,g=0|(g<<6|g>>>26)+h,j+=0|(h^(g|~i))+f[7]+1126891415,j=0|(j<<10|j>>>22)+g,i+=0|(g^(j|~h))+f[14]-1416354905,i=0|(i<<15|i>>>17)+j,h+=0|(j^(i|~g))+f[5]-57434055,h=0|(h<<21|h>>>11)+i,g+=0|(i^(h|~j))+f[12]+1700485571,g=0|(g<<6|g>>>26)+h,j+=0|(h^(g|~i))+f[3]-1894986606,j=0|(j<<10|j>>>22)+g,i+=0|(g^(j|~h))+f[10]-1051523,i=0|(i<<15|i>>>17)+j,h+=0|(j^(i|~g))+f[1]-2054922799,h=0|(h<<21|h>>>11)+i,g+=0|(i^(h|~j))+f[8]+1873313359,g=0|(g<<6|g>>>26)+h,j+=0|(h^(g|~i))+f[15]-30611744,j=0|(j<<10|j>>>22)+g,i+=0|(g^(j|~h))+f[6]-1560198380,i=0|(i<<15|i>>>17)+j,h+=0|(j^(i|~g))+f[13]+1309151649,h=0|(h<<21|h>>>11)+i,g+=0|(i^(h|~j))+f[4]-145523070,g=0|(g<<6|g>>>26)+h,j+=0|(h^(g|~i))+f[11]-1120210379,j=0|(j<<10|j>>>22)+g,i+=0|(g^(j|~h))+f[2]+718787259,i=0|(i<<15|i>>>17)+j,h+=0|(j^(i|~g))+f[9]-343485551,h=0|(h<<21|h>>>11)+i,e[0]=0|g+e[0],e[1]=0|h+e[1],e[2]=0|i+e[2],e[3]=0|j+e[3]}function c(a){var b,c=[];for(b=0;64>b;b+=4)c[b>>2]=a.charCodeAt(b)+(a.charCodeAt(b+1)<<8)+(a.charCodeAt(b+2)<<16)+(a.charCodeAt(b+3)<<24);return c}function d(b){var a,c=[];for(a=0;64>a;a+=4)c[a>>2]=b[a]+(b[a+1]<<8)+(b[a+2]<<16)+(b[a+3]<<24);return c}function e(a){var d,e,f,g,h,j,k=a.length,l=[1732584193,-271733879,-1732584194,271733878];for(d=64;d<=k;d+=64)b(l,c(a.substring(d-64,d)));for(a=a.substring(d-64),e=a.length,f=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],d=0;d>2]|=a.charCodeAt(d)<<(d%4<<3);if(f[d>>2]|=128<<(d%4<<3),55d;d+=1)f[d]=0;return g=8*k,g=g.toString(16).match(/(.*?)(.{0,8})$/),h=parseInt(g[2],16),j=parseInt(g[1],16)||0,f[14]=h,f[15]=j,b(l,f),l}function f(c){var e,f,g,h,j,k,l=c.length,m=[1732584193,-271733879,-1732584194,271733878];for(e=64;e<=l;e+=64)b(m,d(c.subarray(e-64,e)));for(c=e-64>2]|=c[e]<<(e%4<<3);if(g[e>>2]|=128<<(e%4<<3),55e;e+=1)g[e]=0;return h=8*l,h=h.toString(16).match(/(.*?)(.{0,8})$/),j=parseInt(h[2],16),k=parseInt(h[1],16)||0,g[14]=j,g[15]=k,b(m,g),m}function g(a){var b,c="";for(b=0;4>b;b+=1)c+=o[15&a>>8*b+4]+o[15&a>>8*b];return c}function h(a){var b;for(b=0;ba?Gb(a+b,0):Hb(a,b)}ArrayBuffer.prototype.slice=function(c,d){var e,f,g,h,i=this.byteLength,j=b(c,i),k=i;return(d!==a&&(k=b(d,i)),j>k)?new ArrayBuffer(0):(e=k-j,f=new ArrayBuffer(e),g=new Uint8Array(f),h=new Uint8Array(this,j,e),g.set(h),f)}}(),n.prototype.append=function(a){return this.appendBinary(i(a)),this},n.prototype.appendBinary=function(a){this._buff+=a,this._length+=a.length;var d,e=this._buff.length;for(d=64;d<=e;d+=64)b(this._hash,c(this._buff.substring(d-64,d)));return this._buff=this._buff.substring(d-64),this},n.prototype.end=function(a){var b,c,d=this._buff,e=d.length,f=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0];for(b=0;b>2]|=d.charCodeAt(b)<<(b%4<<3);return this._finish(f,e),c=h(this._hash),a&&(c=m(c)),this.reset(),c},n.prototype.reset=function(){return this._buff="",this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},n.prototype.getState=function(){return{buff:this._buff,length:this._length,hash:this._hash.slice()}},n.prototype.setState=function(a){return this._buff=a.buff,this._length=a.length,this._hash=a.hash,this},n.prototype.destroy=function(){delete this._hash,delete this._buff,delete this._length},n.prototype._finish=function(a,c){var d,e,f,g=c;if(a[g>>2]|=128<<(g%4<<3),55g;g+=1)a[g]=0;d=8*this._length,d=d.toString(16).match(/(.*?)(.{0,8})$/),e=parseInt(d[2],16),f=parseInt(d[1],16)||0,a[14]=e,a[15]=f,b(this._hash,a)},n.hash=function(a,b){return n.hashBinary(i(a),b)},n.hashBinary=function(a,b){var c=e(a),d=h(c);return b?m(d):d},n.ArrayBuffer=function(){this.reset()},n.ArrayBuffer.prototype.append=function(a){var c,e=l(this._buff.buffer,a,!0),f=e.length;for(this._length+=a.byteLength,c=64;c<=f;c+=64)b(this._hash,d(e.subarray(c-64,c)));return this._buff=c-64>2]|=d[b]<<(b%4<<3);return this._finish(f,e),c=h(this._hash),a&&(c=m(c)),this.reset(),c},n.ArrayBuffer.prototype.reset=function(){return this._buff=new Uint8Array(0),this._length=0,this._hash=[1732584193,-271733879,-1732584194,271733878],this},n.ArrayBuffer.prototype.getState=function(){var a=n.prototype.getState.call(this);return a.buff=k(a.buff),a},n.ArrayBuffer.prototype.setState=function(a){return a.buff=j(a.buff,!0),n.prototype.setState.call(this,a)},n.ArrayBuffer.prototype.destroy=n.prototype.destroy,n.ArrayBuffer.prototype._finish=n.prototype._finish,n.ArrayBuffer.hash=function(a,b){var c=f(new Uint8Array(a)),d=h(c);return b?m(d):d},n})}),Wd=.7,Xd=1/Wd,Yd="\\s*([+-]?\\d+)\\s*",Zd="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)\\s*",$d="\\s*([+-]?(?:\\d*\\.)?\\d+(?:[eE][+-]?\\d+)?)%\\s*",_d=/^#([0-9a-f]{3,8})$/,ae=new RegExp("^rgb\\(".concat(Yd,",").concat(Yd,",").concat(Yd,"\\)$")),be=new RegExp("^rgb\\(".concat($d,",").concat($d,",").concat($d,"\\)$")),ce=new RegExp("^rgba\\(".concat(Yd,",").concat(Yd,",").concat(Yd,",").concat(Zd,"\\)$")),de=new RegExp("^rgba\\(".concat($d,",").concat($d,",").concat($d,",").concat(Zd,"\\)$")),ee=new RegExp("^hsl\\(".concat(Zd,",").concat($d,",").concat($d,"\\)$")),fe=new RegExp("^hsla\\(".concat(Zd,",").concat($d,",").concat($d,",").concat(Zd,"\\)$")),ge={aliceblue:15792383,antiquewhite:16444375,aqua:65535,aquamarine:8388564,azure:15794175,beige:16119260,bisque:16770244,black:0,blanchedalmond:16772045,blue:255,blueviolet:9055202,brown:10824234,burlywood:14596231,cadetblue:6266528,chartreuse:8388352,chocolate:13789470,coral:16744272,cornflowerblue:6591981,cornsilk:16775388,crimson:14423100,cyan:65535,darkblue:139,darkcyan:35723,darkgoldenrod:12092939,darkgray:11119017,darkgreen:25600,darkgrey:11119017,darkkhaki:12433259,darkmagenta:9109643,darkolivegreen:5597999,darkorange:16747520,darkorchid:10040012,darkred:9109504,darksalmon:15308410,darkseagreen:9419919,darkslateblue:4734347,darkslategray:3100495,darkslategrey:3100495,darkturquoise:52945,darkviolet:9699539,deeppink:16716947,deepskyblue:49151,dimgray:6908265,dimgrey:6908265,dodgerblue:2003199,firebrick:11674146,floralwhite:16775920,forestgreen:2263842,fuchsia:16711935,gainsboro:14474460,ghostwhite:16316671,gold:16766720,goldenrod:14329120,gray:8421504,green:32768,greenyellow:11403055,grey:8421504,honeydew:15794160,hotpink:16738740,indianred:13458524,indigo:4915330,ivory:16777200,khaki:15787660,lavender:15132410,lavenderblush:16773365,lawngreen:8190976,lemonchiffon:16775885,lightblue:11393254,lightcoral:15761536,lightcyan:14745599,lightgoldenrodyellow:16448210,lightgray:13882323,lightgreen:9498256,lightgrey:13882323,lightpink:16758465,lightsalmon:16752762,lightseagreen:2142890,lightskyblue:8900346,lightslategray:7833753,lightslategrey:7833753,lightsteelblue:11584734,lightyellow:16777184,lime:65280,limegreen:3329330,linen:16445670,magenta:16711935,maroon:8388608,mediumaquamarine:6737322,mediumblue:205,mediumorchid:12211667,mediumpurple:9662683,mediumseagreen:3978097,mediumslateblue:8087790,mediumspringgreen:64154,mediumturquoise:4772300,mediumvioletred:13047173,midnightblue:1644912,mintcream:16121850,mistyrose:16770273,moccasin:16770229,navajowhite:16768685,navy:128,oldlace:16643558,olive:8421376,olivedrab:7048739,orange:16753920,orangered:16729344,orchid:14315734,palegoldenrod:15657130,palegreen:10025880,paleturquoise:11529966,palevioletred:14381203,papayawhip:16773077,peachpuff:16767673,peru:13468991,pink:16761035,plum:14524637,powderblue:11591910,purple:8388736,rebeccapurple:6697881,red:16711680,rosybrown:12357519,royalblue:4286945,saddlebrown:9127187,salmon:16416882,sandybrown:16032864,seagreen:3050327,seashell:16774638,sienna:10506797,silver:12632256,skyblue:8900331,slateblue:6970061,slategray:7372944,slategrey:7372944,snow:16775930,springgreen:65407,steelblue:4620980,tan:13808780,teal:32896,thistle:14204888,tomato:16737095,turquoise:4251856,violet:15631086,wheat:16113331,white:16777215,whitesmoke:16119285,yellow:16776960,yellowgreen:10145074};O(La,Oa,{copy(a){return Object.assign(new this.constructor,this,a)},displayable(){return this.rgb().displayable()},hex:Ma,formatHex:Ma,formatHex8:function(){return this.rgb().formatHex8()},formatHsl:function(){return $a(this).formatHsl()},formatRgb:Na,toString:Na}),O(Ta,Sa,Ka(La,{brighter(a){return a=null==a?Xd:Eb(Xd,a),new Ta(this.r*a,this.g*a,this.b*a,this.opacity)},darker(a){return a=null==a?Wd:Eb(Wd,a),new Ta(this.r*a,this.g*a,this.b*a,this.opacity)},rgb(){return this},clamp(){return new Ta(Xa(this.r),Xa(this.g),Xa(this.b),Wa(this.opacity))},displayable(){return-.5<=this.r&&255.5>this.r&&-.5<=this.g&&255.5>this.g&&-.5<=this.b&&255.5>this.b&&0<=this.opacity&&1>=this.opacity},hex:Ua,formatHex:Ua,formatHex8:function(){return"#".concat(Ya(this.r)).concat(Ya(this.g)).concat(Ya(this.b)).concat(Ya(255*(isNaN(this.opacity)?1:this.opacity)))},formatRgb:Va,toString:Va})),O(ab,function(a,b,c,d){return 1===arguments.length?$a(a):new ab(a,b,c,null==d?1:d)},Ka(La,{brighter(a){return a=null==a?Xd:Eb(Xd,a),new ab(this.h,this.s,this.l*a,this.opacity)},darker(a){return a=null==a?Wd:Eb(Wd,a),new ab(this.h,this.s,this.l*a,this.opacity)},rgb(){var a=this.h%360+360*(0>this.h),b=isNaN(a)||isNaN(this.s)?0:this.s,c=this.l,d=c+(.5>c?c:1-c)*b,e=2*c-d;return new Ta(db(240<=a?a-240:a+120,e,d),db(a,e,d),db(120>a?a+240:a-120,e,d),this.opacity)},clamp(){return new ab(bb(this.h),cb(this.s),cb(this.l),Wa(this.opacity))},displayable(){return(0<=this.s&&1>=this.s||isNaN(this.s))&&0<=this.l&&1>=this.l&&0<=this.opacity&&1>=this.opacity},formatHsl(){var b=Wa(this.opacity);return"".concat(1===b?"hsl(":"hsla(").concat(bb(this.h),", ").concat(100*cb(this.s),"%, ").concat(100*cb(this.l),"%").concat(1===b?")":", ".concat(b,")"))}}));var he=wb/180,ie=180/wb,je=18,ke=.96422,le=.82521,me=4/29,ne=6/29,oe=3*ne*ne,pe=ne*ne*ne;O(fb,function(c,d,a,b){return 1===arguments.length?eb(c):new fb(c,d,a,null==b?1:b)},Ka(La,{brighter(a){return new fb(this.l+je*(null==a?1:a),this.a,this.b,this.opacity)},darker(a){return new fb(this.l-je*(null==a?1:a),this.a,this.b,this.opacity)},rgb(){var a=(this.l+16)/116,b=isNaN(this.a)?a:a+this.a/500,c=isNaN(this.b)?a:a-this.b/200;return b=ke*hb(b),a=1*hb(a),c=le*hb(c),new Ta(ib(3.1338561*b-1.6168667*a-.4906146*c),ib(-.9787684*b+1.9161415*a+.033454*c),ib(.0719453*b-.2289914*a+1.4052427*c),this.opacity)}})),O(lb,function(a,b,c,d){return 1===arguments.length?kb(a):new lb(a,b,c,null==d?1:d)},Ka(La,{brighter(a){return new lb(this.h,this.c,this.l+je*(null==a?1:a),this.opacity)},darker(a){return new lb(this.h,this.c,this.l-je*(null==a?1:a),this.opacity)},rgb(){return mb(this).rgb()}}));var qe=-.14861,re=+1.78277,se=-.29227,te=-.90649,ue=+1.97294,ve=ue*te,we=ue*re,xe=re*se-te*qe;O(ob,function(a,b,c,d){return 1===arguments.length?nb(a):new ob(a,b,c,null==d?1:d)},Ka(La,{brighter(a){return a=null==a?Xd:Eb(Xd,a),new ob(this.h,this.s,this.l*a,this.opacity)},darker(a){return a=null==a?Wd:Eb(Wd,a),new ob(this.h,this.s,this.l*a,this.opacity)},rgb(){var b=isNaN(this.h)?0:(this.h+120)*he,c=+this.l,d=isNaN(this.s)?0:this.s*c*(1-c),a=Bb(b),e=Ab(b);return new Ta(255*(c+d*(qe*a+re*e)),255*(c+d*(se*a+te*e)),255*(c+d*(ue*a)),this.opacity)}}));var ye=a=>()=>a,ze=function a(b){function c(a,c){var e=d((a=Sa(a)).r,(c=Sa(c)).r),f=d(a.g,c.g),g=d(a.b,c.b),b=sb(a.opacity,c.opacity);return function(c){return a.r=e(c),a.g=f(c),a.b=g(c),a.opacity=b(c),a+""}}var d=rb(b);return c.gamma=a,c}(1),Ae="https://github.com/kalkih/mini-graph-card/blob/master/README.md",Be=14,Ce=96,De={humidity:"hass:water-percent",illuminance:"hass:brightness-5",temperature:"hass:thermometer",battery:"hass:battery",pressure:"hass:gauge",power:"hass:flash",signal_strength:"hass:wifi",motion:"hass:walk",door:"hass:door-closed",window:"hass:window-closed",presence:"hass:account",light:"hass:lightbulb"},Ee=["var(--accent-color)","#3498db","#e74c3c","#9b59b6","#f1c40f","#2ecc71","#1abc9c","#34495e","#e67e22","#7f8c8d","#27ae60","#2980b9","#8e44ad"],Fe=["entity","line","length","fill","points","tooltip","abs","config"],Ge={name:!0,icon:!0,state:!0,graph:"line",labels:"hover",labels_secondary:"hover",extrema:!1,legend:!0,fill:!0,points:"hover"},He=0,X=1,Y=2,V=3600000;class Ie{constructor(a,b,c){var d=3this._reducer(a,b),[]),c=vb(this.hours*this.points);b.length=c,this.coords=this._calcPoints(b),this.min=Hb.apply(Math,W(this.coords.map(a=>+a[Y]))),this.max=Gb.apply(Math,W(this.coords.map(a=>+a[Y])))}}_reducer(a,b){var c=this._endTime-new Date(b.last_changed).getTime(),d=c/V*this.points-this.hours*this.points;if(0>d){var e=yb(zb(d));a[e]||(a[e]=[]),a[e].push(b)}else a[0]=[b];return a}_calcPoints(a){var b=this.width/(this.hours*this.points-1);b=Number.isFinite(b)?b:this.width;for(var c,d=[],e=a.filter(Boolean)[0],f=0;f{var b=this._logarithmic?ub(Gb(1,a[Y])):a[Y],e=this.height-(b-c)/d+2*this.margin[X];return[a[He],e,a[Y]]});return e}getPoints(){var{coords:a}=this;if(1===a.length&&(a[1]=[this.width+this.margin[He],0,a[0][Y]]),a=this._calcY(this.coords),this._smoothing){var b=a[0];return a.shift(),a.map((a,c)=>{var d=this._midPoint(b[He],b[X],a[He],a[X]),e=(b[Y]+a[Y])/2;return b=a,[d[He],d[X],e,c+1]})}return a.map((a,b)=>[a[He],a[X],a[Y],b])}getPath(){var{coords:a}=this;1===a.length&&(a[1]=[this.width+this.margin[He],0,a[0][Y]]),a=this._calcY(this.coords);var b,c,d="",e=a[0];return d+="M".concat(e[He],",").concat(e[X]),a.forEach(a=>{b=a,c=this._smoothing?this._midPoint(e[He],e[X],b[He],b[X]):b,d+=" ".concat(c[He],",").concat(c[X]),d+=" Q ".concat(b[He],",").concat(b[X]),e=b}),d+=" ".concat(b[He],",").concat(b[X]),d}computeGradient(a,b){var c=b?ub(Gb(1,this._max))-ub(Gb(1,this._min)):this._max-this._min;return a.map((a,d,e)=>{var f;if(a.value>this._max&&e[d+1]){var h=(this._max-e[d+1].value)/(a.value-e[d+1].value);f=ze(e[d+1].color,a.color)(h)}else if(a.value=c?0:b?(ub(Gb(1,this._max))-ub(Gb(1,a.value)))*(100/c):(this._max-a.value)*(100/c),{color:f||a.color,offset:g}})}getFill(a){var b=this.height+4*this.margin[X],c=a;return c+=" L ".concat(this.width-2*this.margin[He],", ").concat(b),c+=" L ".concat(this.coords[0][He],", ").concat(b," z"),c}getBars(a,b){var c=2({x:e*f*b+e*a+c,y:d[X],height:this.height-d[X]+4*this.margin[X],width:e-c,value:d[Y]}))}_midPoint(a,b,c,d){return[(a-c)/2+c,(b-d)/2+d]}_average(a){return a.reduce((a,b)=>a+parseFloat(b.state),0)/a.length}_median(a){var b=W(a).sort((c,a)=>parseFloat(c)-parseFloat(a)),c=yb((b.length-1)/2);return 1==b.length%2?parseFloat(b[c].state):(parseFloat(b[c].state)+parseFloat(b[c+1].state))/2}_maximum(a){return Gb.apply(Math,W(a.map(a=>a.state)))}_minimum(a){return Hb.apply(Math,W(a.map(a=>a.state)))}_first(a){return parseFloat(a[0].state)}_last(a){return parseFloat(a[a.length-1].state)}_sum(a){return a.reduce((a,b)=>a+parseFloat(b.state),0)}_delta(a){return this._maximum(a)-this._minimum(a)}_diff(a){return this._last(a)-this._first(a)}_lastValue(a){return["delta","diff"].includes(this.aggregateFuncName)?0:parseFloat(a[a.length-1].state)||0}_updateEndTime(){switch(this._endTime=new Date,this._groupBy){case"month":this._endTime.setMonth(this._endTime.getMonth()+1),this._endTime.setDate(1);break;case"date":this._endTime.setDate(this._endTime.getDate()+1),this._endTime.setHours(0,0,0,0);break;case"hour":this._endTime.setHours(this._endTime.getHours()+1),this._endTime.setMinutes(0,0,0);}}}var Je=function css(a){for(var b=arguments.length,c=Array(1b+Qc(c)+a[d+1],a[0]);return new Oc(e,Nc)}(R()),Ke=(a,b,c,d,f)=>{var g;switch(d.action){case"more-info":{g=new Event("hass-more-info",{composed:!0}),g.detail={entityId:f},a.dispatchEvent(g);break}case"navigate":{if(!d.navigation_path)return;window.history.pushState(null,"",d.navigation_path),g=new Event("location-changed",{composed:!0}),g.detail={replace:!1},window.dispatchEvent(g);break}case"call-service":{if(!d.service)return;var[h,i]=d.service.split(".",2),j=P({},d.service_data);b.callService(h,i,j);break}case"url":{if(!d.url)return;window.location.href=d.url;break}case"fire-dom-event":{g=new Event("ll-custom",{composed:!0,bubbles:!0}),g.detail=d,a.dispatchEvent(g);break}}},Le=xb,Me=a=>Ne(a,16,function(b){return Le(b)}),Ne=(a,b,c)=>{if(null==a)return"";var d,e,f,g={},h={},j="",k="",l="",m=2,n=3,o=2,p=[],q=0,r=0;for(f=0;fl.charCodeAt(0)){for(d=0;dd;d++)q=q<<1|1&e,r==b-1?(r=0,p.push(c(q)),q=0):r++,e>>=1}else{for(e=1,d=0;dd;d++)q=q<<1|1&e,r==b-1?(r=0,p.push(c(q)),q=0):r++,e>>=1}m--,0==m&&(m=Eb(2,o),o++),delete h[l]}else for(e=g[l],d=0;d>=1;m--,0==m&&(m=Eb(2,o),o++),g[k]=n++,l=j+""}if(""!==l){if(Object.prototype.hasOwnProperty.call(h,l)){if(256>l.charCodeAt(0)){for(d=0;dd;d++)q=q<<1|1&e,r==b-1?(r=0,p.push(c(q)),q=0):r++,e>>=1}else{for(e=1,d=0;dd;d++)q=q<<1|1&e,r==b-1?(r=0,p.push(c(q)),q=0):r++,e>>=1}m--,0==m&&(m=Eb(2,o),o++),delete h[l]}else for(e=g[l],d=0;d>=1;m--,0==m&&(m=Eb(2,o),o++)}for(e=2,d=0;d>=1;for(;;)if(q<<=1,r==b-1){p.push(c(q));break}else r++;return p.join("")},Oe=a=>null==a?"":""==a?null:Pe(a.length,32768,function(b){return a.charCodeAt(b)}),Pe=(a,b,d)=>{var e,f,g,h,j,k,l,m=[],n=4,o=4,p=3,q="",r=[],s={val:d(0),position:b,index:1};for(e=0;3>e;e+=1)m[e]=e;for(g=0,j=Eb(2,2),k=1;k!=j;)h=s.val&s.position,s.position>>=1,0==s.position&&(s.position=b,s.val=d(s.index++)),g|=(0>=1,0==s.position&&(s.position=b,s.val=d(s.index++)),g|=(0>=1,0==s.position&&(s.position=b,s.val=d(s.index++)),g|=(0a)return"";for(g=0,j=Eb(2,p),k=1;k!=j;)h=s.val&s.position,s.position>>=1,0==s.position&&(s.position=b,s.val=d(s.index++)),g|=(0>=1,0==s.position&&(s.position=b,s.val=d(s.index++)),g|=(0>=1,0==s.position&&(s.position=b,s.val=d(s.index++)),g|=(0a.reduce((a,c)=>+c[b]<+a[b]?c:a,a[0]),Re=(a,b)=>a.reduce((a,c)=>a+ +c[b],0)/a.length,Se=(a,b)=>a.reduce((a,c)=>+c[b]>+a[b]?c:a,a[0]),Te=function(a,b){var c=2a*3600*1000,Ve=a=>Me(JSON.stringify(a)),We=a=>"string"==typeof a?JSON.parse(Oe(a)):a,Xe=function(){for(var a=arguments.length,b=Array(a),c=0;c"undefined"!=typeof a)},Ye=(c,a)=>c.length===a.length&&c.every((b,c)=>b===a[c]),Ze=a=>{console.warn("mini-graph-card: ",a)},$e=(a,b)=>{for(var c=b,d=a.length;c{if(!a||!a.length)return a;if(null==a[0].value||null==a[a.length-1].value)throw new Error("The first and last thresholds must have a set \"value\".\n See ".concat(Ae));var b=0,c=null;return a.map((d,e)=>{if(null!=d.value)return b=e,P({},d);null==c?c=$e(a,e):e>c&&(b=c,c=$e(a,e));var f=a[b].value,g=a[c].value,h=(g-f)/(c-b);return{color:"string"==typeof d?d:d.color,value:h*e+f}})},af=(a,b)=>{var c=_e(a);if(c.sort((c,a)=>a.value-c.value),"smooth"===b)return c;var d,e=(d=[]).concat.apply(d,W(c.map((a,b)=>[a,{value:a.value-1e-4,color:c[b+1]?c[b+1].color:a.color}])));return e},bf=a=>{if(!Array.isArray(a.entities))throw new Error("Please provide the \"entities\" option as a list.\n See ".concat(Ae));if(a.line_color_above||a.line_color_below)throw new Error("\"line_color_above/line_color_below\" was removed, please use \"color_thresholds\".\n See ".concat(Ae));var b=P(P({animate:!1,hour24:!1,font_size:Be,font_size_header:14,height:100,hours_to_show:24,points_per_hour:.5,aggregate_func:"avg",group_by:"interval",line_color:[].concat(Ee),color_thresholds:[],color_thresholds_transition:"smooth",line_width:5,bar_spacing:4,compress:!0,smoothing:!0,state_map:[],cache:!0,value_factor:0,tap_action:{action:"more-info"}},JSON.parse(JSON.stringify(a))),{},{show:P(P({},Ge),a.show)});b.entities.forEach((a,c)=>{"string"==typeof a&&(b.entities[c]={entity:a})}),b.state_map.forEach((a,c)=>{"string"==typeof a&&(b.state_map[c]={value:a,label:a}),b.state_map[c].label=b.state_map[c].label||b.state_map[c].value}),"string"==typeof a.line_color&&(b.line_color=[a.line_color].concat(Ee)),b.font_size=a.font_size/100*Be||Be,b.color_thresholds=af(b.color_thresholds,b.color_thresholds_transition);var c=24Ce&&(b.points_per_hour=Ce/(b.hours_to_show*e),Ze("Not enough space, adjusting points_per_hour to ".concat(b.points_per_hour)))}return b},cf="0.13.0";pd.config({name:"mini-graph-card",version:1,storeName:"entity_history_cache",description:"Mini graph card uses caching for the entity history"}),pd.iterate((a,b)=>{var c=b.endsWith("-raw")?a:We(a),d=new Date;d.setHours(d.getHours()-c.hours_to_show),(a.version!==cf||new Date(c.last_fetched){console.warn("Purging has errored: ",a)}),console.info("%c MINI-GRAPH-CARD %c ".concat(cf," "),"color: white; background: coral; font-weight: 700;","color: coral; background: white; font-weight: 700;");class df extends Sc{constructor(){super(),this.id=Math.random().toString(36).substr(2,9),this.config={},this.bound=[0,0],this.boundSecondary=[0,0],this.length=[],this.entity=[],this.line=[],this.bar=[],this.abs=[],this.fill=[],this.points=[],this.gradient=[],this.tooltip={},this.updateQueue=[],this.updating=!1,this.stateChanged=!1,this.initial=!0,this._md5Config=void 0}static get styles(){return Je}set hass(a){this._hass=a;var b=!1,c=[];this.config.entities.forEach((d,e)=>{this.config.entities[e].index=e;var f=a&&a.states[d.entity]||void 0;f&&this.entity[e]!==f&&(this.entity[e]=f,c.push("".concat(f.entity_id,"-").concat(e)),b=!0)}),b&&(this.stateChanged=!0,this.entity=W(this.entity),this.config.update_interval||this.updating?this.updateQueue=[].concat(c,W(this.updateQueue)):setTimeout(()=>{this.updateQueue=[].concat(c,W(this.updateQueue)),this.updateData()},this.initial?0:1e3))}static get properties(){return{id:String,_hass:{},config:{},entity:[],Graph:[],line:[],shadow:[],length:Number,bound:[],boundSecondary:[],abs:[],tooltip:{},updateQueue:[],color:String}}setConfig(a){this.config=bf(a,this.config),this._md5Config=Q.hash(JSON.stringify(this.config));var b=!Ye(this.config.entities||[],a.entities);(!this.Graph||b)&&(this._hass&&(this.hass=this._hass),this.Graph=this.config.entities.map(a=>new Ie(500,this.config.height,[this.config.show.fill?0:this.config.line_width,this.config.line_width],this.config.hours_to_show,this.config.points_per_hour,a.aggregate_func||this.config.aggregate_func,this.config.group_by,Xe(a.smoothing,this.config.smoothing,!a.entity.startsWith("binary_sensor.")),this.config.logarithmic)))}connectedCallback(){super.connectedCallback(),this.config.update_interval&&(window.requestAnimationFrame(()=>{this.updateOnInterval()}),this.interval=setInterval(()=>this.updateOnInterval(),1e3*this.config.update_interval))}disconnectedCallback(){this.interval&&clearInterval(this.interval),super.disconnectedCallback()}shouldUpdate(a){if(Fe.some(b=>a.has(b)))return this.color=this.computeColor(void 0===this.tooltip.value?this.getEntityState(0):this.tooltip.value,this.tooltip.entity||0),!0}firstUpdated(){this.initial=!1}updated(a){this.config.animate&&a.has("line")&&(this.length.length{this.length[a.id]=a.getTotalLength()}),this.length=W(this.length)):this.length=Array(this.entity.length).fill("none"))}render(){var{config:a}=0void 0===this.entity[b])?this.renderWarnings():sc(M(),a.group,a.show.graph&&a.show.fill,"hover"===a.show.points,"hover"===a.show.labels,"hover"===a.show.labels_secondary,0this.handlePopup(b,a.tap_action.entity||this.entity[0]),this.renderHeader(),this.renderStates(),this.renderGraph(),this.renderInfo()):sc(N())}renderWarnings(){return sc(L(),this.config.entities.map((a,b)=>this.entity[b]?sc(J()):sc(K(),this.config.entities[b].entity)))}renderHeader(){var{show:a,align_icon:b,align_header:c,font_size_header:d}=this.config;return a.name||a.icon&&"state"!==b?sc(I(),c,d,this.renderName(),"state"===b?"":this.renderIcon()):""}renderIcon(){if(this.config.icon_image!==void 0)return sc(H(),this.config.icon_image);var{icon:a,icon_adaptive_color:b}=this.config.show;return a?sc(G(),this.config.align_icon,b?"color: ".concat(this.color,";"):"",this.computeIcon(this.entity[0])):""}renderName(){if(this.config.show.name){var a=void 0===this.tooltip.entity?this.config.name||this.computeName(0):this.computeName(this.tooltip.entity),b=this.config.show.name_adaptive_color?"opacity: 1; color: ".concat(this.color,";"):"";return sc(F(),b,a)}}renderStates(){if(this.config.show.state)return sc(E(),this.config.align_state,this.renderState(0),this.config.entities.map((a,b)=>0a&&a[b],a)}getEntityState(a){var b=this.config.entities[a];return"last"===this.config.show.state?this.points[a][this.points[a].length-1][Y]:b.attribute?this.getObjectAttr(this.entity[a].attributes,b.attribute):this.entity[a].state}renderState(a){var b=0===a;if(b||this.config.entities[a].show_state){var c=this.getEntityState(a),{entity:d,value:e}=this.tooltip,f=b&&d!==void 0,g=f?e:c,h=f?d:a,i=this.config.entities[h];return sc(D(),!b&&"state--small",b=>this.handlePopup(b,this.entity[a]),i.state_adaptive_color?"color: ".concat(this.computeColor(g,h)):"",i.show_indicator?this.renderIndicator(g,h):"",this.computeState(g),this.computeUom(h),b&&this.renderStateTime()||"")}}renderStateTime(){return void 0===this.tooltip.value?void 0:sc(C(),this.tooltip.label?sc(B(),this.tooltip.label):sc(A(),this.tooltip.time[0],this.tooltip.time[1]))}renderGraph(){var a=this.entity[0]&&!this.Graph.some((a,b)=>a._history===void 0&&!1!==this.config.entities[b].show_graph)||!1===this.config.show.loading_indicator;return this.config.show.graph?sc(z(),a?sc(y(),this.renderLabels(),this.renderLabelsSecondary(),this.renderSvg(),this.renderLegend()):sc(x())):""}computeLegend(a){var b=this.computeName(a),c=this.getEntityState(a),{show_legend_state:d=!1}=this.config.entities[a];if(d){if(b+=" (".concat(this.computeState(c)),!["unavailable"].includes(c)){var e=this.computeUom(a);["%",""].includes(e)||(b+=" "),b+="".concat(e)}b+=")"}return b}renderLegend(){return 1>=this.visibleLegends.length||!this.config.show.legend?void 0:sc(w(),this.visibleLegends.map(a=>{var b=this.computeLegend(a.index);return sc(v(),b=>this.handlePopup(b,this.entity[a.index]),()=>this.setTooltip(a.index,-1,this.getEntityState(a.index),"Current"),()=>this.tooltip={},this.renderIndicator(this.getEntityState(a.index),a.index),b)}))}renderIndicator(a,b){return tc(u(),this.computeColor(a,b))}renderSvgFill(a,b){if(a){var c="fade"===this.config.show.fill,d=this.length[b]||!1===this.config.entities[b].show_line;return tc(t(),"fill-grad-".concat(this.id,"-").concat(b),"fill-grad-mask-".concat(this.id,"-").concat(b),"url(#fill-grad-".concat(this.id,"-").concat(b,")"),"fill-".concat(this.id,"-").concat(b),this.config.show.fill,b,this.config.animate,d,this.config.animate?"".concat(.5*b,"s"):"0s",c?"url(#fill-grad-mask-".concat(this.id,"-").concat(b,")"):"",this.fill[b])}}renderSvgLine(a,b){if(a){var c=tc(s(),b,this.config.animate,this.length[b],this.config.animate?"".concat(.5*b,"s"):"0s",this.length[b]||"none",this.length[b]||"none","white",this.config.line_width,this.line[b]);return tc(r(),"line-".concat(this.id,"-").concat(b),c)}}renderSvgPoint(a,b){var c=this.gradient[b]?this.computeColor(a[Y],b):"inherit";return tc(q(),this.tooltip.index!==a[3],"--mcg-hover: ".concat(c,";"),c,c,a[He],a[X],this.config.line_width,()=>this.setTooltip(b,a[3],a[Y]),()=>this.tooltip={})}renderSvgPoints(a,b){if(a){var c=this.computeColor(this.entity[b].state,b);return tc(p(),this.tooltip.entity===b,void 0!==this.tooltip.entity&&this.tooltip.entity!==b,this.length[b],this.config.animate&&"hover"!==this.config.show.points,this.config.animate?"".concat(.5*b+.5,"s"):"0s",c,c,this.config.line_width/2,a.map(a=>this.renderSvgPoint(a,b)))}}renderSvgGradient(a){if(a){var b=a.map((a,b)=>a?tc(o(),"grad-".concat(this.id,"-").concat(b),a.map(a=>tc(n(),a.color,"".concat(a.offset,"%")))):void 0);return tc(m(),b)}}renderSvgLineRect(a,b){if(a){var c=this.gradient[b]?"url(#grad-".concat(this.id,"-").concat(b,")"):this.computeColor(this.entity[b].state,b);return tc(l(),void 0!==this.tooltip.entity&&this.tooltip.entity!==b,"rect-".concat(this.id,"-").concat(b),c,"url(#line-".concat(this.id,"-").concat(b,")"))}}renderSvgFillRect(a,b){if(a){var c=this.gradient[b]?"url(#grad-".concat(this.id,"-").concat(b,")"):this.computeColor(this.entity[b].state,b);return tc(k(),void 0!==this.tooltip.entity&&this.tooltip.entity!==b,"fill-rect-".concat(this.id,"-").concat(b),c,"url(#fill-".concat(this.id,"-").concat(b,")"))}}renderSvgBars(a,b){if(a){var c=a.map((a,c)=>{var d=this.config.animate?tc(j(),this.config.height,a.y):"",e=this.computeColor(a.value,b);return tc(h(),a.x,a.y,a.height,a.width,e,()=>this.setTooltip(b,c,a.value),()=>this.tooltip={},d)});return tc(g(),this.config.animate,c)}}renderSvg(){var{height:a}=this.config;return tc(f(),0===a?0:"100%",a,a=>a.stopPropagation(),this.renderSvgGradient(this.gradient),this.fill.map((a,b)=>this.renderSvgFill(a,b)),this.fill.map((a,b)=>this.renderSvgFillRect(a,b)),this.line.map((a,b)=>this.renderSvgLine(a,b)),this.line.map((a,b)=>this.renderSvgLineRect(a,b)),this.bar.map((a,b)=>this.renderSvgBars(a,b)),this.points.map((a,b)=>this.renderSvgPoints(a,b)))}setTooltip(a,b,c){var d=3sc(b(),a.type,this.computeState(a.state),this.computeUom(0),"avg"===a.type?"":Te(new Date(a.last_changed),this.config.format,this._hass.language)))):sc(a())}handlePopup(a,b){a.stopPropagation(),Ke(this,this._hass,this.config,this.config.tap_action,b.entity_id||b)}get visibleEntities(){return this.config.entities.filter(a=>!1!==a.show_graph)}get primaryYaxisEntities(){return this.visibleEntities.filter(a=>a.y_axis===void 0||"primary"===a.y_axis)}get secondaryYaxisEntities(){return this.visibleEntities.filter(a=>"secondary"===a.y_axis)}get visibleLegends(){return this.visibleEntities.filter(a=>!1!==a.show_legend)}get primaryYaxisSeries(){return this.primaryYaxisEntities.map(a=>this.Graph[a.index])}get secondaryYaxisSeries(){return this.secondaryYaxisEntities.map(a=>this.Graph[a.index])}computeColor(a,b){var c,{color_thresholds:d,line_color:e}=this.config,f=+a||0;if(0a.valuea.valueb.value===a);if(f)return f.label;Ze("value [".concat(a,"] not found in state_map"))}var b="string"==typeof a?parseFloat(a.replace(/,/g,".")):+a;var c=this.config.decimals,d=10**this.config.value_factor;if(c===void 0||tb(c)||tb(b))return this.numberFormat(Fb(100*(b*d))/100,this._hass.language);var e=10**c;return this.numberFormat((Fb(b*d*e)/e).toFixed(c),this._hass.language,c)}numberFormat(a,b,c){return!tb(+a)&&Intl?new Intl.NumberFormat(b,{minimumFractionDigits:c}).format(+a):a.toString()}updateOnInterval(){this.stateChanged&&!this.updating&&(this.stateChanged=!1,this.updateData())}updateData(){var a=arguments,b=this;return U(function*(){var{config:c}=0b.updateEntity(a,c,e,d));yield Promise.all(f)}catch(a){Ze(a)}if(c.show.graph&&b.entity.forEach((a,c)=>{a&&b.Graph[c].update()}),b.updateBounds(),c.show.graph){var g=0;b.entity.forEach((a,d)=>{if(a&&0!==b.Graph[d].coords.length){var e="secondary"===c.entities[d].y_axis?b.boundSecondary:b.bound;if([b.Graph[d].min,b.Graph[d].max]=[e[0],e[1]],"bar"===c.show.graph){var f=b.visibleEntities.length;b.bar[d]=b.Graph[d].getBars(g,f,c.bar_spacing),g+=1}else{var h=b.Graph[d].getPath();!1!==c.entities[d].show_line&&(b.line[d]=h),c.show.fill&&!1!==c.entities[d].show_fill&&(b.fill[d]=b.Graph[d].getFill(h)),c.show.points&&!1!==c.entities[d].show_points&&(b.points[d]=b.Graph[d].getPoints()),0b[a])))||d:"~"===c[0]?Math[a].apply(Math,[+c.substr(1)].concat(W(b.map(b=>b[a])))):c}getBoundaries(a,b,c,d,e){var f=[this.getBoundary("min",a,b,d[0]),this.getBoundary("max",a,c,d[1])];if(e){var g=zb(f[0]-f[1]),h=parseFloat(e)-g;if(0c!=="".concat(a.entity_id,"-").concat(b));var f=[],g=c,h=!1,i=e.config.cache?yield e.getCache("".concat(a.entity_id,"_").concat(b),e.config.useCompress):void 0;if(i&&i.hours_to_show===e.config.hours_to_show){f=i.data;var k=f.findIndex(a=>new Date(a.last_changed)>c);-1===k?f=[]:(0g&&(g=new Date(l-1))}var j=yield e.fetchRecent(a.entity_id,g,d,!e.config.entities[b].attribute&&h,!!e.config.entities[b].attribute);if(j[0]&&0{e.config.entities[b].attribute&&(a.state=e.getObjectAttr(a.attributes,e.config.entities[b].attribute),delete a.attributes),0!tb(parseFloat(a.state))),j=j.map(a=>({last_changed:e.config.entities[b].attribute?a.last_updated:a.last_changed,state:a.state})),f=[].concat(W(f),W(j)),e.config.cache&&e.setCache("".concat(a.entity_id,"_").concat(b),{hours_to_show:e.config.hours_to_show,last_fetched:new Date,data:f,version:cf},e.config.useCompress).catch(a=>{Ze(a),pd.clear()})),0!==f.length)if(e.entity[0]&&a.entity_id===e.entity[0].entity_id&&e.updateExtrema(f),!0===e.config.entities[b].fixed_value){var m=f[f.length-1];e.Graph[b].history=[m,m]}else e.Graph[b].history=f}})()}fetchRecent(a,b,c,d,e){var f=this;return U(function*(){var g="history/period";return b&&(g+="/".concat(b.toISOString())),g+="?filter_entity_id=".concat(a),c&&(g+="&end_time=".concat(c.toISOString())),d&&(g+="&skip_initial_state"),e||(g+="&minimal_response&no_attributes"),e&&(g+="&significant_changes_only=0"),f._hass.callApi("GET",g)})()}updateExtrema(a){var{extrema:b,average:c}=this.config.show;this.abs=[].concat(W(b?[P({type:"min"},Qe(a,"state"))]:[]),W(c?[{type:"avg",state:Re(a,"state")}]:[]),W(b?[P({type:"max"},Se(a,"state"))]:[]))}_convertState(a){var b=this.config.state_map.findIndex(b=>b.value===a.state);-1===b||(a.state=b)}getEndDate(){var a=new Date;switch(this.config.group_by){case"date":a.setDate(a.getDate()+1),a.setHours(0,0,0);break;case"hour":a.setHours(a.getHours()+1),a.setMinutes(0,0);}return a}setNextUpdate(){if(!this.config.update_interval){var a=1/this.config.points_per_hour;clearInterval(this.interval),this.interval=setInterval(()=>{this.updating||this.updateData()},a*V)}}getCardSize(){return 3}}customElements.define("mini-graph-card",df),window.customCards=window.customCards||[],window.customCards.push({type:"mini-graph-card",name:"Mini Graph Card",preview:!1,description:"The Mini Graph card is a minimalistic and customizable graph card"})})})(); diff --git a/www/community/mini-graph-card/mini-graph-card-bundle.js.gz b/www/community/mini-graph-card/mini-graph-card-bundle.js.gz new file mode 100644 index 0000000..e361e1e Binary files /dev/null and b/www/community/mini-graph-card/mini-graph-card-bundle.js.gz differ diff --git a/www/community/platinum-weather-card/a-clear-day.svg b/www/community/platinum-weather-card/a-clear-day.svg new file mode 100644 index 0000000..d97d28b --- /dev/null +++ b/www/community/platinum-weather-card/a-clear-day.svg @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-clear-night.svg b/www/community/platinum-weather-card/a-clear-night.svg new file mode 100644 index 0000000..005ac63 --- /dev/null +++ b/www/community/platinum-weather-card/a-clear-night.svg @@ -0,0 +1,159 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-cloudy-1-day.svg b/www/community/platinum-weather-card/a-cloudy-1-day.svg new file mode 100644 index 0000000..823fea1 --- /dev/null +++ b/www/community/platinum-weather-card/a-cloudy-1-day.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-cloudy-1-night.svg b/www/community/platinum-weather-card/a-cloudy-1-night.svg new file mode 100644 index 0000000..3fe1541 --- /dev/null +++ b/www/community/platinum-weather-card/a-cloudy-1-night.svg @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-cloudy-2-day.svg b/www/community/platinum-weather-card/a-cloudy-2-day.svg new file mode 100644 index 0000000..39d1f4a --- /dev/null +++ b/www/community/platinum-weather-card/a-cloudy-2-day.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-cloudy-2-night.svg b/www/community/platinum-weather-card/a-cloudy-2-night.svg new file mode 100644 index 0000000..954ad7a --- /dev/null +++ b/www/community/platinum-weather-card/a-cloudy-2-night.svg @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-cloudy-3-day.svg b/www/community/platinum-weather-card/a-cloudy-3-day.svg new file mode 100644 index 0000000..69b123b --- /dev/null +++ b/www/community/platinum-weather-card/a-cloudy-3-day.svg @@ -0,0 +1,178 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-cloudy-3-night.svg b/www/community/platinum-weather-card/a-cloudy-3-night.svg new file mode 100644 index 0000000..e9bdf81 --- /dev/null +++ b/www/community/platinum-weather-card/a-cloudy-3-night.svg @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-cloudy.svg b/www/community/platinum-weather-card/a-cloudy.svg new file mode 100644 index 0000000..7efebd7 --- /dev/null +++ b/www/community/platinum-weather-card/a-cloudy.svg @@ -0,0 +1,113 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-dust.svg b/www/community/platinum-weather-card/a-dust.svg new file mode 100644 index 0000000..4cfa6cc --- /dev/null +++ b/www/community/platinum-weather-card/a-dust.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-fog-day.svg b/www/community/platinum-weather-card/a-fog-day.svg new file mode 100644 index 0000000..ed834cf --- /dev/null +++ b/www/community/platinum-weather-card/a-fog-day.svg @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + F + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-fog-night.svg b/www/community/platinum-weather-card/a-fog-night.svg new file mode 100644 index 0000000..d59f98f --- /dev/null +++ b/www/community/platinum-weather-card/a-fog-night.svg @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-fog.svg b/www/community/platinum-weather-card/a-fog.svg new file mode 100644 index 0000000..9d4bcb8 --- /dev/null +++ b/www/community/platinum-weather-card/a-fog.svg @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-frost-day.svg b/www/community/platinum-weather-card/a-frost-day.svg new file mode 100644 index 0000000..16d591c --- /dev/null +++ b/www/community/platinum-weather-card/a-frost-day.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + F + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-frost-night.svg b/www/community/platinum-weather-card/a-frost-night.svg new file mode 100644 index 0000000..ff2c8dc --- /dev/null +++ b/www/community/platinum-weather-card/a-frost-night.svg @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-frost.svg b/www/community/platinum-weather-card/a-frost.svg new file mode 100644 index 0000000..58bdbd6 --- /dev/null +++ b/www/community/platinum-weather-card/a-frost.svg @@ -0,0 +1,130 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-hail.svg b/www/community/platinum-weather-card/a-hail.svg new file mode 100644 index 0000000..d36f19c --- /dev/null +++ b/www/community/platinum-weather-card/a-hail.svg @@ -0,0 +1,134 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-haze-day.svg b/www/community/platinum-weather-card/a-haze-day.svg new file mode 100644 index 0000000..d1e6a52 --- /dev/null +++ b/www/community/platinum-weather-card/a-haze-day.svg @@ -0,0 +1,244 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-haze-night.svg b/www/community/platinum-weather-card/a-haze-night.svg new file mode 100644 index 0000000..4d734da --- /dev/null +++ b/www/community/platinum-weather-card/a-haze-night.svg @@ -0,0 +1,309 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-haze.svg b/www/community/platinum-weather-card/a-haze.svg new file mode 100644 index 0000000..b80ce5d --- /dev/null +++ b/www/community/platinum-weather-card/a-haze.svg @@ -0,0 +1,172 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-hurricane.svg b/www/community/platinum-weather-card/a-hurricane.svg new file mode 100644 index 0000000..21c2c72 --- /dev/null +++ b/www/community/platinum-weather-card/a-hurricane.svg @@ -0,0 +1,90 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-isolated-thunderstorms-day.svg b/www/community/platinum-weather-card/a-isolated-thunderstorms-day.svg new file mode 100644 index 0000000..9698e33 --- /dev/null +++ b/www/community/platinum-weather-card/a-isolated-thunderstorms-day.svg @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-isolated-thunderstorms-night.svg b/www/community/platinum-weather-card/a-isolated-thunderstorms-night.svg new file mode 100644 index 0000000..303dade --- /dev/null +++ b/www/community/platinum-weather-card/a-isolated-thunderstorms-night.svg @@ -0,0 +1,401 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-isolated-thunderstorms.svg b/www/community/platinum-weather-card/a-isolated-thunderstorms.svg new file mode 100644 index 0000000..b2cd6e4 --- /dev/null +++ b/www/community/platinum-weather-card/a-isolated-thunderstorms.svg @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-rain-and-sleet-mix.svg b/www/community/platinum-weather-card/a-rain-and-sleet-mix.svg new file mode 100644 index 0000000..172010d --- /dev/null +++ b/www/community/platinum-weather-card/a-rain-and-sleet-mix.svg @@ -0,0 +1,141 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-rain-and-snow-mix.svg b/www/community/platinum-weather-card/a-rain-and-snow-mix.svg new file mode 100644 index 0000000..c89ca79 --- /dev/null +++ b/www/community/platinum-weather-card/a-rain-and-snow-mix.svg @@ -0,0 +1,195 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-rainy-1-day.svg b/www/community/platinum-weather-card/a-rainy-1-day.svg new file mode 100644 index 0000000..2faf06e --- /dev/null +++ b/www/community/platinum-weather-card/a-rainy-1-day.svg @@ -0,0 +1,179 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-rainy-1-night.svg b/www/community/platinum-weather-card/a-rainy-1-night.svg new file mode 100644 index 0000000..ee8ffd8 --- /dev/null +++ b/www/community/platinum-weather-card/a-rainy-1-night.svg @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-rainy-1.svg b/www/community/platinum-weather-card/a-rainy-1.svg new file mode 100644 index 0000000..71fe972 --- /dev/null +++ b/www/community/platinum-weather-card/a-rainy-1.svg @@ -0,0 +1,106 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-rainy-2-day.svg b/www/community/platinum-weather-card/a-rainy-2-day.svg new file mode 100644 index 0000000..affdfff --- /dev/null +++ b/www/community/platinum-weather-card/a-rainy-2-day.svg @@ -0,0 +1,204 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-rainy-2-night.svg b/www/community/platinum-weather-card/a-rainy-2-night.svg new file mode 100644 index 0000000..9c3ae20 --- /dev/null +++ b/www/community/platinum-weather-card/a-rainy-2-night.svg @@ -0,0 +1,256 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-rainy-2.svg b/www/community/platinum-weather-card/a-rainy-2.svg new file mode 100644 index 0000000..8dce3fc --- /dev/null +++ b/www/community/platinum-weather-card/a-rainy-2.svg @@ -0,0 +1,131 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-rainy-3-day.svg b/www/community/platinum-weather-card/a-rainy-3-day.svg new file mode 100644 index 0000000..b0b5754 --- /dev/null +++ b/www/community/platinum-weather-card/a-rainy-3-day.svg @@ -0,0 +1,206 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-rainy-3-night.svg b/www/community/platinum-weather-card/a-rainy-3-night.svg new file mode 100644 index 0000000..4078e7d --- /dev/null +++ b/www/community/platinum-weather-card/a-rainy-3-night.svg @@ -0,0 +1,270 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-rainy-3.svg b/www/community/platinum-weather-card/a-rainy-3.svg new file mode 100644 index 0000000..301d170 --- /dev/null +++ b/www/community/platinum-weather-card/a-rainy-3.svg @@ -0,0 +1,133 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-scattered-thunderstorms-day.svg b/www/community/platinum-weather-card/a-scattered-thunderstorms-day.svg new file mode 100644 index 0000000..0cfbccc --- /dev/null +++ b/www/community/platinum-weather-card/a-scattered-thunderstorms-day.svg @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-scattered-thunderstorms-night.svg b/www/community/platinum-weather-card/a-scattered-thunderstorms-night.svg new file mode 100644 index 0000000..72cf7a6 --- /dev/null +++ b/www/community/platinum-weather-card/a-scattered-thunderstorms-night.svg @@ -0,0 +1,283 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-scattered-thunderstorms.svg b/www/community/platinum-weather-card/a-scattered-thunderstorms.svg new file mode 100644 index 0000000..01b5a1c --- /dev/null +++ b/www/community/platinum-weather-card/a-scattered-thunderstorms.svg @@ -0,0 +1,265 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-severe-thunderstorm.svg b/www/community/platinum-weather-card/a-severe-thunderstorm.svg new file mode 100644 index 0000000..223198b --- /dev/null +++ b/www/community/platinum-weather-card/a-severe-thunderstorm.svg @@ -0,0 +1,307 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-snow-and-sleet-mix.svg b/www/community/platinum-weather-card/a-snow-and-sleet-mix.svg new file mode 100644 index 0000000..4f0516d --- /dev/null +++ b/www/community/platinum-weather-card/a-snow-and-sleet-mix.svg @@ -0,0 +1,227 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-snowy-1-day.svg b/www/community/platinum-weather-card/a-snowy-1-day.svg new file mode 100644 index 0000000..fb73943 --- /dev/null +++ b/www/community/platinum-weather-card/a-snowy-1-day.svg @@ -0,0 +1,241 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-snowy-1-night.svg b/www/community/platinum-weather-card/a-snowy-1-night.svg new file mode 100644 index 0000000..039ea2e --- /dev/null +++ b/www/community/platinum-weather-card/a-snowy-1-night.svg @@ -0,0 +1,269 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-snowy-1.svg b/www/community/platinum-weather-card/a-snowy-1.svg new file mode 100644 index 0000000..6fd9ffe --- /dev/null +++ b/www/community/platinum-weather-card/a-snowy-1.svg @@ -0,0 +1,132 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-snowy-2-day.svg b/www/community/platinum-weather-card/a-snowy-2-day.svg new file mode 100644 index 0000000..323a616 --- /dev/null +++ b/www/community/platinum-weather-card/a-snowy-2-day.svg @@ -0,0 +1,273 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-snowy-2-night.svg b/www/community/platinum-weather-card/a-snowy-2-night.svg new file mode 100644 index 0000000..10dcbfa --- /dev/null +++ b/www/community/platinum-weather-card/a-snowy-2-night.svg @@ -0,0 +1,301 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-snowy-2.svg b/www/community/platinum-weather-card/a-snowy-2.svg new file mode 100644 index 0000000..682995b --- /dev/null +++ b/www/community/platinum-weather-card/a-snowy-2.svg @@ -0,0 +1,160 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/community/platinum-weather-card/a-snowy-3-day.svg b/www/community/platinum-weather-card/a-snowy-3-day.svg new file mode 100644 index 0000000..846c17a --- /dev/null +++ b/www/community/platinum-weather-card/a-snowy-3-day.svg @@ -0,0 +1,334 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-snowy-3-night.svg b/www/community/platinum-weather-card/a-snowy-3-night.svg new file mode 100644 index 0000000..b3c8c24 --- /dev/null +++ b/www/community/platinum-weather-card/a-snowy-3-night.svg @@ -0,0 +1,361 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-snowy-3.svg b/www/community/platinum-weather-card/a-snowy-3.svg new file mode 100644 index 0000000..23aec31 --- /dev/null +++ b/www/community/platinum-weather-card/a-snowy-3.svg @@ -0,0 +1,224 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-thunderstorms.svg b/www/community/platinum-weather-card/a-thunderstorms.svg new file mode 100644 index 0000000..d66c25b --- /dev/null +++ b/www/community/platinum-weather-card/a-thunderstorms.svg @@ -0,0 +1,260 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/community/platinum-weather-card/a-tornado.svg b/www/community/platinum-weather-card/a-tornado.svg new file mode 100644 index 0000000..cb614aa --- /dev/null +++ b/www/community/platinum-weather-card/a-tornado.svg @@ -0,0 +1,367 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-tropical-storm.svg b/www/community/platinum-weather-card/a-tropical-storm.svg new file mode 100644 index 0000000..b2bf88a --- /dev/null +++ b/www/community/platinum-weather-card/a-tropical-storm.svg @@ -0,0 +1,50 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-unknown.svg b/www/community/platinum-weather-card/a-unknown.svg new file mode 100644 index 0000000..d610a3e --- /dev/null +++ b/www/community/platinum-weather-card/a-unknown.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/a-wind.svg b/www/community/platinum-weather-card/a-wind.svg new file mode 100644 index 0000000..e2fae59 --- /dev/null +++ b/www/community/platinum-weather-card/a-wind.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/editor-92788164.js b/www/community/platinum-weather-card/editor-92788164.js new file mode 100644 index 0000000..42fbe5f --- /dev/null +++ b/www/community/platinum-weather-card/editor-92788164.js @@ -0,0 +1,1582 @@ +import{o as e,l as t,_ as i,a as o,s as n,b as a,e as d,c as r,i as l,t as s,x as c,y as m,r as h,d as p,f as u,g as f,n as _,h as g}from"./platinum-weather-card-df6d26e2.js"; +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function v(t){return e({finisher:(e,i)=>{Object.assign(e.prototype[i],t)}})} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function b(t,i){return e({descriptor:e=>{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(i){const i="symbol"==typeof e?Symbol():"__"+e;o.get=function(){var e,o;return void 0===this[i]&&(this[i]=null!==(o=null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t))&&void 0!==o?o:null),this[i]}}return o}})} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function y(t){return e({descriptor:e=>({async get(){var e;return await this.updateComplete,null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(t)},enumerable:!0,configurable:!0})})} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function x(i,o,n){let a,d=i;return"object"==typeof i?(d=i.slot,a=i):a={flatten:o},n?t({slot:d,flatten:o,selector:n}):e({descriptor:e=>({get(){var e,t;const i="slot"+(d?`[name=${d}]`:":not([name])"),o=null===(e=this.renderRoot)||void 0===e?void 0:e.querySelector(i);return null!==(t=null==o?void 0:o.assignedNodes(a))&&void 0!==t?t:[]},enumerable:!0,configurable:!0})})}var w="M11 20V22H3C1.9 22 1 21.1 1 20V4C1 2.9 1.9 2 3 2H21C22.1 2 23 2.9 23 4V12.1L22.8 11.9C22.3 11.4 21.7 11.1 21 11.1V6H3V20H11M21.4 13.3L22.7 14.6C22.9 14.8 22.9 15.2 22.7 15.4L21.7 16.4L19.6 14.3L20.6 13.3C20.7 13.2 20.8 13.1 21 13.1C21.2 13.1 21.3 13.2 21.4 13.3M21.1 16.9L15.1 23H13V20.9L19.1 14.8L21.1 16.9Z",E="M11,4H13V16L18.5,10.5L19.92,11.92L12,19.84L4.08,11.92L5.5,10.5L11,16V4Z",I="M13,20H11V8L5.5,13.5L4.08,12.08L12,4.16L19.92,12.08L18.5,13.5L13,8V20Z",C="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z";const T=["overview","extended","slots","daily_forecast"]; +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */var A=function(){function e(e){void 0===e&&(e={}),this.adapter=e}return Object.defineProperty(e,"cssClasses",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(e,"strings",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(e,"numbers",{get:function(){return{}},enumerable:!1,configurable:!0}),Object.defineProperty(e,"defaultAdapter",{get:function(){return{}},enumerable:!1,configurable:!0}),e.prototype.init=function(){},e.prototype.destroy=function(){},e}(),k={ROOT:"mdc-form-field"},$={LABEL_SELECTOR:".mdc-form-field > label"},O=function(e){function t(i){var n=e.call(this,o(o({},t.defaultAdapter),i))||this;return n.click=function(){n.handleClick()},n}return i(t,e),Object.defineProperty(t,"cssClasses",{get:function(){return k},enumerable:!1,configurable:!0}),Object.defineProperty(t,"strings",{get:function(){return $},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{activateInputRipple:function(){},deactivateInputRipple:function(){},deregisterInteractionHandler:function(){},registerInteractionHandler:function(){}}},enumerable:!1,configurable:!0}),t.prototype.init=function(){this.adapter.registerInteractionHandler("click",this.click)},t.prototype.destroy=function(){this.adapter.deregisterInteractionHandler("click",this.click)},t.prototype.handleClick=function(){var e=this;this.adapter.activateInputRipple(),requestAnimationFrame((function(){e.adapter.deactivateInputRipple()}))},t}(A); +/** + * @license + * Copyright 2017 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +const S=e=>e.nodeType===Node.ELEMENT_NODE;function R(e){return{addClass:t=>{e.classList.add(t)},removeClass:t=>{e.classList.remove(t)},hasClass:t=>e.classList.contains(t)}}const F=()=>{},L={get passive(){return!1}};document.addEventListener("x",F,L),document.removeEventListener("x",F);const D=(e=window.document)=>{let t=e.activeElement;const i=[];if(!t)return i;for(;t&&(i.push(t),t.shadowRoot);)t=t.shadowRoot.activeElement;return i},V=e=>{const t=D();if(!t.length)return!1;const i=t[t.length-1],o=new Event("check-if-focused",{bubbles:!0,composed:!0});let n=[];const a=e=>{n=e.composedPath()};return document.body.addEventListener("check-if-focused",a),i.dispatchEvent(o),document.body.removeEventListener("check-if-focused",a),-1!==n.indexOf(e)}; +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +class N extends n{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 M,P;const B=null!==(P=null===(M=window.ShadyDOM)||void 0===M?void 0:M.inUse)&&void 0!==P&&P;class z extends N{constructor(){super(...arguments),this.disabled=!1,this.containingForm=null,this.formDataListener=e=>{this.disabled||this.setFormData(e.formData)}}findFormElement(){if(!this.shadowRoot||B)return null;const e=this.getRootNode().querySelectorAll("form");for(const t of Array.from(e))if(t.contains(this))return t;return null}connectedCallback(){var e;super.connectedCallback(),this.containingForm=this.findFormElement(),null===(e=this.containingForm)||void 0===e||e.addEventListener("formdata",this.formDataListener)}disconnectedCallback(){var e;super.disconnectedCallback(),null===(e=this.containingForm)||void 0===e||e.removeEventListener("formdata",this.formDataListener),this.containingForm=null}click(){this.formElement&&!this.disabled&&(this.formElement.focus(),this.formElement.click())}firstUpdated(){super.firstUpdated(),this.shadowRoot&&this.mdcRoot.addEventListener("change",(e=>{this.dispatchEvent(new Event("change",e))}))}}z.shadowRootOptions={mode:"open",delegatesFocus:!0},a([d({type:Boolean})],z.prototype,"disabled",void 0); +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +const H=e=>(t,i)=>{if(t.constructor._observers){if(!t.constructor.hasOwnProperty("_observers")){const e=t.constructor._observers;t.constructor._observers=new Map,e.forEach(((e,i)=>t.constructor._observers.set(i,e)))}}else{t.constructor._observers=new Map;const e=t.updated;t.updated=function(t){e.call(this,t),t.forEach(((e,t)=>{const i=this.constructor._observers.get(t);void 0!==i&&i.call(this,this[t],e)}))}}t.constructor._observers.set(i,e)} +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */,U=r(class extends l{constructor(e){var t;if(super(e),e.type!==s.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 i,o;if(void 0===this.nt){this.nt=new Set,void 0!==e.strings&&(this.st=new Set(e.strings.join(" ").split(/\s/).filter((e=>""!==e))));for(const e in t)t[e]&&!(null===(i=this.st)||void 0===i?void 0:i.has(e))&&this.nt.add(e);return this.render(t)}const n=e.element.classList;this.nt.forEach((e=>{e in t||(n.remove(e),this.nt.delete(e))}));for(const e in t){const i=!!t[e];i===this.nt.has(e)||(null===(o=this.st)||void 0===o?void 0:o.has(e))||(i?(n.add(e),this.nt.add(e)):(n.remove(e),this.nt.delete(e)))}return c}}); +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +class j extends N{constructor(){super(...arguments),this.alignEnd=!1,this.spaceBetween=!1,this.nowrap=!1,this.label="",this.mdcFoundationClass=O}createAdapter(){return{registerInteractionHandler:(e,t)=>{this.labelEl.addEventListener(e,t)},deregisterInteractionHandler:(e,t)=>{this.labelEl.removeEventListener(e,t)},activateInputRipple:async()=>{const e=this.input;if(e instanceof z){const t=await e.ripple;t&&t.startPress()}},deactivateInputRipple:async()=>{const e=this.input;if(e instanceof z){const t=await e.ripple;t&&t.endPress()}}}}get input(){var e,t;return null!==(t=null===(e=this.slottedInputs)||void 0===e?void 0:e[0])&&void 0!==t?t:null}render(){const e={"mdc-form-field--align-end":this.alignEnd,"mdc-form-field--space-between":this.spaceBetween,"mdc-form-field--nowrap":this.nowrap};return m` +
    + + +
    `}click(){this._labelClick()}_labelClick(){const e=this.input;e&&(e.focus(),e.click())}}a([d({type:Boolean})],j.prototype,"alignEnd",void 0),a([d({type:Boolean})],j.prototype,"spaceBetween",void 0),a([d({type:Boolean})],j.prototype,"nowrap",void 0),a([d({type:String}),H((async function(e){var t;null===(t=this.input)||void 0===t||t.setAttribute("aria-label",e)}))],j.prototype,"label",void 0),a([b(".mdc-form-field")],j.prototype,"mdcRoot",void 0),a([x("",!0,"*")],j.prototype,"slottedInputs",void 0),a([b("label")],j.prototype,"labelEl",void 0); +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */ +const X=h`.mdc-form-field{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:0.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);color:rgba(0, 0, 0, 0.87);color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87));display:inline-flex;align-items:center;vertical-align:middle}.mdc-form-field>label{margin-left:0;margin-right:auto;padding-left:4px;padding-right:0;order:0}[dir=rtl] .mdc-form-field>label,.mdc-form-field>label[dir=rtl]{margin-left:auto;margin-right:0}[dir=rtl] .mdc-form-field>label,.mdc-form-field>label[dir=rtl]{padding-left:0;padding-right:4px}.mdc-form-field--nowrap>label{text-overflow:ellipsis;overflow:hidden;white-space:nowrap}.mdc-form-field--align-end>label{margin-left:auto;margin-right:0;padding-left:0;padding-right:4px;order:-1}[dir=rtl] .mdc-form-field--align-end>label,.mdc-form-field--align-end>label[dir=rtl]{margin-left:0;margin-right:auto}[dir=rtl] .mdc-form-field--align-end>label,.mdc-form-field--align-end>label[dir=rtl]{padding-left:4px;padding-right:0}.mdc-form-field--space-between{justify-content:space-between}.mdc-form-field--space-between>label{margin:0}[dir=rtl] .mdc-form-field--space-between>label,.mdc-form-field--space-between>label[dir=rtl]{margin:0}:host{display:inline-flex}.mdc-form-field{width:100%}::slotted(*){-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:0.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);color:rgba(0, 0, 0, 0.87);color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87))}::slotted(mwc-switch){margin-right:10px}[dir=rtl] ::slotted(mwc-switch),::slotted(mwc-switch[dir=rtl]){margin-left:10px}`,G={"mwc-formfield":class extends j{static get styles(){return X}}}; +/** + * @license + * Copyright 2020 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var Y="Unknown",q="Backspace",W="Enter",K="Spacebar",Q="PageUp",Z="PageDown",J="End",ee="Home",te="ArrowLeft",ie="ArrowUp",oe="ArrowRight",ne="ArrowDown",ae="Delete",de="Escape",re="Tab",le=new Set;le.add(q),le.add(W),le.add(K),le.add(Q),le.add(Z),le.add(J),le.add(ee),le.add(te),le.add(ie),le.add(oe),le.add(ne),le.add(ae),le.add(de),le.add(re);var se=8,ce=13,me=32,he=33,pe=34,ue=35,fe=36,_e=37,ge=38,ve=39,be=40,ye=46,xe=27,we=9,Ee=new Map;Ee.set(se,q),Ee.set(ce,W),Ee.set(me,K),Ee.set(he,Q),Ee.set(pe,Z),Ee.set(ue,J),Ee.set(fe,ee),Ee.set(_e,te),Ee.set(ge,ie),Ee.set(ve,oe),Ee.set(be,ne),Ee.set(ye,ae),Ee.set(xe,de),Ee.set(we,re);var Ie,Ce,Te=new Set;function Ae(e){var t=e.key;if(le.has(t))return t;var i=Ee.get(e.keyCode);return i||Y} +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */Te.add(Q),Te.add(Z),Te.add(J),Te.add(ee),Te.add(te),Te.add(ie),Te.add(oe),Te.add(ne);var ke="mdc-list-item--activated",$e="mdc-list-item",Oe="mdc-list-item--disabled",Se="mdc-list-item--selected",Re="mdc-list-item__text",Fe="mdc-list-item__primary-text",Le="mdc-list";(Ie={})[""+ke]="mdc-list-item--activated",Ie[""+$e]="mdc-list-item",Ie[""+Oe]="mdc-list-item--disabled",Ie[""+Se]="mdc-list-item--selected",Ie[""+Fe]="mdc-list-item__primary-text",Ie[""+Le]="mdc-list";var De=((Ce={})[""+ke]="mdc-deprecated-list-item--activated",Ce[""+$e]="mdc-deprecated-list-item",Ce[""+Oe]="mdc-deprecated-list-item--disabled",Ce[""+Se]="mdc-deprecated-list-item--selected",Ce[""+Re]="mdc-deprecated-list-item__text",Ce[""+Fe]="mdc-deprecated-list-item__primary-text",Ce[""+Le]="mdc-deprecated-list",Ce),Ve={ACTION_EVENT:"MDCList:action",ARIA_CHECKED:"aria-checked",ARIA_CHECKED_CHECKBOX_SELECTOR:'[role="checkbox"][aria-checked="true"]',ARIA_CHECKED_RADIO_SELECTOR:'[role="radio"][aria-checked="true"]',ARIA_CURRENT:"aria-current",ARIA_DISABLED:"aria-disabled",ARIA_ORIENTATION:"aria-orientation",ARIA_ORIENTATION_HORIZONTAL:"horizontal",ARIA_ROLE_CHECKBOX_SELECTOR:'[role="checkbox"]',ARIA_SELECTED:"aria-selected",ARIA_INTERACTIVE_ROLES_SELECTOR:'[role="listbox"], [role="menu"]',ARIA_MULTI_SELECTABLE_SELECTOR:'[aria-multiselectable="true"]',CHECKBOX_RADIO_SELECTOR:'input[type="checkbox"], input[type="radio"]',CHECKBOX_SELECTOR:'input[type="checkbox"]',CHILD_ELEMENTS_TO_TOGGLE_TABINDEX:"\n ."+$e+" button:not(:disabled),\n ."+$e+" a,\n ."+De[$e]+" button:not(:disabled),\n ."+De[$e]+" a\n ",DEPRECATED_SELECTOR:".mdc-deprecated-list",FOCUSABLE_CHILD_ELEMENTS:"\n ."+$e+" button:not(:disabled),\n ."+$e+" a,\n ."+$e+' input[type="radio"]:not(:disabled),\n .'+$e+' input[type="checkbox"]:not(:disabled),\n .'+De[$e]+" button:not(:disabled),\n ."+De[$e]+" a,\n ."+De[$e]+' input[type="radio"]:not(:disabled),\n .'+De[$e]+' input[type="checkbox"]:not(:disabled)\n ',RADIO_SELECTOR:'input[type="radio"]',SELECTED_ITEM_SELECTOR:'[aria-selected="true"], [aria-current="true"]'},Ne={UNSET_INDEX:-1,TYPEAHEAD_BUFFER_CLEAR_TIMEOUT_MS:300},Me=["input","button","textarea","select"],Pe=function(e){var t=e.target;if(t){var i=(""+t.tagName).toLowerCase();-1===Me.indexOf(i)&&e.preventDefault()}};function Be(e,t){for(var i=new Map,o=0;ot&&!i(a[r].index)){l=r;break}if(-1!==l)return o.sortedIndexCursor=l,a[o.sortedIndexCursor].index;return-1}(a,d,l,t):function(e,t,i){var o=i.typeaheadBuffer[0],n=e.get(o);if(!n)return-1;var a=n[i.sortedIndexCursor];if(0===a.text.lastIndexOf(i.typeaheadBuffer,0)&&!t(a.index))return a.index;var d=(i.sortedIndexCursor+1)%n.length,r=-1;for(;d!==i.sortedIndexCursor;){var l=n[d],s=0===l.text.lastIndexOf(i.typeaheadBuffer,0),c=!t(l.index);if(s&&c){r=d;break}d=(d+1)%n.length}if(-1!==r)return i.sortedIndexCursor=r,n[i.sortedIndexCursor].index;return-1}(a,l,t),-1===i||r||n(i),i}function He(e){return e.typeaheadBuffer.length>0} +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var Ue={LABEL_FLOAT_ABOVE:"mdc-floating-label--float-above",LABEL_REQUIRED:"mdc-floating-label--required",LABEL_SHAKE:"mdc-floating-label--shake",ROOT:"mdc-floating-label"},je=function(e){function t(i){var n=e.call(this,o(o({},t.defaultAdapter),i))||this;return n.shakeAnimationEndHandler=function(){n.handleShakeAnimationEnd()},n}return i(t,e),Object.defineProperty(t,"cssClasses",{get:function(){return Ue},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},getWidth:function(){return 0},registerInteractionHandler:function(){},deregisterInteractionHandler:function(){}}},enumerable:!1,configurable:!0}),t.prototype.init=function(){this.adapter.registerInteractionHandler("animationend",this.shakeAnimationEndHandler)},t.prototype.destroy=function(){this.adapter.deregisterInteractionHandler("animationend",this.shakeAnimationEndHandler)},t.prototype.getWidth=function(){return this.adapter.getWidth()},t.prototype.shake=function(e){var i=t.cssClasses.LABEL_SHAKE;e?this.adapter.addClass(i):this.adapter.removeClass(i)},t.prototype.float=function(e){var i=t.cssClasses,o=i.LABEL_FLOAT_ABOVE,n=i.LABEL_SHAKE;e?this.adapter.addClass(o):(this.adapter.removeClass(o),this.adapter.removeClass(n))},t.prototype.setRequired=function(e){var i=t.cssClasses.LABEL_REQUIRED;e?this.adapter.addClass(i):this.adapter.removeClass(i)},t.prototype.handleShakeAnimationEnd=function(){var e=t.cssClasses.LABEL_SHAKE;this.adapter.removeClass(e)},t}(A); +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */const Xe=r(class extends l{constructor(e){switch(super(e),this.foundation=null,this.previousPart=null,e.type){case s.ATTRIBUTE:case s.PROPERTY:break;default:throw new Error("FloatingLabel directive only support attribute and property parts")}}update(e,[t]){if(e!==this.previousPart){this.foundation&&this.foundation.destroy(),this.previousPart=e;const t=e.element;t.classList.add("mdc-floating-label");const i=(e=>({addClass:t=>e.classList.add(t),removeClass:t=>e.classList.remove(t),getWidth:()=>e.scrollWidth,registerInteractionHandler:(t,i)=>{e.addEventListener(t,i)},deregisterInteractionHandler:(t,i)=>{e.removeEventListener(t,i)}}))(t);this.foundation=new je(i),this.foundation.init()}return this.render(t)}render(e){return this.foundation}}); +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */var Ge={LINE_RIPPLE_ACTIVE:"mdc-line-ripple--active",LINE_RIPPLE_DEACTIVATING:"mdc-line-ripple--deactivating"},Ye=function(e){function t(i){var n=e.call(this,o(o({},t.defaultAdapter),i))||this;return n.transitionEndHandler=function(e){n.handleTransitionEnd(e)},n}return i(t,e),Object.defineProperty(t,"cssClasses",{get:function(){return Ge},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},hasClass:function(){return!1},setStyle:function(){},registerEventHandler:function(){},deregisterEventHandler:function(){}}},enumerable:!1,configurable:!0}),t.prototype.init=function(){this.adapter.registerEventHandler("transitionend",this.transitionEndHandler)},t.prototype.destroy=function(){this.adapter.deregisterEventHandler("transitionend",this.transitionEndHandler)},t.prototype.activate=function(){this.adapter.removeClass(Ge.LINE_RIPPLE_DEACTIVATING),this.adapter.addClass(Ge.LINE_RIPPLE_ACTIVE)},t.prototype.setRippleCenter=function(e){this.adapter.setStyle("transform-origin",e+"px center")},t.prototype.deactivate=function(){this.adapter.addClass(Ge.LINE_RIPPLE_DEACTIVATING)},t.prototype.handleTransitionEnd=function(e){var t=this.adapter.hasClass(Ge.LINE_RIPPLE_DEACTIVATING);"opacity"===e.propertyName&&t&&(this.adapter.removeClass(Ge.LINE_RIPPLE_ACTIVE),this.adapter.removeClass(Ge.LINE_RIPPLE_DEACTIVATING))},t}(A); +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */const qe=r(class extends l{constructor(e){switch(super(e),this.previousPart=null,this.foundation=null,e.type){case s.ATTRIBUTE:case s.PROPERTY:return;default:throw new Error("LineRipple only support attribute and property parts.")}}update(e,t){if(this.previousPart!==e){this.foundation&&this.foundation.destroy(),this.previousPart=e;const t=e.element;t.classList.add("mdc-line-ripple");const i=(e=>({addClass:t=>e.classList.add(t),removeClass:t=>e.classList.remove(t),hasClass:t=>e.classList.contains(t),setStyle:(t,i)=>e.style.setProperty(t,i),registerEventHandler:(t,i)=>{e.addEventListener(t,i)},deregisterEventHandler:(t,i)=>{e.removeEventListener(t,i)}}))(t);this.foundation=new Ye(i),this.foundation.init()}return this.render()}render(){return this.foundation}}); +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */var We,Ke,Qe={ANCHOR:"mdc-menu-surface--anchor",ANIMATING_CLOSED:"mdc-menu-surface--animating-closed",ANIMATING_OPEN:"mdc-menu-surface--animating-open",FIXED:"mdc-menu-surface--fixed",IS_OPEN_BELOW:"mdc-menu-surface--is-open-below",OPEN:"mdc-menu-surface--open",ROOT:"mdc-menu-surface"},Ze={CLOSED_EVENT:"MDCMenuSurface:closed",CLOSING_EVENT:"MDCMenuSurface:closing",OPENED_EVENT:"MDCMenuSurface:opened",FOCUSABLE_ELEMENTS:["button:not(:disabled)",'[href]:not([aria-disabled="true"])',"input:not(:disabled)","select:not(:disabled)","textarea:not(:disabled)",'[tabindex]:not([tabindex="-1"]):not([aria-disabled="true"])'].join(", ")},Je={TRANSITION_OPEN_DURATION:120,TRANSITION_CLOSE_DURATION:75,MARGIN_TO_EDGE:32,ANCHOR_TO_MENU_SURFACE_WIDTH_RATIO:.67,TOUCH_EVENT_WAIT_MS:30};!function(e){e[e.BOTTOM=1]="BOTTOM",e[e.CENTER=2]="CENTER",e[e.RIGHT=4]="RIGHT",e[e.FLIP_RTL=8]="FLIP_RTL"}(We||(We={})),function(e){e[e.TOP_LEFT=0]="TOP_LEFT",e[e.TOP_RIGHT=4]="TOP_RIGHT",e[e.BOTTOM_LEFT=1]="BOTTOM_LEFT",e[e.BOTTOM_RIGHT=5]="BOTTOM_RIGHT",e[e.TOP_START=8]="TOP_START",e[e.TOP_END=12]="TOP_END",e[e.BOTTOM_START=9]="BOTTOM_START",e[e.BOTTOM_END=13]="BOTTOM_END"}(Ke||(Ke={})); +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var et={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"},tt={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"},it={LABEL_SCALE:.75,UNSET_INDEX:-1,CLICK_DEBOUNCE_TIMEOUT_MS:330},ot=function(e){function t(i,n){void 0===n&&(n={});var a=e.call(this,o(o({},t.defaultAdapter),i))||this;return a.disabled=!1,a.isMenuOpen=!1,a.useDefaultValidation=!0,a.customValidity=!0,a.lastSelectedIndex=it.UNSET_INDEX,a.clickDebounceTimeout=0,a.recentlyClicked=!1,a.leadingIcon=n.leadingIcon,a.helperText=n.helperText,a}return i(t,e),Object.defineProperty(t,"cssClasses",{get:function(){return et},enumerable:!1,configurable:!0}),Object.defineProperty(t,"numbers",{get:function(){return it},enumerable:!1,configurable:!0}),Object.defineProperty(t,"strings",{get:function(){return tt},enumerable:!1,configurable:!0}),Object.defineProperty(t,"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}),t.prototype.getSelectedIndex=function(){return this.adapter.getSelectedIndex()},t.prototype.setSelectedIndex=function(e,t,i){void 0===t&&(t=!1),void 0===i&&(i=!1),e>=this.adapter.getMenuItemCount()||(e===it.UNSET_INDEX?this.adapter.setSelectedText(""):this.adapter.setSelectedText(this.adapter.getMenuItemTextAtIndex(e).trim()),this.adapter.setSelectedIndex(e),t&&this.adapter.closeMenu(),i||this.lastSelectedIndex===e||this.handleChange(),this.lastSelectedIndex=e)},t.prototype.setValue=function(e,t){void 0===t&&(t=!1);var i=this.adapter.getMenuItemValues().indexOf(e);this.setSelectedIndex(i,!1,t)},t.prototype.getValue=function(){var e=this.adapter.getSelectedIndex(),t=this.adapter.getMenuItemValues();return e!==it.UNSET_INDEX?t[e]:""},t.prototype.getDisabled=function(){return this.disabled},t.prototype.setDisabled=function(e){this.disabled=e,this.disabled?(this.adapter.addClass(et.DISABLED),this.adapter.closeMenu()):this.adapter.removeClass(et.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())},t.prototype.openMenu=function(){this.adapter.addClass(et.ACTIVATED),this.adapter.openMenu(),this.isMenuOpen=!0,this.adapter.setSelectAnchorAttr("aria-expanded","true")},t.prototype.setHelperTextContent=function(e){this.helperText&&this.helperText.setContent(e)},t.prototype.layout=function(){if(this.adapter.hasLabel()){var e=this.getValue().length>0,t=this.adapter.hasClass(et.FOCUSED),i=e||t,o=this.adapter.hasClass(et.REQUIRED);this.notchOutline(i),this.adapter.floatLabel(i),this.adapter.setLabelRequired(o)}},t.prototype.layoutOptions=function(){var e=this.adapter.getMenuItemValues().indexOf(this.getValue());this.setSelectedIndex(e,!1,!0)},t.prototype.handleMenuOpened=function(){if(0!==this.adapter.getMenuItemValues().length){var e=this.getSelectedIndex(),t=e>=0?e:0;this.adapter.focusMenuItemAtIndex(t)}},t.prototype.handleMenuClosing=function(){this.adapter.setSelectAnchorAttr("aria-expanded","false")},t.prototype.handleMenuClosed=function(){this.adapter.removeClass(et.ACTIVATED),this.isMenuOpen=!1,this.adapter.isSelectAnchorFocused()||this.blur()},t.prototype.handleChange=function(){this.layout(),this.adapter.notifyChange(this.getValue()),this.adapter.hasClass(et.REQUIRED)&&this.useDefaultValidation&&this.setValid(this.isValid())},t.prototype.handleMenuItemAction=function(e){this.setSelectedIndex(e,!0)},t.prototype.handleFocus=function(){this.adapter.addClass(et.FOCUSED),this.layout(),this.adapter.activateBottomLine()},t.prototype.handleBlur=function(){this.isMenuOpen||this.blur()},t.prototype.handleClick=function(e){this.disabled||this.recentlyClicked||(this.setClickDebounceTimeout(),this.isMenuOpen?this.adapter.closeMenu():(this.adapter.setRippleCenter(e),this.openMenu()))},t.prototype.handleKeydown=function(e){if(!this.isMenuOpen&&this.adapter.hasClass(et.FOCUSED)){var t=Ae(e)===W,i=Ae(e)===K,o=Ae(e)===ie,n=Ae(e)===ne;if(!(e.ctrlKey||e.metaKey)&&(!i&&e.key&&1===e.key.length||i&&this.adapter.isTypeaheadInProgress())){var a=i?" ":e.key,d=this.adapter.typeaheadMatchItem(a,this.getSelectedIndex());return d>=0&&this.setSelectedIndex(d),void e.preventDefault()}(t||i||o||n)&&(o&&this.getSelectedIndex()>0?this.setSelectedIndex(this.getSelectedIndex()-1):n&&this.getSelectedIndex()null!=e?e:p +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */,at=(e={})=>{const t={};for(const i in e)t[i]=e[i];return Object.assign({badInput:!1,customError:!1,patternMismatch:!1,rangeOverflow:!1,rangeUnderflow:!1,stepMismatch:!1,tooLong:!1,tooShort:!1,typeMismatch:!1,valid:!0,valueMissing:!1},t)};class dt extends z{constructor(){super(...arguments),this.mdcFoundationClass=ot,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=at()}get items(){return this.menuElement_||(this.menuElement_=this.menuElement),this.menuElement_?this.menuElement_.items:[]}get selected(){const e=this.menuElement;return e?e.selected:null}get index(){const e=this.menuElement;return e?e.index:-1}get shouldRenderHelperText(){return!!this.helper||!!this.validationMessage}get validity(){return this._checkValidity(this.value),this._validity}render(){const e={"mdc-select--disabled":this.disabled,"mdc-select--no-label":!this.label,"mdc-select--filled":!this.outlined,"mdc-select--outlined":this.outlined,"mdc-select--with-leading-icon":!!this.icon,"mdc-select--required":this.required,"mdc-select--invalid":!this.isUiValid},t={"mdc-select__menu--invalid":!this.isUiValid},i=this.label?"label":void 0,o=this.shouldRenderHelperText?"helper-text":void 0;return m` +
    + + +
    + ${this.renderRipple()} + ${this.outlined?this.renderOutline():this.renderLabel()} + ${this.renderLeadingIcon()} + + ${this.selectedText} + + + + + + + + + + ${this.renderLineRipple()} +
    + + + +
    + ${this.renderHelperText()}`}renderRipple(){return this.outlined?p:m` + + `}renderOutline(){return this.outlined?m` + + ${this.renderLabel()} + `:p}renderLabel(){return this.label?m` + ${this.label} + `:p}renderLeadingIcon(){return this.icon?m`
    ${this.icon}
    `:p}renderLineRipple(){return this.outlined?p:m` + + `}renderHelperText(){if(!this.shouldRenderHelperText)return p;const e=this.validationMessage&&!this.isUiValid;return m` +

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

    `}createAdapter(){return Object.assign(Object.assign({},R(this.mdcRoot)),{activateBottomLine:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.activate()},deactivateBottomLine:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.deactivate()},hasLabel:()=>!!this.label,floatLabel:e=>{this.labelElement&&this.labelElement.floatingLabelFoundation.float(e)},getLabelWidth:()=>this.labelElement?this.labelElement.floatingLabelFoundation.getWidth():0,setLabelRequired:e=>{this.labelElement&&this.labelElement.floatingLabelFoundation.setRequired(e)},hasOutline:()=>this.outlined,notchOutline:e=>{this.outlineElement&&!this.outlineOpen&&(this.outlineWidth=e,this.outlineOpen=!0)},closeOutline:()=>{this.outlineElement&&(this.outlineOpen=!1)},setRippleCenter:e=>{if(this.lineRippleElement){this.lineRippleElement.lineRippleFoundation.setRippleCenter(e)}},notifyChange:async e=>{if(!this.valueSetDirectly&&e===this.value)return;this.valueSetDirectly=!1,this.value=e,await this.updateComplete;const t=new Event("change",{bubbles:!0});this.dispatchEvent(t)},setSelectedText:e=>this.selectedText=e,isSelectAnchorFocused:()=>{const e=this.anchorElement;if(!e)return!1;return e.getRootNode().activeElement===e},getSelectAnchorAttr:e=>{const t=this.anchorElement;return t?t.getAttribute(e):null},setSelectAnchorAttr:(e,t)=>{const i=this.anchorElement;i&&i.setAttribute(e,t)},removeSelectAnchorAttr:e=>{const t=this.anchorElement;t&&t.removeAttribute(e)},openMenu:()=>{this.menuOpen=!0},closeMenu:()=>{this.menuOpen=!1},addMenuClass:()=>{},removeMenuClass:()=>{},getAnchorElement:()=>this.anchorElement,setMenuAnchorElement:()=>{},setMenuAnchorCorner:()=>{const e=this.menuElement;e&&(e.corner="BOTTOM_START")},setMenuWrapFocus:e=>{const t=this.menuElement;t&&(t.wrapFocus=e)},focusMenuItemAtIndex:e=>{const t=this.menuElement;if(!t)return;const i=t.items[e];i&&i.focus()},getMenuItemCount:()=>{const e=this.menuElement;return e?e.items.length:0},getMenuItemValues:()=>{const e=this.menuElement;if(!e)return[];return e.items.map((e=>e.value))},getMenuItemTextAtIndex:e=>{const t=this.menuElement;if(!t)return"";const i=t.items[e];return i?i.text:""},getSelectedIndex:()=>this.index,setSelectedIndex:()=>{},isTypeaheadInProgress:()=>He(this.typeaheadState),typeaheadMatchItem:(e,t)=>{if(!this.menuElement)return-1;const i={focusItemAtIndex:e=>{this.menuElement.focusItemAtIndex(e)},focusedItemIndex:t||this.menuElement.getFocusedItemIndex(),nextChar:e,sortedIndexByFirstChar:this.sortedIndexByFirstChar,skipFocus:!1,isItemAtIndexDisabled:e=>this.items[e].disabled},o=ze(i,this.typeaheadState);return-1!==o&&this.select(o),o}})}checkValidity(){const e=this._checkValidity(this.value);if(!e){const e=new Event("invalid",{bubbles:!1,cancelable:!0});this.dispatchEvent(e)}return e}reportValidity(){const e=this.checkValidity();return this.isUiValid=e,e}_checkValidity(e){const t=this.formElement.validity;let i=at(t);if(this.validityTransform){const t=this.validityTransform(e,i);i=Object.assign(Object.assign({},i),t)}return this._validity=i,this._validity.valid}setCustomValidity(e){this.validationMessage=e,this.formElement.setCustomValidity(e)}async getUpdateComplete(){await this._menuUpdateComplete;return await super.getUpdateComplete()}async firstUpdated(){const e=this.menuElement;if(e&&(this._menuUpdateComplete=e.updateComplete,await this._menuUpdateComplete),super.firstUpdated(),this.mdcFoundation.isValid=()=>!0,this.mdcFoundation.setValid=()=>{},this.mdcFoundation.setDisabled(this.disabled),this.validateOnInitialRender&&this.reportValidity(),!this.selected){!this.items.length&&this.slotElement&&this.slotElement.assignedNodes({flatten:!0}).length&&(await new Promise((e=>requestAnimationFrame(e))),await this.layout());const e=this.items.length&&""===this.items[0].value;if(!this.value&&e)return void this.select(0);this.selectByValue(this.value)}this.sortedIndexByFirstChar=Be(this.items.length,(e=>this.items[e].text))}onItemsUpdated(){this.sortedIndexByFirstChar=Be(this.items.length,(e=>this.items[e].text))}select(e){const t=this.menuElement;t&&t.select(e)}selectByValue(e){let t=-1;for(let i=0;i0,n=i&&this.index{this.menuElement.focusItemAtIndex(e)},focusedItemIndex:t,isTargetListItem:!!i&&i.hasAttribute("mwc-list-item"),sortedIndexByFirstChar:this.sortedIndexByFirstChar,isItemAtIndexDisabled:e=>this.items[e].disabled},this.typeaheadState)}async onSelected(e){this.mdcFoundation||await this.updateComplete,this.mdcFoundation.handleMenuItemAction(e.detail.index);const t=this.items[e.detail.index];t&&(this.value=t.value)}onOpened(){this.mdcFoundation&&(this.menuOpen=!0,this.mdcFoundation.handleMenuOpened())}onClosed(){this.mdcFoundation&&(this.menuOpen=!1,this.mdcFoundation.handleMenuClosed())}setFormData(e){this.name&&null!==this.selected&&e.append(this.name,this.value)}async layout(e=!0){this.mdcFoundation&&this.mdcFoundation.layout(),await this.updateComplete;const t=this.menuElement;t&&t.layout(e);const i=this.labelElement;if(!i)return void(this.outlineOpen=!1);const 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()}}a([b(".mdc-select")],dt.prototype,"mdcRoot",void 0),a([b(".formElement")],dt.prototype,"formElement",void 0),a([b("slot")],dt.prototype,"slotElement",void 0),a([b("select")],dt.prototype,"nativeSelectElement",void 0),a([b("input")],dt.prototype,"nativeInputElement",void 0),a([b(".mdc-line-ripple")],dt.prototype,"lineRippleElement",void 0),a([b(".mdc-floating-label")],dt.prototype,"labelElement",void 0),a([b("mwc-notched-outline")],dt.prototype,"outlineElement",void 0),a([b(".mdc-menu")],dt.prototype,"menuElement",void 0),a([b(".mdc-select__anchor")],dt.prototype,"anchorElement",void 0),a([d({type:Boolean,attribute:"disabled",reflect:!0}),H((function(e){this.mdcFoundation&&this.mdcFoundation.setDisabled(e)}))],dt.prototype,"disabled",void 0),a([d({type:Boolean}),H((function(e,t){void 0!==t&&this.outlined!==t&&this.layout(!1)}))],dt.prototype,"outlined",void 0),a([d({type:String}),H((function(e,t){void 0!==t&&this.label!==t&&this.layout(!1)}))],dt.prototype,"label",void 0),a([u()],dt.prototype,"outlineOpen",void 0),a([u()],dt.prototype,"outlineWidth",void 0),a([d({type:String}),H((function(e){if(this.mdcFoundation){const t=null===this.selected&&!!e,i=this.selected&&this.selected.value!==e;(t||i)&&this.selectByValue(e),this.reportValidity()}}))],dt.prototype,"value",void 0),a([d()],dt.prototype,"name",void 0),a([u()],dt.prototype,"selectedText",void 0),a([d({type:String})],dt.prototype,"icon",void 0),a([u()],dt.prototype,"menuOpen",void 0),a([d({type:String})],dt.prototype,"helper",void 0),a([d({type:Boolean})],dt.prototype,"validateOnInitialRender",void 0),a([d({type:String})],dt.prototype,"validationMessage",void 0),a([d({type:Boolean})],dt.prototype,"required",void 0),a([d({type:Boolean})],dt.prototype,"naturalMenuWidth",void 0),a([u()],dt.prototype,"isUiValid",void 0),a([d({type:Boolean})],dt.prototype,"fixedMenuPosition",void 0),a([v({capture:!0})],dt.prototype,"handleTypeahead",null); +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +const rt=(e,t)=>e-t,lt=["input","button","textarea","select"];function st(e){return e instanceof Set}const ct=e=>{const t=e===Ne.UNSET_INDEX?new Set:e;return st(t)?new Set(t):new Set([t])};class mt extends A{constructor(e){super(Object.assign(Object.assign({},mt.defaultAdapter),e)),this.isMulti_=!1,this.wrapFocus_=!1,this.isVertical_=!0,this.selectedIndex_=Ne.UNSET_INDEX,this.focusedItemIndex_=Ne.UNSET_INDEX,this.useActivatedClass_=!1,this.ariaCurrentAttrValue_=null}static get strings(){return Ve}static get numbers(){return Ne}static get defaultAdapter(){return{focusItemAtIndex:()=>{},getFocusedElementIndex:()=>0,getListItemCount:()=>0,isFocusInsideList:()=>!1,isRootFocused:()=>!1,notifyAction:()=>{},notifySelected:()=>{},getSelectedStateForElementIndex:()=>!1,setDisabledStateForElementIndex:()=>{},getDisabledStateForElementIndex:()=>!1,setSelectedStateForElementIndex:()=>{},setActivatedStateForElementIndex:()=>{},setTabIndexForElementIndex:()=>{},setAttributeForElementIndex:()=>{},getAttributeForElementIndex:()=>null}}setWrapFocus(e){this.wrapFocus_=e}setMulti(e){this.isMulti_=e;const t=this.selectedIndex_;if(e){if(!st(t)){const e=t===Ne.UNSET_INDEX;this.selectedIndex_=e?new Set:new Set([t])}}else if(st(t))if(t.size){const e=Array.from(t).sort(rt);this.selectedIndex_=e[0]}else this.selectedIndex_=Ne.UNSET_INDEX}setVerticalOrientation(e){this.isVertical_=e}setUseActivatedClass(e){this.useActivatedClass_=e}getSelectedIndex(){return this.selectedIndex_}setSelectedIndex(e){this.isIndexValid_(e)&&(this.isMulti_?this.setMultiSelectionAtIndex_(ct(e)):this.setSingleSelectionAtIndex_(e))}handleFocusIn(e,t){t>=0&&this.adapter.setTabIndexForElementIndex(t,0)}handleFocusOut(e,t){t>=0&&this.adapter.setTabIndexForElementIndex(t,-1),setTimeout((()=>{this.adapter.isFocusInsideList()||this.setTabindexToFirstSelectedItem_()}),0)}handleKeydown(e,t,i){const o="ArrowLeft"===Ae(e),n="ArrowUp"===Ae(e),a="ArrowRight"===Ae(e),d="ArrowDown"===Ae(e),r="Home"===Ae(e),l="End"===Ae(e),s="Enter"===Ae(e),c="Spacebar"===Ae(e);if(this.adapter.isRootFocused())return void(n||l?(e.preventDefault(),this.focusLastElement()):(d||r)&&(e.preventDefault(),this.focusFirstElement()));let m,h=this.adapter.getFocusedElementIndex();if(!(-1===h&&(h=i,h<0))){if(this.isVertical_&&d||!this.isVertical_&&a)this.preventDefaultEvent(e),m=this.focusNextElement(h);else if(this.isVertical_&&n||!this.isVertical_&&o)this.preventDefaultEvent(e),m=this.focusPrevElement(h);else if(r)this.preventDefaultEvent(e),m=this.focusFirstElement();else if(l)this.preventDefaultEvent(e),m=this.focusLastElement();else if((s||c)&&t){const t=e.target;if(t&&"A"===t.tagName&&s)return;this.preventDefaultEvent(e),this.setSelectedIndexOnAction_(h,!0)}this.focusedItemIndex_=h,void 0!==m&&(this.setTabindexAtIndex_(m),this.focusedItemIndex_=m)}}handleSingleSelection(e,t,i){e!==Ne.UNSET_INDEX&&(this.setSelectedIndexOnAction_(e,t,i),this.setTabindexAtIndex_(e),this.focusedItemIndex_=e)}focusNextElement(e){let t=e+1;if(t>=this.adapter.getListItemCount()){if(!this.wrapFocus_)return e;t=0}return this.adapter.focusItemAtIndex(t),t}focusPrevElement(e){let t=e-1;if(t<0){if(!this.wrapFocus_)return e;t=this.adapter.getListItemCount()-1}return this.adapter.focusItemAtIndex(t),t}focusFirstElement(){return this.adapter.focusItemAtIndex(0),0}focusLastElement(){const e=this.adapter.getListItemCount()-1;return this.adapter.focusItemAtIndex(e),e}setEnabled(e,t){this.isIndexValid_(e)&&this.adapter.setDisabledStateForElementIndex(e,!t)}preventDefaultEvent(e){const t=`${e.target.tagName}`.toLowerCase();-1===lt.indexOf(t)&&e.preventDefault()}setSingleSelectionAtIndex_(e,t=!0){this.selectedIndex_!==e&&(this.selectedIndex_!==Ne.UNSET_INDEX&&(this.adapter.setSelectedStateForElementIndex(this.selectedIndex_,!1),this.useActivatedClass_&&this.adapter.setActivatedStateForElementIndex(this.selectedIndex_,!1)),t&&this.adapter.setSelectedStateForElementIndex(e,!0),this.useActivatedClass_&&this.adapter.setActivatedStateForElementIndex(e,!0),this.setAriaForSingleSelectionAtIndex_(e),this.selectedIndex_=e,this.adapter.notifySelected(e))}setMultiSelectionAtIndex_(e,t=!0){const i=((e,t)=>{const i=Array.from(e),o=Array.from(t),n={added:[],removed:[]},a=i.sort(rt),d=o.sort(rt);let r=0,l=0;for(;r=0&&this.focusedItemIndex_!==e&&this.adapter.setTabIndexForElementIndex(this.focusedItemIndex_,-1),this.adapter.setTabIndexForElementIndex(e,0)}setTabindexToFirstSelectedItem_(){let e=0;"number"==typeof this.selectedIndex_&&this.selectedIndex_!==Ne.UNSET_INDEX?e=this.selectedIndex_:st(this.selectedIndex_)&&this.selectedIndex_.size>0&&(e=Math.min(...this.selectedIndex_)),this.setTabindexAtIndex_(e)}isIndexValid_(e){if(e instanceof Set){if(!this.isMulti_)throw new Error("MDCListFoundation: Array of index is only supported for checkbox based list");if(0===e.size)return!0;{let t=!1;for(const i of e)if(t=this.isIndexInRange_(i),t)break;return t}}if("number"==typeof e){if(this.isMulti_)throw new Error("MDCListFoundation: Expected array of index for checkbox based list but got number: "+e);return e===Ne.UNSET_INDEX||this.isIndexInRange_(e)}return!1}isIndexInRange_(e){const t=this.adapter.getListItemCount();return e>=0&&ee.hasAttribute("mwc-list-item");function pt(){const e=this.itemsReadyResolver;this.itemsReady=new Promise((e=>this.itemsReadyResolver=e)),e()}class ut extends N{constructor(){super(),this.mdcAdapter=null,this.mdcFoundationClass=mt,this.activatable=!1,this.multi=!1,this.wrapFocus=!1,this.itemRoles=null,this.innerRole=null,this.innerAriaLabel=null,this.rootTabbable=!1,this.previousTabindex=null,this.noninteractive=!1,this.itemsReadyResolver=()=>{},this.itemsReady=Promise.resolve([]),this.items_=[];const e=function(e,t=50){let i;return function(o=!0){clearTimeout(i),i=setTimeout((()=>{e(o)}),t)}}(this.layout.bind(this));this.debouncedLayout=(t=!0)=>{pt.call(this),e(t)}}async getUpdateComplete(){const e=await super.getUpdateComplete();return await this.itemsReady,e}get items(){return this.items_}updateItems(){var e;const t=null!==(e=this.assignedElements)&&void 0!==e?e:[],i=[];for(const e of t)ht(e)&&(i.push(e),e._managingList=this),e.hasAttribute("divider")&&!e.hasAttribute("role")&&e.setAttribute("role","separator");this.items_=i;const o=new Set;if(this.items_.forEach(((e,t)=>{this.itemRoles?e.setAttribute("role",this.itemRoles):e.removeAttribute("role"),e.selected&&o.add(t)})),this.multi)this.select(o);else{const e=o.size?o.entries().next().value[1]:-1;this.select(e)}const n=new Event("items-updated",{bubbles:!0,composed:!0});this.dispatchEvent(n)}get selected(){const e=this.index;if(!st(e))return-1===e?null:this.items[e];const t=[];for(const i of e)t.push(this.items[i]);return t}get index(){return this.mdcFoundation?this.mdcFoundation.getSelectedIndex():-1}render(){const e=null===this.innerRole?void 0:this.innerRole,t=null===this.innerAriaLabel?void 0:this.innerAriaLabel,i=this.rootTabbable?"0":"-1";return m` + +
      + + ${this.renderPlaceholder()} +
    + `}renderPlaceholder(){var e;const t=null!==(e=this.assignedElements)&&void 0!==e?e:[];return void 0!==this.emptyMessage&&0===t.length?m` + ${this.emptyMessage} + `:null}firstUpdated(){super.firstUpdated(),this.items.length||(this.mdcFoundation.setMulti(this.multi),this.layout())}onFocusIn(e){if(this.mdcFoundation&&this.mdcRoot){const t=this.getIndexOfTarget(e);this.mdcFoundation.handleFocusIn(e,t)}}onFocusOut(e){if(this.mdcFoundation&&this.mdcRoot){const t=this.getIndexOfTarget(e);this.mdcFoundation.handleFocusOut(e,t)}}onKeydown(e){if(this.mdcFoundation&&this.mdcRoot){const t=this.getIndexOfTarget(e),i=e.target,o=ht(i);this.mdcFoundation.handleKeydown(e,o,t)}}onRequestSelected(e){if(this.mdcFoundation){let t=this.getIndexOfTarget(e);if(-1===t&&(this.layout(),t=this.getIndexOfTarget(e),-1===t))return;if(this.items[t].disabled)return;const i=e.detail.selected,o=e.detail.source;this.mdcFoundation.handleSingleSelection(t,"interaction"===o,i),e.stopPropagation()}}getIndexOfTarget(e){const t=this.items,i=e.composedPath();for(const e of i){let i=-1;if(S(e)&&ht(e)&&(i=t.indexOf(e)),-1!==i)return i}return-1}createAdapter(){return this.mdcAdapter={getListItemCount:()=>this.mdcRoot?this.items.length:0,getFocusedElementIndex:this.getFocusedItemIndex,getAttributeForElementIndex:(e,t)=>{if(!this.mdcRoot)return"";const i=this.items[e];return i?i.getAttribute(t):""},setAttributeForElementIndex:(e,t,i)=>{if(!this.mdcRoot)return;const o=this.items[e];o&&o.setAttribute(t,i)},focusItemAtIndex:e=>{const t=this.items[e];t&&t.focus()},setTabIndexForElementIndex:(e,t)=>{const i=this.items[e];i&&(i.tabindex=t)},notifyAction:e=>{const t={bubbles:!0,composed:!0};t.detail={index:e};const i=new CustomEvent("action",t);this.dispatchEvent(i)},notifySelected:(e,t)=>{const i={bubbles:!0,composed:!0};i.detail={index:e,diff:t};const o=new CustomEvent("selected",i);this.dispatchEvent(o)},isFocusInsideList:()=>V(this),isRootFocused:()=>{const e=this.mdcRoot;return e.getRootNode().activeElement===e},setDisabledStateForElementIndex:(e,t)=>{const i=this.items[e];i&&(i.disabled=t)},getDisabledStateForElementIndex:e=>{const t=this.items[e];return!!t&&t.disabled},setSelectedStateForElementIndex:(e,t)=>{const i=this.items[e];i&&(i.selected=t)},getSelectedStateForElementIndex:e=>{const t=this.items[e];return!!t&&t.selected},setActivatedStateForElementIndex:(e,t)=>{const i=this.items[e];i&&(i.activated=t)}},this.mdcAdapter}selectUi(e,t=!1){const i=this.items[e];i&&(i.selected=!0,i.activated=t)}deselectUi(e){const t=this.items[e];t&&(t.selected=!1,t.activated=!1)}select(e){this.mdcFoundation&&this.mdcFoundation.setSelectedIndex(e)}toggle(e,t){this.multi&&this.mdcFoundation.toggleMultiAtIndex(e,t)}onListItemConnected(e){const t=e.target;this.layout(-1===this.items.indexOf(t))}layout(e=!0){e&&this.updateItems();const t=this.items[0];for(const e of this.items)e.tabindex=-1;t&&(this.noninteractive?this.previousTabindex||(this.previousTabindex=t):t.tabindex=0),this.itemsReadyResolver()}getFocusedItemIndex(){if(!this.mdcRoot)return-1;if(!this.items.length)return-1;const e=D();if(!e.length)return-1;for(let t=e.length-1;t>=0;t--){const i=e[t];if(ht(i))return this.items.indexOf(i)}return-1}focusItemAtIndex(e){for(const e of this.items)if(0===e.tabindex){e.tabindex=-1;break}this.items[e].tabindex=0,this.items[e].focus()}focus(){const e=this.mdcRoot;e&&e.focus()}blur(){const e=this.mdcRoot;e&&e.blur()}}a([d({type:String})],ut.prototype,"emptyMessage",void 0),a([b(".mdc-deprecated-list")],ut.prototype,"mdcRoot",void 0),a([x("",!0,"*")],ut.prototype,"assignedElements",void 0),a([x("",!0,'[tabindex="0"]')],ut.prototype,"tabbableElements",void 0),a([d({type:Boolean}),H((function(e){this.mdcFoundation&&this.mdcFoundation.setUseActivatedClass(e)}))],ut.prototype,"activatable",void 0),a([d({type:Boolean}),H((function(e,t){this.mdcFoundation&&this.mdcFoundation.setMulti(e),void 0!==t&&this.layout()}))],ut.prototype,"multi",void 0),a([d({type:Boolean}),H((function(e){this.mdcFoundation&&this.mdcFoundation.setWrapFocus(e)}))],ut.prototype,"wrapFocus",void 0),a([d({type:String}),H((function(e,t){void 0!==t&&this.updateItems()}))],ut.prototype,"itemRoles",void 0),a([d({type:String})],ut.prototype,"innerRole",void 0),a([d({type:String})],ut.prototype,"innerAriaLabel",void 0),a([d({type:Boolean})],ut.prototype,"rootTabbable",void 0),a([d({type:Boolean,reflect:!0}),H((function(e){var t,i;if(e){const e=null!==(i=null===(t=this.tabbableElements)||void 0===t?void 0:t[0])&&void 0!==i?i:null;this.previousTabindex=e,e&&e.setAttribute("tabindex","-1")}else!e&&this.previousTabindex&&(this.previousTabindex.setAttribute("tabindex","0"),this.previousTabindex=null)}))],ut.prototype,"noninteractive",void 0); +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +class ft{constructor(e){this.startPress=t=>{e().then((e=>{e&&e.startPress(t)}))},this.endPress=()=>{e().then((e=>{e&&e.endPress()}))},this.startFocus=()=>{e().then((e=>{e&&e.startFocus()}))},this.endFocus=()=>{e().then((e=>{e&&e.endFocus()}))},this.startHover=()=>{e().then((e=>{e&&e.startHover()}))},this.endHover=()=>{e().then((e=>{e&&e.endHover()}))}}} +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */class _t extends n{constructor(){super(...arguments),this.value="",this.group=null,this.tabindex=-1,this.disabled=!1,this.twoline=!1,this.activated=!1,this.graphic=null,this.multipleGraphics=!1,this.hasMeta=!1,this.noninteractive=!1,this.selected=!1,this.shouldRenderRipple=!1,this._managingList=null,this.boundOnClick=this.onClick.bind(this),this._firstChanged=!0,this._skipPropRequest=!1,this.rippleHandlers=new ft((()=>(this.shouldRenderRipple=!0,this.ripple))),this.listeners=[{target:this,eventNames:["click"],cb:()=>{this.onClick()}},{target:this,eventNames:["mouseenter"],cb:this.rippleHandlers.startHover},{target:this,eventNames:["mouseleave"],cb:this.rippleHandlers.endHover},{target:this,eventNames:["focus"],cb:this.rippleHandlers.startFocus},{target:this,eventNames:["blur"],cb:this.rippleHandlers.endFocus},{target:this,eventNames:["mousedown","touchstart"],cb:e=>{const t=e.type;this.onDown("mousedown"===t?"mouseup":"touchend",e)}}]}get text(){const e=this.textContent;return e?e.trim():""}render(){const e=this.renderText(),t=this.graphic?this.renderGraphic():m``,i=this.hasMeta?this.renderMeta():m``;return m` + ${this.renderRipple()} + ${t} + ${e} + ${i}`}renderRipple(){return this.shouldRenderRipple?m` + + `:this.activated?m`
    `:""}renderGraphic(){const e={multi:this.multipleGraphics};return m` + + + `}renderMeta(){return m` + + + `}renderText(){const e=this.twoline?this.renderTwoline():this.renderSingleLine();return m` + + ${e} + `}renderSingleLine(){return m``}renderTwoline(){return m` + + + + + + + `}onClick(){this.fireRequestSelected(!this.selected,"interaction")}onDown(e,t){const i=()=>{window.removeEventListener(e,i),this.rippleHandlers.endPress()};window.addEventListener(e,i),this.rippleHandlers.startPress(t)}fireRequestSelected(e,t){if(this.noninteractive)return;const i=new CustomEvent("request-selected",{bubbles:!0,composed:!0,detail:{source:t,selected:e}});this.dispatchEvent(i)}connectedCallback(){super.connectedCallback(),this.noninteractive||this.setAttribute("mwc-list-item","");for(const e of this.listeners)for(const t of e.eventNames)e.target.addEventListener(t,e.cb,{passive:!0})}disconnectedCallback(){super.disconnectedCallback();for(const e of this.listeners)for(const t of e.eventNames)e.target.removeEventListener(t,e.cb);this._managingList&&(this._managingList.debouncedLayout?this._managingList.debouncedLayout(!0):this._managingList.layout(!0))}firstUpdated(){const e=new Event("list-item-rendered",{bubbles:!0,composed:!0});this.dispatchEvent(e)}}a([b("slot")],_t.prototype,"slotElement",void 0),a([y("mwc-ripple")],_t.prototype,"ripple",void 0),a([d({type:String})],_t.prototype,"value",void 0),a([d({type:String,reflect:!0})],_t.prototype,"group",void 0),a([d({type:Number,reflect:!0})],_t.prototype,"tabindex",void 0),a([d({type:Boolean,reflect:!0}),H((function(e){e?this.setAttribute("aria-disabled","true"):this.setAttribute("aria-disabled","false")}))],_t.prototype,"disabled",void 0),a([d({type:Boolean,reflect:!0})],_t.prototype,"twoline",void 0),a([d({type:Boolean,reflect:!0})],_t.prototype,"activated",void 0),a([d({type:String,reflect:!0})],_t.prototype,"graphic",void 0),a([d({type:Boolean})],_t.prototype,"multipleGraphics",void 0),a([d({type:Boolean})],_t.prototype,"hasMeta",void 0),a([d({type:Boolean,reflect:!0}),H((function(e){e?(this.removeAttribute("aria-checked"),this.removeAttribute("mwc-list-item"),this.selected=!1,this.activated=!1,this.tabIndex=-1):this.setAttribute("mwc-list-item","")}))],_t.prototype,"noninteractive",void 0),a([d({type:Boolean,reflect:!0}),H((function(e){const t=this.getAttribute("role"),i="gridcell"===t||"option"===t||"row"===t||"tab"===t;i&&e?this.setAttribute("aria-selected","true"):i&&this.setAttribute("aria-selected","false"),this._firstChanged?this._firstChanged=!1:this._skipPropRequest||this.fireRequestSelected(e,"property")}))],_t.prototype,"selected",void 0),a([u()],_t.prototype,"shouldRenderRipple",void 0),a([u()],_t.prototype,"_managingList",void 0); +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var gt,vt={MENU_SELECTED_LIST_ITEM:"mdc-menu-item--selected",MENU_SELECTION_GROUP:"mdc-menu__selection-group",ROOT:"mdc-menu"},bt={ARIA_CHECKED_ATTR:"aria-checked",ARIA_DISABLED_ATTR:"aria-disabled",CHECKBOX_SELECTOR:'input[type="checkbox"]',LIST_SELECTOR:".mdc-list,.mdc-deprecated-list",SELECTED_EVENT:"MDCMenu:selected",SKIP_RESTORE_FOCUS:"data-menu-item-skip-restore-focus"},yt={FOCUS_ROOT_INDEX:-1};!function(e){e[e.NONE=0]="NONE",e[e.LIST_ROOT=1]="LIST_ROOT",e[e.FIRST_ITEM=2]="FIRST_ITEM",e[e.LAST_ITEM=3]="LAST_ITEM"}(gt||(gt={})); +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var xt=function(e){function t(i){var n=e.call(this,o(o({},t.defaultAdapter),i))||this;return n.isSurfaceOpen=!1,n.isQuickOpen=!1,n.isHoistedElement=!1,n.isFixedPosition=!1,n.isHorizontallyCenteredOnViewport=!1,n.maxHeight=0,n.openBottomBias=0,n.openAnimationEndTimerId=0,n.closeAnimationEndTimerId=0,n.animationRequestId=0,n.anchorCorner=Ke.TOP_START,n.originCorner=Ke.TOP_START,n.anchorMargin={top:0,right:0,bottom:0,left:0},n.position={x:0,y:0},n}return i(t,e),Object.defineProperty(t,"cssClasses",{get:function(){return Qe},enumerable:!1,configurable:!0}),Object.defineProperty(t,"strings",{get:function(){return Ze},enumerable:!1,configurable:!0}),Object.defineProperty(t,"numbers",{get:function(){return Je},enumerable:!1,configurable:!0}),Object.defineProperty(t,"Corner",{get:function(){return Ke},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},hasClass:function(){return!1},hasAnchor:function(){return!1},isElementInContainer:function(){return!1},isFocused:function(){return!1},isRtl:function(){return!1},getInnerDimensions:function(){return{height:0,width:0}},getAnchorDimensions:function(){return null},getWindowDimensions:function(){return{height:0,width:0}},getBodyDimensions:function(){return{height:0,width:0}},getWindowScroll:function(){return{x:0,y:0}},setPosition:function(){},setMaxHeight:function(){},setTransformOrigin:function(){},saveFocus:function(){},restoreFocus:function(){},notifyClose:function(){},notifyOpen:function(){},notifyClosing:function(){}}},enumerable:!1,configurable:!0}),t.prototype.init=function(){var e=t.cssClasses,i=e.ROOT,o=e.OPEN;if(!this.adapter.hasClass(i))throw new Error(i+" class required in root element.");this.adapter.hasClass(o)&&(this.isSurfaceOpen=!0)},t.prototype.destroy=function(){clearTimeout(this.openAnimationEndTimerId),clearTimeout(this.closeAnimationEndTimerId),cancelAnimationFrame(this.animationRequestId)},t.prototype.setAnchorCorner=function(e){this.anchorCorner=e},t.prototype.flipCornerHorizontally=function(){this.originCorner=this.originCorner^We.RIGHT},t.prototype.setAnchorMargin=function(e){this.anchorMargin.top=e.top||0,this.anchorMargin.right=e.right||0,this.anchorMargin.bottom=e.bottom||0,this.anchorMargin.left=e.left||0},t.prototype.setIsHoisted=function(e){this.isHoistedElement=e},t.prototype.setFixedPosition=function(e){this.isFixedPosition=e},t.prototype.isFixed=function(){return this.isFixedPosition},t.prototype.setAbsolutePosition=function(e,t){this.position.x=this.isFinite(e)?e:0,this.position.y=this.isFinite(t)?t:0},t.prototype.setIsHorizontallyCenteredOnViewport=function(e){this.isHorizontallyCenteredOnViewport=e},t.prototype.setQuickOpen=function(e){this.isQuickOpen=e},t.prototype.setMaxHeight=function(e){this.maxHeight=e},t.prototype.setOpenBottomBias=function(e){this.openBottomBias=e},t.prototype.isOpen=function(){return this.isSurfaceOpen},t.prototype.open=function(){var e=this;this.isSurfaceOpen||(this.adapter.saveFocus(),this.isQuickOpen?(this.isSurfaceOpen=!0,this.adapter.addClass(t.cssClasses.OPEN),this.dimensions=this.adapter.getInnerDimensions(),this.autoposition(),this.adapter.notifyOpen()):(this.adapter.addClass(t.cssClasses.ANIMATING_OPEN),this.animationRequestId=requestAnimationFrame((function(){e.dimensions=e.adapter.getInnerDimensions(),e.autoposition(),e.adapter.addClass(t.cssClasses.OPEN),e.openAnimationEndTimerId=setTimeout((function(){e.openAnimationEndTimerId=0,e.adapter.removeClass(t.cssClasses.ANIMATING_OPEN),e.adapter.notifyOpen()}),Je.TRANSITION_OPEN_DURATION)})),this.isSurfaceOpen=!0))},t.prototype.close=function(e){var i=this;if(void 0===e&&(e=!1),this.isSurfaceOpen){if(this.adapter.notifyClosing(),this.isQuickOpen)return this.isSurfaceOpen=!1,e||this.maybeRestoreFocus(),this.adapter.removeClass(t.cssClasses.OPEN),this.adapter.removeClass(t.cssClasses.IS_OPEN_BELOW),void this.adapter.notifyClose();this.adapter.addClass(t.cssClasses.ANIMATING_CLOSED),requestAnimationFrame((function(){i.adapter.removeClass(t.cssClasses.OPEN),i.adapter.removeClass(t.cssClasses.IS_OPEN_BELOW),i.closeAnimationEndTimerId=setTimeout((function(){i.closeAnimationEndTimerId=0,i.adapter.removeClass(t.cssClasses.ANIMATING_CLOSED),i.adapter.notifyClose()}),Je.TRANSITION_CLOSE_DURATION)})),this.isSurfaceOpen=!1,e||this.maybeRestoreFocus()}},t.prototype.handleBodyClick=function(e){var t=e.target;this.adapter.isElementInContainer(t)||this.close()},t.prototype.handleKeydown=function(e){var t=e.keyCode;("Escape"===e.key||27===t)&&this.close()},t.prototype.autoposition=function(){var e;this.measurements=this.getAutoLayoutmeasurements();var i=this.getoriginCorner(),o=this.getMenuSurfaceMaxHeight(i),n=this.hasBit(i,We.BOTTOM)?"bottom":"top",a=this.hasBit(i,We.RIGHT)?"right":"left",d=this.getHorizontalOriginOffset(i),r=this.getVerticalOriginOffset(i),l=this.measurements,s=l.anchorSize,c=l.surfaceSize,m=((e={})[a]=d,e[n]=r,e);s.width/c.width>Je.ANCHOR_TO_MENU_SURFACE_WIDTH_RATIO&&(a="center"),(this.isHoistedElement||this.isFixedPosition)&&this.adjustPositionForHoistedElement(m),this.adapter.setTransformOrigin(a+" "+n),this.adapter.setPosition(m),this.adapter.setMaxHeight(o?o+"px":""),this.hasBit(i,We.BOTTOM)||this.adapter.addClass(t.cssClasses.IS_OPEN_BELOW)},t.prototype.getAutoLayoutmeasurements=function(){var e=this.adapter.getAnchorDimensions(),t=this.adapter.getBodyDimensions(),i=this.adapter.getWindowDimensions(),o=this.adapter.getWindowScroll();return e||(e={top:this.position.y,right:this.position.x,bottom:this.position.y,left:this.position.x,width:0,height:0}),{anchorSize:e,bodySize:t,surfaceSize:this.dimensions,viewportDistance:{top:e.top,right:i.width-e.right,bottom:i.height-e.bottom,left:e.left},viewportSize:i,windowScroll:o}},t.prototype.getoriginCorner=function(){var e,i,o=this.originCorner,n=this.measurements,a=n.viewportDistance,d=n.anchorSize,r=n.surfaceSize,l=t.numbers.MARGIN_TO_EDGE;this.hasBit(this.anchorCorner,We.BOTTOM)?(e=a.top-l+this.anchorMargin.bottom,i=a.bottom-l-this.anchorMargin.bottom):(e=a.top-l+this.anchorMargin.top,i=a.bottom-l+d.height-this.anchorMargin.top),!(i-r.height>0)&&e>i+this.openBottomBias&&(o=this.setBit(o,We.BOTTOM));var s,c,m=this.adapter.isRtl(),h=this.hasBit(this.anchorCorner,We.FLIP_RTL),p=this.hasBit(this.anchorCorner,We.RIGHT)||this.hasBit(o,We.RIGHT),u=!1;(u=m&&h?!p:p)?(s=a.left+d.width+this.anchorMargin.right,c=a.right-this.anchorMargin.right):(s=a.left+this.anchorMargin.left,c=a.right+d.width-this.anchorMargin.left);var f=s-r.width>0,_=c-r.width>0,g=this.hasBit(o,We.FLIP_RTL)&&this.hasBit(o,We.RIGHT);return _&&g&&m||!f&&g?o=this.unsetBit(o,We.RIGHT):(f&&u&&m||f&&!u&&p||!_&&s>=c)&&(o=this.setBit(o,We.RIGHT)),o},t.prototype.getMenuSurfaceMaxHeight=function(e){if(this.maxHeight>0)return this.maxHeight;var i=this.measurements.viewportDistance,o=0,n=this.hasBit(e,We.BOTTOM),a=this.hasBit(this.anchorCorner,We.BOTTOM),d=t.numbers.MARGIN_TO_EDGE;return n?(o=i.top+this.anchorMargin.top-d,a||(o+=this.measurements.anchorSize.height)):(o=i.bottom-this.anchorMargin.bottom+this.measurements.anchorSize.height-d,a&&(o-=this.measurements.anchorSize.height)),o},t.prototype.getHorizontalOriginOffset=function(e){var t=this.measurements.anchorSize,i=this.hasBit(e,We.RIGHT),o=this.hasBit(this.anchorCorner,We.RIGHT);if(i){var n=o?t.width-this.anchorMargin.left:this.anchorMargin.right;return this.isHoistedElement||this.isFixedPosition?n-(this.measurements.viewportSize.width-this.measurements.bodySize.width):n}return o?t.width-this.anchorMargin.right:this.anchorMargin.left},t.prototype.getVerticalOriginOffset=function(e){var t=this.measurements.anchorSize,i=this.hasBit(e,We.BOTTOM),o=this.hasBit(this.anchorCorner,We.BOTTOM);return i?o?t.height-this.anchorMargin.top:-this.anchorMargin.bottom:o?t.height+this.anchorMargin.bottom:this.anchorMargin.top},t.prototype.adjustPositionForHoistedElement=function(e){var t,i,o=this.measurements,n=o.windowScroll,a=o.viewportDistance,d=o.surfaceSize,r=o.viewportSize,l=Object.keys(e);try{for(var s=f(l),c=s.next();!c.done;c=s.next()){var m=c.value,h=e[m]||0;!this.isHorizontallyCenteredOnViewport||"left"!==m&&"right"!==m?(h+=a[m],this.isFixedPosition||("top"===m?h+=n.y:"bottom"===m?h-=n.y:"left"===m?h+=n.x:h-=n.x),e[m]=h):e[m]=(r.width-d.width)/2}}catch(e){t={error:e}}finally{try{c&&!c.done&&(i=s.return)&&i.call(s)}finally{if(t)throw t.error}}},t.prototype.maybeRestoreFocus=function(){var e=this,t=this.adapter.isFocused(),i=document.activeElement&&this.adapter.isElementInContainer(document.activeElement);(t||i)&&setTimeout((function(){e.adapter.restoreFocus()}),Je.TOUCH_EVENT_WAIT_MS)},t.prototype.hasBit=function(e,t){return Boolean(e&t)},t.prototype.setBit=function(e,t){return e|t},t.prototype.unsetBit=function(e,t){return e^t},t.prototype.isFinite=function(e){return"number"==typeof e&&isFinite(e)},t}(A),wt=xt,Et=function(e){function t(i){var n=e.call(this,o(o({},t.defaultAdapter),i))||this;return n.closeAnimationEndTimerId=0,n.defaultFocusState=gt.LIST_ROOT,n.selectedIndex=-1,n}return i(t,e),Object.defineProperty(t,"cssClasses",{get:function(){return vt},enumerable:!1,configurable:!0}),Object.defineProperty(t,"strings",{get:function(){return bt},enumerable:!1,configurable:!0}),Object.defineProperty(t,"numbers",{get:function(){return yt},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{addClassToElementAtIndex:function(){},removeClassFromElementAtIndex:function(){},addAttributeToElementAtIndex:function(){},removeAttributeFromElementAtIndex:function(){},getAttributeFromElementAtIndex:function(){return null},elementContainsClass:function(){return!1},closeSurface:function(){},getElementIndex:function(){return-1},notifySelected:function(){},getMenuItemCount:function(){return 0},focusItemAtIndex:function(){},focusListRoot:function(){},getSelectedSiblingOfItemAtIndex:function(){return-1},isSelectableItemAtIndex:function(){return!1}}},enumerable:!1,configurable:!0}),t.prototype.destroy=function(){this.closeAnimationEndTimerId&&clearTimeout(this.closeAnimationEndTimerId),this.adapter.closeSurface()},t.prototype.handleKeydown=function(e){var t=e.key,i=e.keyCode;("Tab"===t||9===i)&&this.adapter.closeSurface(!0)},t.prototype.handleItemAction=function(e){var t=this,i=this.adapter.getElementIndex(e);if(!(i<0)){this.adapter.notifySelected({index:i});var o="true"===this.adapter.getAttributeFromElementAtIndex(i,bt.SKIP_RESTORE_FOCUS);this.adapter.closeSurface(o),this.closeAnimationEndTimerId=setTimeout((function(){var i=t.adapter.getElementIndex(e);i>=0&&t.adapter.isSelectableItemAtIndex(i)&&t.setSelectedIndex(i)}),xt.numbers.TRANSITION_CLOSE_DURATION)}},t.prototype.handleMenuSurfaceOpened=function(){switch(this.defaultFocusState){case gt.FIRST_ITEM:this.adapter.focusItemAtIndex(0);break;case gt.LAST_ITEM:this.adapter.focusItemAtIndex(this.adapter.getMenuItemCount()-1);break;case gt.NONE:break;default:this.adapter.focusListRoot()}},t.prototype.setDefaultFocusState=function(e){this.defaultFocusState=e},t.prototype.getSelectedIndex=function(){return this.selectedIndex},t.prototype.setSelectedIndex=function(e){if(this.validatedIndex(e),!this.adapter.isSelectableItemAtIndex(e))throw new Error("MDCMenuFoundation: No selection group at specified index.");var t=this.adapter.getSelectedSiblingOfItemAtIndex(e);t>=0&&(this.adapter.removeAttributeFromElementAtIndex(t,bt.ARIA_CHECKED_ATTR),this.adapter.removeClassFromElementAtIndex(t,vt.MENU_SELECTED_LIST_ITEM)),this.adapter.addClassToElementAtIndex(e,vt.MENU_SELECTED_LIST_ITEM),this.adapter.addAttributeToElementAtIndex(e,bt.ARIA_CHECKED_ATTR,"true"),this.selectedIndex=e},t.prototype.setEnabled=function(e,t){this.validatedIndex(e),t?(this.adapter.removeClassFromElementAtIndex(e,Oe),this.adapter.addAttributeToElementAtIndex(e,bt.ARIA_DISABLED_ATTR,"false")):(this.adapter.addClassToElementAtIndex(e,Oe),this.adapter.addAttributeToElementAtIndex(e,bt.ARIA_DISABLED_ATTR,"true"))},t.prototype.validatedIndex=function(e){var t=this.adapter.getMenuItemCount();if(!(e>=0&&e + + + + `}createAdapter(){return{addClassToElementAtIndex:(e,t)=>{const i=this.listElement;if(!i)return;const o=i.items[e];o&&("mdc-menu-item--selected"===t?this.forceGroupSelection&&!o.selected&&i.toggle(e,!0):o.classList.add(t))},removeClassFromElementAtIndex:(e,t)=>{const i=this.listElement;if(!i)return;const o=i.items[e];o&&("mdc-menu-item--selected"===t?o.selected&&i.toggle(e,!1):o.classList.remove(t))},addAttributeToElementAtIndex:(e,t,i)=>{const o=this.listElement;if(!o)return;const n=o.items[e];n&&n.setAttribute(t,i)},removeAttributeFromElementAtIndex:(e,t)=>{const i=this.listElement;if(!i)return;const o=i.items[e];o&&o.removeAttribute(t)},getAttributeFromElementAtIndex:(e,t)=>{const i=this.listElement;if(!i)return null;const o=i.items[e];return o?o.getAttribute(t):null},elementContainsClass:(e,t)=>e.classList.contains(t),closeSurface:()=>{this.open=!1},getElementIndex:e=>{const t=this.listElement;return t?t.items.indexOf(e):-1},notifySelected:()=>{},getMenuItemCount:()=>{const e=this.listElement;return e?e.items.length:0},focusItemAtIndex:e=>{const t=this.listElement;if(!t)return;const i=t.items[e];i&&i.focus()},focusListRoot:()=>{this.listElement&&this.listElement.focus()},getSelectedSiblingOfItemAtIndex:e=>{const t=this.listElement;if(!t)return-1;const i=t.items[e];if(!i||!i.group)return-1;for(let o=0;o{const t=this.listElement;if(!t)return!1;const i=t.items[e];return!!i&&i.hasAttribute("group")}}}onKeydown(e){this.mdcFoundation&&this.mdcFoundation.handleKeydown(e)}onAction(e){const t=this.listElement;if(this.mdcFoundation&&t){const i=e.detail.index,o=t.items[i];o&&this.mdcFoundation.handleItemAction(o)}}onOpened(){this.open=!0,this.mdcFoundation&&this.mdcFoundation.handleMenuSurfaceOpened()}onClosed(){this.open=!1}async getUpdateComplete(){await this._listUpdateComplete;return await super.getUpdateComplete()}async firstUpdated(){super.firstUpdated();const e=this.listElement;e&&(this._listUpdateComplete=e.updateComplete,await this._listUpdateComplete)}select(e){const t=this.listElement;t&&t.select(e)}close(){this.open=!1}show(){this.open=!0}getFocusedItemIndex(){const e=this.listElement;return e?e.getFocusedItemIndex():-1}focusItemAtIndex(e){const t=this.listElement;t&&t.focusItemAtIndex(e)}layout(e=!0){const t=this.listElement;t&&t.layout(e)}}a([b(".mdc-menu")],It.prototype,"mdcRoot",void 0),a([b("slot")],It.prototype,"slotElement",void 0),a([d({type:Object})],It.prototype,"anchor",void 0),a([d({type:Boolean,reflect:!0})],It.prototype,"open",void 0),a([d({type:Boolean})],It.prototype,"quick",void 0),a([d({type:Boolean})],It.prototype,"wrapFocus",void 0),a([d({type:String})],It.prototype,"innerRole",void 0),a([d({type:String})],It.prototype,"innerAriaLabel",void 0),a([d({type:String})],It.prototype,"corner",void 0),a([d({type:Number})],It.prototype,"x",void 0),a([d({type:Number})],It.prototype,"y",void 0),a([d({type:Boolean})],It.prototype,"absolute",void 0),a([d({type:Boolean})],It.prototype,"multi",void 0),a([d({type:Boolean})],It.prototype,"activatable",void 0),a([d({type:Boolean})],It.prototype,"fixed",void 0),a([d({type:Boolean})],It.prototype,"forceGroupSelection",void 0),a([d({type:Boolean})],It.prototype,"fullwidth",void 0),a([d({type:String})],It.prototype,"menuCorner",void 0),a([d({type:Boolean})],It.prototype,"stayOpenOnBodyClick",void 0),a([d({type:String}),H((function(e){this.mdcFoundation&&this.mdcFoundation.setDefaultFocusState(gt[e])}))],It.prototype,"defaultFocus",void 0); +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const Ct=r(class extends l{constructor(e){var t;if(super(e),e.type!==s.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,i)=>{const o=e[i];return null==o?t:t+`${i=i.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase()}:${o};`}),"")}update(e,[t]){const{style:i}=e.element;if(void 0===this.vt){this.vt=new Set;for(const e in t)this.vt.add(e);return this.render(t)}this.vt.forEach((e=>{null==t[e]&&(this.vt.delete(e),e.includes("-")?i.removeProperty(e):i[e]="")}));for(const e in t){const o=t[e];null!=o&&(this.vt.add(e),e.includes("-")?i.setProperty(e,o):i[e]=o)}return c}}),Tt={TOP_LEFT:Ke.TOP_LEFT,TOP_RIGHT:Ke.TOP_RIGHT,BOTTOM_LEFT:Ke.BOTTOM_LEFT,BOTTOM_RIGHT:Ke.BOTTOM_RIGHT,TOP_START:Ke.TOP_START,TOP_END:Ke.TOP_END,BOTTOM_START:Ke.BOTTOM_START,BOTTOM_END:Ke.BOTTOM_END}; +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */class At extends N{constructor(){super(...arguments),this.mdcFoundationClass=wt,this.absolute=!1,this.fullwidth=!1,this.fixed=!1,this.x=null,this.y=null,this.quick=!1,this.open=!1,this.stayOpenOnBodyClick=!1,this.bitwiseCorner=Ke.TOP_START,this.previousMenuCorner=null,this.menuCorner="START",this.corner="TOP_START",this.styleTop="",this.styleLeft="",this.styleRight="",this.styleBottom="",this.styleMaxHeight="",this.styleTransformOrigin="",this.anchor=null,this.previouslyFocused=null,this.previousAnchor=null,this.onBodyClickBound=()=>{}}render(){const e={"mdc-menu-surface--fixed":this.fixed,"mdc-menu-surface--fullwidth":this.fullwidth},t={top:this.styleTop,left:this.styleLeft,right:this.styleRight,bottom:this.styleBottom,"max-height":this.styleMaxHeight,"transform-origin":this.styleTransformOrigin};return m` +
    + +
    `}createAdapter(){return Object.assign(Object.assign({},R(this.mdcRoot)),{hasAnchor:()=>!!this.anchor,notifyClose:()=>{const e=new CustomEvent("closed",{bubbles:!0,composed:!0});this.open=!1,this.mdcRoot.dispatchEvent(e)},notifyClosing:()=>{const e=new CustomEvent("closing",{bubbles:!0,composed:!0});this.mdcRoot.dispatchEvent(e)},notifyOpen:()=>{const e=new CustomEvent("opened",{bubbles:!0,composed:!0});this.open=!0,this.mdcRoot.dispatchEvent(e)},isElementInContainer:()=>!1,isRtl:()=>!!this.mdcRoot&&"rtl"===getComputedStyle(this.mdcRoot).direction,setTransformOrigin:e=>{this.mdcRoot&&(this.styleTransformOrigin=e)},isFocused:()=>V(this),saveFocus:()=>{const e=D(),t=e.length;t||(this.previouslyFocused=null),this.previouslyFocused=e[t-1]},restoreFocus:()=>{this.previouslyFocused&&"focus"in this.previouslyFocused&&this.previouslyFocused.focus()},getInnerDimensions:()=>{const e=this.mdcRoot;return e?{width:e.offsetWidth,height:e.offsetHeight}:{width:0,height:0}},getAnchorDimensions:()=>{const e=this.anchor;return e?e.getBoundingClientRect():null},getBodyDimensions:()=>({width:document.body.clientWidth,height:document.body.clientHeight}),getWindowDimensions:()=>({width:window.innerWidth,height:window.innerHeight}),getWindowScroll:()=>({x:window.pageXOffset,y:window.pageYOffset}),setPosition:e=>{this.mdcRoot&&(this.styleLeft="left"in e?`${e.left}px`:"",this.styleRight="right"in e?`${e.right}px`:"",this.styleTop="top"in e?`${e.top}px`:"",this.styleBottom="bottom"in e?`${e.bottom}px`:"")},setMaxHeight:async e=>{this.mdcRoot&&(this.styleMaxHeight=e,await this.updateComplete,this.styleMaxHeight=`var(--mdc-menu-max-height, ${e})`)}})}onKeydown(e){this.mdcFoundation&&this.mdcFoundation.handleKeydown(e)}onBodyClick(e){if(this.stayOpenOnBodyClick)return;-1===e.composedPath().indexOf(this)&&this.close()}registerBodyClick(){this.onBodyClickBound=this.onBodyClick.bind(this),document.body.addEventListener("click",this.onBodyClickBound,{passive:!0,capture:!0})}deregisterBodyClick(){document.body.removeEventListener("click",this.onBodyClickBound,{capture:!0})}close(){this.open=!1}show(){this.open=!0}}a([b(".mdc-menu-surface")],At.prototype,"mdcRoot",void 0),a([b("slot")],At.prototype,"slotElement",void 0),a([d({type:Boolean}),H((function(e){this.mdcFoundation&&!this.fixed&&this.mdcFoundation.setIsHoisted(e)}))],At.prototype,"absolute",void 0),a([d({type:Boolean})],At.prototype,"fullwidth",void 0),a([d({type:Boolean}),H((function(e){this.mdcFoundation&&!this.absolute&&this.mdcFoundation.setFixedPosition(e)}))],At.prototype,"fixed",void 0),a([d({type:Number}),H((function(e){this.mdcFoundation&&null!==this.y&&null!==e&&(this.mdcFoundation.setAbsolutePosition(e,this.y),this.mdcFoundation.setAnchorMargin({left:e,top:this.y,right:-e,bottom:this.y}))}))],At.prototype,"x",void 0),a([d({type:Number}),H((function(e){this.mdcFoundation&&null!==this.x&&null!==e&&(this.mdcFoundation.setAbsolutePosition(this.x,e),this.mdcFoundation.setAnchorMargin({left:this.x,top:e,right:-this.x,bottom:e}))}))],At.prototype,"y",void 0),a([d({type:Boolean}),H((function(e){this.mdcFoundation&&this.mdcFoundation.setQuickOpen(e)}))],At.prototype,"quick",void 0),a([d({type:Boolean,reflect:!0}),H((function(e,t){this.mdcFoundation&&(e?this.mdcFoundation.open():void 0!==t&&this.mdcFoundation.close())}))],At.prototype,"open",void 0),a([d({type:Boolean})],At.prototype,"stayOpenOnBodyClick",void 0),a([u(),H((function(e){this.mdcFoundation&&this.mdcFoundation.setAnchorCorner(e)}))],At.prototype,"bitwiseCorner",void 0),a([d({type:String}),H((function(e){if(this.mdcFoundation){const t="START"===e||"END"===e,i=null===this.previousMenuCorner,o=!i&&e!==this.previousMenuCorner,n=i&&"END"===e;t&&(o||n)&&(this.bitwiseCorner=this.bitwiseCorner^We.RIGHT,this.mdcFoundation.flipCornerHorizontally(),this.previousMenuCorner=e)}}))],At.prototype,"menuCorner",void 0),a([d({type:String}),H((function(e){if(this.mdcFoundation&&e){let t=Tt[e];"END"===this.menuCorner&&(t^=We.RIGHT),this.bitwiseCorner=t}}))],At.prototype,"corner",void 0),a([u()],At.prototype,"styleTop",void 0),a([u()],At.prototype,"styleLeft",void 0),a([u()],At.prototype,"styleRight",void 0),a([u()],At.prototype,"styleBottom",void 0),a([u()],At.prototype,"styleMaxHeight",void 0),a([u()],At.prototype,"styleTransformOrigin",void 0); +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var kt={BG_FOCUSED:"mdc-ripple-upgraded--background-focused",FG_ACTIVATION:"mdc-ripple-upgraded--foreground-activation",FG_DEACTIVATION:"mdc-ripple-upgraded--foreground-deactivation",ROOT:"mdc-ripple-upgraded",UNBOUNDED:"mdc-ripple-upgraded--unbounded"},$t={VAR_FG_SCALE:"--mdc-ripple-fg-scale",VAR_FG_SIZE:"--mdc-ripple-fg-size",VAR_FG_TRANSLATE_END:"--mdc-ripple-fg-translate-end",VAR_FG_TRANSLATE_START:"--mdc-ripple-fg-translate-start",VAR_LEFT:"--mdc-ripple-left",VAR_TOP:"--mdc-ripple-top"},Ot={DEACTIVATION_TIMEOUT_MS:225,FG_DEACTIVATION_MS:150,INITIAL_ORIGIN_SCALE:.6,PADDING:10,TAP_DELAY_MS:300}; +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var St=["touchstart","pointerdown","mousedown","keydown"],Rt=["touchend","pointerup","mouseup","contextmenu"],Ft=[],Lt=function(e){function t(i){var n=e.call(this,o(o({},t.defaultAdapter),i))||this;return n.activationAnimationHasEnded=!1,n.activationTimer=0,n.fgDeactivationRemovalTimer=0,n.fgScale="0",n.frame={width:0,height:0},n.initialSize=0,n.layoutFrame=0,n.maxRadius=0,n.unboundedCoords={left:0,top:0},n.activationState=n.defaultActivationState(),n.activationTimerCallback=function(){n.activationAnimationHasEnded=!0,n.runDeactivationUXLogicIfReady()},n.activateHandler=function(e){n.activateImpl(e)},n.deactivateHandler=function(){n.deactivateImpl()},n.focusHandler=function(){n.handleFocus()},n.blurHandler=function(){n.handleBlur()},n.resizeHandler=function(){n.layout()},n}return i(t,e),Object.defineProperty(t,"cssClasses",{get:function(){return kt},enumerable:!1,configurable:!0}),Object.defineProperty(t,"strings",{get:function(){return $t},enumerable:!1,configurable:!0}),Object.defineProperty(t,"numbers",{get:function(){return Ot},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{addClass:function(){},browserSupportsCssVars:function(){return!0},computeBoundingRect:function(){return{top:0,right:0,bottom:0,left:0,width:0,height:0}},containsEventTarget:function(){return!0},deregisterDocumentInteractionHandler:function(){},deregisterInteractionHandler:function(){},deregisterResizeHandler:function(){},getWindowPageOffset:function(){return{x:0,y:0}},isSurfaceActive:function(){return!0},isSurfaceDisabled:function(){return!0},isUnbounded:function(){return!0},registerDocumentInteractionHandler:function(){},registerInteractionHandler:function(){},registerResizeHandler:function(){},removeClass:function(){},updateCssVariable:function(){}}},enumerable:!1,configurable:!0}),t.prototype.init=function(){var e=this,i=this.supportsPressRipple();if(this.registerRootHandlers(i),i){var o=t.cssClasses,n=o.ROOT,a=o.UNBOUNDED;requestAnimationFrame((function(){e.adapter.addClass(n),e.adapter.isUnbounded()&&(e.adapter.addClass(a),e.layoutInternal())}))}},t.prototype.destroy=function(){var e=this;if(this.supportsPressRipple()){this.activationTimer&&(clearTimeout(this.activationTimer),this.activationTimer=0,this.adapter.removeClass(t.cssClasses.FG_ACTIVATION)),this.fgDeactivationRemovalTimer&&(clearTimeout(this.fgDeactivationRemovalTimer),this.fgDeactivationRemovalTimer=0,this.adapter.removeClass(t.cssClasses.FG_DEACTIVATION));var i=t.cssClasses,o=i.ROOT,n=i.UNBOUNDED;requestAnimationFrame((function(){e.adapter.removeClass(o),e.adapter.removeClass(n),e.removeCssVars()}))}this.deregisterRootHandlers(),this.deregisterDeactivationHandlers()},t.prototype.activate=function(e){this.activateImpl(e)},t.prototype.deactivate=function(){this.deactivateImpl()},t.prototype.layout=function(){var e=this;this.layoutFrame&&cancelAnimationFrame(this.layoutFrame),this.layoutFrame=requestAnimationFrame((function(){e.layoutInternal(),e.layoutFrame=0}))},t.prototype.setUnbounded=function(e){var i=t.cssClasses.UNBOUNDED;e?this.adapter.addClass(i):this.adapter.removeClass(i)},t.prototype.handleFocus=function(){var e=this;requestAnimationFrame((function(){return e.adapter.addClass(t.cssClasses.BG_FOCUSED)}))},t.prototype.handleBlur=function(){var e=this;requestAnimationFrame((function(){return e.adapter.removeClass(t.cssClasses.BG_FOCUSED)}))},t.prototype.supportsPressRipple=function(){return this.adapter.browserSupportsCssVars()},t.prototype.defaultActivationState=function(){return{activationEvent:void 0,hasDeactivationUXRun:!1,isActivated:!1,isProgrammatic:!1,wasActivatedByPointer:!1,wasElementMadeActive:!1}},t.prototype.registerRootHandlers=function(e){var t,i;if(e){try{for(var o=f(St),n=o.next();!n.done;n=o.next()){var a=n.value;this.adapter.registerInteractionHandler(a,this.activateHandler)}}catch(e){t={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(t)throw t.error}}this.adapter.isUnbounded()&&this.adapter.registerResizeHandler(this.resizeHandler)}this.adapter.registerInteractionHandler("focus",this.focusHandler),this.adapter.registerInteractionHandler("blur",this.blurHandler)},t.prototype.registerDeactivationHandlers=function(e){var t,i;if("keydown"===e.type)this.adapter.registerInteractionHandler("keyup",this.deactivateHandler);else try{for(var o=f(Rt),n=o.next();!n.done;n=o.next()){var a=n.value;this.adapter.registerDocumentInteractionHandler(a,this.deactivateHandler)}}catch(e){t={error:e}}finally{try{n&&!n.done&&(i=o.return)&&i.call(o)}finally{if(t)throw t.error}}},t.prototype.deregisterRootHandlers=function(){var e,t;try{for(var i=f(St),o=i.next();!o.done;o=i.next()){var n=o.value;this.adapter.deregisterInteractionHandler(n,this.activateHandler)}}catch(t){e={error:t}}finally{try{o&&!o.done&&(t=i.return)&&t.call(i)}finally{if(e)throw e.error}}this.adapter.deregisterInteractionHandler("focus",this.focusHandler),this.adapter.deregisterInteractionHandler("blur",this.blurHandler),this.adapter.isUnbounded()&&this.adapter.deregisterResizeHandler(this.resizeHandler)},t.prototype.deregisterDeactivationHandlers=function(){var e,t;this.adapter.deregisterInteractionHandler("keyup",this.deactivateHandler);try{for(var i=f(Rt),o=i.next();!o.done;o=i.next()){var n=o.value;this.adapter.deregisterDocumentInteractionHandler(n,this.deactivateHandler)}}catch(t){e={error:t}}finally{try{o&&!o.done&&(t=i.return)&&t.call(i)}finally{if(e)throw e.error}}},t.prototype.removeCssVars=function(){var e=this,i=t.strings;Object.keys(i).forEach((function(t){0===t.indexOf("VAR_")&&e.adapter.updateCssVariable(i[t],null)}))},t.prototype.activateImpl=function(e){var t=this;if(!this.adapter.isSurfaceDisabled()){var i=this.activationState;if(!i.isActivated){var o=this.previousActivationEvent;if(!(o&&void 0!==e&&o.type!==e.type))i.isActivated=!0,i.isProgrammatic=void 0===e,i.activationEvent=e,i.wasActivatedByPointer=!i.isProgrammatic&&(void 0!==e&&("mousedown"===e.type||"touchstart"===e.type||"pointerdown"===e.type)),void 0!==e&&Ft.length>0&&Ft.some((function(e){return t.adapter.containsEventTarget(e)}))?this.resetActivationState():(void 0!==e&&(Ft.push(e.target),this.registerDeactivationHandlers(e)),i.wasElementMadeActive=this.checkElementMadeActive(e),i.wasElementMadeActive&&this.animateActivation(),requestAnimationFrame((function(){Ft=[],i.wasElementMadeActive||void 0===e||" "!==e.key&&32!==e.keyCode||(i.wasElementMadeActive=t.checkElementMadeActive(e),i.wasElementMadeActive&&t.animateActivation()),i.wasElementMadeActive||(t.activationState=t.defaultActivationState())})))}}},t.prototype.checkElementMadeActive=function(e){return void 0===e||"keydown"!==e.type||this.adapter.isSurfaceActive()},t.prototype.animateActivation=function(){var e=this,i=t.strings,o=i.VAR_FG_TRANSLATE_START,n=i.VAR_FG_TRANSLATE_END,a=t.cssClasses,d=a.FG_DEACTIVATION,r=a.FG_ACTIVATION,l=t.numbers.DEACTIVATION_TIMEOUT_MS;this.layoutInternal();var s="",c="";if(!this.adapter.isUnbounded()){var m=this.getFgTranslationCoordinates(),h=m.startPoint,p=m.endPoint;s=h.x+"px, "+h.y+"px",c=p.x+"px, "+p.y+"px"}this.adapter.updateCssVariable(o,s),this.adapter.updateCssVariable(n,c),clearTimeout(this.activationTimer),clearTimeout(this.fgDeactivationRemovalTimer),this.rmBoundedActivationClasses(),this.adapter.removeClass(d),this.adapter.computeBoundingRect(),this.adapter.addClass(r),this.activationTimer=setTimeout((function(){e.activationTimerCallback()}),l)},t.prototype.getFgTranslationCoordinates=function(){var e,t=this.activationState,i=t.activationEvent;return e=t.wasActivatedByPointer?function(e,t,i){if(!e)return{x:0,y:0};var o,n,a=t.x,d=t.y,r=a+i.left,l=d+i.top;if("touchstart"===e.type){var s=e;o=s.changedTouches[0].pageX-r,n=s.changedTouches[0].pageY-l}else{var c=e;o=c.pageX-r,n=c.pageY-l}return{x:o,y:n}}(i,this.adapter.getWindowPageOffset(),this.adapter.computeBoundingRect()):{x:this.frame.width/2,y:this.frame.height/2},{startPoint:e={x:e.x-this.initialSize/2,y:e.y-this.initialSize/2},endPoint:{x:this.frame.width/2-this.initialSize/2,y:this.frame.height/2-this.initialSize/2}}},t.prototype.runDeactivationUXLogicIfReady=function(){var e=this,i=t.cssClasses.FG_DEACTIVATION,o=this.activationState,n=o.hasDeactivationUXRun,a=o.isActivated;(n||!a)&&this.activationAnimationHasEnded&&(this.rmBoundedActivationClasses(),this.adapter.addClass(i),this.fgDeactivationRemovalTimer=setTimeout((function(){e.adapter.removeClass(i)}),Ot.FG_DEACTIVATION_MS))},t.prototype.rmBoundedActivationClasses=function(){var e=t.cssClasses.FG_ACTIVATION;this.adapter.removeClass(e),this.activationAnimationHasEnded=!1,this.adapter.computeBoundingRect()},t.prototype.resetActivationState=function(){var e=this;this.previousActivationEvent=this.activationState.activationEvent,this.activationState=this.defaultActivationState(),setTimeout((function(){return e.previousActivationEvent=void 0}),t.numbers.TAP_DELAY_MS)},t.prototype.deactivateImpl=function(){var e=this,t=this.activationState;if(t.isActivated){var i=o({},t);t.isProgrammatic?(requestAnimationFrame((function(){e.animateDeactivation(i)})),this.resetActivationState()):(this.deregisterDeactivationHandlers(),requestAnimationFrame((function(){e.activationState.hasDeactivationUXRun=!0,e.animateDeactivation(i),e.resetActivationState()})))}},t.prototype.animateDeactivation=function(e){var t=e.wasActivatedByPointer,i=e.wasElementMadeActive;(t||i)&&this.runDeactivationUXLogicIfReady()},t.prototype.layoutInternal=function(){var e=this;this.frame=this.adapter.computeBoundingRect();var i=Math.max(this.frame.height,this.frame.width);this.maxRadius=this.adapter.isUnbounded()?i:Math.sqrt(Math.pow(e.frame.width,2)+Math.pow(e.frame.height,2))+t.numbers.PADDING;var o=Math.floor(i*t.numbers.INITIAL_ORIGIN_SCALE);this.adapter.isUnbounded()&&o%2!=0?this.initialSize=o-1:this.initialSize=o,this.fgScale=""+this.maxRadius/this.initialSize,this.updateLayoutCssVars()},t.prototype.updateLayoutCssVars=function(){var e=t.strings,i=e.VAR_FG_SIZE,o=e.VAR_LEFT,n=e.VAR_TOP,a=e.VAR_FG_SCALE;this.adapter.updateCssVariable(i,this.initialSize+"px"),this.adapter.updateCssVariable(a,this.fgScale),this.adapter.isUnbounded()&&(this.unboundedCoords={left:Math.round(this.frame.width/2-this.initialSize/2),top:Math.round(this.frame.height/2-this.initialSize/2)},this.adapter.updateCssVariable(o,this.unboundedCoords.left+"px"),this.adapter.updateCssVariable(n,this.unboundedCoords.top+"px"))},t}(A),Dt=Lt; +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +class Vt extends N{constructor(){super(...arguments),this.primary=!1,this.accent=!1,this.unbounded=!1,this.disabled=!1,this.activated=!1,this.selected=!1,this.internalUseStateLayerCustomProperties=!1,this.hovering=!1,this.bgFocused=!1,this.fgActivation=!1,this.fgDeactivation=!1,this.fgScale="",this.fgSize="",this.translateStart="",this.translateEnd="",this.leftPos="",this.topPos="",this.mdcFoundationClass=Dt}get isActive(){return e=this.parentElement||this,t=":active",(e.matches||e.webkitMatchesSelector||e.msMatchesSelector).call(e,t); +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var e,t}createAdapter(){return{browserSupportsCssVars:()=>!0,isUnbounded:()=>this.unbounded,isSurfaceActive:()=>this.isActive,isSurfaceDisabled:()=>this.disabled,addClass:e=>{switch(e){case"mdc-ripple-upgraded--background-focused":this.bgFocused=!0;break;case"mdc-ripple-upgraded--foreground-activation":this.fgActivation=!0;break;case"mdc-ripple-upgraded--foreground-deactivation":this.fgDeactivation=!0}},removeClass:e=>{switch(e){case"mdc-ripple-upgraded--background-focused":this.bgFocused=!1;break;case"mdc-ripple-upgraded--foreground-activation":this.fgActivation=!1;break;case"mdc-ripple-upgraded--foreground-deactivation":this.fgDeactivation=!1}},containsEventTarget:()=>!0,registerInteractionHandler:()=>{},deregisterInteractionHandler:()=>{},registerDocumentInteractionHandler:()=>{},deregisterDocumentInteractionHandler:()=>{},registerResizeHandler:()=>{},deregisterResizeHandler:()=>{},updateCssVariable:(e,t)=>{switch(e){case"--mdc-ripple-fg-scale":this.fgScale=t;break;case"--mdc-ripple-fg-size":this.fgSize=t;break;case"--mdc-ripple-fg-translate-end":this.translateEnd=t;break;case"--mdc-ripple-fg-translate-start":this.translateStart=t;break;case"--mdc-ripple-left":this.leftPos=t;break;case"--mdc-ripple-top":this.topPos=t}},computeBoundingRect:()=>(this.parentElement||this).getBoundingClientRect(),getWindowPageOffset:()=>({x:window.pageXOffset,y:window.pageYOffset})}}startPress(e){this.waitForFoundation((()=>{this.mdcFoundation.activate(e)}))}endPress(){this.waitForFoundation((()=>{this.mdcFoundation.deactivate()}))}startFocus(){this.waitForFoundation((()=>{this.mdcFoundation.handleFocus()}))}endFocus(){this.waitForFoundation((()=>{this.mdcFoundation.handleBlur()}))}startHover(){this.hovering=!0}endHover(){this.hovering=!1}waitForFoundation(e){this.mdcFoundation?e():this.updateComplete.then(e)}update(e){e.has("disabled")&&this.disabled&&this.endHover(),super.update(e)}render(){const e=this.activated&&(this.primary||!this.accent),t=this.selected&&(this.primary||!this.accent),i={"mdc-ripple-surface--accent":this.accent,"mdc-ripple-surface--primary--activated":e,"mdc-ripple-surface--accent--activated":this.accent&&this.activated,"mdc-ripple-surface--primary--selected":t,"mdc-ripple-surface--accent--selected":this.accent&&this.selected,"mdc-ripple-surface--disabled":this.disabled,"mdc-ripple-surface--hover":this.hovering,"mdc-ripple-surface--primary":this.primary,"mdc-ripple-surface--selected":this.selected,"mdc-ripple-upgraded--background-focused":this.bgFocused,"mdc-ripple-upgraded--foreground-activation":this.fgActivation,"mdc-ripple-upgraded--foreground-deactivation":this.fgDeactivation,"mdc-ripple-upgraded--unbounded":this.unbounded,"mdc-ripple-surface--internal-use-state-layer-custom-properties":this.internalUseStateLayerCustomProperties};return m` +
    `}}a([b(".mdc-ripple-surface")],Vt.prototype,"mdcRoot",void 0),a([d({type:Boolean})],Vt.prototype,"primary",void 0),a([d({type:Boolean})],Vt.prototype,"accent",void 0),a([d({type:Boolean})],Vt.prototype,"unbounded",void 0),a([d({type:Boolean})],Vt.prototype,"disabled",void 0),a([d({type:Boolean})],Vt.prototype,"activated",void 0),a([d({type:Boolean})],Vt.prototype,"selected",void 0),a([d({type:Boolean})],Vt.prototype,"internalUseStateLayerCustomProperties",void 0),a([u()],Vt.prototype,"hovering",void 0),a([u()],Vt.prototype,"bgFocused",void 0),a([u()],Vt.prototype,"fgActivation",void 0),a([u()],Vt.prototype,"fgDeactivation",void 0),a([u()],Vt.prototype,"fgScale",void 0),a([u()],Vt.prototype,"fgSize",void 0),a([u()],Vt.prototype,"translateStart",void 0),a([u()],Vt.prototype,"translateEnd",void 0),a([u()],Vt.prototype,"leftPos",void 0),a([u()],Vt.prototype,"topPos",void 0); +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var Nt={NOTCH_ELEMENT_SELECTOR:".mdc-notched-outline__notch"},Mt={NOTCH_ELEMENT_PADDING:8},Pt={NO_LABEL:"mdc-notched-outline--no-label",OUTLINE_NOTCHED:"mdc-notched-outline--notched",OUTLINE_UPGRADED:"mdc-notched-outline--upgraded"},Bt=function(e){function t(i){return e.call(this,o(o({},t.defaultAdapter),i))||this}return i(t,e),Object.defineProperty(t,"strings",{get:function(){return Nt},enumerable:!1,configurable:!0}),Object.defineProperty(t,"cssClasses",{get:function(){return Pt},enumerable:!1,configurable:!0}),Object.defineProperty(t,"numbers",{get:function(){return Mt},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},setNotchWidthProperty:function(){},removeNotchWidthProperty:function(){}}},enumerable:!1,configurable:!0}),t.prototype.notch=function(e){var i=t.cssClasses.OUTLINE_NOTCHED;e>0&&(e+=Mt.NOTCH_ELEMENT_PADDING),this.adapter.setNotchWidthProperty(e),this.adapter.addClass(i)},t.prototype.closeNotch=function(){var e=t.cssClasses.OUTLINE_NOTCHED;this.adapter.removeClass(e),this.adapter.removeNotchWidthProperty()},t}(A); +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +class zt extends N{constructor(){super(...arguments),this.mdcFoundationClass=Bt,this.width=0,this.open=!1,this.lastOpen=this.open}createAdapter(){return{addClass:e=>this.mdcRoot.classList.add(e),removeClass:e=>this.mdcRoot.classList.remove(e),setNotchWidthProperty:e=>this.notchElement.style.setProperty("width",`${e}px`),removeNotchWidthProperty:()=>this.notchElement.style.removeProperty("width")}}openOrClose(e,t){this.mdcFoundation&&(e&&void 0!==t?this.mdcFoundation.notch(t):this.mdcFoundation.closeNotch())}render(){this.openOrClose(this.open,this.width);const e=U({"mdc-notched-outline--notched":this.open});return m` + + + + + + + `}}a([b(".mdc-notched-outline")],zt.prototype,"mdcRoot",void 0),a([d({type:Number})],zt.prototype,"width",void 0),a([d({type:Boolean,reflect:!0})],zt.prototype,"open",void 0),a([b(".mdc-notched-outline__notch")],zt.prototype,"notchElement",void 0); +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */ +const Ht=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;z-index:1}.mdc-line-ripple::after{transform:scaleX(0);border-bottom-width:2px;opacity:0;z-index:2}.mdc-line-ripple::after{transition:transform 180ms cubic-bezier(0.4, 0, 0.2, 1),opacity 180ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-line-ripple--active::after{transform:scaleX(1);opacity:1}.mdc-line-ripple--deactivating::after{opacity:0}.mdc-notched-outline{display:flex;position:absolute;top:0;right:0;left:0;box-sizing:border-box;width:100%;max-width:100%;height:100%;text-align:left;pointer-events:none}[dir=rtl] .mdc-notched-outline,.mdc-notched-outline[dir=rtl]{text-align:right}.mdc-notched-outline__leading,.mdc-notched-outline__notch,.mdc-notched-outline__trailing{box-sizing:border-box;height:100%;border-top:1px solid;border-bottom:1px solid;pointer-events:none}.mdc-notched-outline__leading{border-left:1px solid;border-right:none;width:12px}[dir=rtl] .mdc-notched-outline__leading,.mdc-notched-outline__leading[dir=rtl]{border-left:none;border-right:1px solid}.mdc-notched-outline__trailing{border-left:none;border-right:1px solid;flex-grow:1}[dir=rtl] .mdc-notched-outline__trailing,.mdc-notched-outline__trailing[dir=rtl]{border-left:1px solid;border-right:none}.mdc-notched-outline__notch{flex:0 0 auto;width:auto;max-width:calc(100% - 12px * 2)}.mdc-notched-outline .mdc-floating-label{display:inline-block;position:relative;max-width:100%}.mdc-notched-outline .mdc-floating-label--float-above{text-overflow:clip}.mdc-notched-outline--upgraded .mdc-floating-label--float-above{max-width:calc(100% / 0.75)}.mdc-notched-outline--notched .mdc-notched-outline__notch{padding-left:0;padding-right:8px;border-top:none}[dir=rtl] .mdc-notched-outline--notched .mdc-notched-outline__notch,.mdc-notched-outline--notched .mdc-notched-outline__notch[dir=rtl]{padding-left:8px;padding-right:0}.mdc-notched-outline--no-label .mdc-notched-outline__notch{display:none}.mdc-select{display:inline-flex;position:relative}.mdc-select:not(.mdc-select--disabled) .mdc-select__selected-text{color:rgba(0, 0, 0, 0.87)}.mdc-select.mdc-select--disabled .mdc-select__selected-text{color:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled) .mdc-floating-label{color:rgba(0, 0, 0, 0.6)}.mdc-select:not(.mdc-select--disabled).mdc-select--focused .mdc-floating-label{color:rgba(98, 0, 238, 0.87)}.mdc-select.mdc-select--disabled .mdc-floating-label{color:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled) .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.54)}.mdc-select:not(.mdc-select--disabled).mdc-select--focused .mdc-select__dropdown-icon{fill:#6200ee;fill:var(--mdc-theme-primary, #6200ee)}.mdc-select.mdc-select--disabled .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled)+.mdc-select-helper-text{color:rgba(0, 0, 0, 0.6)}.mdc-select.mdc-select--disabled+.mdc-select-helper-text{color:rgba(0, 0, 0, 0.38)}.mdc-select:not(.mdc-select--disabled) .mdc-select__icon{color:rgba(0, 0, 0, 0.54)}.mdc-select.mdc-select--disabled .mdc-select__icon{color:rgba(0, 0, 0, 0.38)}@media screen and (forced-colors: active),(-ms-high-contrast: active){.mdc-select.mdc-select--disabled .mdc-select__selected-text{color:GrayText}.mdc-select.mdc-select--disabled .mdc-select__dropdown-icon{fill:red}.mdc-select.mdc-select--disabled .mdc-floating-label{color:GrayText}.mdc-select.mdc-select--disabled .mdc-line-ripple::before{border-bottom-color:GrayText}.mdc-select.mdc-select--disabled .mdc-notched-outline__leading,.mdc-select.mdc-select--disabled .mdc-notched-outline__notch,.mdc-select.mdc-select--disabled .mdc-notched-outline__trailing{border-color:GrayText}.mdc-select.mdc-select--disabled .mdc-select__icon{color:GrayText}.mdc-select.mdc-select--disabled+.mdc-select-helper-text{color:GrayText}}.mdc-select .mdc-floating-label{top:50%;transform:translateY(-50%);pointer-events:none}.mdc-select .mdc-select__anchor{padding-left:16px;padding-right:0}[dir=rtl] .mdc-select .mdc-select__anchor,.mdc-select .mdc-select__anchor[dir=rtl]{padding-left:0;padding-right:16px}.mdc-select.mdc-select--with-leading-icon .mdc-select__anchor{padding-left:0;padding-right:0}[dir=rtl] .mdc-select.mdc-select--with-leading-icon .mdc-select__anchor,.mdc-select.mdc-select--with-leading-icon .mdc-select__anchor[dir=rtl]{padding-left:0;padding-right:0}.mdc-select .mdc-select__icon{width:24px;height:24px;font-size:24px}.mdc-select .mdc-select__dropdown-icon{width:24px;height:24px}.mdc-select .mdc-select__menu .mdc-deprecated-list-item{padding-left:16px;padding-right:16px}[dir=rtl] .mdc-select .mdc-select__menu .mdc-deprecated-list-item,.mdc-select .mdc-select__menu .mdc-deprecated-list-item[dir=rtl]{padding-left:16px;padding-right:16px}.mdc-select .mdc-select__menu .mdc-deprecated-list-item__graphic{margin-left:0;margin-right:12px}[dir=rtl] .mdc-select .mdc-select__menu .mdc-deprecated-list-item__graphic,.mdc-select .mdc-select__menu .mdc-deprecated-list-item__graphic[dir=rtl]{margin-left:12px;margin-right:0}.mdc-select__dropdown-icon{margin-left:12px;margin-right:12px;display:inline-flex;position:relative;align-self:center;align-items:center;justify-content:center;flex-shrink:0;pointer-events:none}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-active,.mdc-select__dropdown-icon .mdc-select__dropdown-icon-inactive{position:absolute;top:0;left:0}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-graphic{width:41.6666666667%;height:20.8333333333%}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-inactive{opacity:1;transition:opacity 75ms linear 75ms}.mdc-select__dropdown-icon .mdc-select__dropdown-icon-active{opacity:0;transition:opacity 75ms linear}[dir=rtl] .mdc-select__dropdown-icon,.mdc-select__dropdown-icon[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-select--activated .mdc-select__dropdown-icon .mdc-select__dropdown-icon-inactive{opacity:0;transition:opacity 49.5ms linear}.mdc-select--activated .mdc-select__dropdown-icon .mdc-select__dropdown-icon-active{opacity:1;transition:opacity 100.5ms linear 49.5ms}.mdc-select__anchor{width:200px;min-width:0;flex:1 1 auto;position:relative;box-sizing:border-box;overflow:hidden;outline:none;cursor:pointer}.mdc-select__anchor .mdc-floating-label--float-above{transform:translateY(-106%) scale(0.75)}.mdc-select__selected-text-container{display:flex;appearance:none;pointer-events:none;box-sizing:border-box;width:auto;min-width:0;flex-grow:1;height:28px;border:none;outline:none;padding:0;background-color:transparent;color:inherit}.mdc-select__selected-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);line-height:1.75rem;line-height:var(--mdc-typography-subtitle1-line-height, 1.75rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block;width:100%;text-align:left}[dir=rtl] .mdc-select__selected-text,.mdc-select__selected-text[dir=rtl]{text-align:right}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-floating-label{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-floating-label{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--invalid+.mdc-select-helper-text--validation-msg{color:#b00020;color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-select__dropdown-icon{fill:#b00020;fill:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-select__dropdown-icon{fill:#b00020;fill:var(--mdc-theme-error, #b00020)}.mdc-select--disabled{cursor:default;pointer-events:none}.mdc-select--with-leading-icon .mdc-select__menu .mdc-deprecated-list-item{padding-left:12px;padding-right:12px}[dir=rtl] .mdc-select--with-leading-icon .mdc-select__menu .mdc-deprecated-list-item,.mdc-select--with-leading-icon .mdc-select__menu .mdc-deprecated-list-item[dir=rtl]{padding-left:12px;padding-right:12px}.mdc-select__menu .mdc-deprecated-list .mdc-select__icon,.mdc-select__menu .mdc-list .mdc-select__icon{margin-left:0;margin-right:0}[dir=rtl] .mdc-select__menu .mdc-deprecated-list .mdc-select__icon,[dir=rtl] .mdc-select__menu .mdc-list .mdc-select__icon,.mdc-select__menu .mdc-deprecated-list .mdc-select__icon[dir=rtl],.mdc-select__menu .mdc-list .mdc-select__icon[dir=rtl]{margin-left:0;margin-right:0}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--activated,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--selected,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--activated{color:#000;color:var(--mdc-theme-on-surface, #000)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__graphic,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--activated .mdc-deprecated-list-item__graphic,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__graphic,.mdc-select__menu .mdc-list .mdc-deprecated-list-item--activated .mdc-deprecated-list-item__graphic{color:#000;color:var(--mdc-theme-on-surface, #000)}.mdc-select__menu .mdc-list-item__start{display:inline-flex;align-items:center}.mdc-select__option{padding-left:16px;padding-right:16px}[dir=rtl] .mdc-select__option,.mdc-select__option[dir=rtl]{padding-left:16px;padding-right:16px}.mdc-select__one-line-option.mdc-list-item--with-one-line{height:48px}.mdc-select__two-line-option.mdc-list-item--with-two-lines{height:64px}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__start{margin-top:20px}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__primary-text{display:block;margin-top:0;line-height:normal;margin-bottom:-20px}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__primary-text::before{display:inline-block;width:0;height:28px;content:"";vertical-align:0}.mdc-select__two-line-option.mdc-list-item--with-two-lines .mdc-list-item__primary-text::after{display:inline-block;width:0;height:20px;content:"";vertical-align:-20px}.mdc-select__two-line-option.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end{display:block;margin-top:0;line-height:normal}.mdc-select__two-line-option.mdc-list-item--with-two-lines.mdc-list-item--with-trailing-meta .mdc-list-item__end::before{display:inline-block;width:0;height:36px;content:"";vertical-align:0}.mdc-select__option-with-leading-content{padding-left:0;padding-right:12px}.mdc-select__option-with-leading-content.mdc-list-item{padding-left:0;padding-right:auto}[dir=rtl] .mdc-select__option-with-leading-content.mdc-list-item,.mdc-select__option-with-leading-content.mdc-list-item[dir=rtl]{padding-left:auto;padding-right:0}.mdc-select__option-with-leading-content .mdc-list-item__start{margin-left:12px;margin-right:0}[dir=rtl] .mdc-select__option-with-leading-content .mdc-list-item__start,.mdc-select__option-with-leading-content .mdc-list-item__start[dir=rtl]{margin-left:0;margin-right:12px}.mdc-select__option-with-leading-content .mdc-list-item__start{width:36px;height:24px}[dir=rtl] .mdc-select__option-with-leading-content,.mdc-select__option-with-leading-content[dir=rtl]{padding-left:12px;padding-right:0}.mdc-select__option-with-meta.mdc-list-item{padding-left:auto;padding-right:0}[dir=rtl] .mdc-select__option-with-meta.mdc-list-item,.mdc-select__option-with-meta.mdc-list-item[dir=rtl]{padding-left:0;padding-right:auto}.mdc-select__option-with-meta .mdc-list-item__end{margin-left:12px;margin-right:12px}[dir=rtl] .mdc-select__option-with-meta .mdc-list-item__end,.mdc-select__option-with-meta .mdc-list-item__end[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-select--filled .mdc-select__anchor{height:56px;display:flex;align-items:baseline}.mdc-select--filled .mdc-select__anchor::before{display:inline-block;width:0;height:40px;content:"";vertical-align:0}.mdc-select--filled.mdc-select--no-label .mdc-select__anchor .mdc-select__selected-text::before{content:"​"}.mdc-select--filled.mdc-select--no-label .mdc-select__anchor .mdc-select__selected-text-container{height:100%;display:inline-flex;align-items:center}.mdc-select--filled.mdc-select--no-label .mdc-select__anchor::before{display:none}.mdc-select--filled .mdc-select__anchor{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:0;border-bottom-left-radius:0}.mdc-select--filled:not(.mdc-select--disabled) .mdc-select__anchor{background-color:whitesmoke}.mdc-select--filled.mdc-select--disabled .mdc-select__anchor{background-color:#fafafa}.mdc-select--filled:not(.mdc-select--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.42)}.mdc-select--filled:not(.mdc-select--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.87)}.mdc-select--filled:not(.mdc-select--disabled) .mdc-line-ripple::after{border-bottom-color:#6200ee;border-bottom-color:var(--mdc-theme-primary, #6200ee)}.mdc-select--filled.mdc-select--disabled .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.06)}.mdc-select--filled .mdc-floating-label{max-width:calc(100% - 64px)}.mdc-select--filled .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 64px / 0.75)}.mdc-select--filled .mdc-menu-surface--is-open-below{border-top-left-radius:0px;border-top-right-radius:0px}.mdc-select--filled.mdc-select--focused.mdc-line-ripple::after{transform:scale(1, 2);opacity:1}.mdc-select--filled .mdc-floating-label{left:16px;right:initial}[dir=rtl] .mdc-select--filled .mdc-floating-label,.mdc-select--filled .mdc-floating-label[dir=rtl]{left:initial;right:16px}.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label{left:48px;right:initial}[dir=rtl] .mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label,.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label[dir=rtl]{left:initial;right:48px}.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label{max-width:calc(100% - 96px)}.mdc-select--filled.mdc-select--with-leading-icon .mdc-floating-label--float-above{max-width:calc(100% / 0.75 - 96px / 0.75)}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled):hover .mdc-line-ripple::before{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--invalid:not(.mdc-select--disabled) .mdc-line-ripple::after{border-bottom-color:#b00020;border-bottom-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined{border:none}.mdc-select--outlined .mdc-select__anchor{height:56px}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{transform:translateY(-37.25px) scale(1)}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{font-size:.75rem}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) scale(0.75)}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined-56px 250ms 1}@keyframes mdc-floating-label-shake-float-above-select-outlined-56px{0%{transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 0%)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 0%)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - 0%)) translateY(-34.75px) scale(0.75)}}.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}[dir=rtl] .mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading[dir=rtl]{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}@supports(top: max(0%)){.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__leading{width:max(12px, var(--mdc-shape-small, 4px))}}@supports(top: max(0%)){.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__notch{max-width:calc(100% - max(12px, var(--mdc-shape-small, 4px)) * 2)}}.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}[dir=rtl] .mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing,.mdc-select--outlined .mdc-notched-outline .mdc-notched-outline__trailing[dir=rtl]{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}@supports(top: max(0%)){.mdc-select--outlined .mdc-select__anchor{padding-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}[dir=rtl] .mdc-select--outlined .mdc-select__anchor,.mdc-select--outlined .mdc-select__anchor[dir=rtl]{padding-left:0}@supports(top: max(0%)){[dir=rtl] .mdc-select--outlined .mdc-select__anchor,.mdc-select--outlined .mdc-select__anchor[dir=rtl]{padding-right:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}@supports(top: max(0%)){.mdc-select--outlined+.mdc-select-helper-text{margin-left:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}[dir=rtl] .mdc-select--outlined+.mdc-select-helper-text,.mdc-select--outlined+.mdc-select-helper-text[dir=rtl]{margin-left:0}@supports(top: max(0%)){[dir=rtl] .mdc-select--outlined+.mdc-select-helper-text,.mdc-select--outlined+.mdc-select-helper-text[dir=rtl]{margin-right:max(16px, calc(var(--mdc-shape-small, 4px) + 4px))}}.mdc-select--outlined:not(.mdc-select--disabled) .mdc-select__anchor{background-color:transparent}.mdc-select--outlined.mdc-select--disabled .mdc-select__anchor{background-color:transparent}.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled) .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.38)}.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.87)}.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-width:2px}.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#6200ee;border-color:var(--mdc-theme-primary, #6200ee)}.mdc-select--outlined.mdc-select--disabled .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--disabled .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--disabled .mdc-notched-outline__trailing{border-color:rgba(0, 0, 0, 0.06)}.mdc-select--outlined .mdc-select__anchor :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 60px)}.mdc-select--outlined .mdc-select__anchor{display:flex;align-items:baseline;overflow:visible}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined 250ms 1}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{transform:translateY(-37.25px) scale(1)}.mdc-select--outlined .mdc-select__anchor .mdc-floating-label--float-above{font-size:.75rem}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) scale(0.75)}.mdc-select--outlined .mdc-select__anchor.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-select--outlined .mdc-select__anchor .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:1px}.mdc-select--outlined .mdc-select__anchor .mdc-select__selected-text::before{content:"​"}.mdc-select--outlined .mdc-select__anchor .mdc-select__selected-text-container{height:100%;display:inline-flex;align-items:center}.mdc-select--outlined .mdc-select__anchor::before{display:none}.mdc-select--outlined .mdc-select__selected-text-container{display:flex;border:none;z-index:1;background-color:transparent}.mdc-select--outlined .mdc-select__icon{z-index:2}.mdc-select--outlined .mdc-floating-label{line-height:1.15rem;left:4px;right:initial}[dir=rtl] .mdc-select--outlined .mdc-floating-label,.mdc-select--outlined .mdc-floating-label[dir=rtl]{left:initial;right:4px}.mdc-select--outlined.mdc-select--focused .mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:2px}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled) .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled) .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled) .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled):not(.mdc-select--focused) .mdc-select__anchor:hover .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-width:2px}.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__leading,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__notch,.mdc-select--outlined.mdc-select--invalid:not(.mdc-select--disabled).mdc-select--focused .mdc-notched-outline .mdc-notched-outline__trailing{border-color:#b00020;border-color:var(--mdc-theme-error, #b00020)}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label{left:36px;right:initial}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label[dir=rtl]{left:initial;right:36px}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above{transform:translateY(-37.25px) translateX(-32px) scale(1)}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above[dir=rtl]{transform:translateY(-37.25px) translateX(32px) scale(1)}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--float-above{font-size:.75rem}.mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above{transform:translateY(-34.75px) translateX(-32px) scale(0.75)}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above,[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl],.mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above[dir=rtl]{transform:translateY(-34.75px) translateX(32px) scale(0.75)}.mdc-select--outlined.mdc-select--with-leading-icon.mdc-notched-outline--upgraded .mdc-floating-label--float-above,.mdc-select--outlined.mdc-select--with-leading-icon .mdc-notched-outline--upgraded .mdc-floating-label--float-above{font-size:1rem}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px 250ms 1}@keyframes mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px{0%{transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - 32px)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - 32px)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - 32px)) translateY(-34.75px) scale(0.75)}}[dir=rtl] .mdc-select--outlined.mdc-select--with-leading-icon .mdc-floating-label--shake,.mdc-select--outlined.mdc-select--with-leading-icon[dir=rtl] .mdc-floating-label--shake{animation:mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px 250ms 1}@keyframes mdc-floating-label-shake-float-above-select-outlined-leading-icon-56px-rtl{0%{transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}33%{animation-timing-function:cubic-bezier(0.5, 0, 0.701732, 0.495819);transform:translateX(calc(4% - -32px)) translateY(-34.75px) scale(0.75)}66%{animation-timing-function:cubic-bezier(0.302435, 0.381352, 0.55, 0.956352);transform:translateX(calc(-4% - -32px)) translateY(-34.75px) scale(0.75)}100%{transform:translateX(calc(0 - -32px)) translateY(-34.75px) scale(0.75)}}.mdc-select--outlined.mdc-select--with-leading-icon .mdc-select__anchor :not(.mdc-notched-outline--notched) .mdc-notched-outline__notch{max-width:calc(100% - 96px)}.mdc-select--outlined .mdc-menu-surface{margin-bottom:8px}.mdc-select--outlined.mdc-select--no-label .mdc-menu-surface,.mdc-select--outlined .mdc-menu-surface--is-open-below{margin-bottom:0}.mdc-select__anchor{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);will-change:transform,opacity}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-select__anchor .mdc-select__ripple::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1;z-index:var(--mdc-ripple-z-index, 1)}.mdc-select__anchor .mdc-select__ripple::after{z-index:0;z-index:var(--mdc-ripple-z-index, 0)}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::before{transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::after{top:0;left:0;transform:scale(0);transform-origin:center center}.mdc-select__anchor.mdc-ripple-upgraded--unbounded .mdc-select__ripple::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-select__anchor.mdc-ripple-upgraded--foreground-activation .mdc-select__ripple::after{animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-select__anchor.mdc-ripple-upgraded--foreground-deactivation .mdc-select__ripple::after{animation:mdc-ripple-fg-opacity-out 150ms;transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-select__anchor.mdc-ripple-upgraded .mdc-select__ripple::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-select__anchor .mdc-select__ripple::before,.mdc-select__anchor .mdc-select__ripple::after{background-color:rgba(0, 0, 0, 0.87);background-color:var(--mdc-ripple-color, rgba(0, 0, 0, 0.87))}.mdc-select__anchor:hover .mdc-select__ripple::before,.mdc-select__anchor.mdc-ripple-surface--hover .mdc-select__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-select__anchor.mdc-ripple-upgraded--background-focused .mdc-select__ripple::before,.mdc-select__anchor:not(.mdc-ripple-upgraded):focus .mdc-select__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-select__anchor .mdc-select__ripple{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-deprecated-list-item__ripple::after{background-color:#000;background-color:var(--mdc-ripple-color, var(--mdc-theme-on-surface, #000))}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:hover .mdc-deprecated-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-surface--hover .mdc-deprecated-list-item__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded--background-focused .mdc-deprecated-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):focus .mdc-deprecated-list-item__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded) .mdc-deprecated-list-item__ripple::after{transition:opacity 150ms linear}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):active .mdc-deprecated-list-item__ripple::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected .mdc-list-item__ripple::after{background-color:#000;background-color:var(--mdc-ripple-color, var(--mdc-theme-on-surface, #000))}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:hover .mdc-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-surface--hover .mdc-list-item__ripple::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded--background-focused .mdc-list-item__ripple::before,.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):focus .mdc-list-item__ripple::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded) .mdc-list-item__ripple::after{transition:opacity 150ms linear}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected:not(.mdc-ripple-upgraded):active .mdc-list-item__ripple::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select__menu .mdc-deprecated-list .mdc-deprecated-list-item--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-select-helper-text{margin:0;margin-left:16px;margin-right:16px;-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:0.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit);display:block;margin-top:0;line-height:normal}[dir=rtl] .mdc-select-helper-text,.mdc-select-helper-text[dir=rtl]{margin-left:16px;margin-right:16px}.mdc-select-helper-text::before{display:inline-block;width:0;height:16px;content:"";vertical-align:0}.mdc-select-helper-text--validation-msg{opacity:0;transition:opacity 180ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-select--invalid+.mdc-select-helper-text--validation-msg,.mdc-select-helper-text--validation-msg-persistent{opacity:1}.mdc-select--with-leading-icon .mdc-select__icon{display:inline-block;box-sizing:border-box;border:none;text-decoration:none;cursor:pointer;user-select:none;flex-shrink:0;align-self:center;background-color:transparent;fill:currentColor}.mdc-select--with-leading-icon .mdc-select__icon{margin-left:12px;margin-right:12px}[dir=rtl] .mdc-select--with-leading-icon .mdc-select__icon,.mdc-select--with-leading-icon .mdc-select__icon[dir=rtl]{margin-left:12px;margin-right:12px}.mdc-select__icon:not([tabindex]),.mdc-select__icon[tabindex="-1"]{cursor:default;pointer-events:none}.material-icons{font-family:var(--mdc-icon-font, "Material Icons");font-weight:normal;font-style:normal;font-size:var(--mdc-icon-size, 24px);line-height:1;letter-spacing:normal;text-transform:none;display:inline-block;white-space:nowrap;word-wrap:normal;direction:ltr;-webkit-font-smoothing:antialiased;text-rendering:optimizeLegibility;-moz-osx-font-smoothing:grayscale;font-feature-settings:"liga"}:host{display:inline-block;vertical-align:top;outline:none}.mdc-select{width:100%}[hidden]{display:none}.mdc-select__icon{z-index:2}.mdc-select--with-leading-icon{--mdc-list-item-graphic-margin: calc( 48px - var(--mdc-list-item-graphic-size, 24px) - var(--mdc-list-side-padding, 16px) )}.mdc-select .mdc-select__anchor .mdc-select__selected-text{overflow:hidden}.mdc-select .mdc-select__anchor *{display:inline-flex}.mdc-select .mdc-select__anchor .mdc-floating-label{display:inline-block}mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-outlined-idle-border-color, rgba(0, 0, 0, 0.38) );--mdc-notched-outline-notch-offset: 1px}:host(:not([disabled]):hover) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-outlined-hover-border-color, rgba(0, 0, 0, 0.87) )}:host(:not([disabled])) .mdc-select:not(.mdc-select--disabled) .mdc-select__selected-text{color:rgba(0, 0, 0, 0.87);color:var(--mdc-select-ink-color, rgba(0, 0, 0, 0.87))}:host(:not([disabled])) .mdc-select:not(.mdc-select--disabled) .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.42);border-bottom-color:var(--mdc-select-idle-line-color, rgba(0, 0, 0, 0.42))}:host(:not([disabled])) .mdc-select:not(.mdc-select--disabled):hover .mdc-line-ripple::before{border-bottom-color:rgba(0, 0, 0, 0.87);border-bottom-color:var(--mdc-select-hover-line-color, rgba(0, 0, 0, 0.87))}:host(:not([disabled])) .mdc-select:not(.mdc-select--outlined):not(.mdc-select--disabled) .mdc-select__anchor{background-color:whitesmoke;background-color:var(--mdc-select-fill-color, whitesmoke)}:host(:not([disabled])) .mdc-select.mdc-select--invalid .mdc-select__dropdown-icon{fill:var(--mdc-select-error-dropdown-icon-color, var(--mdc-select-error-color, var(--mdc-theme-error, #b00020)))}:host(:not([disabled])) .mdc-select.mdc-select--invalid .mdc-floating-label,:host(:not([disabled])) .mdc-select.mdc-select--invalid .mdc-floating-label::after{color:var(--mdc-select-error-color, var(--mdc-theme-error, #b00020))}:host(:not([disabled])) .mdc-select.mdc-select--invalid mwc-notched-outline{--mdc-notched-outline-border-color: var(--mdc-select-error-color, var(--mdc-theme-error, #b00020))}.mdc-select__menu--invalid{--mdc-theme-primary: var(--mdc-select-error-color, var(--mdc-theme-error, #b00020))}:host(:not([disabled])) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label,:host(:not([disabled])) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label::after{color:rgba(0, 0, 0, 0.6);color:var(--mdc-select-label-ink-color, rgba(0, 0, 0, 0.6))}:host(:not([disabled])) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.54);fill:var(--mdc-select-dropdown-icon-color, rgba(0, 0, 0, 0.54))}:host(:not([disabled])) .mdc-select.mdc-select--focused mwc-notched-outline{--mdc-notched-outline-stroke-width: 2px;--mdc-notched-outline-notch-offset: 2px}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-focused-label-color, var(--mdc-theme-primary, rgba(98, 0, 238, 0.87)) )}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) .mdc-select__dropdown-icon{fill:rgba(98,0,238,.87);fill:var(--mdc-select-focused-dropdown-icon-color, var(--mdc-theme-primary, rgba(98, 0, 238, 0.87)))}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) .mdc-floating-label{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}:host(:not([disabled])) .mdc-select.mdc-select--focused:not(.mdc-select--invalid) .mdc-floating-label::after{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}:host(:not([disabled])) .mdc-select-helper-text:not(.mdc-select-helper-text--validation-msg){color:var(--mdc-select-label-ink-color, rgba(0, 0, 0, 0.6))}:host([disabled]){pointer-events:none}:host([disabled]) .mdc-select:not(.mdc-select--outlined).mdc-select--disabled .mdc-select__anchor{background-color:#fafafa;background-color:var(--mdc-select-disabled-fill-color, #fafafa)}:host([disabled]) .mdc-select.mdc-select--outlined mwc-notched-outline{--mdc-notched-outline-border-color: var( --mdc-select-outlined-disabled-border-color, rgba(0, 0, 0, 0.06) )}:host([disabled]) .mdc-select .mdc-select__dropdown-icon{fill:rgba(0, 0, 0, 0.38);fill:var(--mdc-select-disabled-dropdown-icon-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label,:host([disabled]) .mdc-select:not(.mdc-select--invalid):not(.mdc-select--focused) .mdc-floating-label::after{color:rgba(0, 0, 0, 0.38);color:var(--mdc-select-disabled-ink-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-select-helper-text{color:rgba(0, 0, 0, 0.38);color:var(--mdc-select-disabled-ink-color, rgba(0, 0, 0, 0.38))}:host([disabled]) .mdc-select__selected-text{color:rgba(0, 0, 0, 0.38);color:var(--mdc-select-disabled-ink-color, rgba(0, 0, 0, 0.38))}` +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */,Ut=h`@keyframes mdc-ripple-fg-radius-in{from{animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-opacity-in{from{animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-out{from{animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}:host{display:block}.mdc-deprecated-list{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-subtitle1-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:1rem;font-size:var(--mdc-typography-subtitle1-font-size, 1rem);line-height:1.75rem;line-height:var(--mdc-typography-subtitle1-line-height, 1.75rem);font-weight:400;font-weight:var(--mdc-typography-subtitle1-font-weight, 400);letter-spacing:0.009375em;letter-spacing:var(--mdc-typography-subtitle1-letter-spacing, 0.009375em);text-decoration:inherit;text-decoration:var(--mdc-typography-subtitle1-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-subtitle1-text-transform, inherit);line-height:1.5rem;margin:0;padding:8px 0;list-style-type:none;color:rgba(0, 0, 0, 0.87);color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87));padding:var(--mdc-list-vertical-padding, 8px) 0}.mdc-deprecated-list:focus{outline:none}.mdc-deprecated-list-item{height:48px}.mdc-deprecated-list--dense{padding-top:4px;padding-bottom:4px;font-size:.812rem}.mdc-deprecated-list ::slotted([divider]){height:0;margin:0;border:none;border-bottom-width:1px;border-bottom-style:solid;border-bottom-color:rgba(0, 0, 0, 0.12)}.mdc-deprecated-list ::slotted([divider][padded]){margin:0 var(--mdc-list-side-padding, 16px)}.mdc-deprecated-list ::slotted([divider][inset]){margin-left:var(--mdc-list-inset-margin, 72px);margin-right:0;width:calc( 100% - var(--mdc-list-inset-margin, 72px) )}[dir=rtl] .mdc-deprecated-list ::slotted([divider][inset]),.mdc-deprecated-list ::slotted([divider][inset][dir=rtl]){margin-left:0;margin-right:var(--mdc-list-inset-margin, 72px)}.mdc-deprecated-list ::slotted([divider][inset][padded]){width:calc( 100% - var(--mdc-list-inset-margin, 72px) - var(--mdc-list-side-padding, 16px) )}.mdc-deprecated-list--dense ::slotted([mwc-list-item]){height:40px}.mdc-deprecated-list--dense ::slotted([mwc-list]){--mdc-list-item-graphic-size: 20px}.mdc-deprecated-list--two-line.mdc-deprecated-list--dense ::slotted([mwc-list-item]),.mdc-deprecated-list--avatar-list.mdc-deprecated-list--dense ::slotted([mwc-list-item]){height:60px}.mdc-deprecated-list--avatar-list.mdc-deprecated-list--dense ::slotted([mwc-list]){--mdc-list-item-graphic-size: 36px}:host([noninteractive]){pointer-events:none;cursor:default}.mdc-deprecated-list--dense ::slotted(.mdc-deprecated-list-item__primary-text){display:block;margin-top:0;line-height:normal;margin-bottom:-20px}.mdc-deprecated-list--dense ::slotted(.mdc-deprecated-list-item__primary-text)::before{display:inline-block;width:0;height:24px;content:"";vertical-align:0}.mdc-deprecated-list--dense ::slotted(.mdc-deprecated-list-item__primary-text)::after{display:inline-block;width:0;height:20px;content:"";vertical-align:-20px}` +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */,jt=h`:host{cursor:pointer;user-select:none;-webkit-tap-highlight-color:transparent;height:48px;display:flex;position:relative;align-items:center;justify-content:flex-start;overflow:hidden;padding:0;padding-left:var(--mdc-list-side-padding, 16px);padding-right:var(--mdc-list-side-padding, 16px);outline:none;height:48px;color:rgba(0,0,0,.87);color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87))}:host:focus{outline:none}:host([activated]){color:#6200ee;color:var(--mdc-theme-primary, #6200ee);--mdc-ripple-color: var( --mdc-theme-primary, #6200ee )}:host([activated]) .mdc-deprecated-list-item__graphic{color:#6200ee;color:var(--mdc-theme-primary, #6200ee)}:host([activated]) .fake-activated-ripple::before{position:absolute;display:block;top:0;bottom:0;left:0;right:0;width:100%;height:100%;pointer-events:none;z-index:1;content:"";opacity:0.12;opacity:var(--mdc-ripple-activated-opacity, 0.12);background-color:#6200ee;background-color:var(--mdc-ripple-color, var(--mdc-theme-primary, #6200ee))}.mdc-deprecated-list-item__graphic{flex-shrink:0;align-items:center;justify-content:center;fill:currentColor;display:inline-flex}.mdc-deprecated-list-item__graphic ::slotted(*){flex-shrink:0;align-items:center;justify-content:center;fill:currentColor;width:100%;height:100%;text-align:center}.mdc-deprecated-list-item__meta{width:var(--mdc-list-item-meta-size, 24px);height:var(--mdc-list-item-meta-size, 24px);margin-left:auto;margin-right:0;color:rgba(0, 0, 0, 0.38);color:var(--mdc-theme-text-hint-on-background, rgba(0, 0, 0, 0.38))}.mdc-deprecated-list-item__meta.multi{width:auto}.mdc-deprecated-list-item__meta ::slotted(*){width:var(--mdc-list-item-meta-size, 24px);line-height:var(--mdc-list-item-meta-size, 24px)}.mdc-deprecated-list-item__meta ::slotted(.material-icons),.mdc-deprecated-list-item__meta ::slotted(mwc-icon){line-height:var(--mdc-list-item-meta-size, 24px) !important}.mdc-deprecated-list-item__meta ::slotted(:not(.material-icons):not(mwc-icon)){-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-caption-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.75rem;font-size:var(--mdc-typography-caption-font-size, 0.75rem);line-height:1.25rem;line-height:var(--mdc-typography-caption-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-caption-font-weight, 400);letter-spacing:0.0333333333em;letter-spacing:var(--mdc-typography-caption-letter-spacing, 0.0333333333em);text-decoration:inherit;text-decoration:var(--mdc-typography-caption-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-caption-text-transform, inherit)}[dir=rtl] .mdc-deprecated-list-item__meta,.mdc-deprecated-list-item__meta[dir=rtl]{margin-left:0;margin-right:auto}.mdc-deprecated-list-item__meta ::slotted(*){width:100%;height:100%}.mdc-deprecated-list-item__text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.mdc-deprecated-list-item__text ::slotted([for]),.mdc-deprecated-list-item__text[for]{pointer-events:none}.mdc-deprecated-list-item__primary-text{text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block;margin-top:0;line-height:normal;margin-bottom:-20px;display:block}.mdc-deprecated-list-item__primary-text::before{display:inline-block;width:0;height:32px;content:"";vertical-align:0}.mdc-deprecated-list-item__primary-text::after{display:inline-block;width:0;height:20px;content:"";vertical-align:-20px}.mdc-deprecated-list-item__secondary-text{-moz-osx-font-smoothing:grayscale;-webkit-font-smoothing:antialiased;font-family:Roboto, sans-serif;font-family:var(--mdc-typography-body2-font-family, var(--mdc-typography-font-family, Roboto, sans-serif));font-size:0.875rem;font-size:var(--mdc-typography-body2-font-size, 0.875rem);line-height:1.25rem;line-height:var(--mdc-typography-body2-line-height, 1.25rem);font-weight:400;font-weight:var(--mdc-typography-body2-font-weight, 400);letter-spacing:0.0178571429em;letter-spacing:var(--mdc-typography-body2-letter-spacing, 0.0178571429em);text-decoration:inherit;text-decoration:var(--mdc-typography-body2-text-decoration, inherit);text-transform:inherit;text-transform:var(--mdc-typography-body2-text-transform, inherit);text-overflow:ellipsis;white-space:nowrap;overflow:hidden;display:block;margin-top:0;line-height:normal;display:block}.mdc-deprecated-list-item__secondary-text::before{display:inline-block;width:0;height:20px;content:"";vertical-align:0}.mdc-deprecated-list--dense .mdc-deprecated-list-item__secondary-text{font-size:inherit}* ::slotted(a),a{color:inherit;text-decoration:none}:host([twoline]){height:72px}:host([twoline]) .mdc-deprecated-list-item__text{align-self:flex-start}:host([disabled]),:host([noninteractive]){cursor:default;pointer-events:none}:host([disabled]) .mdc-deprecated-list-item__text ::slotted(*){opacity:.38}:host([disabled]) .mdc-deprecated-list-item__text ::slotted(*),:host([disabled]) .mdc-deprecated-list-item__primary-text ::slotted(*),:host([disabled]) .mdc-deprecated-list-item__secondary-text ::slotted(*){color:#000;color:var(--mdc-theme-on-surface, #000)}.mdc-deprecated-list-item__secondary-text ::slotted(*){color:rgba(0, 0, 0, 0.54);color:var(--mdc-theme-text-secondary-on-background, rgba(0, 0, 0, 0.54))}.mdc-deprecated-list-item__graphic ::slotted(*){background-color:transparent;color:rgba(0, 0, 0, 0.38);color:var(--mdc-theme-text-icon-on-background, rgba(0, 0, 0, 0.38))}.mdc-deprecated-list-group__subheader ::slotted(*){color:rgba(0, 0, 0, 0.87);color:var(--mdc-theme-text-primary-on-background, rgba(0, 0, 0, 0.87))}:host([graphic=avatar]) .mdc-deprecated-list-item__graphic{width:var(--mdc-list-item-graphic-size, 40px);height:var(--mdc-list-item-graphic-size, 40px)}:host([graphic=avatar]) .mdc-deprecated-list-item__graphic.multi{width:auto}:host([graphic=avatar]) .mdc-deprecated-list-item__graphic ::slotted(*){width:var(--mdc-list-item-graphic-size, 40px);line-height:var(--mdc-list-item-graphic-size, 40px)}:host([graphic=avatar]) .mdc-deprecated-list-item__graphic ::slotted(.material-icons),:host([graphic=avatar]) .mdc-deprecated-list-item__graphic ::slotted(mwc-icon){line-height:var(--mdc-list-item-graphic-size, 40px) !important}:host([graphic=avatar]) .mdc-deprecated-list-item__graphic ::slotted(*){border-radius:50%}:host([graphic=avatar]) .mdc-deprecated-list-item__graphic,:host([graphic=medium]) .mdc-deprecated-list-item__graphic,:host([graphic=large]) .mdc-deprecated-list-item__graphic,:host([graphic=control]) .mdc-deprecated-list-item__graphic{margin-left:0;margin-right:var(--mdc-list-item-graphic-margin, 16px)}[dir=rtl] :host([graphic=avatar]) .mdc-deprecated-list-item__graphic,[dir=rtl] :host([graphic=medium]) .mdc-deprecated-list-item__graphic,[dir=rtl] :host([graphic=large]) .mdc-deprecated-list-item__graphic,[dir=rtl] :host([graphic=control]) .mdc-deprecated-list-item__graphic,:host([graphic=avatar]) .mdc-deprecated-list-item__graphic[dir=rtl],:host([graphic=medium]) .mdc-deprecated-list-item__graphic[dir=rtl],:host([graphic=large]) .mdc-deprecated-list-item__graphic[dir=rtl],:host([graphic=control]) .mdc-deprecated-list-item__graphic[dir=rtl]{margin-left:var(--mdc-list-item-graphic-margin, 16px);margin-right:0}:host([graphic=icon]) .mdc-deprecated-list-item__graphic{width:var(--mdc-list-item-graphic-size, 24px);height:var(--mdc-list-item-graphic-size, 24px);margin-left:0;margin-right:var(--mdc-list-item-graphic-margin, 32px)}:host([graphic=icon]) .mdc-deprecated-list-item__graphic.multi{width:auto}:host([graphic=icon]) .mdc-deprecated-list-item__graphic ::slotted(*){width:var(--mdc-list-item-graphic-size, 24px);line-height:var(--mdc-list-item-graphic-size, 24px)}:host([graphic=icon]) .mdc-deprecated-list-item__graphic ::slotted(.material-icons),:host([graphic=icon]) .mdc-deprecated-list-item__graphic ::slotted(mwc-icon){line-height:var(--mdc-list-item-graphic-size, 24px) !important}[dir=rtl] :host([graphic=icon]) .mdc-deprecated-list-item__graphic,:host([graphic=icon]) .mdc-deprecated-list-item__graphic[dir=rtl]{margin-left:var(--mdc-list-item-graphic-margin, 32px);margin-right:0}:host([graphic=avatar]:not([twoLine])),:host([graphic=icon]:not([twoLine])){height:56px}:host([graphic=medium]:not([twoLine])),:host([graphic=large]:not([twoLine])){height:72px}:host([graphic=medium]) .mdc-deprecated-list-item__graphic,:host([graphic=large]) .mdc-deprecated-list-item__graphic{width:var(--mdc-list-item-graphic-size, 56px);height:var(--mdc-list-item-graphic-size, 56px)}:host([graphic=medium]) .mdc-deprecated-list-item__graphic.multi,:host([graphic=large]) .mdc-deprecated-list-item__graphic.multi{width:auto}:host([graphic=medium]) .mdc-deprecated-list-item__graphic ::slotted(*),:host([graphic=large]) .mdc-deprecated-list-item__graphic ::slotted(*){width:var(--mdc-list-item-graphic-size, 56px);line-height:var(--mdc-list-item-graphic-size, 56px)}:host([graphic=medium]) .mdc-deprecated-list-item__graphic ::slotted(.material-icons),:host([graphic=medium]) .mdc-deprecated-list-item__graphic ::slotted(mwc-icon),:host([graphic=large]) .mdc-deprecated-list-item__graphic ::slotted(.material-icons),:host([graphic=large]) .mdc-deprecated-list-item__graphic ::slotted(mwc-icon){line-height:var(--mdc-list-item-graphic-size, 56px) !important}:host([graphic=large]){padding-left:0px}` +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */,Xt=h`.mdc-ripple-surface{--mdc-ripple-fg-size: 0;--mdc-ripple-left: 0;--mdc-ripple-top: 0;--mdc-ripple-fg-scale: 1;--mdc-ripple-fg-translate-end: 0;--mdc-ripple-fg-translate-start: 0;-webkit-tap-highlight-color:rgba(0,0,0,0);will-change:transform,opacity;position:relative;outline:none;overflow:hidden}.mdc-ripple-surface::before,.mdc-ripple-surface::after{position:absolute;border-radius:50%;opacity:0;pointer-events:none;content:""}.mdc-ripple-surface::before{transition:opacity 15ms linear,background-color 15ms linear;z-index:1;z-index:var(--mdc-ripple-z-index, 1)}.mdc-ripple-surface::after{z-index:0;z-index:var(--mdc-ripple-z-index, 0)}.mdc-ripple-surface.mdc-ripple-upgraded::before{transform:scale(var(--mdc-ripple-fg-scale, 1))}.mdc-ripple-surface.mdc-ripple-upgraded::after{top:0;left:0;transform:scale(0);transform-origin:center center}.mdc-ripple-surface.mdc-ripple-upgraded--unbounded::after{top:var(--mdc-ripple-top, 0);left:var(--mdc-ripple-left, 0)}.mdc-ripple-surface.mdc-ripple-upgraded--foreground-activation::after{animation:mdc-ripple-fg-radius-in 225ms forwards,mdc-ripple-fg-opacity-in 75ms forwards}.mdc-ripple-surface.mdc-ripple-upgraded--foreground-deactivation::after{animation:mdc-ripple-fg-opacity-out 150ms;transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}.mdc-ripple-surface::before,.mdc-ripple-surface::after{top:calc(50% - 100%);left:calc(50% - 100%);width:200%;height:200%}.mdc-ripple-surface.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-ripple-surface[data-mdc-ripple-is-unbounded],.mdc-ripple-upgraded--unbounded{overflow:visible}.mdc-ripple-surface[data-mdc-ripple-is-unbounded]::before,.mdc-ripple-surface[data-mdc-ripple-is-unbounded]::after,.mdc-ripple-upgraded--unbounded::before,.mdc-ripple-upgraded--unbounded::after{top:calc(50% - 50%);left:calc(50% - 50%);width:100%;height:100%}.mdc-ripple-surface[data-mdc-ripple-is-unbounded].mdc-ripple-upgraded::before,.mdc-ripple-surface[data-mdc-ripple-is-unbounded].mdc-ripple-upgraded::after,.mdc-ripple-upgraded--unbounded.mdc-ripple-upgraded::before,.mdc-ripple-upgraded--unbounded.mdc-ripple-upgraded::after{top:var(--mdc-ripple-top, calc(50% - 50%));left:var(--mdc-ripple-left, calc(50% - 50%));width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-ripple-surface[data-mdc-ripple-is-unbounded].mdc-ripple-upgraded::after,.mdc-ripple-upgraded--unbounded.mdc-ripple-upgraded::after{width:var(--mdc-ripple-fg-size, 100%);height:var(--mdc-ripple-fg-size, 100%)}.mdc-ripple-surface::before,.mdc-ripple-surface::after{background-color:#000;background-color:var(--mdc-ripple-color, #000)}.mdc-ripple-surface:hover::before,.mdc-ripple-surface.mdc-ripple-surface--hover::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-ripple-surface.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-ripple-surface:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-ripple-surface.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}@keyframes mdc-ripple-fg-radius-in{from{animation-timing-function:cubic-bezier(0.4, 0, 0.2, 1);transform:translate(var(--mdc-ripple-fg-translate-start, 0)) scale(1)}to{transform:translate(var(--mdc-ripple-fg-translate-end, 0)) scale(var(--mdc-ripple-fg-scale, 1))}}@keyframes mdc-ripple-fg-opacity-in{from{animation-timing-function:linear;opacity:0}to{opacity:var(--mdc-ripple-fg-opacity, 0)}}@keyframes mdc-ripple-fg-opacity-out{from{animation-timing-function:linear;opacity:var(--mdc-ripple-fg-opacity, 0)}to{opacity:0}}:host{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;display:block}:host .mdc-ripple-surface{position:absolute;top:0;left:0;width:100%;height:100%;pointer-events:none;will-change:unset}.mdc-ripple-surface--primary::before,.mdc-ripple-surface--primary::after{background-color:#6200ee;background-color:var(--mdc-ripple-color, var(--mdc-theme-primary, #6200ee))}.mdc-ripple-surface--primary:hover::before,.mdc-ripple-surface--primary.mdc-ripple-surface--hover::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-ripple-surface--primary.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--primary:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-ripple-surface--primary:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--primary:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-ripple-surface--primary.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-ripple-surface--primary--activated::before{opacity:0.12;opacity:var(--mdc-ripple-activated-opacity, 0.12)}.mdc-ripple-surface--primary--activated::before,.mdc-ripple-surface--primary--activated::after{background-color:#6200ee;background-color:var(--mdc-ripple-color, var(--mdc-theme-primary, #6200ee))}.mdc-ripple-surface--primary--activated:hover::before,.mdc-ripple-surface--primary--activated.mdc-ripple-surface--hover::before{opacity:0.16;opacity:var(--mdc-ripple-hover-opacity, 0.16)}.mdc-ripple-surface--primary--activated.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--primary--activated:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.24;opacity:var(--mdc-ripple-focus-opacity, 0.24)}.mdc-ripple-surface--primary--activated:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--primary--activated:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.24;opacity:var(--mdc-ripple-press-opacity, 0.24)}.mdc-ripple-surface--primary--activated.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.24)}.mdc-ripple-surface--primary--selected::before{opacity:0.08;opacity:var(--mdc-ripple-selected-opacity, 0.08)}.mdc-ripple-surface--primary--selected::before,.mdc-ripple-surface--primary--selected::after{background-color:#6200ee;background-color:var(--mdc-ripple-color, var(--mdc-theme-primary, #6200ee))}.mdc-ripple-surface--primary--selected:hover::before,.mdc-ripple-surface--primary--selected.mdc-ripple-surface--hover::before{opacity:0.12;opacity:var(--mdc-ripple-hover-opacity, 0.12)}.mdc-ripple-surface--primary--selected.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--primary--selected:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.2;opacity:var(--mdc-ripple-focus-opacity, 0.2)}.mdc-ripple-surface--primary--selected:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--primary--selected:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.2;opacity:var(--mdc-ripple-press-opacity, 0.2)}.mdc-ripple-surface--primary--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.2)}.mdc-ripple-surface--accent::before,.mdc-ripple-surface--accent::after{background-color:#018786;background-color:var(--mdc-ripple-color, var(--mdc-theme-secondary, #018786))}.mdc-ripple-surface--accent:hover::before,.mdc-ripple-surface--accent.mdc-ripple-surface--hover::before{opacity:0.04;opacity:var(--mdc-ripple-hover-opacity, 0.04)}.mdc-ripple-surface--accent.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--accent:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-opacity, 0.12)}.mdc-ripple-surface--accent:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--accent:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-ripple-surface--accent.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.12)}.mdc-ripple-surface--accent--activated::before{opacity:0.12;opacity:var(--mdc-ripple-activated-opacity, 0.12)}.mdc-ripple-surface--accent--activated::before,.mdc-ripple-surface--accent--activated::after{background-color:#018786;background-color:var(--mdc-ripple-color, var(--mdc-theme-secondary, #018786))}.mdc-ripple-surface--accent--activated:hover::before,.mdc-ripple-surface--accent--activated.mdc-ripple-surface--hover::before{opacity:0.16;opacity:var(--mdc-ripple-hover-opacity, 0.16)}.mdc-ripple-surface--accent--activated.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--accent--activated:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.24;opacity:var(--mdc-ripple-focus-opacity, 0.24)}.mdc-ripple-surface--accent--activated:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--accent--activated:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.24;opacity:var(--mdc-ripple-press-opacity, 0.24)}.mdc-ripple-surface--accent--activated.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.24)}.mdc-ripple-surface--accent--selected::before{opacity:0.08;opacity:var(--mdc-ripple-selected-opacity, 0.08)}.mdc-ripple-surface--accent--selected::before,.mdc-ripple-surface--accent--selected::after{background-color:#018786;background-color:var(--mdc-ripple-color, var(--mdc-theme-secondary, #018786))}.mdc-ripple-surface--accent--selected:hover::before,.mdc-ripple-surface--accent--selected.mdc-ripple-surface--hover::before{opacity:0.12;opacity:var(--mdc-ripple-hover-opacity, 0.12)}.mdc-ripple-surface--accent--selected.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--accent--selected:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.2;opacity:var(--mdc-ripple-focus-opacity, 0.2)}.mdc-ripple-surface--accent--selected:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--accent--selected:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.2;opacity:var(--mdc-ripple-press-opacity, 0.2)}.mdc-ripple-surface--accent--selected.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-press-opacity, 0.2)}.mdc-ripple-surface--disabled{opacity:0}.mdc-ripple-surface--internal-use-state-layer-custom-properties::before,.mdc-ripple-surface--internal-use-state-layer-custom-properties::after{background-color:#000;background-color:var(--mdc-ripple-hover-state-layer-color, #000)}.mdc-ripple-surface--internal-use-state-layer-custom-properties:hover::before,.mdc-ripple-surface--internal-use-state-layer-custom-properties.mdc-ripple-surface--hover::before{opacity:0.04;opacity:var(--mdc-ripple-hover-state-layer-opacity, 0.04)}.mdc-ripple-surface--internal-use-state-layer-custom-properties.mdc-ripple-upgraded--background-focused::before,.mdc-ripple-surface--internal-use-state-layer-custom-properties:not(.mdc-ripple-upgraded):focus::before{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-focus-state-layer-opacity, 0.12)}.mdc-ripple-surface--internal-use-state-layer-custom-properties:not(.mdc-ripple-upgraded)::after{transition:opacity 150ms linear}.mdc-ripple-surface--internal-use-state-layer-custom-properties:not(.mdc-ripple-upgraded):active::after{transition-duration:75ms;opacity:0.12;opacity:var(--mdc-ripple-pressed-state-layer-opacity, 0.12)}.mdc-ripple-surface--internal-use-state-layer-custom-properties.mdc-ripple-upgraded{--mdc-ripple-fg-opacity:var(--mdc-ripple-pressed-state-layer-opacity, 0.12)}` +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */,Gt=h`mwc-list ::slotted([mwc-list-item]:not([twoline])),mwc-list ::slotted([noninteractive]:not([twoline])){height:var(--mdc-menu-item-height, 48px)}` +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */,Yt=h`.mdc-menu-surface{display:none;position:absolute;box-sizing:border-box;max-width:calc(100vw - 32px);max-width:var(--mdc-menu-max-width, calc(100vw - 32px));max-height:calc(100vh - 32px);max-height:var(--mdc-menu-max-height, calc(100vh - 32px));margin:0;padding:0;transform:scale(1);transform-origin:top left;opacity:0;overflow:auto;will-change:transform,opacity;z-index:8;transition:opacity .03s linear,transform .12s cubic-bezier(0, 0, 0.2, 1),height 250ms cubic-bezier(0, 0, 0.2, 1);box-shadow:0px 5px 5px -3px rgba(0, 0, 0, 0.2),0px 8px 10px 1px rgba(0, 0, 0, 0.14),0px 3px 14px 2px rgba(0,0,0,.12);background-color:#fff;background-color:var(--mdc-theme-surface, #fff);color:#000;color:var(--mdc-theme-on-surface, #000);border-radius:4px;border-radius:var(--mdc-shape-medium, 4px);transform-origin-left:top left;transform-origin-right:top right}.mdc-menu-surface:focus{outline:none}.mdc-menu-surface--animating-open{display:inline-block;transform:scale(0.8);opacity:0}.mdc-menu-surface--open{display:inline-block;transform:scale(1);opacity:1}.mdc-menu-surface--animating-closed{display:inline-block;opacity:0;transition:opacity .075s linear}[dir=rtl] .mdc-menu-surface,.mdc-menu-surface[dir=rtl]{transform-origin-left:top right;transform-origin-right:top left}.mdc-menu-surface--anchor{position:relative;overflow:visible}.mdc-menu-surface--fixed{position:fixed}.mdc-menu-surface--fullwidth{width:100%}:host(:not([open])){display:none}.mdc-menu-surface{z-index:8;z-index:var(--mdc-menu-z-index, 8);min-width:112px;min-width:var(--mdc-menu-min-width, 112px)}` +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */,qt=h`.mdc-notched-outline{display:flex;position:absolute;top:0;right:0;left:0;box-sizing:border-box;width:100%;max-width:100%;height:100%;text-align:left;pointer-events:none}[dir=rtl] .mdc-notched-outline,.mdc-notched-outline[dir=rtl]{text-align:right}.mdc-notched-outline__leading,.mdc-notched-outline__notch,.mdc-notched-outline__trailing{box-sizing:border-box;height:100%;border-top:1px solid;border-bottom:1px solid;pointer-events:none}.mdc-notched-outline__leading{border-left:1px solid;border-right:none;width:12px}[dir=rtl] .mdc-notched-outline__leading,.mdc-notched-outline__leading[dir=rtl]{border-left:none;border-right:1px solid}.mdc-notched-outline__trailing{border-left:none;border-right:1px solid;flex-grow:1}[dir=rtl] .mdc-notched-outline__trailing,.mdc-notched-outline__trailing[dir=rtl]{border-left:1px solid;border-right:none}.mdc-notched-outline__notch{flex:0 0 auto;width:auto;max-width:calc(100% - 12px * 2)}.mdc-notched-outline .mdc-floating-label{display:inline-block;position:relative;max-width:100%}.mdc-notched-outline .mdc-floating-label--float-above{text-overflow:clip}.mdc-notched-outline--upgraded .mdc-floating-label--float-above{max-width:calc(100% / 0.75)}.mdc-notched-outline--notched .mdc-notched-outline__notch{padding-left:0;padding-right:8px;border-top:none}[dir=rtl] .mdc-notched-outline--notched .mdc-notched-outline__notch,.mdc-notched-outline--notched .mdc-notched-outline__notch[dir=rtl]{padding-left:8px;padding-right:0}.mdc-notched-outline--no-label .mdc-notched-outline__notch{display:none}:host{display:block;position:absolute;right:0;left:0;box-sizing:border-box;width:100%;max-width:100%;height:100%;text-align:left;pointer-events:none}[dir=rtl] :host,:host([dir=rtl]){text-align:right}::slotted(.mdc-floating-label){display:inline-block;position:relative;top:17px;bottom:auto;max-width:100%}::slotted(.mdc-floating-label--float-above){text-overflow:clip}.mdc-notched-outline--upgraded ::slotted(.mdc-floating-label--float-above){max-width:calc(100% / 0.75)}.mdc-notched-outline .mdc-notched-outline__leading{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}[dir=rtl] .mdc-notched-outline .mdc-notched-outline__leading,.mdc-notched-outline .mdc-notched-outline__leading[dir=rtl]{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}@supports(top: max(0%)){.mdc-notched-outline .mdc-notched-outline__leading{width:max(12px, var(--mdc-shape-small, 4px))}}@supports(top: max(0%)){.mdc-notched-outline .mdc-notched-outline__notch{max-width:calc(100% - max(12px, var(--mdc-shape-small, 4px)) * 2)}}.mdc-notched-outline .mdc-notched-outline__trailing{border-top-left-radius:0;border-top-right-radius:4px;border-top-right-radius:var(--mdc-shape-small, 4px);border-bottom-right-radius:4px;border-bottom-right-radius:var(--mdc-shape-small, 4px);border-bottom-left-radius:0}[dir=rtl] .mdc-notched-outline .mdc-notched-outline__trailing,.mdc-notched-outline .mdc-notched-outline__trailing[dir=rtl]{border-top-left-radius:4px;border-top-left-radius:var(--mdc-shape-small, 4px);border-top-right-radius:0;border-bottom-right-radius:0;border-bottom-left-radius:4px;border-bottom-left-radius:var(--mdc-shape-small, 4px)}.mdc-notched-outline__leading,.mdc-notched-outline__notch,.mdc-notched-outline__trailing{border-color:var(--mdc-notched-outline-border-color, var(--mdc-theme-primary, #6200ee));border-width:1px;border-width:var(--mdc-notched-outline-stroke-width, 1px)}.mdc-notched-outline--notched .mdc-notched-outline__notch{padding-top:0;padding-top:var(--mdc-notched-outline-notch-offset, 0)}`,Wt={"mwc-select":class extends dt{static get styles(){return Ht}},"mwc-list":class extends ut{static get styles(){return Ut}},"mwc-list-item":class extends _t{static get styles(){return jt}},"mwc-ripple":class extends Vt{static get styles(){return Xt}},"mwc-menu":class extends It{static get styles(){return Gt}},"mwc-menu-surface":class extends At{static get styles(){return Yt}},"mwc-notched-outline":class extends zt{static get styles(){return qt}}};function Kt(e,t,i){if(void 0!==t) +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +return function(e,t,i){const o=e.constructor;if(!i){const e=`__${t}`;if(!(i=o.getPropertyDescriptor(t,e)))throw new Error("@ariaProperty must be used after a @property decorator")}const n=i;let a="";if(!n.set)throw new Error(`@ariaProperty requires a setter for ${t}`);if(e.dispatchWizEvent)return i;const d={configurable:!0,enumerable:!0,set(e){if(""===a){const e=o.getPropertyOptions(t);a="string"==typeof e.attribute?e.attribute:t}this.hasAttribute(a)&&this.removeAttribute(a),n.set.call(this,e)}};return n.get&&(d.get=function(){return n.get.call(this)}),d}(e,t,i);throw new Error("@ariaProperty only supports TypeScript Decorators")} +/** + * @license + * Copyright 2018 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */var Qt={CHECKED:"mdc-switch--checked",DISABLED:"mdc-switch--disabled"},Zt={ARIA_CHECKED_ATTR:"aria-checked",NATIVE_CONTROL_SELECTOR:".mdc-switch__native-control",RIPPLE_SURFACE_SELECTOR:".mdc-switch__thumb-underlay"},Jt=function(e){function t(i){return e.call(this,o(o({},t.defaultAdapter),i))||this}return i(t,e),Object.defineProperty(t,"strings",{get:function(){return Zt},enumerable:!1,configurable:!0}),Object.defineProperty(t,"cssClasses",{get:function(){return Qt},enumerable:!1,configurable:!0}),Object.defineProperty(t,"defaultAdapter",{get:function(){return{addClass:function(){},removeClass:function(){},setNativeControlChecked:function(){},setNativeControlDisabled:function(){},setNativeControlAttr:function(){}}},enumerable:!1,configurable:!0}),t.prototype.setChecked=function(e){this.adapter.setNativeControlChecked(e),this.updateAriaChecked(e),this.updateCheckedStyling(e)},t.prototype.setDisabled=function(e){this.adapter.setNativeControlDisabled(e),e?this.adapter.addClass(Qt.DISABLED):this.adapter.removeClass(Qt.DISABLED)},t.prototype.handleChange=function(e){var t=e.target;this.updateAriaChecked(t.checked),this.updateCheckedStyling(t.checked)},t.prototype.updateCheckedStyling=function(e){e?this.adapter.addClass(Qt.CHECKED):this.adapter.removeClass(Qt.CHECKED)},t.prototype.updateAriaChecked=function(e){this.adapter.setNativeControlAttr(Zt.ARIA_CHECKED_ATTR,""+!!e)},t}(A); +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ +class ei extends N{constructor(){super(...arguments),this.checked=!1,this.disabled=!1,this.shouldRenderRipple=!1,this.mdcFoundationClass=Jt,this.rippleHandlers=new ft((()=>(this.shouldRenderRipple=!0,this.ripple)))}changeHandler(e){this.mdcFoundation.handleChange(e),this.checked=this.formElement.checked}createAdapter(){return Object.assign(Object.assign({},R(this.mdcRoot)),{setNativeControlChecked:e=>{this.formElement.checked=e},setNativeControlDisabled:e=>{this.formElement.disabled=e},setNativeControlAttr:(e,t)=>{this.formElement.setAttribute(e,t)}})}renderRipple(){return this.shouldRenderRipple?m` + + `:""}focus(){const e=this.formElement;e&&(this.rippleHandlers.startFocus(),e.focus())}blur(){const e=this.formElement;e&&(this.rippleHandlers.endFocus(),e.blur())}click(){this.formElement&&!this.disabled&&(this.formElement.focus(),this.formElement.click())}firstUpdated(){super.firstUpdated(),this.shadowRoot&&this.mdcRoot.addEventListener("change",(e=>{this.dispatchEvent(new Event("change",e))}))}render(){return m` +
    +
    +
    + ${this.renderRipple()} +
    + +
    +
    +
    `}handleRippleMouseDown(e){const t=()=>{window.removeEventListener("mouseup",t),this.handleRippleDeactivate()};window.addEventListener("mouseup",t),this.rippleHandlers.startPress(e)}handleRippleTouchStart(e){this.rippleHandlers.startPress(e)}handleRippleDeactivate(){this.rippleHandlers.endPress()}handleRippleMouseEnter(){this.rippleHandlers.startHover()}handleRippleMouseLeave(){this.rippleHandlers.endHover()}handleRippleFocus(){this.rippleHandlers.startFocus()}handleRippleBlur(){this.rippleHandlers.endFocus()}}a([d({type:Boolean}),H((function(e){this.mdcFoundation.setChecked(e)}))],ei.prototype,"checked",void 0),a([d({type:Boolean}),H((function(e){this.mdcFoundation.setDisabled(e)}))],ei.prototype,"disabled",void 0),a([Kt,d({attribute:"aria-label"})],ei.prototype,"ariaLabel",void 0),a([Kt,d({attribute:"aria-labelledby"})],ei.prototype,"ariaLabelledBy",void 0),a([b(".mdc-switch")],ei.prototype,"mdcRoot",void 0),a([b("input")],ei.prototype,"formElement",void 0),a([y("mwc-ripple")],ei.prototype,"ripple",void 0),a([u()],ei.prototype,"shouldRenderRipple",void 0),a([v({passive:!0})],ei.prototype,"handleRippleMouseDown",null),a([v({passive:!0})],ei.prototype,"handleRippleTouchStart",null); +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */ +const ti=h`.mdc-switch__thumb-underlay{left:-14px;right:initial;top:-17px;width:48px;height:48px}[dir=rtl] .mdc-switch__thumb-underlay,.mdc-switch__thumb-underlay[dir=rtl]{left:initial;right:-14px}.mdc-switch__native-control{width:64px;height:48px}.mdc-switch{display:inline-block;position:relative;outline:none;user-select:none}.mdc-switch.mdc-switch--checked .mdc-switch__track{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786)}.mdc-switch.mdc-switch--checked .mdc-switch__thumb{background-color:#018786;background-color:var(--mdc-theme-secondary, #018786);border-color:#018786;border-color:var(--mdc-theme-secondary, #018786)}.mdc-switch:not(.mdc-switch--checked) .mdc-switch__track{background-color:#000;background-color:var(--mdc-theme-on-surface, #000)}.mdc-switch:not(.mdc-switch--checked) .mdc-switch__thumb{background-color:#fff;background-color:var(--mdc-theme-surface, #fff);border-color:#fff;border-color:var(--mdc-theme-surface, #fff)}.mdc-switch__native-control{left:0;right:initial;position:absolute;top:0;margin:0;opacity:0;cursor:pointer;pointer-events:auto;transition:transform 90ms cubic-bezier(0.4, 0, 0.2, 1)}[dir=rtl] .mdc-switch__native-control,.mdc-switch__native-control[dir=rtl]{left:initial;right:0}.mdc-switch__track{box-sizing:border-box;width:36px;height:14px;border:1px solid transparent;border-radius:7px;opacity:.38;transition:opacity 90ms cubic-bezier(0.4, 0, 0.2, 1),background-color 90ms cubic-bezier(0.4, 0, 0.2, 1),border-color 90ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-switch__thumb-underlay{display:flex;position:absolute;align-items:center;justify-content:center;transform:translateX(0);transition:transform 90ms cubic-bezier(0.4, 0, 0.2, 1),background-color 90ms cubic-bezier(0.4, 0, 0.2, 1),border-color 90ms cubic-bezier(0.4, 0, 0.2, 1)}.mdc-switch__thumb{box-shadow:0px 3px 1px -2px rgba(0, 0, 0, 0.2),0px 2px 2px 0px rgba(0, 0, 0, 0.14),0px 1px 5px 0px rgba(0,0,0,.12);box-sizing:border-box;width:20px;height:20px;border:10px solid;border-radius:50%;pointer-events:none;z-index:1}.mdc-switch--checked .mdc-switch__track{opacity:.54}.mdc-switch--checked .mdc-switch__thumb-underlay{transform:translateX(16px)}[dir=rtl] .mdc-switch--checked .mdc-switch__thumb-underlay,.mdc-switch--checked .mdc-switch__thumb-underlay[dir=rtl]{transform:translateX(-16px)}.mdc-switch--checked .mdc-switch__native-control{transform:translateX(-16px)}[dir=rtl] .mdc-switch--checked .mdc-switch__native-control,.mdc-switch--checked .mdc-switch__native-control[dir=rtl]{transform:translateX(16px)}.mdc-switch--disabled{opacity:.38;pointer-events:none}.mdc-switch--disabled .mdc-switch__thumb{border-width:1px}.mdc-switch--disabled .mdc-switch__native-control{cursor:default;pointer-events:none}:host{display:inline-flex;outline:none;-webkit-tap-highlight-color:transparent}`,ii={"mwc-switch":class extends ei{static get styles(){return ti}},"mwc-ripple":class extends Vt{static get styles(){return Xt}}}; +/** + * @license + * Copyright 2016 Google Inc. + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + * THE SOFTWARE. + */ +var oi={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"},ni={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"},ai={LABEL_SCALE:.75},di=["pattern","min","max","required","step","minlength","maxlength"],ri=["color","date","datetime-local","month","range","time","week"],li=["mousedown","touchstart"],si=["click","keydown"],ci=function(e){function t(i,n){void 0===n&&(n={});var a=e.call(this,o(o({},t.defaultAdapter),i))||this;return a.isFocused=!1,a.receivedUserInput=!1,a.valid=!0,a.useNativeValidation=!0,a.validateOnValueChange=!0,a.helperText=n.helperText,a.characterCounter=n.characterCounter,a.leadingIcon=n.leadingIcon,a.trailingIcon=n.trailingIcon,a.inputFocusHandler=function(){a.activateFocus()},a.inputBlurHandler=function(){a.deactivateFocus()},a.inputInputHandler=function(){a.handleInput()},a.setPointerXOffset=function(e){a.setTransformOrigin(e)},a.textFieldInteractionHandler=function(){a.handleTextFieldInteraction()},a.validationAttributeChangeHandler=function(e){a.handleValidationAttributeChange(e)},a}return i(t,e),Object.defineProperty(t,"cssClasses",{get:function(){return ni},enumerable:!1,configurable:!0}),Object.defineProperty(t,"strings",{get:function(){return oi},enumerable:!1,configurable:!0}),Object.defineProperty(t,"numbers",{get:function(){return ai},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"shouldAlwaysFloat",{get:function(){var e=this.getNativeInput().type;return ri.indexOf(e)>=0},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"shouldFloat",{get:function(){return this.shouldAlwaysFloat||this.isFocused||!!this.getValue()||this.isBadInput()},enumerable:!1,configurable:!0}),Object.defineProperty(t.prototype,"shouldShake",{get:function(){return!this.isFocused&&!this.isValid()&&!!this.getValue()},enumerable:!1,configurable:!0}),Object.defineProperty(t,"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}),t.prototype.init=function(){var e,t,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=f(li),a=n.next();!a.done;a=n.next()){var d=a.value;this.adapter.registerInputInteractionHandler(d,this.setPointerXOffset)}}catch(t){e={error:t}}finally{try{a&&!a.done&&(t=n.return)&&t.call(n)}finally{if(e)throw e.error}}try{for(var r=f(si),l=r.next();!l.done;l=r.next()){d=l.value;this.adapter.registerTextFieldInteractionHandler(d,this.textFieldInteractionHandler)}}catch(e){i={error:e}}finally{try{l&&!l.done&&(o=r.return)&&o.call(r)}finally{if(i)throw i.error}}this.validationObserver=this.adapter.registerValidationAttributeChangeHandler(this.validationAttributeChangeHandler),this.setcharacterCounter(this.getValue().length)},t.prototype.destroy=function(){var e,t,i,o;this.adapter.deregisterInputInteractionHandler("focus",this.inputFocusHandler),this.adapter.deregisterInputInteractionHandler("blur",this.inputBlurHandler),this.adapter.deregisterInputInteractionHandler("input",this.inputInputHandler);try{for(var n=f(li),a=n.next();!a.done;a=n.next()){var d=a.value;this.adapter.deregisterInputInteractionHandler(d,this.setPointerXOffset)}}catch(t){e={error:t}}finally{try{a&&!a.done&&(t=n.return)&&t.call(n)}finally{if(e)throw e.error}}try{for(var r=f(si),l=r.next();!l.done;l=r.next()){d=l.value;this.adapter.deregisterTextFieldInteractionHandler(d,this.textFieldInteractionHandler)}}catch(e){i={error:e}}finally{try{l&&!l.done&&(o=r.return)&&o.call(r)}finally{if(i)throw i.error}}this.adapter.deregisterValidationAttributeChangeHandler(this.validationObserver)},t.prototype.handleTextFieldInteraction=function(){var e=this.adapter.getNativeInput();e&&e.disabled||(this.receivedUserInput=!0)},t.prototype.handleValidationAttributeChange=function(e){var t=this;e.some((function(e){return di.indexOf(e)>-1&&(t.styleValidity(!0),t.adapter.setLabelRequired(t.getNativeInput().required),!0)})),e.indexOf("maxlength")>-1&&this.setcharacterCounter(this.getValue().length)},t.prototype.notchOutline=function(e){if(this.adapter.hasOutline()&&this.adapter.hasLabel())if(e){var t=this.adapter.getLabelWidth()*ai.LABEL_SCALE;this.adapter.notchOutline(t)}else this.adapter.closeOutline()},t.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()},t.prototype.setTransformOrigin=function(e){if(!this.isDisabled()&&!this.adapter.hasOutline()){var t=e.touches,i=t?t[0]:e,o=i.target.getBoundingClientRect(),n=i.clientX-o.left;this.adapter.setLineRippleTransformOrigin(n)}},t.prototype.handleInput=function(){this.autoCompleteFocus(),this.setcharacterCounter(this.getValue().length)},t.prototype.autoCompleteFocus=function(){this.receivedUserInput||this.activateFocus()},t.prototype.deactivateFocus=function(){this.isFocused=!1,this.adapter.deactivateLineRipple();var e=this.isValid();this.styleValidity(e),this.styleFocused(this.isFocused),this.adapter.hasLabel()&&(this.notchOutline(this.shouldFloat),this.adapter.floatLabel(this.shouldFloat),this.styleFloating(this.shouldFloat),this.adapter.shakeLabel(this.shouldShake)),this.shouldFloat||(this.receivedUserInput=!1)},t.prototype.getValue=function(){return this.getNativeInput().value},t.prototype.setValue=function(e){if(this.getValue()!==e&&(this.getNativeInput().value=e),this.setcharacterCounter(e.length),this.validateOnValueChange){var t=this.isValid();this.styleValidity(t)}this.adapter.hasLabel()&&(this.notchOutline(this.shouldFloat),this.adapter.floatLabel(this.shouldFloat),this.styleFloating(this.shouldFloat),this.validateOnValueChange&&this.adapter.shakeLabel(this.shouldShake))},t.prototype.isValid=function(){return this.useNativeValidation?this.isNativeInputValid():this.valid},t.prototype.setValid=function(e){this.valid=e,this.styleValidity(e);var t=!e&&!this.isFocused&&!!this.getValue();this.adapter.hasLabel()&&this.adapter.shakeLabel(t)},t.prototype.setValidateOnValueChange=function(e){this.validateOnValueChange=e},t.prototype.getValidateOnValueChange=function(){return this.validateOnValueChange},t.prototype.setUseNativeValidation=function(e){this.useNativeValidation=e},t.prototype.isDisabled=function(){return this.getNativeInput().disabled},t.prototype.setDisabled=function(e){this.getNativeInput().disabled=e,this.styleDisabled(e)},t.prototype.setHelperTextContent=function(e){this.helperText&&this.helperText.setContent(e)},t.prototype.setLeadingIconAriaLabel=function(e){this.leadingIcon&&this.leadingIcon.setAriaLabel(e)},t.prototype.setLeadingIconContent=function(e){this.leadingIcon&&this.leadingIcon.setContent(e)},t.prototype.setTrailingIconAriaLabel=function(e){this.trailingIcon&&this.trailingIcon.setAriaLabel(e)},t.prototype.setTrailingIconContent=function(e){this.trailingIcon&&this.trailingIcon.setContent(e)},t.prototype.setcharacterCounter=function(e){if(this.characterCounter){var t=this.getNativeInput().maxLength;if(-1===t)throw new Error("MDCTextFieldFoundation: Expected maxlength html property on text input or textarea.");this.characterCounter.setCounterValue(e,t)}},t.prototype.isBadInput=function(){return this.getNativeInput().validity.badInput||!1},t.prototype.isNativeInputValid=function(){return this.getNativeInput().validity.valid},t.prototype.styleValidity=function(e){var i=t.cssClasses.INVALID;if(e?this.adapter.removeClass(i):this.adapter.addClass(i),this.helperText){if(this.helperText.setValidity(e),!this.helperText.isValidation())return;var o=this.helperText.isVisible(),n=this.helperText.getId();o&&n?this.adapter.setInputAttr(oi.ARIA_DESCRIBEDBY,n):this.adapter.removeInputAttr(oi.ARIA_DESCRIBEDBY)}},t.prototype.styleFocused=function(e){var i=t.cssClasses.FOCUSED;e?this.adapter.addClass(i):this.adapter.removeClass(i)},t.prototype.styleDisabled=function(e){var i=t.cssClasses,o=i.DISABLED,n=i.INVALID;e?(this.adapter.addClass(o),this.adapter.removeClass(n)):this.adapter.removeClass(o),this.leadingIcon&&this.leadingIcon.setDisabled(e),this.trailingIcon&&this.trailingIcon.setDisabled(e)},t.prototype.styleFloating=function(e){var i=t.cssClasses.LABEL_FLOATING;e?this.adapter.addClass(i):this.adapter.removeClass(i)},t.prototype.getNativeInput=function(){return(this.adapter?this.adapter.getNativeInput():null)||{disabled:!1,maxLength:-1,required:!1,type:"input",validity:{badInput:!1,valid:!0},value:""}},t}(A),mi=ci; +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const hi={},pi=r(class extends l{constructor(e){if(super(e),e.type!==s.PROPERTY&&e.type!==s.ATTRIBUTE&&e.type!==s.BOOLEAN_ATTRIBUTE)throw Error("The `live` directive is not allowed on child or event bindings");if(!(e=>void 0===e.strings)(e))throw Error("`live` bindings can only contain a single expression")}render(e){return e}update(e,[t]){if(t===c||t===p)return t;const i=e.element,o=e.name;if(e.type===s.PROPERTY){if(t===i[o])return c}else if(e.type===s.BOOLEAN_ATTRIBUTE){if(!!t===i.hasAttribute(o))return c}else if(e.type===s.ATTRIBUTE&&i.getAttribute(o)===t+"")return c;return((e,t=hi)=>{e._$AH=t; +/** + * @license + * Copyright 2020 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */})(e),t}}),ui=["touchstart","touchmove","scroll","mousewheel"],fi=(e={})=>{const t={};for(const i in e)t[i]=e[i];return Object.assign({badInput:!1,customError:!1,patternMismatch:!1,rangeOverflow:!1,rangeUnderflow:!1,stepMismatch:!1,tooLong:!1,tooShort:!1,typeMismatch:!1,valid:!0,valueMissing:!1},t)};class _i extends z{constructor(){super(...arguments),this.mdcFoundationClass=mi,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=fi(),this.validityTransform=null}get validity(){return this._checkValidity(this.value),this._validity}get willValidate(){return this.formElement.willValidate}get selectionStart(){return this.formElement.selectionStart}get selectionEnd(){return this.formElement.selectionEnd}focus(){const e=new CustomEvent("focus");this.formElement.dispatchEvent(e),this.formElement.focus()}blur(){const e=new CustomEvent("blur");this.formElement.dispatchEvent(e),this.formElement.blur()}select(){this.formElement.select()}setSelectionRange(e,t,i){this.formElement.setSelectionRange(e,t,i)}update(e){e.has("autoValidate")&&this.mdcFoundation&&this.mdcFoundation.setValidateOnValueChange(this.autoValidate),e.has("value")&&"string"!=typeof this.value&&(this.value=`${this.value}`),super.update(e)}setFormData(e){this.name&&e.append(this.name,this.value)}render(){const e=this.charCounter&&-1!==this.maxLength,t=!!this.helper||!!this.validationMessage||e,i={"mdc-text-field--disabled":this.disabled,"mdc-text-field--no-label":!this.label,"mdc-text-field--filled":!this.outlined,"mdc-text-field--outlined":this.outlined,"mdc-text-field--with-leading-icon":this.icon,"mdc-text-field--with-trailing-icon":this.iconTrailing,"mdc-text-field--end-aligned":this.endAligned};return m` + + ${this.renderHelperText(t,e)} + `}updated(e){e.has("value")&&void 0!==e.get("value")&&(this.mdcFoundation.setValue(this.value),this.autoValidate&&this.reportValidity())}renderRipple(){return this.outlined?"":m` + + `}renderOutline(){return this.outlined?m` + + ${this.renderLabel()} + `:""}renderLabel(){return this.label?m` + ${this.label} + `:""}renderLeadingIcon(){return this.icon?this.renderIcon(this.icon):""}renderTrailingIcon(){return this.iconTrailing?this.renderIcon(this.iconTrailing,!0):""}renderIcon(e,t=!1){return m`${e}`}renderPrefix(){return this.prefix?this.renderAffix(this.prefix):""}renderSuffix(){return this.suffix?this.renderAffix(this.suffix,!0):""}renderAffix(e,t=!1){return m` + ${e}`}renderInput(e){const t=-1===this.minLength?void 0:this.minLength,i=-1===this.maxLength?void 0:this.maxLength,o=this.autocapitalize?this.autocapitalize:void 0,n=this.validationMessage&&!this.isUiValid,a=this.label?"label":void 0,d=e?"helper-text":void 0,r=this.focused||this.helperPersistent||n?"helper-text":void 0;return m` + `}renderLineRipple(){return this.outlined?"":m` + + `}renderHelperText(e,t){const i=this.validationMessage&&!this.isUiValid,o={"mdc-text-field-helper-text--persistent":this.helperPersistent,"mdc-text-field-helper-text--validation-msg":i},n=this.focused||this.helperPersistent||i?void 0:"true",a=i?this.validationMessage:this.helper;return e?m` +
    +
    ${a}
    + ${this.renderCharCounter(t)} +
    `:""}renderCharCounter(e){const t=Math.min(this.value.length,this.maxLength);return e?m` + ${t} / ${this.maxLength}`:""}onInputFocus(){this.focused=!0}onInputBlur(){this.focused=!1,this.reportValidity()}checkValidity(){const e=this._checkValidity(this.value);if(!e){const e=new Event("invalid",{bubbles:!1,cancelable:!0});this.dispatchEvent(e)}return e}reportValidity(){const e=this.checkValidity();return this.mdcFoundation.setValid(e),this.isUiValid=e,e}_checkValidity(e){const t=this.formElement.validity;let i=fi(t);if(this.validityTransform){const t=this.validityTransform(e,i);i=Object.assign(Object.assign({},i),t),this.mdcFoundation.setUseNativeValidation(!1)}else this.mdcFoundation.setUseNativeValidation(!0);return this._validity=i,this._validity.valid}setCustomValidity(e){this.validationMessage=e,this.formElement.setCustomValidity(e)}handleInputChange(){this.value=this.formElement.value}createAdapter(){return Object.assign(Object.assign(Object.assign(Object.assign(Object.assign({},this.getRootAdapterMethods()),this.getInputAdapterMethods()),this.getLabelAdapterMethods()),this.getLineRippleAdapterMethods()),this.getOutlineAdapterMethods())}getRootAdapterMethods(){return Object.assign({registerTextFieldInteractionHandler:(e,t)=>this.addEventListener(e,t),deregisterTextFieldInteractionHandler:(e,t)=>this.removeEventListener(e,t),registerValidationAttributeChangeHandler:e=>{const t=new MutationObserver((t=>{e((e=>e.map((e=>e.attributeName)).filter((e=>e)))(t))}));return t.observe(this.formElement,{attributes:!0}),t},deregisterValidationAttributeChangeHandler:e=>e.disconnect()},R(this.mdcRoot))}getInputAdapterMethods(){return{getNativeInput:()=>this.formElement,setInputAttr:()=>{},removeInputAttr:()=>{},isFocused:()=>!!this.shadowRoot&&this.shadowRoot.activeElement===this.formElement,registerInputInteractionHandler:(e,t)=>this.formElement.addEventListener(e,t,{passive:e in ui}),deregisterInputInteractionHandler:(e,t)=>this.formElement.removeEventListener(e,t)}}getLabelAdapterMethods(){return{floatLabel:e=>this.labelElement&&this.labelElement.floatingLabelFoundation.float(e),getLabelWidth:()=>this.labelElement?this.labelElement.floatingLabelFoundation.getWidth():0,hasLabel:()=>Boolean(this.labelElement),shakeLabel:e=>this.labelElement&&this.labelElement.floatingLabelFoundation.shake(e),setLabelRequired:e=>{this.labelElement&&this.labelElement.floatingLabelFoundation.setRequired(e)}}}getLineRippleAdapterMethods(){return{activateLineRipple:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.activate()},deactivateLineRipple:()=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.deactivate()},setLineRippleTransformOrigin:e=>{this.lineRippleElement&&this.lineRippleElement.lineRippleFoundation.setRippleCenter(e)}}}async getUpdateComplete(){var e;const t=await super.getUpdateComplete();return await(null===(e=this.outlineElement)||void 0===e?void 0:e.updateComplete),t}firstUpdated(){var e;super.firstUpdated(),this.mdcFoundation.setValidateOnValueChange(this.autoValidate),this.validateOnInitialRender&&this.reportValidity(),null===(e=this.outlineElement)||void 0===e||e.updateComplete.then((()=>{var e;this.outlineWidth=(null===(e=this.labelElement)||void 0===e?void 0:e.floatingLabelFoundation.getWidth())||0}))}getOutlineAdapterMethods(){return{closeOutline:()=>this.outlineElement&&(this.outlineOpen=!1),hasOutline:()=>Boolean(this.outlineElement),notchOutline:e=>{this.outlineElement&&!this.outlineOpen&&(this.outlineWidth=e,this.outlineOpen=!0)}}}async layout(){await this.updateComplete;const e=this.labelElement;if(!e)return void(this.outlineOpen=!1);const t=!!this.label&&!!this.value;if(e.floatingLabelFoundation.float(t),!this.outlined)return;this.outlineOpen=t,await this.updateComplete;const i=e.floatingLabelFoundation.getWidth();this.outlineOpen&&(this.outlineWidth=i,await this.updateComplete)}}a([b(".mdc-text-field")],_i.prototype,"mdcRoot",void 0),a([b("input")],_i.prototype,"formElement",void 0),a([b(".mdc-floating-label")],_i.prototype,"labelElement",void 0),a([b(".mdc-line-ripple")],_i.prototype,"lineRippleElement",void 0),a([b("mwc-notched-outline")],_i.prototype,"outlineElement",void 0),a([b(".mdc-notched-outline__notch")],_i.prototype,"notchElement",void 0),a([d({type:String})],_i.prototype,"value",void 0),a([d({type:String})],_i.prototype,"type",void 0),a([d({type:String})],_i.prototype,"placeholder",void 0),a([d({type:String}),H((function(e,t){void 0!==t&&this.label!==t&&this.layout()}))],_i.prototype,"label",void 0),a([d({type:String})],_i.prototype,"icon",void 0),a([d({type:String})],_i.prototype,"iconTrailing",void 0),a([d({type:Boolean,reflect:!0})],_i.prototype,"disabled",void 0),a([d({type:Boolean})],_i.prototype,"required",void 0),a([d({type:Number})],_i.prototype,"minLength",void 0),a([d({type:Number})],_i.prototype,"maxLength",void 0),a([d({type:Boolean,reflect:!0}),H((function(e,t){void 0!==t&&this.outlined!==t&&this.layout()}))],_i.prototype,"outlined",void 0),a([d({type:String})],_i.prototype,"helper",void 0),a([d({type:Boolean})],_i.prototype,"validateOnInitialRender",void 0),a([d({type:String})],_i.prototype,"validationMessage",void 0),a([d({type:Boolean})],_i.prototype,"autoValidate",void 0),a([d({type:String})],_i.prototype,"pattern",void 0),a([d({type:String})],_i.prototype,"min",void 0),a([d({type:String})],_i.prototype,"max",void 0),a([d({type:String})],_i.prototype,"step",void 0),a([d({type:Number})],_i.prototype,"size",void 0),a([d({type:Boolean})],_i.prototype,"helperPersistent",void 0),a([d({type:Boolean})],_i.prototype,"charCounter",void 0),a([d({type:Boolean})],_i.prototype,"endAligned",void 0),a([d({type:String})],_i.prototype,"prefix",void 0),a([d({type:String})],_i.prototype,"suffix",void 0),a([d({type:String})],_i.prototype,"name",void 0),a([d({type:String})],_i.prototype,"inputMode",void 0),a([d({type:Boolean})],_i.prototype,"readOnly",void 0),a([d({type:String})],_i.prototype,"autocapitalize",void 0),a([u()],_i.prototype,"outlineOpen",void 0),a([u()],_i.prototype,"outlineWidth",void 0),a([u()],_i.prototype,"isUiValid",void 0),a([u()],_i.prototype,"focused",void 0),a([v({passive:!0})],_i.prototype,"handleInputChange",null); +/** + * @license + * Copyright 2021 Google LLC + * SPDX-LIcense-Identifier: Apache-2.0 + */ +const gi=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;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))}`,vi={"mwc-textfield":class extends _i{static get styles(){return gi}},"mwc-notched-outline":class extends zt{static get styles(){return qt}}};let bi=class extends n{constructor(){super(...arguments),this._subElementEditor=void 0,this._initialized=!1,this._config_version=8}setConfig(e){this._config=e;let t=!1;null===this._section_order?(this._config=Object.assign(Object.assign({},this._config),{section_order:T}),t=!0):(this._config.section_order.forEach((e=>{var i,o;if(!T.includes(e)){const n=null===(i=this._config)||void 0===i?void 0:i.section_order.indexOf(e);void 0!==n&&-1!==n&&(null===(o=this._config)||void 0===o||o.section_order.splice(n,1)),t=!0}})),T.forEach((e=>{this._config&&!this._config.section_order.includes(e)&&(this._config.section_order.push(e),t=!0)}))),t&&_(this,"config-changed",{config:this.sortObjectByKeys(this._config)}),this.loadCardHelpers()}sortObjectByKeys(e){return Object.keys(e).sort().reduce(((t,i)=>(t[i]=e[i],t)),{})}_configCleanup(){if(!this._config||!this.hass)return;let e=Object.assign({},this._config);e.static_icons&&(e.option_static_icons=e.static_icons,delete e.static_icons),e.time_format&&(e.option_time_format="12"===e.time_format?"12hour":"24hour",delete e.time_format),e.locale&&(e.option_locale=e.locale,delete e.locale),e.option_today_temperature_decimals&&(e.option_today_temperature_decimals=e.show_today_decimals,delete e.show_today_decimals),e.show_decimals_pressure&&(e.option_pressure_decimals=e.show_decimals_pressure,delete e.show_decimals_pressure),e.tooltips&&(e.option_tooltips=e.tooltips,delete e.tooltips),e.show_beaufort&&(e.option_show_beaufort=e.show_beaufort,delete e.show_beaufort),e.entity_daytime_high&&(e.Entity_forecast_max=e.entity_daytime_high,delete e.entity_daytime_high),e.entity_daytime_low&&(e.entity_forecast_min=e.entity_daytime_low,delete e.entity_daytime_low),e.entity_current_conditions&&(e.entity_forecast_icon=e.entity_current_conditions,delete e.entity_current_conditions),e.entity_current_text&&(e.entity_summary=e.entity_current_text,delete e.entity_current_text),e.entity_daily_summary&&(e.entity_extended=e.entity_daily_summary,delete e.entity_daily_summary),e.entity_forecast_high_temp_1&&(e.entity_forecast_max_1=e.entity_forecast_high_temp_1,delete e.entity_forecast_high_temp_1),e.entity_forecast_low_temp_1&&(e.entity_forecast_min_1=e.entity_forecast_low_temp_1,delete e.entity_forecast_low_temp_1),e.entity_possible_today&&(e.entity_pos=e.entity_possible_today,delete e.entity_possible_today),e.entity_fire_danger_summary&&(e.entity_fire_danger=e.entity_fire_danger_summary,delete e.entity_fire_danger_summary),e.show_decimals&&(e.option_show_overview_decimals=e.show_decimals,delete e.show_decimals),e.show_separator&&(e.option_show_overview_separator=e.show_separator,delete e.show_separator);for(const t of["slot_l1, slot_l2, slot_l3, slot_l4, slot_l5, slot_l6, slot_l7, slot_l8, slot_r1, slot_r2, slot_r3, slot_r4, slot_r5, slot_r6, slot_r7, slot_r8"])"daytime_high"===e[t]&&(e[t]="forecast_max"),"daytime_low"===e[t]&&(e[t]="forecast_min");const t=["type","card_config_version","section_order","show_section_overview","show_section_extended","show_section_slots","show_section_daily_forecast","overview_layout","text_card_title","text_card_title_2","entity_update_time","update_time_use_attr","update_time_name_attr","text_update_time_prefix","entity_temperature","entity_apparent_temp","entity_forecast_icon","entity_summary","option_show_overview_decimals","option_show_overview_separator","entity_extended","extended_use_attr","extended_name_attr","slot_l1","slot_l2","slot_l3","slot_l4","slot_l5","slot_l6","slot_l7","slot_l8","slot_r1","slot_r2","slot_r3","slot_r4","slot_r5","slot_r6","slot_r7","slot_r8","entity_humidity","entity_pressure","entity_visibility","entity_wind_bearing","entity_wind_speed","entity_wind_gust","entity_wind_speed_kt","entity_wind_gust_kt","entity_temp_next","entity_temp_next_label","entity_temp_following","entity_temp_following_label","entity_forecast_max","entity_forecast_min","entity_observed_max","entity_observed_min","entity_fire_danger","entity_pop","entity_pos","entity_sun","entity_uv_alert_summary","entity_rainfall","entity_todays_fire_danger","entity_todays_uv_forecast","custom1_value","custom1_icon","custom1_units","custom2_value","custom2_icon","custom2_units","custom3_value","custom3_icon","custom3_units","custom4_value","custom4_icon","custom4_units","entity_forecast_icon_1","entity_pop_1","entity_pos_1","entity_summary_1","entity_forecast_min_1","entity_forecast_max_1","entity_extended_1","entity_fire_danger_1","daily_forecast_layout","daily_forecast_days","daily_extended_forecast_days","daily_extended_use_attr","daily_extended_name_attr","option_today_temperature_decimals","option_today_rainfall_decimals","option_pressure_decimals","option_color_fire_danger","option_locale","option_static_icons","option_time_format","option_tooltips","old_daily_format","option_show_beaufort","entity","tap_action","hold_action","style","index","view_index"];for(const i in this._config)t.includes(i)||delete e[i];e=Object.assign(Object.assign({},e),{card_config_version:this._config_version}),this._config=e,_(this,"config-changed",{config:this.sortObjectByKeys(this._config)})}shouldUpdate(){return this._initialized||this._initialize(),!0}get _section_order(){var e;return(null===(e=this._config)||void 0===e?void 0:e.section_order)||null}get _text_card_title(){var e;return(null===(e=this._config)||void 0===e?void 0:e.text_card_title)||""}get _text_card_title_2(){var e;return(null===(e=this._config)||void 0===e?void 0:e.text_card_title_2)||""}get _entity_update_time(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_update_time)||""}get _update_time_use_attr(){var e;return!0===(null===(e=this._config)||void 0===e?void 0:e.update_time_use_attr)}get _update_time_name_attr(){var e;return(null===(e=this._config)||void 0===e?void 0:e.update_time_name_attr)||""}get _text_update_time_prefix(){var e;return(null===(e=this._config)||void 0===e?void 0:e.text_update_time_prefix)||""}get _overview_layout(){var e;return(null===(e=this._config)||void 0===e?void 0:e.overview_layout)||""}get _entity_temperature(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_temperature)||""}get _entity_apparent_temp(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_apparent_temp)||""}get _entity_forecast_icon(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_forecast_icon)||""}get _entity_summary(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_summary)||""}get _option_show_overview_decimals(){var e;return!0===(null===(e=this._config)||void 0===e?void 0:e.option_show_overview_decimals)}get _option_show_overview_separator(){var e;return!0===(null===(e=this._config)||void 0===e?void 0:e.option_show_overview_separator)}get _entity_extended(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_extended)||""}get _extended_use_attr(){var e;return!0===(null===(e=this._config)||void 0===e?void 0:e.extended_use_attr)}get _extended_name_attr(){var e;return(null===(e=this._config)||void 0===e?void 0:e.extended_name_attr)||""}get _entity_todays_fire_danger(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_todays_fire_danger)||""}get _entity_todays_uv_forecast(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_todays_uv_forecast)||""}get _slot_l1(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_l1)||""}get _slot_l2(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_l2)||""}get _slot_l3(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_l3)||""}get _slot_l4(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_l4)||""}get _slot_l5(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_l5)||""}get _slot_l6(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_l6)||""}get _slot_l7(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_l7)||""}get _slot_l8(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_l8)||""}get _slot_r1(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_r1)||""}get _slot_r2(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_r2)||""}get _slot_r3(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_r3)||""}get _slot_r4(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_r4)||""}get _slot_r5(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_r5)||""}get _slot_r6(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_r6)||""}get _slot_r7(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_r7)||""}get _slot_r8(){var e;return(null===(e=this._config)||void 0===e?void 0:e.slot_r8)||""}get _entity_observed_max(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_observed_max)||""}get _entity_observed_min(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_observed_min)||""}get _entity_forecast_max(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_forecast_max)||""}get _entity_forecast_min(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_forecast_min)||""}get _entity_temp_next(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_temp_next)||""}get _entity_temp_next_label(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_temp_next_label)||""}get _entity_temp_following(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_temp_following)||""}get _entity_temp_following_label(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_temp_following_label)||""}get _entity_wind_bearing(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_wind_bearing)||""}get _entity_wind_speed(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_wind_speed)||""}get _entity_wind_gust(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_wind_gust)||""}get _entity_wind_speed_kt(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_wind_speed_kt)||""}get _entity_wind_gust_kt(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_wind_gust_kt)||""}get _entity_visibility(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_visibility)||""}get _entity_sun(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_sun)||""}get _entity_pop(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_pop)||""}get _entity_pos(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_pos)||""}get _entity_possible_tomorrow(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_possible_tomorrow)||""}get _entity_humidity(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_humidity)||""}get _entity_pressure(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_pressure)||""}get _entity_uv_alert_summary(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_uv_alert_summary)||""}get _entity_fire_danger(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_fire_danger)||""}get _entity_rainfall(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_rainfall)||""}get _custom1_value(){var e;return(null===(e=this._config)||void 0===e?void 0:e.custom1_value)||""}get _custom1_icon(){var e;return(null===(e=this._config)||void 0===e?void 0:e.custom1_icon)||""}get _custom1_units(){var e;return(null===(e=this._config)||void 0===e?void 0:e.custom1_units)||""}get _custom2_value(){var e;return(null===(e=this._config)||void 0===e?void 0:e.custom2_value)||""}get _custom2_icon(){var e;return(null===(e=this._config)||void 0===e?void 0:e.custom2_icon)||""}get _custom2_units(){var e;return(null===(e=this._config)||void 0===e?void 0:e.custom2_units)||""}get _custom3_value(){var e;return(null===(e=this._config)||void 0===e?void 0:e.custom3_value)||""}get _custom3_icon(){var e;return(null===(e=this._config)||void 0===e?void 0:e.custom3_icon)||""}get _custom3_units(){var e;return(null===(e=this._config)||void 0===e?void 0:e.custom3_units)||""}get _custom4_value(){var e;return(null===(e=this._config)||void 0===e?void 0:e.custom4_value)||""}get _custom4_icon(){var e;return(null===(e=this._config)||void 0===e?void 0:e.custom4_icon)||""}get _custom4_units(){var e;return(null===(e=this._config)||void 0===e?void 0:e.custom4_units)||""}get _daily_forecast_layout(){var e;return(null===(e=this._config)||void 0===e?void 0:e.daily_forecast_layout)||""}get _daily_forecast_days(){var e;return(null===(e=this._config)||void 0===e?void 0:e.daily_forecast_days)||null}get _daily_extended_forecast_days(){var e,t;return null!==(t=null===(e=this._config)||void 0===e?void 0:e.daily_extended_forecast_days)&&void 0!==t?t:null}get _entity_forecast_icon_1(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_forecast_icon_1)||""}get _entity_summary_1(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_summary_1)||""}get _entity_forecast_min_1(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_forecast_min_1)||""}get _entity_forecast_max_1(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_forecast_max_1)||""}get _entity_pop_1(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_pop_1)||""}get _entity_pos_1(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_pos_1)||""}get _entity_extended_1(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_extended_1)||""}get _entity_fire_danger_1(){var e;return(null===(e=this._config)||void 0===e?void 0:e.entity_fire_danger_1)||""}get _daily_extended_use_attr(){var e;return!0===(null===(e=this._config)||void 0===e?void 0:e.daily_extended_use_attr)}get _daily_extended_name_attr(){var e;return(null===(e=this._config)||void 0===e?void 0:e.daily_extended_name_attr)||""}get _option_today_temperature_decimals(){var e;return!0===(null===(e=this._config)||void 0===e?void 0:e.option_today_temperature_decimals)}get _option_today_rainfall_decimals(){var e;return!0===(null===(e=this._config)||void 0===e?void 0:e.option_today_rainfall_decimals)}get _option_pressure_decimals(){var e;return(null===(e=this._config)||void 0===e?void 0:e.option_pressure_decimals)||null}get _option_color_fire_danger(){var e;return!1!==(null===(e=this._config)||void 0===e?void 0:e.option_color_fire_danger)}get _option_daily_color_fire_danger(){var e;return!1!==(null===(e=this._config)||void 0===e?void 0:e.option_daily_color_fire_danger)}get _option_tooltips(){var e;return!0===(null===(e=this._config)||void 0===e?void 0:e.option_tooltips)}get _option_static_icons(){var e;return!0===(null===(e=this._config)||void 0===e?void 0:e.option_static_icons)}get _option_time_format(){var e,t;return null!==(t=null===(e=this._config)||void 0===e?void 0:e.option_time_format)&&void 0!==t?t:null}get _option_locale(){var e;return(null===(e=this._config)||void 0===e?void 0:e.option_locale)||""}get _optional_entities(){var e,t,i,o,n,a,d,r,l,s,c,h,p,u,f,_;const g=new Set;for(const m of[(null===(e=this._config)||void 0===e?void 0:e.slot_l1)||"forecast_max",(null===(t=this._config)||void 0===t?void 0:t.slot_l2)||"forecast_min",(null===(i=this._config)||void 0===i?void 0:i.slot_l3)||"wind",(null===(o=this._config)||void 0===o?void 0:o.slot_l4)||"pressure",(null===(n=this._config)||void 0===n?void 0:n.slot_l5)||"sun_next",(null===(a=this._config)||void 0===a?void 0:a.slot_l6)||"remove",(null===(d=this._config)||void 0===d?void 0:d.slot_l7)||"remove",(null===(r=this._config)||void 0===r?void 0:r.slot_l8)||"remove",(null===(l=this._config)||void 0===l?void 0:l.slot_r1)||"popforecast",(null===(s=this._config)||void 0===s?void 0:s.slot_r2)||"humidity",(null===(c=this._config)||void 0===c?void 0:c.slot_r3)||"uv_summary",(null===(h=this._config)||void 0===h?void 0:h.slot_r4)||"fire_danger",(null===(p=this._config)||void 0===p?void 0:p.slot_r5)||"sun_following",(null===(u=this._config)||void 0===u?void 0:u.slot_r6)||"remove",(null===(f=this._config)||void 0===f?void 0:f.slot_r7)||"remove",(null===(_=this._config)||void 0===_?void 0:_.slot_r8)||"remove"])switch(m){case"observed_max":g.add("entity_observed_max");break;case"observed_min":g.add("entity_observed_min");break;case"forecast_max":g.add("entity_forecast_max");break;case"forecast_min":g.add("entity_forecast_min");break;case"temp_next":g.add("entity_temp_next").add("entity_temp_next_label");break;case"temp_following":g.add("entity_temp_following").add("entity_temp_following_label");break;case"temp_maximums":g.add("entity_forecast_max").add("entity_observed_max");break;case"temp_minimums":g.add("entity_forecast_min").add("entity_observed_min");break;case"wind":g.add("entity_wind_bearing").add("entity_wind_speed").add("entity_wind_gust");break;case"wind_kt":g.add("entity_wind_bearing").add("entity_wind_speed_kt").add("entity_wind_gust_kt");break;case"visibility":g.add("entity_visibility");break;case"sun_next":case"sun_following":g.add("entity_sun");break;case"pop":g.add("entity_pop");break;case"popforecast":g.add("entity_pop").add("entity_pos");break;case"humidity":g.add("entity_humidity");break;case"pressure":g.add("entity_pressure");break;case"uv_summary":g.add("entity_uv_alert_summary");break;case"fire_danger":g.add("entity_fire_danger");break;case"possible_today":g.add("entity_pos");break;case"possible_tomorrow":g.add("entity_possible_tomorrow");break;case"rainfall":g.add("entity_rainfall");break;case"custom1":g.add("custom1");break;case"custom2":g.add("custom2");break;case"custom3":g.add("custom3");break;case"custom4":g.add("custom4")}const v=g.has("entity_observed_max")?m` + + + `:"",b=g.has("entity_observed_min")?m` + + + `:"",y=g.has("entity_forecast_max")?m` + + + `:"",x=g.has("entity_forecast_min")?m` + + + `:"",w=g.has("entity_temp_next")?m` + + + `:"",E=g.has("entity_temp_next_label")?m` + + + `:"",I=g.has("entity_temp_following")?m` + + + `:"",C=g.has("entity_temp_following_label")?m` + + + `:"",T=g.has("entity_wind_bearing")?m` + + + `:"",A=g.has("entity_wind_speed")?m` + + + `:"",k=g.has("entity_wind_gust")?m` + + + `:"",$=g.has("entity_wind_speed_kt")?m` + + + `:"",O=g.has("entity_wind_gust_kt")?m` + + + `:"",S=g.has("entity_visibility")?m` + + + `:"",R=g.has("entity_sun")?m` + + + `:"",F=g.has("entity_pop")?m` + + + `:"",L=g.has("entity_pos")?m` + + + `:"",D=g.has("entity_possible_tomorrow")?m` + + + `:"",V=g.has("entity_humidity")?m` + + + `:"",N=g.has("entity_pressure")?m` + + + `:"",M=g.has("entity_uv_alert_summary")?m` + + + `:"",P=g.has("entity_fire_danger")?m` + + + `:"",B=g.has("entity_rainfall")?m` + + + `:"",z=g.has("custom1")?m` + + +
    + + + + +
    + `:"",H=g.has("custom2")?m` + + +
    + + + + +
    + `:"",U=g.has("custom3")?m` + + +
    + + + + +
    + `:"",j=g.has("custom4")?m` + + +
    + + + + +
    + `:"";return m` + ${v} + ${b} + ${y} + ${x} + ${w} + ${E} + ${I} + ${C} + ${T} + ${A} + ${k} + ${$} + ${O} + ${S} + ${R} + ${F} + ${L} + ${D} + ${V} + ${N} + ${M} + ${P} + ${B} + ${z} + ${H} + ${U} + ${j}`}get _show_warning(){var e;return(null===(e=this._config)||void 0===e?void 0:e.show_warning)||!1}get _show_error(){var e;return(null===(e=this._config)||void 0===e?void 0:e.show_error)||!1}async firstUpdated(){var e;this._config&&this.hass&&this._config.card_config_version!==this._config_version&&this._configCleanup(),customElements.get("ha-switch")&&customElements.get("ha-textfield")&&customElements.get("ha-entity-picker")||null===(e=customElements.get("hui-entities-card"))||void 0===e||e.getConfigElement()}_sectionOverviewEditor(){return m` + + + + + + + ${""!==this._entity_update_time?m` +
    +
    + + + + +
    + ${""!==this._entity_update_time&&!0===this._update_time_use_attr?m` + `:m``} +
    `:m``} + + + ${"forecast"!==this._overview_layout?m` + + + `:m``} + ${"observations"!==this._overview_layout?m` + + + `:m``} + `}_optionOverviewEditor(){return m` +
    + e.stopPropagation()} @selected=${this._valueChanged}> + + complete + observations + forecast + title only + +
    +
    +
    +
    + + + + +
    +
    + + + + +
    +
    + `}_sectionExtendedEditor(){return!0===this._extended_use_attr&&(void 0===this.hass||void 0===this.hass.states[this._entity_extended]||this.hass.states[this._entity_extended].attributes),m` + + + ${""!==this._entity_extended?m` +
    +
    + + + + +
    + ${""!==this._entity_extended&&!0===this._extended_use_attr?m` + `:m``} +
    `:m``} + + + + + `}_sectionSlotsEditor(){const e=m` + Current humidity + Today's recorded rainfall + Current air pressure + Current wind conditions + Current wind conditions kts + Current visibility + Today's observed max + Today's observed min + Today's forecast max + Today's forecast min + Next temp min/max + Following temp min/max + Observed/forecast max + Observed/forecast min + Next sun rise/set time + Following sun rise/set time + Chance of rain + Rainfall forecast + Today's forecast rainfall + Tomorrow's forecast rainfall + Today's uv forecast + Today's fire danger + Custom entity 1 + Custom entity 2 + Custom entity 3 + Custom entity 4 + Blank slot + Remove slot`;return m` +
    + e.stopPropagation()}> + ${e} + + e.stopPropagation()}> + ${e} + +
    +
    + e.stopPropagation()}> + ${e} + + e.stopPropagation()}> + ${e} + +
    +
    + e.stopPropagation()}> + ${e} + + e.stopPropagation()}> + ${e} + +
    +
    + e.stopPropagation()}> + ${e} + + e.stopPropagation()}> + ${e} + +
    +
    + e.stopPropagation()}> + ${e} + + e.stopPropagation()}> + ${e} + +
    +
    + e.stopPropagation()}> + ${e} + + e.stopPropagation()}> + ${e} + +
    +
    + e.stopPropagation()}> + ${e} + + e.stopPropagation()}> + ${e} + +
    +
    + e.stopPropagation()}> + ${e} + + e.stopPropagation()}> + ${e} + +
    + ${this._optional_entities} + `}_optionSlotsEditor(){return m` +
    +
    + + + + +
    + e.stopPropagation()} @selected=${this._valueChangedNumber}> + + 0 + 1 + 2 + 3 + +
    +
    +
    + + + + +
    +
    +
    +
    +
    +
    + + + + +
    +
    +
    +
    + `}_sectionDailyForecastEditor(){return!0===this._daily_extended_use_attr&&(void 0===this.hass||void 0===this.hass.states[this._entity_extended_1]||this.hass.states[this._entity_extended_1].attributes),m` + + + + + + + + + + + + + ${"vertical"===this._daily_forecast_layout?m` + + + ${""!==this._entity_extended_1?m` +
    +
    + + + + +
    + ${""!==this._entity_extended_1&&!0===this._daily_extended_use_attr?m` + + `:m``} +
    `:m``} + + + `:""} + `}_optionDailyForecastEditor(){return m` +
    + e.stopPropagation()} @selected=${this._valueChanged}> + + horizontal + vertical + +
    +
    +
    + e.stopPropagation()} @selected=${this._valueChangedNumber}> + + 1 + 2 + 3 + 4 + 5 + ${"vertical"===this._daily_forecast_layout?m` + 6 + 7`:m``} + + ${"vertical"===this._daily_forecast_layout?m`e.stopPropagation()} @selected=${this._valueChangedNumber}> + + 0 + 1 + 2 + 3 + 4 + 5 + 6 + 7 + `:m`
    `} +
    + +
    +
    + ${"vertical"!==this._daily_forecast_layout?m` + + + + `:m``} +
    +
    +
    +
    + ${"vertical"===this._daily_forecast_layout?m`
    + + + + +
    `:m``} +
    +
    +
    + `}_optionGlobalOptionsEditor(){return m` +
    +
    + + + + +
    +
    +
    +
    + e.stopPropagation()} @selected=${this._valueChanged}> + + System + 12 hour + 24 hour + + + +
    + `}_renderSubElementEditor(){const e=[m` +
    +
    + + + +
    +
    + `];switch(this._subElementEditor){case"section_overview":e.push(this._sectionOverviewEditor());break;case"option_overview":e.push(this._optionOverviewEditor());break;case"section_extended":e.push(this._sectionExtendedEditor());break;case"section_slots":e.push(this._sectionSlotsEditor());break;case"option_slots":e.push(this._optionSlotsEditor());break;case"section_daily_forecast":e.push(this._sectionDailyForecastEditor());break;case"option_daily_forecast":e.push(this._optionDailyForecastEditor());break;case"option_global_options":e.push(this._optionGlobalOptionsEditor())}return m`${e}`}_goBack(){this._subElementEditor=void 0}get _show_section_overview(){var e;return!1!==(null===(e=this._config)||void 0===e?void 0:e.show_section_overview)}get _show_section_extended(){var e;return!1!==(null===(e=this._config)||void 0===e?void 0:e.show_section_extended)}get _show_section_slots(){var e;return!1!==(null===(e=this._config)||void 0===e?void 0:e.show_section_slots)}get _show_section_daily_forecast(){var e;return!1!==(null===(e=this._config)||void 0===e?void 0:e.show_section_daily_forecast)}getConfigBlock(e,t,i){switch(e){case"overview":return m` +
    + + + + +
    + + + + + + + + +
    +
    + `;case"extended":return m` +
    + + + + +
    + + + + + + +
    +
    +
    + `;case"slots":return m` +
    + + + + +
    + + + + + + + + +
    +
    + `;case"daily_forecast":return m` +
    + + + + +
    + + + + + + + + +
    +
    + `;case"global_options":return m` +
    + + +
    +
    + + +
    +
    + `}return m``}render(){if(!this.hass||!this._helpers)return m``;if(this._subElementEditor)return this._renderSubElementEditor();const e=[],t=this._section_order||[];return t.forEach(((i,o)=>{e.push(this.getConfigBlock(i,0===o,o+1===t.length))})),e.push(this.getConfigBlock("global_options",!1,!1)),m`${e}`}_initialize(){void 0!==this.hass&&void 0!==this._config&&void 0!==this._helpers&&(this._initialized=!0)}async loadCardHelpers(){this._helpers=await window.loadCardHelpers()}_valueChangedPicker(e){if(!this._config||!this.hass)return;const t=e.target,i=e.detail.value;this[`_${t.configValue}`]!==i&&(t.configValue&&(i?this._config=Object.assign(Object.assign({},this._config),{[t.configValue]:i}):(this._config=Object.assign({},this._config),delete this._config[t.configValue])),_(this,"config-changed",{config:this.sortObjectByKeys(this._config)}))}_editSubmenu(e){if(e.currentTarget){const t=e.currentTarget;this._subElementEditor=t.value}}_moveUp(e){if(this._config&&this.hass){if(e.currentTarget){const t=e.currentTarget;if(this._config.section_order){const e=this._config.section_order.findIndex((e=>e===t.value)),i=[...this._config.section_order];[i[e],i[e-1]]=[this._config.section_order[e-1],this._config.section_order[e]],this._config=Object.assign(Object.assign({},this._config),{section_order:i})}}_(this,"config-changed",{config:this.sortObjectByKeys(this._config)})}}_moveDown(e){if(this._config&&this.hass){if(e.currentTarget){const t=e.currentTarget;if(this._config.section_order){const e=this._config.section_order.findIndex((e=>e===t.value)),i=[...this._config.section_order];[i[e],i[e+1]]=[this._config.section_order[e+1],this._config.section_order[e]],this._config=Object.assign(Object.assign({},this._config),{section_order:i})}}_(this,"config-changed",{config:this.sortObjectByKeys(this._config)})}}_valueChanged(e){if(!this._config||!this.hass)return;const t=e.target;if(this[`_${t.configValue}`]!==t.value){if(t.configValue)if(""===t.value){const e=Object.assign({},this._config);delete e[t.configValue],this._config=e}else this._config=Object.assign(Object.assign({},this._config),{[t.configValue]:void 0!==t.checked?t.checked:t.value});_(this,"config-changed",{config:this.sortObjectByKeys(this._config)})}}_valueChangedNumber(e){if(!this._config||!this.hass)return;const t=e.target;this[`_${t.configValue}`]!==t.value&&(t.configValue&&(""===t.value||null===t.value?delete this._config[t.configValue]:this._config=Object.assign(Object.assign({},this._config),{[t.configValue]:Number(t.value)})),_(this,"config-changed",{config:this.sortObjectByKeys(this._config)}))}};bi.elementDefinitions=Object.assign(Object.assign(Object.assign(Object.assign({"ha-card":customElements.get("ha-card")},vi),Wt),ii),G),bi.styles=h` + :host { + display: block; + /* --mdc-menu-min-width: var(--parentWidth); */ + --mdc-menu-item-height: 36px; + --mdc-typography-subtitle1-font-size: 13px; + } + mwc-select { + display: block; + } + ha-textfield { + display: block; + } + ha-switch { + --mdc-theme-secondary: var(--switch-checked-color); + } + ha-formfield { + height: 56px; + } + .no-icon { + display: inline-flex; + width: var(--mds-icon-button-size, 48px); + } + /* .option { + cursor: pointer; + } */ + /* .row { + display: flex; + margin-bottom: -14px; + pointer-events: none; + } */ + /* .title { + padding-left: 16px; + margin-top: -6px; + pointer-events: none; + } */ + /* .secondary { + padding-left: 40px; + color: var(--secondary-text-color); + pointer-events: none; + } */ + /* .values { + padding-left: 16px; + background: var(--secondary-background-color); + } */ + .section-flex { + display: flex; + justify-content: space-between; + align-items: center; + } + .side-by-side { + display: flex; + } + .side-by-side > * { + flex: 1; + } + .side-by-side :not(:last-child) { + padding-right: 4px; + } + .icon-side-by-side { + display: flex; + } + .icon-condition { + flex-grow: 1; + } + .no-switch { + padding-left: 48px; + } + .condition_icon { + position: relative; + } + .condition_icon .condition_icon_big { + visibility: hidden; + width: 96px; + background-color: var(--card-background-color); + border-radius: 6px; + border-style: solid; + border-width: 2px; + /* Position the tooltip */ + position: absolute; + z-index: 1; + bottom: -50%; + left: 60%; + -webkit-transform: translateX(0%); /* Safari iOS */ + transform: translateX(-40%); + } + .condition_icon .condition_icon_big:after { + content: ""; + position: absolute; + top: 100%; + left: 50%; + } + .condition_icon:hover .condition_icon_big { + visibility: visible + } + `,a([d({attribute:!1})],bi.prototype,"hass",void 0),a([u()],bi.prototype,"_config",void 0),a([u()],bi.prototype,"_helpers",void 0),a([u()],bi.prototype,"_subElementEditor",void 0),bi=a([g("platinum-weather-card-editor")],bi);export{bi as WeatherCardEditor}; diff --git a/www/community/platinum-weather-card/editor-92788164.js.gz b/www/community/platinum-weather-card/editor-92788164.js.gz new file mode 100644 index 0000000..276f129 Binary files /dev/null and b/www/community/platinum-weather-card/editor-92788164.js.gz differ diff --git a/www/community/platinum-weather-card/fire-ban-black.svg b/www/community/platinum-weather-card/fire-ban-black.svg new file mode 100644 index 0000000..7cd8e57 --- /dev/null +++ b/www/community/platinum-weather-card/fire-ban-black.svg @@ -0,0 +1,74 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/community/platinum-weather-card/fire-ban-white.svg b/www/community/platinum-weather-card/fire-ban-white.svg new file mode 100644 index 0000000..86d9cec --- /dev/null +++ b/www/community/platinum-weather-card/fire-ban-white.svg @@ -0,0 +1,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/www/community/platinum-weather-card/platinum-weather-card-df6d26e2.js b/www/community/platinum-weather-card/platinum-weather-card-df6d26e2.js new file mode 100644 index 0000000..23b08ed --- /dev/null +++ b/www/community/platinum-weather-card/platinum-weather-card-df6d26e2.js @@ -0,0 +1,922 @@ +/*! ***************************************************************************** +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. +***************************************************************************** */ +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)};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 s(){this.constructor=e}t(e,i),e.prototype=null===i?Object.create(i):(s.prototype=i.prototype,new s)}var i=function(){return i=Object.assign||function(t){for(var e,i=1,s=arguments.length;i=0;a--)(o=t[a])&&(r=(n<3?o(r):n>3?o(e,i,r):o(e,i))||r);return n>3&&r&&Object.defineProperty(e,i,r),r}function o(t){var e="function"==typeof Symbol&&Symbol.iterator,i=e&&t[e],s=0;if(i)return i.call(t);if(t&&"number"==typeof t.length)return{next:function(){return t&&s>=t.length&&(t=void 0),{value:t&&t[s++],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 n=window,r=n.ShadowRoot&&(void 0===n.ShadyCSS||n.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,a=Symbol(),c=new WeakMap;class l{constructor(t,e,i){if(this._$cssResult$=!0,i!==a)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=t,this.t=e}get styleSheet(){let t=this.o;const e=this.t;if(r&&void 0===t){const i=void 0!==e&&1===e.length;i&&(t=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 h=r?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return(t=>new l("string"==typeof t?t:t+"",void 0,a))(e)})(t):t +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */;var d;const u=window,_=u.trustedTypes,p=_?_.emptyScript:"",g=u.reactiveElementPolyfillSupport,f={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}},v=(t,e)=>e!==t&&(e==e||t==t),m={attribute:!0,type:String,converter:f,reflect:!1,hasChanged:v};class y extends HTMLElement{constructor(){super(),this._$Ei=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$El=null,this.u()}static addInitializer(t){var e;this.finalize(),(null!==(e=this.h)&&void 0!==e?e:this.h=[]).push(t)}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((e,i)=>{const s=this._$Ep(i,e);void 0!==s&&(this._$Ev.set(s,i),t.push(s))})),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 i="symbol"==typeof t?Symbol():"__"+t,s=this.getPropertyDescriptor(t,i,e);void 0!==s&&Object.defineProperty(this.prototype,t,s)}}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(s){const o=this[t];this[e]=s,this.requestUpdate(t,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||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 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(h(t))}else void 0!==t&&e.push(h(t));return e}static _$Ep(t,e){const i=e.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof t?t.toLowerCase():void 0}u(){var t;this._$E_=new Promise((t=>this.enableUpdating=t)),this._$AL=new Map,this._$Eg(),this.requestUpdate(),null===(t=this.constructor.h)||void 0===t||t.forEach((t=>t(this)))}addController(t){var e,i;(null!==(e=this._$ES)&&void 0!==e?e:this._$ES=[]).push(t),void 0!==this.renderRoot&&this.isConnected&&(null===(i=t.hostConnected)||void 0===i||i.call(t))}removeController(t){var e;null===(e=this._$ES)||void 0===e||e.splice(this._$ES.indexOf(t)>>>0,1)}_$Eg(){this.constructor.elementProperties.forEach(((t,e)=>{this.hasOwnProperty(e)&&(this._$Ei.set(e,this[e]),delete this[e])}))}createRenderRoot(){var t;const e=null!==(t=this.shadowRoot)&&void 0!==t?t:this.attachShadow(this.constructor.shadowRootOptions);return((t,e)=>{r?t.adoptedStyleSheets=e.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):e.forEach((e=>{const i=document.createElement("style"),s=n.litNonce;void 0!==s&&i.setAttribute("nonce",s),i.textContent=e.cssText,t.appendChild(i)}))})(e,this.constructor.elementStyles),e}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this._$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=m){var s;const o=this.constructor._$Ep(t,i);if(void 0!==o&&!0===i.reflect){const n=(void 0!==(null===(s=i.converter)||void 0===s?void 0:s.toAttribute)?i.converter:f).toAttribute(e,i.type);this._$El=t,null==n?this.removeAttribute(o):this.setAttribute(o,n),this._$El=null}}_$AK(t,e){var i;const s=this.constructor,o=s._$Ev.get(t);if(void 0!==o&&this._$El!==o){const t=s.getPropertyOptions(o),n="function"==typeof t.converter?{fromAttribute:t.converter}:void 0!==(null===(i=t.converter)||void 0===i?void 0:i.fromAttribute)?t.converter:f;this._$El=o,this[o]=n.fromAttribute(e,t.type),this._$El=null}}requestUpdate(t,e,i){let s=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||v)(this[t],e)?(this._$AL.has(t)||this._$AL.set(t,e),!0===i.reflect&&this._$El!==t&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(t,i))):s=!1),!this.isUpdatePending&&s&&(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 b;y.finalized=!0,y.elementProperties=new Map,y.elementStyles=[],y.shadowRootOptions={mode:"open"},null==g||g({ReactiveElement:y}),(null!==(d=u.reactiveElementVersions)&&void 0!==d?d:u.reactiveElementVersions=[]).push("1.5.0");const w=window,x=w.trustedTypes,$=x?x.createPolicy("lit-html",{createHTML:t=>t}):void 0,S=`lit$${(Math.random()+"").slice(9)}$`,E="?"+S,O=`<${E}>`,A=document,N=(t="")=>A.createComment(t),T=t=>null===t||"object"!=typeof t&&"function"!=typeof t,k=Array.isArray,M=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,C=/-->/g,F=/>/g,P=RegExp(">|[ \t\n\f\r](?:([^\\s\"'>=/]+)([ \t\n\f\r]*=[ \t\n\f\r]*(?:[^ \t\n\f\r\"'`<>=]|(\"|')|))|$)","g"),L=/'/g,D=/"/g,U=/^(?:script|style|textarea|title)$/i,R=(t=>(e,...i)=>({_$litType$:t,strings:e,values:i}))(1),z=Symbol.for("lit-noChange"),W=Symbol.for("lit-nothing"),H=new WeakMap,V=A.createTreeWalker(A,129,null,!1);class j{constructor({strings:t,_$litType$:e},i){let s;this.parts=[];let o=0,n=0;const r=t.length-1,a=this.parts,[c,l]=((t,e)=>{const i=t.length-1,s=[];let o,n=2===e?"":"",r=M;for(let e=0;e"===c[0]?(r=null!=o?o:M,l=-1):void 0===c[1]?l=-2:(l=r.lastIndex-c[2].length,a=c[1],r=void 0===c[3]?P:'"'===c[3]?D:L):r===D||r===L?r=P:r===C||r===F?r=M:(r=P,o=void 0);const d=r===P&&t[e+1].startsWith("/>")?" ":"";n+=r===M?i+O:l>=0?(s.push(a),i.slice(0,l)+"$lit$"+i.slice(l)+S+d):i+S+(-2===l?(s.push(void 0),e):d)}const a=n+(t[i]||"")+(2===e?"":"");if(!Array.isArray(t)||!t.hasOwnProperty("raw"))throw Error("invalid template strings array");return[void 0!==$?$.createHTML(a):a,s]})(t,e);if(this.el=j.createElement(c,i),V.currentNode=this.el.content,2===e){const t=this.el.content,e=t.firstChild;e.remove(),t.append(...e.childNodes)}for(;null!==(s=V.nextNode())&&a.length0){s.textContent=x?x.emptyScript:"";for(let i=0;ik(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!==W&&T(this._$AH)?this._$AA.nextSibling.data=t:this.T(A.createTextNode(t)),this._$AH=t}$(t){var e;const{values:i,_$litType$:s}=t,o="number"==typeof s?this._$AC(t):(void 0===s.el&&(s.el=j.createElement(s.h,this.options)),s);if((null===(e=this._$AH)||void 0===e?void 0:e._$AD)===o)this._$AH.p(i);else{const t=new B(o,this),e=t.v(this.options);t.p(i),this.T(e),this._$AH=t}}_$AC(t){let e=H.get(t.strings);return void 0===e&&H.set(t.strings,e=new j(t)),e}k(t){k(this._$AH)||(this._$AH=[],this._$AR());const e=this._$AH;let i,s=0;for(const o of t)s===e.length?e.push(i=new q(this.O(N()),this.O(N()),this,this.options)):i=e[s],i._$AI(o),s++;s2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=W}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(t,e=this,i,s){const o=this.strings;let n=!1;if(void 0===o)t=I(this,t,e,0),n=!T(t)||t!==this._$AH&&t!==z,n&&(this._$AH=t);else{const s=t;let r,a;for(t=o[0],r=0;rnew st("string"==typeof t?t:t+"",et),nt=(t,...e)=>{const i=1===t.length?t[0]:e.reduce(((e,i,s)=>e+(t=>{if(!0===t._$cssResult$)return t.cssText;if("number"==typeof t)return t;throw Error("Value passed to 'css' function must be a 'css' function result: "+t+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+t[s+1]),t[0]);return new st(i,et)},rt=tt?t=>t:t=>t instanceof CSSStyleSheet?(t=>{let e="";for(const i of t.cssRules)e+=i.cssText;return ot(e)})(t):t +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */;var at;const ct=window.trustedTypes,lt=ct?ct.emptyScript:"",ht=window.reactiveElementPolyfillSupport,dt={toAttribute(t,e){switch(e){case Boolean:t=t?lt: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}},ut=(t,e)=>e!==t&&(e==e||t==t),_t={attribute:!0,type:String,converter:dt,reflect:!1,hasChanged:ut};class pt extends HTMLElement{constructor(){super(),this._$Et=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$Ei=null,this.o()}static addInitializer(t){var e;null!==(e=this.l)&&void 0!==e||(this.l=[]),this.l.push(t)}static get observedAttributes(){this.finalize();const t=[];return this.elementProperties.forEach(((e,i)=>{const s=this._$Eh(i,e);void 0!==s&&(this._$Eu.set(s,i),t.push(s))})),t}static createProperty(t,e=_t){if(e.state&&(e.attribute=!1),this.finalize(),this.elementProperties.set(t,e),!e.noAccessor&&!this.prototype.hasOwnProperty(t)){const i="symbol"==typeof t?Symbol():"__"+t,s=this.getPropertyDescriptor(t,i,e);void 0!==s&&Object.defineProperty(this.prototype,t,s)}}static getPropertyDescriptor(t,e,i){return{get(){return this[e]},set(s){const o=this[t];this[e]=s,this.requestUpdate(t,o,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(t){return this.elementProperties.get(t)||_t}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(rt(t))}else void 0!==t&&e.push(rt(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)=>{tt?t.adoptedStyleSheets=e.map((t=>t instanceof CSSStyleSheet?t:t.styleSheet)):e.forEach((e=>{const i=document.createElement("style"),s=window.litNonce;void 0!==s&&i.setAttribute("nonce",s),i.textContent=e.cssText,t.appendChild(i)}))})(e,this.constructor.elementStyles),e}connectedCallback(){var t;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostConnected)||void 0===e?void 0:e.call(t)}))}enableUpdating(t){}disconnectedCallback(){var t;null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostDisconnected)||void 0===e?void 0:e.call(t)}))}attributeChangedCallback(t,e,i){this._$AK(t,i)}_$ES(t,e,i=_t){var s,o;const n=this.constructor._$Eh(t,i);if(void 0!==n&&!0===i.reflect){const r=(null!==(o=null===(s=i.converter)||void 0===s?void 0:s.toAttribute)&&void 0!==o?o:dt.toAttribute)(e,i.type);this._$Ei=t,null==r?this.removeAttribute(n):this.setAttribute(n,r),this._$Ei=null}}_$AK(t,e){var i,s,o;const n=this.constructor,r=n._$Eu.get(t);if(void 0!==r&&this._$Ei!==r){const t=n.getPropertyOptions(r),a=t.converter,c=null!==(o=null!==(s=null===(i=a)||void 0===i?void 0:i.fromAttribute)&&void 0!==s?s:"function"==typeof a?a:null)&&void 0!==o?o:dt.fromAttribute;this._$Ei=r,this[r]=c(e,t.type),this._$Ei=null}}requestUpdate(t,e,i){let s=!0;void 0!==t&&(((i=i||this.constructor.getPropertyOptions(t)).hasChanged||ut)(this[t],e)?(this._$AL.has(t)||this._$AL.set(t,e),!0===i.reflect&&this._$Ei!==t&&(void 0===this._$E_&&(this._$E_=new Map),this._$E_.set(t,i))):s=!1),!this.isUpdatePending&&s&&(this._$Ep=this._$EC())}async _$EC(){this.isUpdatePending=!0;try{await this._$Ep}catch(t){Promise.reject(t)}const t=this.scheduleUpdate();return null!=t&&await t,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var t;if(!this.isUpdatePending)return;this.hasUpdated,this._$Et&&(this._$Et.forEach(((t,e)=>this[e]=t)),this._$Et=void 0);let e=!1;const i=this._$AL;try{e=this.shouldUpdate(i),e?(this.willUpdate(i),null===(t=this._$Eg)||void 0===t||t.forEach((t=>{var e;return null===(e=t.hostUpdate)||void 0===e?void 0:e.call(t)})),this.update(i)):this._$EU()}catch(t){throw e=!1,this._$EU(),t}e&&this._$AE(i)}willUpdate(t){}_$AE(t){var e;null===(e=this._$Eg)||void 0===e||e.forEach((t=>{var e;return null===(e=t.hostUpdated)||void 0===e?void 0:e.call(t)})),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(t)),this.updated(t)}_$EU(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$Ep}shouldUpdate(t){return!0}update(t){void 0!==this._$E_&&(this._$E_.forEach(((t,e)=>this._$ES(e,this[e],t))),this._$E_=void 0),this._$EU()}updated(t){}firstUpdated(t){}} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +var gt,ft;pt.finalized=!0,pt.elementProperties=new Map,pt.elementStyles=[],pt.shadowRootOptions={mode:"open"},null==ht||ht({ReactiveElement:pt}),(null!==(at=globalThis.reactiveElementVersions)&&void 0!==at?at:globalThis.reactiveElementVersions=[]).push("1.2.1");class vt extends pt{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 s,o;const n=null!==(s=null==i?void 0:i.renderBefore)&&void 0!==s?s:e;let r=n._$litPart$;if(void 0===r){const t=null!==(o=null==i?void 0:i.renderBefore)&&void 0!==o?o:null;n._$litPart$=r=new q(e.insertBefore(N(),t),t,void 0,null!=i?i:{})}return r._$AI(t),r})(e,this.renderRoot,this.renderOptions)}connectedCallback(){var t;super.connectedCallback(),null===(t=this._$Dt)||void 0===t||t.setConnected(!0)}disconnectedCallback(){var t;super.disconnectedCallback(),null===(t=this._$Dt)||void 0===t||t.setConnected(!1)}render(){return z}}vt.finalized=!0,vt._$litElement$=!0,null===(gt=globalThis.litElementHydrateSupport)||void 0===gt||gt.call(globalThis,{LitElement:vt});const mt=globalThis.litElementPolyfillSupport;null==mt||mt({LitElement:vt}),(null!==(ft=globalThis.litElementVersions)&&void 0!==ft?ft:globalThis.litElementVersions=[]).push("3.1.2"); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +const yt=t=>e=>"function"==typeof e?((t,e)=>(customElements.define(t,e),e))(t,e):((t,e)=>{const{kind:i,elements:s}=e;return{kind:i,elements:s,finisher(e){customElements.define(t,e)}}})(t,e) +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */,bt=(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 wt(t){return(e,i)=>void 0!==i?((t,e,i)=>{e.constructor.createProperty(i,t)})(t,e,i):bt(t,e) +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */}function xt(t){return wt({...t,state:!0})} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const $t=({finisher:t,descriptor:e})=>(i,s)=>{var o;if(void 0===s){const s=null!==(o=i.originalKey)&&void 0!==o?o:i.key,n=null!=e?{kind:"method",placement:"prototype",key:s,descriptor:e(i.key)}:{...i,key:s};return null!=t&&(n.finisher=function(e){t(e,s)}),n}{const o=i.constructor;void 0!==e&&Object.defineProperty(i,s,e(s)),null==t||t(o,s)}} +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */;var St;const Et=null!=(null===(St=window.HTMLSlotElement)||void 0===St?void 0:St.prototype.assignedElements)?(t,e)=>t.assignedElements(e):(t,e)=>t.assignedNodes(e).filter((t=>t.nodeType===Node.ELEMENT_NODE));function Ot(t){const{slot:e,selector:i}=null!=t?t:{};return $t({descriptor:s=>({get(){var s;const o="slot"+(e?`[name=${e}]`:":not([name])"),n=null===(s=this.renderRoot)||void 0===s?void 0:s.querySelector(o),r=null!=n?Et(n,t):[];return i?r.filter((t=>t.matches(i))):r},enumerable:!0,configurable:!0})})}var At,Nt,Tt=function(t){return new Intl.DateTimeFormat(t.language,{year:"numeric",month:"long",day:"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"}(At||(At={})),function(t){t.language="language",t.system="system",t.am_pm="12",t.twenty_four="24"}(Nt||(Nt={}));var kt=function(t){if(t.time_format===Nt.language||t.time_format===Nt.system){var e=t.time_format===Nt.language?t.language:void 0,i=(new Date).toLocaleString(e);return i.includes("AM")||i.includes("PM")}return t.time_format===Nt.am_pm},Mt=function(t){return new Intl.DateTimeFormat(t.language,{year:"numeric",month:"long",day:"numeric",hour:kt(t)?"numeric":"2-digit",minute:"2-digit",hour12:kt(t)})},Ct=function(t){return new Intl.DateTimeFormat(t.language,{hour:"numeric",minute:"2-digit",hour12:kt(t)})};var Ft=["closed","locked","off"],Pt=function(t,e,i,s){s=s||{},i=null==i?{}:i;var o=new Event(e,{bubbles:void 0===s.bubbles||s.bubbles,cancelable:Boolean(s.cancelable),composed:void 0===s.composed||s.composed});return o.detail=i,t.dispatchEvent(o),o},Lt=function(t){Pt(window,"haptic",t)},Dt=function(t,e,i,s){if(s||(s={action:"more-info"}),!s.confirmation||s.confirmation.exemptions&&s.confirmation.exemptions.some((function(t){return t.user===e.user.id}))||(Lt("warning"),confirm(s.confirmation.text||"Are you sure you want to "+s.action+"?")))switch(s.action){case"more-info":(i.entity||i.camera_image)&&Pt(t,"hass-more-info",{entityId:i.entity?i.entity:i.camera_image});break;case"navigate":s.navigation_path&&function(t,e,i){void 0===i&&(i=!1),i?history.replaceState(null,"",e):history.pushState(null,"",e),Pt(window,"location-changed",{replace:i})}(0,s.navigation_path);break;case"url":s.url_path&&window.open(s.url_path);break;case"toggle":i.entity&&(function(t,e){(function(t,e,i){void 0===i&&(i=!0);var s,o=function(t){return t.substr(0,t.indexOf("."))}(e),n="group"===o?"homeassistant":o;switch(o){case"lock":s=i?"unlock":"lock";break;case"cover":s=i?"open_cover":"close_cover";break;default:s=i?"turn_on":"turn_off"}t.callService(n,s,{entity_id:e})})(t,e,Ft.includes(t.states[e].state))}(e,i.entity),Lt("success"));break;case"call-service":if(!s.service)return void Lt("failure");var o=s.service.split(".",2);e.callService(o[0],o[1],s.service_data,s.target),Lt("success");break;case"fire-dom-event":Pt(t,"ll-custom",s)}};function Ut(t){return void 0!==t&&"none"!==t.action}var Rt=function(){if("undefined"!=typeof Map)return Map;function t(t,e){var i=-1;return t.some((function(t,s){return t[0]===e&&(i=s,!0)})),i}return function(){function e(){this.__entries__=[]}return Object.defineProperty(e.prototype,"size",{get:function(){return this.__entries__.length},enumerable:!0,configurable:!0}),e.prototype.get=function(e){var i=t(this.__entries__,e),s=this.__entries__[i];return s&&s[1]},e.prototype.set=function(e,i){var s=t(this.__entries__,e);~s?this.__entries__[s][1]=i:this.__entries__.push([e,i])},e.prototype.delete=function(e){var i=this.__entries__,s=t(i,e);~s&&i.splice(s,1)},e.prototype.has=function(e){return!!~t(this.__entries__,e)},e.prototype.clear=function(){this.__entries__.splice(0)},e.prototype.forEach=function(t,e){void 0===e&&(e=null);for(var i=0,s=this.__entries__;i0},t.prototype.connect_=function(){zt&&!this.connected_&&(document.addEventListener("transitionend",this.onTransitionEnd_),window.addEventListener("resize",this.refresh),jt?(this.mutationsObserver_=new MutationObserver(this.refresh),this.mutationsObserver_.observe(document,{attributes:!0,childList:!0,characterData:!0,subtree:!0})):(document.addEventListener("DOMSubtreeModified",this.refresh),this.mutationEventsAdded_=!0),this.connected_=!0)},t.prototype.disconnect_=function(){zt&&this.connected_&&(document.removeEventListener("transitionend",this.onTransitionEnd_),window.removeEventListener("resize",this.refresh),this.mutationsObserver_&&this.mutationsObserver_.disconnect(),this.mutationEventsAdded_&&document.removeEventListener("DOMSubtreeModified",this.refresh),this.mutationsObserver_=null,this.mutationEventsAdded_=!1,this.connected_=!1)},t.prototype.onTransitionEnd_=function(t){var e=t.propertyName,i=void 0===e?"":e;Vt.some((function(t){return!!~i.indexOf(t)}))&&this.refresh()},t.getInstance=function(){return this.instance_||(this.instance_=new t),this.instance_},t.instance_=null,t}(),Bt=function(t,e){for(var i=0,s=Object.keys(e);i0},t}(),se="undefined"!=typeof WeakMap?new WeakMap:new Rt,oe=function t(e){if(!(this instanceof t))throw new TypeError("Cannot call a class as a function.");if(!arguments.length)throw new TypeError("1 argument required, but only 0 present.");var i=It.getInstance(),s=new ie(e,i,this);se.set(this,s)};["observe","unobserve","disconnect"].forEach((function(t){oe.prototype[t]=function(){var e;return(e=se.get(this))[t].apply(e,arguments)}}));var ne=void 0!==Wt.ResizeObserver?Wt.ResizeObserver:oe;const re=t=>t.locale||{language:t.language,number_format:At.system};const ae=(t,e,i)=>{if("unknown"===e.state||"unavailable"===e.state)return t(`state.default.${e.state}`);if(e.attributes.unit_of_measurement)return`${e.state}${e.attributes.unit_of_measurement}`;const s=e.entity_id.split(".")[0];if("input_datetime"===s){let t;if(!e.attributes.has_time)return t=new Date(e.attributes.year,e.attributes.month-1,e.attributes.day),function(t,e){return Tt(e).format(t)}(t,i);if(!e.attributes.has_date){const s=new Date;return t=new Date(s.getFullYear(),s.getMonth(),s.getDay(),e.attributes.hour,e.attributes.minute),function(t,e){return Ct(e).format(t)}(t,i)}return t=new Date(e.attributes.year,e.attributes.month-1,e.attributes.day,e.attributes.hour,e.attributes.minute),function(t,e){return Mt(e).format(t)}(t,i)}return e.attributes.device_class&&t(`component.${s}.state.${e.attributes.device_class}.${e.state}`)||t(`component.${s}.state._.${e.state}`)||e.state},ce={ATTRIBUTE:1,CHILD:2,PROPERTY:3,BOOLEAN_ATTRIBUTE:4,EVENT:5,ELEMENT:6},le=t=>(...e)=>({_$litDirective$:t,values:e});class he{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 de="ontouchstart"in window||navigator.maxTouchPoints>0||navigator.maxTouchPoints>0;class ue extends HTMLElement{constructor(){super(),this.holdTime=500,this.held=!1,this.ripple=document.createElement("mwc-ripple")}connectedCallback(){Object.assign(this.style,{position:"absolute",width:de?"100px":"50px",height:de?"100px":"50px",transform:"translate(-50%, -50%)",pointerEvents:"none",zIndex:"999"}),this.appendChild(this.ripple),this.ripple.primary=!0,["touchcancel","mouseout","mouseup","touchmove","mousewheel","wheel","scroll"].forEach((t=>{document.addEventListener(t,(()=>{clearTimeout(this.timer),this.stopAnimation(),this.timer=void 0}),{passive:!0})}))}bind(t,e){if(t.actionHandler)return;t.actionHandler=!0,t.addEventListener("contextmenu",(t=>{const e=t||window.event;return e.preventDefault&&e.preventDefault(),e.stopPropagation&&e.stopPropagation(),e.cancelBubble=!0,e.returnValue=!1,!1}));const i=t=>{let e,i;this.held=!1,t.touches?(e=t.touches[0].pageX,i=t.touches[0].pageY):(e=t.pageX,i=t.pageY),this.timer=window.setTimeout((()=>{this.startAnimation(e,i),this.held=!0}),this.holdTime)},s=i=>{i.preventDefault(),["touchend","touchcancel"].includes(i.type)&&void 0===this.timer||(clearTimeout(this.timer),this.stopAnimation(),this.timer=void 0,this.held?Pt(t,"action",{action:"hold"}):e.hasDoubleClick?"click"===i.type&&i.detail<2||!this.dblClickTimeout?this.dblClickTimeout=window.setTimeout((()=>{this.dblClickTimeout=void 0,Pt(t,"action",{action:"tap"})}),250):(clearTimeout(this.dblClickTimeout),this.dblClickTimeout=void 0,Pt(t,"action",{action:"double_tap"})):Pt(t,"action",{action:"tap"}))};t.addEventListener("touchstart",i,{passive:!0}),t.addEventListener("touchend",s),t.addEventListener("touchcancel",s),t.addEventListener("mousedown",i,{passive:!0}),t.addEventListener("click",s),t.addEventListener("keyup",(t=>{13===t.keyCode&&s(t)}))}startAnimation(t,e){Object.assign(this.style,{left:`${t}px`,top:`${e}px`,display:null}),this.ripple.disabled=!1,this.ripple.active=!0,this.ripple.unbounded=!0}stopAnimation(){this.ripple.active=!1,this.ripple.disabled=!0,this.style.display="none"}}customElements.define("action-handler-weather",ue);const _e=(t,e)=>{const i=(()=>{const t=document.body;if(t.querySelector("action-handler-weather"))return t.querySelector("action-handler-weather");const e=document.createElement("action-handler-weather");return t.appendChild(e),e})();i&&i.bind(t,e)},pe=le(class extends he{update(t,[e]){return _e(t.element,e),z}render(t){}});console.info("%c PLATINUM-WEATHER-CARD \n%c Version 1.0.5 ","color: orange; font-weight: bold; background: black","color: white; font-weight: bold; background: dimgray"),window.customCards=window.customCards||[],window.customCards.push({type:"platinum-weather-card",name:"Platinum Weather Card",description:"An fully customisable weather card with a GUI configuration"});let ge=class extends vt{constructor(){super(...arguments),this._cardWidth=492,this._error=[]}static async getConfigElement(){return await import("./editor-92788164.js"),document.createElement("platinum-weather-card-editor")}static getStubConfig(){return{}}getCardSize(){const t=16+this._getCardSizeOverviewSection()+this._getCardSizeExtendedSection()+this._getCardSizeSlotsSection()+this._getCardSizeDailyForecastSection();return Math.ceil(t/50)}setConfig(t){if(!t)throw new Error("Invalid configuration");t.test_gui&&function(){var t=document.querySelector("home-assistant");if(t=(t=(t=(t=(t=(t=(t=(t=t&&t.shadowRoot)&&t.querySelector("home-assistant-main"))&&t.shadowRoot)&&t.querySelector("app-drawer-layout partial-panel-resolver"))&&t.shadowRoot||t)&&t.querySelector("ha-panel-lovelace"))&&t.shadowRoot)&&t.querySelector("hui-root")){var e=t.lovelace;return e.current_view=t.___curView,e}return null}().setEditMode(!0),this._config=Object.assign({name:"Weather"},t)}shouldUpdate(t){if(!this._config)return!1;const e=t.get("hass")||void 0;if(!e||e.themes!==this.hass.themes||e.locale!==this.hass.locale)return!0;if(!1===Object.keys(this._config).every((t=>null===t.match(/^entity_/)||e.states[this._config[t]]===this.hass.states[this._config[t]])))return!0;if(this._config.show_section_daily_forecast){const t=this._config.daily_forecast_days||5;for(const s of["entity_forecast_icon_1","entity_summary_1","entity_forecast_min_1","entity_forecast_max_1","entity_pop_1","entity_pos_1"])if(void 0!==this._config[s]&&null===this._config[s].match("^weather.")){const o=this._config[s].match(/(\d+)(?!.*\d)/g);if(o)for(var i=1;ithis._resize()),250,!1)));(null===(t=this.shadowRoot)||void 0===t?void 0:t.querySelector("ha-card"))&&this._resizeObserver.observe(this)}_resize(){var t;if(!this.isConnected)return;const e=null===(t=this.shadowRoot)||void 0===t?void 0:t.querySelector("ha-card");e&&(this._cardWidth=e.getBoundingClientRect().width)}_checkForErrors(){this._error=[],Object.keys(this._config).forEach((t=>{null!==t.match(/^entity_/)&&void 0===this.hass.states[this._config[t]]&&this._error.push(`'${t}=${this._config[t]}' not found`)})),this._config.daily_forecast_days;for(const t of["entity_forecast_icon_1","entity_summary_1","entity_forecast_min_1","entity_forecast_max_1","entity_pop_1","entity_pos_1"])if(void 0!==this._config[t]){const e=this.hass.states[this._config[t]];if(this._config[t].match("^weather.")){const i=new Date;switch(i.setDate(i.getDate()+1),t){case"entity_forecast_icon_1":void 0===this._getForecastPropFromWeather(e.attributes.forecast,i,"condition")&&this._error.push(`'${t} attribute forecast[1].condition not found`);break;case"entity_forecast_min_1":void 0===this._getForecastPropFromWeather(e.attributes.forecast,i,"templow")&&this._error.push(`'${t} attribute forecast[1].templow not found`);break;case"entity_forecast_max_1":void 0===this._getForecastPropFromWeather(e.attributes.forecast,i,"temperature")&&this._error.push(`'${t} attribute forecast[1].temperature not found`);break;case"entity_pop_1":void 0===this._getForecastPropFromWeather(e.attributes.forecast,i,"precipitation_probability")&&this._error.push(`'${t} attribute forecast[1].precipitation_probability not found`);break;case"entity_pos_1":void 0===this._getForecastPropFromWeather(e.attributes.forecast,i,"precipitation")&&this._error.push(`'${t} attribute forecast[1].precipitation not found`)}}else{const e=this._config[t].match(/(\d+)(?!.*\d)/g);if(e){const i=this._config[t].replace(/(\d+)(?!.*\d)/g,Number(e)+1);void 0===this.hass.states[i]&&this._error.push(`'${t}'+'1'=${i}' not found`)}else this._error.push(`'${t}=${this._config[t]}' value needs to have a number`)}}return 0!==this._error.length}_renderUpdateTime(){if(this._config.entity_update_time&&this.hass.states[this._config.entity_update_time]&&void 0!==this.hass.states[this._config.entity_update_time].state)if(!0===this._config.update_time_use_attr){if(void 0!==this._config.update_time_name_attr){const t=this._config.update_time_name_attr.toLowerCase().split(".").reduce(((t,e)=>void 0!==t?t[e]:void 0),this.hass.states[this._config.entity_update_time].attributes);if(void 0!==t){const e=new Date(`${t}`);switch(this.timeFormat){case"12hour":return R`${e.toLocaleString(this.locale||navigator.language,{hour:"numeric",minute:"2-digit",hour12:!0}).replace(" ","")+", "+e.toLocaleDateString(this.locale,{weekday:"short",day:"numeric",month:"short",year:"numeric"}).replace(",","")}`;case"24hour":return R`${e.toLocaleString(this.locale||navigator.language,{hour:"2-digit",minute:"2-digit",hour12:!1})+", "+e.toLocaleDateString(this.locale,{weekday:"short",day:"numeric",month:"short",year:"numeric"}).replace(",","")}`;case"system":return R`${e.toLocaleTimeString(navigator.language,{timeStyle:"short"}).replace(" ","")+", "+e.toLocaleDateString(navigator.language).replace(",","")}`}}}}else{const t=new Date(this.hass.states[this._config.entity_update_time].state);switch(this.timeFormat){case"12hour":return R`${t.toLocaleString(this.locale||navigator.language,{hour:"numeric",minute:"2-digit",hour12:!0}).replace(" ","")+", "+t.toLocaleDateString(this.locale,{weekday:"short",day:"numeric",month:"short",year:"numeric"}).replace(",","")}`;case"24hour":return R`${t.toLocaleString(this.locale||navigator.language,{hour:"2-digit",minute:"2-digit",hour12:!1})+", "+t.toLocaleDateString(this.locale,{weekday:"short",day:"numeric",month:"short",year:"numeric"}).replace(",","")}`;case"system":return R`${t.toLocaleTimeString(navigator.language,{timeStyle:"short"}).replace(" ","")+", "+t.toLocaleDateString(navigator.language).replace(",","")}`}}return R`---`}_renderCompleteOverviewSection(){var t,e;if(!1===(null===(t=this._config)||void 0===t?void 0:t.show_section_overview))return R``;const i=this._weatherIcon(this.forecastIcon),s=new URL((this._config.option_static_icons?"s-":"a-")+i+".svg",import.meta.url),o="unknown"!==i?"":`Unknown condition\n${this.forecastIcon}`,n="unknown"!==i?R``:R`
    ${this.forecastIcon}
    `,r=R`
    `,a=R` +
    +
    ${this.currentTemperature}
    +
    ${this.getUOM("temperature")}
    +
    + `,c=this.currentApparentTemperature,l=""!=c?R` +
    +
    ${this.localeTextFeelsLike} ${c}
    +
    ${this.getUOM("temperature")}
    +
    + `:R``,h=!0===this._config.option_show_overview_separator?R`
    `:"",d=this._config.entity_summary&&this.hass.states[this._config.entity_summary]?null!==(e=R`
    ${ae(this.hass.localize,this.hass.states[this._config.entity_summary],re(this.hass))}
    `)&&void 0!==e?e:R`
    ---
    `:R``;return R` +
    + ${this._config.text_card_title?R`
    ${this._config.text_card_title}
    `:R``} + ${this._config.text_card_title_2?R`
    ${this._config.text_card_title_2}
    `:R``} + ${this._config.entity_update_time?R`
    ${this._config.text_update_time_prefix?this._config.text_update_time_prefix+" ":""}${this._renderUpdateTime()}
    `:R``} +
    +
    ${r}${n}
    +
    ${a}${l}
    +
    + ${d} + ${h} +
    + `}_renderObservationsOverviewSection(){var t;if(!1===(null===(t=this._config)||void 0===t?void 0:t.show_section_overview))return R``;const e=this._cardWidth>=344?" stacked":"",i=R` +
    +
    ${this.currentTemperature}
    +
    ${this.getUOM("temperature")}
    +
    + `,s=this.currentApparentTemperature,o=""!=s?R` +
    +
    ${this.localeTextFeelsLike} ${s}
    +
    ${this.getUOM("temperature")}
    +
    + `:R``,n=!0===this._config.option_show_overview_separator?R`
    `:"";return R` +
    + ${this._config.text_card_title?R`
    ${this._config.text_card_title}
    `:R``} + ${this._config.text_card_title_2?R`
    ${this._config.text_card_title_2}
    `:R``} + ${this._config.entity_update_time?R`
    ${this._config.text_update_time_prefix?this._config.text_update_time_prefix+" ":""}${this._renderUpdateTime()}
    `:R``} +
    +
    +
    +
    +
    ${i}${o}
    +
    + ${n} +
    + `}_renderTitleOnlyOverviewSection(){var t;if(!1===(null===(t=this._config)||void 0===t?void 0:t.show_section_overview))return R``;const e=!0===this._config.option_show_overview_separator?R`
    `:"";return R` +
    + ${this._config.text_card_title?R`
    ${this._config.text_card_title}
    `:R``} + ${this._config.text_card_title_2?R`
    ${this._config.text_card_title_2}
    `:R``} + ${this._config.entity_update_time?R`
    ${this._config.text_update_time_prefix?this._config.text_update_time_prefix+" ":""}${this._renderUpdateTime()}
    `:R``} + ${e} +
    + `}_renderForecastOverviewSection(){var t,e;if(!1===(null===(t=this._config)||void 0===t?void 0:t.show_section_overview))return R``;const i=this._weatherIcon(this.forecastIcon),s=new URL((this._config.option_static_icons?"s-":"a-")+i+".svg",import.meta.url),o="unknown"!==i?"":`Unknown condition\n${this.forecastIcon}`,n="unknown"!==i?R``:R`
    ${this.forecastIcon}
    `,r=R`
    `,a=!0===this._config.option_show_overview_separator?R`
    `:"",c=this._config.entity_summary&&this.hass.states[this._config.entity_summary]?null!==(e=R`
    ${ae(this.hass.localize,this.hass.states[this._config.entity_summary],re(this.hass))}
    `)&&void 0!==e?e:R`
    ---
    `:R``;return R` +
    + ${this._config.text_card_title?R`
    ${this._config.text_card_title}
    `:R``} + ${this._config.text_card_title_2?R`
    ${this._config.text_card_title_2}
    `:R``} + ${this._config.entity_update_time?R`
    ${this._config.text_update_time_prefix?this._config.text_update_time_prefix+" ":""}${this._renderUpdateTime()}
    `:R``} +
    +
    ${r}${n}
    + ${c} +
    + ${a} +
    + `}_getCardSizeOverviewSection(){var t=0;if(!1!==this._config.show_section_overview){if("observations"===this._config.overview_layout)return 76;t=16,t+=void 0!==this._config.text_card_title?20:0,t+=void 0!==this._config.text_card_title_2?20:0,t+=void 0!==this._config.entity_update_time?20:0,"title only"!==this._config.overview_layout&&(t+="forecast"!==this._config.overview_layout&&void 0!==this._config.entity_summary?145:120)}return t}_renderOverviewSection(){var t;if(!1===(null===(t=this._config)||void 0===t?void 0:t.show_section_overview))return R``;switch(this._config.overview_layout||"complete"){case"observations":return this._renderObservationsOverviewSection();case"forecast":return this._renderForecastOverviewSection();case"title only":return this._renderTitleOnlyOverviewSection();default:return this._renderCompleteOverviewSection()}}_getCardSizeExtendedSection(){var t=0;return!1!==this._config.show_section_extended&&(t+=16,t+=this._config.entity_extended?40:0,t+=void 0!==this._config.entity_todays_uv_forecast||void 0!==this._config.entity_todays_fire_danger?20:0),t}_renderExtendedSection(){var t;if(!1===(null===(t=this._config)||void 0===t?void 0:t.show_section_extended)||void 0===this._config.entity_extended&&void 0===this._config.entity_todays_uv_forecast&&void 0===this._config.entity_todays_fire_danger)return R``;const e=this._config.entity_extended||"";var i=[];if(void 0!==this.hass.states[e])if(!0===this._config.extended_use_attr){if(void 0!==this._config.extended_name_attr){const t=this._config.extended_name_attr.toLowerCase().split(".").reduce(((t,e)=>void 0!==t?t[e]:void 0),this.hass.states[e].attributes);void 0!==t&&i.push(R`${t}`)}}else void 0!==this.hass.states[e]&&i.push(R`${this.hass.states[e].state}`);return i.push(R`${this._config.entity_todays_uv_forecast&&this.hass.states[this._config.entity_todays_uv_forecast]&&"unknown"!==this.hass.states[this._config.entity_todays_uv_forecast].state?" "+this.hass.states[this._config.entity_todays_uv_forecast].state:""}`),i.push(R`${this._config.entity_todays_fire_danger&&this.hass.states[this._config.entity_todays_fire_danger]&&"unknown"!==this.hass.states[this._config.entity_todays_fire_danger].state?" "+this.hass.states[this._config.entity_todays_fire_danger].state:""}`),R` +
    +
    + ${i} +
    +
    + `}_getCardSizeSlotsSection(){var t=0;if(!1!==this._config.show_section_slots){var e=("remove"!==this._config.slot_l1?1:0)+("remove"!==this._config.slot_l2?1:0)+("remove"!==this._config.slot_l3?1:0)+("remove"!==this._config.slot_l4?1:0)+("remove"!==this._config.slot_l5?1:0)+(void 0!==this._config.slot_l6&&"remove"!==this._config.slot_l6?1:0)+(void 0!==this._config.slot_l7&&"remove"!==this._config.slot_l7?1:0)+(void 0!==this._config.slot_l8&&"remove"!==this._config.slot_l8?1:0),i=("remove"!==this._config.slot_r1?1:0)+("remove"!==this._config.slot_r2?1:0)+("remove"!==this._config.slot_r3?1:0)+("remove"!==this._config.slot_r4?1:0)+("remove"!==this._config.slot_r5?1:0)+(void 0!==this._config.slot_r6&&"remove"!==this._config.slot_r6?1:0)+(void 0!==this._config.slot_r7&&"remove"!==this._config.slot_r7?1:0)+(void 0!==this._config.slot_r8&&"remove"!==this._config.slot_r8?1:0);t+=16+24*Math.max(e,i)}return t}_renderSlotsSection(){var t;if(!1===(null===(t=this._config)||void 0===t?void 0:t.show_section_slots))return R``;var e=!0===this._config.use_old_column_format?R` +
    +
      +
    • +
        ${this.slotL1}${this.slotL2}${this.slotL3}${this.slotL4}${this.slotL5}${this.slotL6}${this.slotL7}${this.slotL8}
      +
    • +
    • +
        ${this.slotR1}${this.slotR2}${this.slotR3}${this.slotR4}${this.slotR5}${this.slotR6}${this.slotR7}${this.slotR8}
      +
    • +
    +
    + `:R` +
    +
      +
    • +
        + ${this.slotL1}${this.slotL2}${this.slotL3}${this.slotL4}${this.slotL5}${this.slotL6}${this.slotL7}${this.slotL8} +
      +
    • +
    • +
        + ${this.slotR1}${this.slotR2}${this.slotR3}${this.slotR4}${this.slotR5}${this.slotR6}${this.slotR7}${this.slotR8} +
      +
    • +
    +
    + `;return R` +
    ${e}
    + `}_renderHorizontalDailyForecastSection(){var t,e,i,s,o,n,r;const a=[],c=this._config.daily_forecast_days||5;for(var l=0;l`}else{var _=!!this._config.entity_forecast_icon_1&&this._config.entity_forecast_icon_1.match(/(\d+)(?!.*\d)/g);const t=this._config.entity_forecast_icon_1?this._config.entity_forecast_icon_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+l)):void 0;if(void 0===t||void 0===this.hass.states[t])break;const e=new URL(((this._config.option_static_icons?"s-":"a-")+(t&&this.hass.states[t]?this._weatherIcon(this.hass.states[t].state):"unknown")+".svg").replace("-night","-day"),import.meta.url);h=R``}d=(null===(e=this._config.entity_forecast_max_1)||void 0===e?void 0:e.match("^weather."))?this._getForecastPropFromWeather(this.hass.states[this._config.entity_forecast_max_1].attributes.forecast,y,"temperature"):(_=!!this._config.entity_forecast_max_1&&this._config.entity_forecast_max_1.match(/(\d+)(?!.*\d)/g))&&this._config.entity_forecast_max_1?this.hass.states[this._config.entity_forecast_max_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+l))].state:void 0,u=(null===(i=this._config.entity_forecast_min_1)||void 0===i?void 0:i.match("^weather."))?this._getForecastPropFromWeather(this.hass.states[this._config.entity_forecast_min_1].attributes.forecast,y,"templow"):(_=!!this._config.entity_forecast_min_1&&this._config.entity_forecast_min_1.match(/(\d+)(?!.*\d)/g))&&this._config.entity_forecast_min_1?this.hass.states[this._config.entity_forecast_min_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+l))].state:void 0;const b=R`
    ${this.getUOM("temperature")}
    `,w=!0===this._config.old_daily_format?R` +
  • + +
    ${d?Math.round(Number(d)):"---"}
    + ${b} +
    +
  • +
  • + +
    ${u?Math.round(Number(u)):"---"}
    + ${b} +
    +
  • `:"highlow"===this._config.tempformat?R` +
  • + +
    ${d?Math.round(Number(d)):"---"}
    +
    /
    +
    ${u?Math.round(Number(u)):"---"}
    + ${b} +
    +
  • `:R` +
  • + +
    ${u?Math.round(Number(u)):"---"}
    +
    /
    +
    ${d?Math.round(Number(d)):"---"}
    + ${b} +
    +
  • + `;var p,g,f;if(null===(s=this._config.entity_pop_1)||void 0===s?void 0:s.match("^weather.")){const t=this._config.entity_pop_1,e=this._getForecastPropFromWeather(this.hass.states[t].attributes.forecast,y,"precipitation_probability");p=t?R`
  • ${this.hass.states[t]&&void 0!==e?Math.round(Number(e)):"---"}
    %
  • `:R``}else{const t=(_=!!this._config.entity_pop_1&&this._config.entity_pop_1.match(/(\d+)(?!.*\d)/g))&&this._config.entity_pop_1?this._config.entity_pop_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+l)):void 0;p=_?R`
  • ${t&&this.hass.states[t]?Math.round(Number(this.hass.states[t].state)):"---"}
    %
  • `:R``}if(null===(o=this._config.entity_pos_1)||void 0===o?void 0:o.match("^weather.")){const t=this._config.entity_pos_1,e=this._getForecastPropFromWeather(this.hass.states[t].attributes.forecast,y,"precipitation");g=t?R`
  • ${this.hass.states[t]&&void 0!==e?e:"---"}
    ${this.getUOM("precipitation")}
  • `:R``}else{const t=(_=!!this._config.entity_pos_1&&this._config.entity_pos_1.match(/(\d+)(?!.*\d)/g))&&this._config.entity_pos_1?this._config.entity_pos_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+l)):void 0;g=_?R`
  • ${t&&this.hass.states[t]?this.hass.states[t].state:"---"}
    ${this.getUOM("precipitation")}
  • `:R``}if(null===(n=this._config.entity_summary_1)||void 0===n?void 0:n.match("^weather.")){const t=this._config.entity_summary_1,e=null!==(r=this._getForecastPropFromWeather(this.hass.states[t].attributes.forecast,y,"detailed_description"))&&void 0!==r?r:this._getForecastPropFromWeather(this.hass.states[t].attributes.forecast,y,"condition");f=R`
    ${this.hass.states[t]&&void 0!==e?(v=this.hass.localize,m=e,v(`component.weather.state._.${m}`)||m):"---"}
    + +
    `}else{const t=(_=!!this._config.entity_summary_1&&this._config.entity_summary_1.match(/(\d+)(?!.*\d)/g))&&this._config.entity_summary_1?this._config.entity_summary_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+l)):void 0;f=R`
    ${this._config.option_tooltips&&t?this.hass.states[t]?this.hass.states[t].state:"---":""}
    + +
    `}a.push(R` +
    +
      +
    • ${y?y.toLocaleDateString(this.locale,{weekday:"short"}):"---"}
    • + ${h} + ${w} + ${p} + ${g} +
    + ${f} +
    + `)}var v,m;return R` +
    + ${a} +
    + `}_renderVerticalDailyForecastSection(){var t,e,i,s,o;const n=[],r=this._config.daily_forecast_days||5;for(var a=0;a
    `}else{var _=!!this._config.entity_forecast_icon_1&&this._config.entity_forecast_icon_1.match(/(\d+)(?!.*\d)/g);const t=_&&this._config.entity_forecast_icon_1?this._config.entity_forecast_icon_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+a)):void 0;if(!t||void 0===this.hass.states[t]||"unknown"===this.hass.states[t].state)break;const e=new URL(((this._config.option_static_icons?"s-":"a-")+(void 0!==this.hass.states[t]?this._weatherIcon(this.hass.states[t].state):"unknown")+".svg").replace("-night","-day"),import.meta.url);c=R`
    `}const v=(_=!!this._config.entity_summary_1&&this._config.entity_summary_1.match(/(\d+)(?!.*\d)/g))&&this._config.entity_summary_1?this._config.entity_summary_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+a)):void 0,m=_?R` +
    ${v&&this.hass.states[v]?this.hass.states[v].state:"---"}
    `:"";l=(null===(e=this._config.entity_forecast_max_1)||void 0===e?void 0:e.match("^weather."))?this._getForecastPropFromWeather(this.hass.states[this._config.entity_forecast_max_1].attributes.forecast,r,"temperature"):(_=!!this._config.entity_forecast_max_1&&this._config.entity_forecast_max_1.match(/(\d+)(?!.*\d)/g))&&this._config.entity_forecast_max_1?this.hass.states[this._config.entity_forecast_max_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+a))].state:void 0,h=(null===(i=this._config.entity_forecast_min_1)||void 0===i?void 0:i.match("^weather."))?this._getForecastPropFromWeather(this.hass.states[this._config.entity_forecast_min_1].attributes.forecast,r,"templow"):(_=!!this._config.entity_forecast_min_1&&this._config.entity_forecast_min_1.match(/(\d+)(?!.*\d)/g))&&this._config.entity_forecast_min_1?this.hass.states[this._config.entity_forecast_min_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+a))].state:void 0;const y=R`
    ${this.getUOM("temperature")}
    `,b=h?R` +
    +
    Min:
    +
    ${Math.round(Number(h))}
    ${y} +
    `:R`---`,w=l?R` +
    +
    Max:
    +
    ${Math.round(Number(l))}
    ${y} +
    `:R`---`;if(null===(s=this._config.entity_pop_1)||void 0===s?void 0:s.match("^weather.")){const t=this._config.entity_pop_1,e=this._getForecastPropFromWeather(this.hass.states[t].attributes.forecast,r,"precipitation_probability");d=t?R`
    Chance of rain
    +
    ${this.hass.states[t]&&void 0!==e?Math.round(Number(e)):"---"}
    %
    `:R``}else{const t=(_=!!this._config.entity_pop_1&&this._config.entity_pop_1.match(/(\d+)(?!.*\d)/g))&&this._config.entity_pop_1?this._config.entity_pop_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+a)):void 0;d=_?R` +
    Chance of rain
    +
    ${t&&this.hass.states[t]?Math.round(Number(this.hass.states[t].state)):"---"}
    %
    `:R``}if(null===(o=this._config.entity_pos_1)||void 0===o?void 0:o.match("^weather.")){const t=this._config.entity_pos_1,e=this._getForecastPropFromWeather(this.hass.states[t].attributes.forecast,r,"precipitation");u=t?R`
    Possible rain
    +
    ${this.hass.states[t]&&void 0!==e?e:"---"}
    ${this.getUOM("precipitation")}
    `:R``}else{const t=(_=!!this._config.entity_pos_1&&this._config.entity_pos_1.match(/(\d+)(?!.*\d)/g))&&this._config.entity_pos_1?this._config.entity_pos_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+a)):void 0;u=_?R` +
    Possible rain
    +
    ${t&&this.hass.states[t]?this.hass.states[t].state:"---"}
    +
    ${this.getUOM("precipitation")}
    `:R``}_=!!(this._config.entity_extended_1&&a<(0!==this._config.daily_extended_forecast_days?this._config.daily_extended_forecast_days||7:0))&&this._config.entity_extended_1.match(/(\d+)(?!.*\d)/g);var p=R``;if(a<(this._config.daily_extended_forecast_days?this._config.daily_extended_forecast_days:7))if(!0===this._config.daily_extended_use_attr){const t=(_=!!this._config.entity_extended_1&&this._config.entity_extended_1.match(/(\d+)(?!.*\d)/g))&&this._config.entity_extended_1?this._config.entity_extended_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+a)):this._config.entity_extended_1;if(t&&void 0!==this.hass.states[t]){const e=null==(_=!!(this._config.daily_extended_name_attr&&a<(0!==this._config.daily_extended_forecast_days?this._config.daily_extended_forecast_days||7:0))&&this._config.daily_extended_name_attr.match(/(\d+)(?!.*\d)/g))&&t&&this._config.daily_extended_name_attr?this.hass.states[t].attributes[this._config.daily_extended_name_attr]:_&&this._config.daily_extended_name_attr&&t?this._config.daily_extended_name_attr.replace(/(\d+)(?!.*\d)/g,String(Number(_)+a)).toLowerCase().split(".").reduce(((t,e)=>void 0!==t?t[e]:void 0),this.hass.states[t].attributes):void 0;p=e?R`
    ${e}
    `:R``}}else{const t=_&&this._config.entity_extended_1?this._config.entity_extended_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+a)):void 0;p=_?R`
    ${t&&this.hass.states[t]?this.hass.states[t].state:"---"}
    `:R``}_=!!this._config.entity_fire_danger_1&&this._config.entity_fire_danger_1.match(/(\d+)(?!.*\d)/g);var g=R``;const x=_&&this._config.entity_fire_danger_1?this._config.entity_fire_danger_1.replace(/(\d+)(?!.*\d)/g,String(Number(_)+a)):void 0;if(_&&x){var f=!1!==this._config.option_daily_color_fire_danger&&this.hass.states[x].attributes.color_fill?`background-color:${this.hass.states[x].attributes.color_fill}; color:${this.hass.states[x].attributes.color_text};`:"";!1===this._config.option_daily_color_fire_danger?g=_&&"unknown"!==this.hass.states[x].state?R` +
    ${x&&this.hass.states[x]?this.hass.states[x].state:"---"}
    `:R``:(""===f&&(f="font-weight:300;"),g=_&&"unknown"!==this.hass.states[x].state?R` +
    +

    ${x&&this.hass.states[x]?this.hass.states[x].state.toUpperCase():"---"}

    +
    `:R``)}n.push(R` +
    +
    +
    ${r?r.toLocaleDateString(this.locale,{weekday:"short"}):"---"}
    + ${m} +
    +
    + ${g} +
    +
    +
    + ${c} +
    +
    + ${b} + ${w} +
    +
    + ${d} + ${u} +
    +
    +
    + ${p} +
    +
    + `)}return R` +
    + ${n} +
    + `}_getForecastPropFromWeather(t,e,i){const s=e.toDateString(),o=t.filter((t=>new Date(t.datetime).toDateString()===s));if(1===o.length)return void 0!==o[0][i]?String(o[0][i]):void 0;if(2===o.length){const t=o.find((t=>!0===t.daytime)),e=o.find((t=>!1===t.daytime));return"templow"===i?void 0!==e.temperature?String(e.temperature):void 0:void 0!==t[i]?String(t[i]):void 0}}_getCardSizeDailyForecastSection(){var t=0;return!1!==this._config.show_section_daily_forecast&&("vertical"!==this._config.daily_forecast_layout?t+=146:(t+=18+87*(this._config.daily_forecast_days||5),0!==this._config.daily_extended_forecast_days&&(t+=48*Math.min(this._config.daily_forecast_days||5,this._config.daily_extended_forecast_days||7)))),t}_renderDailyForecastSection(){var t;return!1===(null===(t=this._config)||void 0===t?void 0:t.show_section_daily_forecast)?R``:"vertical"!==this._config.daily_forecast_layout?this._renderHorizontalDailyForecastSection():this._renderVerticalDailyForecastSection()}render(){const t=[];this._checkForErrors()&&t.push(this._showConfigWarning(this._error));const e=[];return void 0!==this._config.section_order&&this._config.section_order.forEach((t=>{switch(t){case"overview":e.push(this._renderOverviewSection());break;case"extended":e.push(this._renderExtendedSection());break;case"slots":e.push(this._renderSlotsSection());break;case"daily_forecast":e.push(this._renderDailyForecastSection())}})),t.push(R` + +
    + ${e} +
    +
    + `),R`${t}`}_handleAction(t){this.hass&&this._config&&t.detail.action&&function(t,e,i,s){var o;"double_tap"===s&&i.double_tap_action?o=i.double_tap_action:"hold"===s&&i.hold_action?o=i.hold_action:"tap"===s&&i.tap_action&&(o=i.tap_action),Dt(t,e,i,o)}(this,this.hass,this._config,t.detail.action)}get slotL1(){return this.slotValue("l1",this._config.slot_l1)}get slotL2(){return this.slotValue("l2",this._config.slot_l2)}get slotL3(){return this.slotValue("l3",this._config.slot_l3)}get slotL4(){return this.slotValue("l4",this._config.slot_l4)}get slotL5(){return this.slotValue("l5",this._config.slot_l5)}get slotL6(){return this.slotValue("l6",this._config.slot_l6)}get slotL7(){return this.slotValue("l7",this._config.slot_l7)}get slotL8(){return this.slotValue("l8",this._config.slot_l8)}get slotR1(){return this.slotValue("r1",this._config.slot_r1)}get slotR2(){return this.slotValue("r2",this._config.slot_r2)}get slotR3(){return this.slotValue("r3",this._config.slot_r3)}get slotR4(){return this.slotValue("r4",this._config.slot_r4)}get slotR5(){return this.slotValue("r5",this._config.slot_r5)}get slotR6(){return this.slotValue("r6",this._config.slot_r6)}get slotR7(){return this.slotValue("r7",this._config.slot_r7)}get slotR8(){return this.slotValue("r8",this._config.slot_r8)}slotValue(t,e){switch(e){case"pop":return this.slotPop;case"popforecast":return this.slotPopForecast;case"possible_today":return this.slotPos;case"possible_tomorrow":return this.slotPossibleTomorrow;case"rainfall":return this.slotRainfall;case"humidity":return this.slotHumidity;case"pressure":return this.slotPressure;case"observed_max":return this.slotObservedMax;case"observed_min":return this.slotObservedMin;case"forecast_max":return this.slotForecastMax;case"forecast_min":return this.slotForecastMin;case"temp_next":return this.slotTempNext;case"temp_following":return this.slotTempFollowing;case"temp_maximums":return this.slotTempMaximums;case"temp_minimums":return this.slotTempMinimums;case"uv_summary":return this.slotUvSummary;case"fire_danger":return this.slotFireDanger;case"wind":return this.slotWind;case"wind_kt":return this.slotWindKt;case"visibility":return this.slotVisibility;case"sun_next":return this.slotSunNext;case"sun_following":return this.slotSunFollowing;case"custom1":return this.slotCustom1;case"custom2":return this.slotCustom2;case"custom3":return this.slotCustom3;case"custom4":return this.slotCustom4;case"empty":return this.slotEmpty;case"remove":return this.slotRemove}switch(t){case"l1":return this.slotForecastMax;case"l2":return this.slotForecastMin;case"l3":return this.slotWind;case"l4":return this.slotPressure;case"l5":return this.slotSunNext;case"l6":case"l7":case"l8":case"r6":case"r7":case"r8":return this.slotRemove;case"r1":return this.slotPopForecast;case"r2":return this.slotHumidity;case"r3":return this.slotUvSummary;case"r4":return this.slotFireDanger;case"r5":return this.slotSunFollowing}return this.slotEmpty}get slotEmpty(){return R`
  •  
  • `}get slotRemove(){return R``}get slotPopForecast(){const t=this._config.entity_pop&&void 0!==this.hass.states[this._config.entity_pop]?null===this._config.entity_pop.match("^weather.")?Math.round(Number(this.hass.states[this._config.entity_pop].state)):void 0!==this.hass.states[this._config.entity_pop].attributes.forecast[0].precipitation_probability?Math.round(Number(this.hass.states[this._config.entity_pop].attributes.forecast[0].precipitation_probability)):"---":"---",e="---"!==t?R`
    %
    `:R``,i=this._config.entity_pos&&void 0!==this.hass.states[this._config.entity_pos]?null===this._config.entity_pos.match("^weather.")?this.hass.states[this._config.entity_pos].state:void 0!==this.hass.states[this._config.entity_pos].attributes.forecast[0].precipitation?this.hass.states[this._config.entity_pos].attributes.forecast[0].precipitation:"---":"---",s="---"!==i?R`
    ${this.getUOM("precipitation")}
    `:R``;return R` +
  • +
    +
    + +
    +
    ${t}
    ${e}
     
    +
    ${i}
    ${s} +
    +
  • + `}get slotPop(){const t=this._config.entity_pop&&void 0!==this.hass.states[this._config.entity_pop]?null===this._config.entity_pop.match("^weather.")?Math.round(Number(this.hass.states[this._config.entity_pop].state)):void 0!==this.hass.states[this._config.entity_pop].attributes.forecast[0].precipitation_probability?Math.round(Number(this.hass.states[this._config.entity_pop].attributes.forecast[0].precipitation_probability)):"---":"---",e="---"!==t?R`
    %
    `:R``;return R` +
  • +
    +
    + +
    +
    ${t}
    ${e}
    +
    +
  • + `}get slotPos(){const t=this._config.entity_pos&&void 0!==this.hass.states[this._config.entity_pos]?null===this._config.entity_pos.match("^weather.")?this.hass.states[this._config.entity_pos].state:void 0!==this.hass.states[this._config.entity_pos].attributes.forecast[0].precipitation?this.hass.states[this._config.entity_pos].attributes.forecast[0].precipitation:"---":"---",e="---"!==t?R`
    ${this.getUOM("precipitation")}
    `:R``;return R` +
  • +
    +
    + +
    ${this.localeTextPosToday} 
    ${t}
    ${e} +
    +
  • + `}get slotPossibleTomorrow(){const t=this._config.entity_possible_tomorrow&&void 0!==this.hass.states[this._config.entity_possible_tomorrow]?null===this._config.entity_possible_tomorrow.match("^weather.")?this.hass.states[this._config.entity_possible_tomorrow].state:void 0!==this.hass.states[this._config.entity_possible_tomorrow].attributes.forecast[1].precipitation?this.hass.states[this._config.entity_possible_tomorrow].attributes.forecast[1].precipitation:"---":"---",e="---"!==t?R`
    ${this.getUOM("precipitation")}
    `:R``;return R` +
  • +
    +
    + +
    ${this.localeTextPosTomorrow} 
    ${t}
    ${e} +
    +
  • + `}get slotRainfall(){const t=this.currentRainfall,e="---"!==t?R`
    ${this.getUOM("precipitation")}
    `:R``;return R` +
  • +
    +
    + +
    +
    ${t}
    ${e} +
    +
  • + `}get slotHumidity(){const t=this.currentHumidity,e="---"!==t?R`
    %
    `:R``;return R` +
  • +
    +
    + +
    +
    ${t}
    ${e} +
    +
  • `}get slotPressure(){const t="---"!==this.currentPressure?R`
    ${this._config.pressure_units?this._config.pressure_units:this.getUOM("air_pressure")}
    `:R``;return R` +
  • +
    +
    + +
    +
    ${this.currentPressure}
    ${t} +
    +
  • + `}get slotObservedMax(){const t=!0===this._config.option_today_temperature_decimals?1:0,e=this._config.entity_observed_max&&void 0!==this.hass.states[this._config.entity_observed_max]?Number(this.hass.states[this._config.entity_observed_max].state).toLocaleString(this.locale,{minimumFractionDigits:t,maximumFractionDigits:t}):"---",i="---"!==e?R`
    ${this.getUOM("temperature")}
    `:R``;return R` +
  • +
    +
    + +
    +
    ${this.localeTextObservedMax} 
    +
    ${e}
    ${i} +
    +
  • + `}get slotObservedMin(){const t=!0===this._config.option_today_temperature_decimals?1:0,e=this._config.entity_observed_min&&void 0!==this.hass.states[this._config.entity_observed_min]?Number(this.hass.states[this._config.entity_observed_min].state).toLocaleString(this.locale,{minimumFractionDigits:t,maximumFractionDigits:t}):"---",i="---"!==e?R`
    ${this.getUOM("temperature")}
    `:R``;return R` +
  • +
    +
    + +
    +
    ${this.localeTextObservedMin} 
    +
    ${e}
    ${i} +
    +
  • + `}get slotForecastMax(){const t=!0===this._config.option_today_temperature_decimals?1:0,e=this._config.entity_forecast_max&&void 0!==this.hass.states[this._config.entity_forecast_max]?null===this._config.entity_forecast_max.match("^weather.")?Number(this.hass.states[this._config.entity_forecast_max].state).toLocaleString(this.locale,{minimumFractionDigits:t,maximumFractionDigits:t}):void 0!==this.hass.states[this._config.entity_forecast_max].attributes.forecast[0].temperature?Number(this.hass.states[this._config.entity_forecast_max].attributes.forecast[0].temperature).toLocaleString(this.locale,{minimumFractionDigits:t,maximumFractionDigits:t}):"---":"---",i="---"!==e?R`
    ${this.getUOM("temperature")}
    `:R``;return R` +
  • +
    +
    + +
    +
    ${this.localeTextForecastMax} 
    +
    ${e}
    ${i} +
    +
  • + `}get slotForecastMin(){const t=!0===this._config.option_today_temperature_decimals?1:0,e=this._config.entity_forecast_min&&void 0!==this.hass.states[this._config.entity_forecast_min]?null===this._config.entity_forecast_min.match("^weather.")?Number(this.hass.states[this._config.entity_forecast_min].state).toLocaleString(this.locale,{minimumFractionDigits:t,maximumFractionDigits:t}):void 0!==this.hass.states[this._config.entity_forecast_min].attributes.forecast[0].templow?Number(this.hass.states[this._config.entity_forecast_min].attributes.forecast[0].templow).toLocaleString(this.locale,{minimumFractionDigits:t,maximumFractionDigits:t}):"---":"---",i="---"!==e?R`
    ${this.getUOM("temperature")}
    `:R``;return R` +
  • +
    +
    + +
    +
    ${this.localeTextForecastMin} 
    +
    ${e}
    ${i} +
    +
  • + `}get slotTempNext(){const t=!0===this._config.option_today_temperature_decimals?1:0,e=this._config.entity_temp_next_label&&void 0!==this.hass.states[this._config.entity_temp_next_label]?this.hass.states[this._config.entity_temp_next_label].state.toLowerCase().includes("min")||this.hass.states[this._config.entity_temp_next_label].state.toLowerCase().includes("low")?"mdi:thermometer-low":"mdi:thermometer-high":"mdi:help-box",i=this._config.entity_temp_next&&void 0!==this.hass.states[this._config.entity_temp_next]?Number(this.hass.states[this._config.entity_temp_next].state).toLocaleString(this.locale,{minimumFractionDigits:t,maximumFractionDigits:t}):"---",s=this._config.entity_temp_next_label&&void 0!==this.hass.states[this._config.entity_temp_next_label]?this.hass.states[this._config.entity_temp_next_label].state:"",o="---"!==i?R`
    ${this.getUOM("temperature")}
    `:R``;return R` +
  • +
    +
    + +
    +
    ${s} ${i}
    + ${o} +
    +
  • + `}get slotTempFollowing(){const t=!0===this._config.option_today_temperature_decimals?1:0,e=this._config.entity_temp_following_label&&void 0!==this.hass.states[this._config.entity_temp_following_label]?this.hass.states[this._config.entity_temp_following_label].state.toLowerCase().includes("min")||this.hass.states[this._config.entity_temp_following_label].state.toLowerCase().includes("low")?"mdi:thermometer-low":"mdi:thermometer-high":"mdi:help-box",i=this._config.entity_temp_following&&void 0!==this.hass.states[this._config.entity_temp_following]?Number(this.hass.states[this._config.entity_temp_following].state).toLocaleString(this.locale,{minimumFractionDigits:t,maximumFractionDigits:t}):"---",s=this._config.entity_temp_following_label&&void 0!==this.hass.states[this._config.entity_temp_following_label]?this.hass.states[this._config.entity_temp_following_label].state:"",o="---"!==i?R`
    ${this.getUOM("temperature")}
    `:R``;return R` +
  • +
    +
    + +
    +
    ${s} ${i}
    + ${o} +
    +
  • + `}get slotTempMaximums(){const t=!0===this._config.option_today_temperature_decimals?1:0,e=this._config.entity_observed_max&&void 0!==this.hass.states[this._config.entity_observed_max]?Number(this.hass.states[this._config.entity_observed_max].state).toLocaleString(this.locale,{minimumFractionDigits:t,maximumFractionDigits:t}):"---",i=this._config.entity_forecast_max&&void 0!==this.hass.states[this._config.entity_forecast_max]?Number(this.hass.states[this._config.entity_forecast_max].state).toLocaleString(this.locale,{minimumFractionDigits:t,maximumFractionDigits:t}):"---",s="---"!==e?R`
    ${this.getUOM("temperature")}
    `:R``;return R` +
  • +
    +
    + +
    +
    ${this.localeTextObsMax} 
    +
    ${e}
    ${s} +
     (${this.localeTextFore} 
    +
    ${i}
    ${s} +
    )
    +
    +
  • + `}get slotTempMinimums(){const t=!0===this._config.option_today_temperature_decimals?1:0,e=this._config.entity_observed_min&&void 0!==this.hass.states[this._config.entity_observed_min]?Number(this.hass.states[this._config.entity_observed_min].state).toLocaleString(this.locale,{minimumFractionDigits:t,maximumFractionDigits:t}):"---",i=this._config.entity_forecast_min&&void 0!==this.hass.states[this._config.entity_forecast_min]?Number(this.hass.states[this._config.entity_forecast_min].state).toLocaleString(this.locale,{minimumFractionDigits:t,maximumFractionDigits:t}):"---",s="---"!==e?R`
    ${this.getUOM("temperature")}
    `:R``;return R` +
  • +
    +
    + +
    +
    ${this.localeTextObsMin} 
    +
    ${e}
    ${s} +
     (${this.localeTextFore} 
    +
    ${i}
    ${s} +
    )
    +
    +
  • + `}get slotUvSummary(){const t=this._config.entity_uv_alert_summary&&void 0!==this.hass.states[this._config.entity_uv_alert_summary]?"unknown"!==this.hass.states[this._config.entity_uv_alert_summary].state?this.hass.states[this._config.entity_uv_alert_summary].state:"Not Applicable":"---";return R` +
  • +
    +
    + +
    +
    ${this.localeTextUVRating} ${t}
    +
    +
  • + `}get slotFireDanger(){const t=this._config.entity_fire_danger,e=t&&void 0!==this.hass.states[t]?"unknown"!==this.hass.states[t].state?!1===this._config.option_color_fire_danger?this.hass.states[t].state:this.hass.states[t].state.toLocaleUpperCase():"Not Applicable":"---";var i=t&&!1!==this._config.option_color_fire_danger&&this.hass.states[t].attributes.color_fill?`background-color:${this.hass.states[t].attributes.color_fill}; color:${this.hass.states[t].attributes.color_text};`:"";return!1===this._config.option_color_fire_danger?R` +
  • +
    +
    + +
    +
    ${e}
    +
    +
  • `:(""===i&&(i="font-weight:300; padding-left:0px;"),R` +
  • +
    +
    + +
    +
    +

    ${e}

    +
    +
    +
  • `)}get slotWind(){const t=this._config.entity_wind_speed&&this._config.option_show_beaufort?R`
    BFT: ${this.currentBeaufort} - `:"",e=this._config.entity_wind_bearing?R`
    ${this.currentWindBearing} 
    `:"",i=R`
    ${this.getUOM("length")}/h
    `,s=this._config.entity_wind_speed?R`
    ${this.currentWindSpeed}
    ${i} `:"",o=this._config.entity_wind_gust?R`
    (Gust ${this.currentWindGust}
    ${i})`:"";return R` +
  • +
    +
    + +
    + ${t}${e}${s}${o} +
    +
  • + `}get slotWindKt(){const t=this._config.entity_wind_speed_kt&&this._config.option_show_beaufort?R`
    BFT: ${this.currentBeaufortKt} - `:"",e=this._config.entity_wind_bearing?R`
    ${this.currentWindBearing} 
    `:"",i=R`
    Kt
    `,s=this._config.entity_wind_speed_kt?R`
    ${this.currentWindSpeedKt}
    ${i} `:"",o=this._config.entity_wind_gust_kt?R`
    (Gust ${this.currentWindGustKt}
    ${i})`:"";return R` +
  • +
    +
    + +
    + ${t}${e}${s}${o} +
    +
  • + `}get slotVisibility(){const t=this.currentVisibility,e="---"!==t?this.getUOM("length"):"";return R` +
  • +
    + +
    +
    ${t}
    +
    ${e} +
    +
  • + `}get slotSunNext(){return this._config.entity_sun?this.sunSet.next:R``}get slotSunFollowing(){return this._config.entity_sun?this.sunSet.following:R``}get slotCustom1(){var t=this._config.custom1_icon?this._config.custom1_icon:"mdi:help-box",e=this._config.custom1_value&&void 0!==this.hass.states[this._config.custom1_value]?this.hass.states[this._config.custom1_value].state:"unknown",i=this._config.custom1_units?this._config.custom1_units:"";return R` +
  • +
    + +
    +
    ${e}
    ${i}
    +
  • + `}get slotCustom2(){var t=this._config.custom2_icon?this._config.custom2_icon:"mdi:help-box",e=this._config.custom2_value&&void 0!==this.hass.states[this._config.custom2_value]?this.hass.states[this._config.custom2_value].state:"unknown",i=this._config.custom2_units?this._config.custom2_units:"";return R` +
  • +
    + +
    +
    ${e}
    ${i}
    +
  • + `}get slotCustom3(){var t=this._config.custom3_icon?this._config.custom3_icon:"mdi:help-box",e=this._config.custom3_value&&void 0!==this.hass.states[this._config.custom3_value]?this.hass.states[this._config.custom3_value].state:"unknown",i=this._config.custom3_units?this._config.custom3_units:"";return R` +
  • +
    + +
    +
    ${e}
    ${i}
    +
  • + `}get slotCustom4(){var t=this._config.custom4_icon?this._config.custom4_icon:"mdi:help-box",e=this._config.custom4_value&&void 0!==this.hass.states[this._config.custom4_value]?this.hass.states[this._config.custom4_value].state:"unknown",i=this._config.custom4_units?this._config.custom4_units:"";return R` +
  • +
    + +
    +
    ${e}
    ${i}
    +
  • + `}get forecastIcon(){const t=this._config.entity_forecast_icon;return t&&this.hass.states[t]?this.hass.states[t].state:"---"}get currentTemperature(){const t=this._config.entity_temperature,e=!0===this._config.option_show_overview_decimals?1:0;return t&&this.hass.states[t]?null===t.match("^weather.")?Number(this.hass.states[t].state).toLocaleString(this.locale,{minimumFractionDigits:e,maximumFractionDigits:e}):void 0!==this.hass.states[t].attributes.temperature?Number(this.hass.states[t].attributes.temperature).toLocaleString(this.locale,{minimumFractionDigits:e,maximumFractionDigits:e}):"---":"---"}get currentApparentTemperature(){const t=this._config.entity_apparent_temp,e=!0===this._config.option_show_overview_decimals?1:0;return t&&this.hass.states[t]?Number(this.hass.states[t].state).toLocaleString(this.locale,{minimumFractionDigits:e,maximumFractionDigits:e}):""}get currentHumidity(){const t=this._config.entity_humidity;return t&&this.hass.states[t]?null===t.match("^weather.")?Number(this.hass.states[t].state).toLocaleString(this.locale):void 0!==this.hass.states[t].attributes.humidity?Number(this.hass.states[t].attributes.humidity).toLocaleString(this.locale):"---":"---"}get currentRainfall(){const t=this._config.entity_rainfall,e=!0===this._config.option_today_rainfall_decimals?1:0;return t&&this.hass.states[t]?Number(this.hass.states[t].state).toLocaleString(this.locale,{minimumFractionDigits:e,maximumFractionDigits:e}):"---"}get currentPressure(){const t=this._config.entity_pressure;var e=this._config.option_pressure_decimals?Math.max(Math.min(this._config.option_pressure_decimals,3),0):0;return t&&this.hass.states[t]?null===t.match("^weather.")?Number(this.hass.states[t].state).toLocaleString(this.locale,{minimumFractionDigits:e,maximumFractionDigits:e}):void 0!==this.hass.states[t].attributes.pressure?Number(this.hass.states[t].attributes.pressure).toLocaleString(this.locale):"---":"---"}get currentVisibility(){const t=this._config.entity_visibility;return t&&this.hass.states[t]?null===t.match("^weather.")?Number(this.hass.states[t].state).toLocaleString(this.locale):void 0!==this.hass.states[t].attributes.visibility?Number(this.hass.states[t].attributes.visibility).toLocaleString(this.locale):"---":"---"}get currentWindBearing(){const t=this._config.entity_wind_bearing;return t&&this.hass.states[t]?null===t.match("^weather.")?isNaN(Number(this.hass.states[t].state))?this.hass.states[t].state:this.windDirections[Math.round(Number(this.hass.states[t].state)/360*16)]:void 0!==this.hass.states[t].attributes.wind_bearing?isNaN(Number(this.hass.states[t].attributes.wind_bearing))?this.hass.states[t].attributes.wind_bearing:this.windDirections[Math.round(Number(this.hass.states[t].attributes.wind_bearing)/360*16)]:"---":"---"}get currentWindSpeed(){const t=this._config.entity_wind_speed;return t&&this.hass.states[t]?null===t.match("^weather.")?Math.round(Number(this.hass.states[t].state)).toLocaleString(this.locale):void 0!==this.hass.states[t].attributes.wind_speed?Math.round(Number(this.hass.states[t].attributes.wind_speed)).toLocaleString(this.locale):"---":"---"}get currentWindGust(){const t=this._config.entity_wind_gust;return t&&this.hass.states[t]?Math.round(Number(this.hass.states[t].state)).toLocaleString(this.locale):"---"}get currentWindSpeedKt(){const t=this._config.entity_wind_speed_kt;return t&&this.hass.states[t]?null===t.match("^weather.")?Math.round(Number(this.hass.states[t].state)).toLocaleString(this.locale):void 0!==this.hass.states[t].attributes.wind_speed?Math.round(Number(this.hass.states[t].attributes.wind_speed)).toLocaleString(this.locale):"---":"---"}get currentWindGustKt(){const t=this._config.entity_wind_gust_kt;return t&&this.hass.states[t]?Math.round(Number(this.hass.states[t].state)).toLocaleString(this.locale):"---"}get windDirections(){const t=["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSW","SW","WSW","W","WNW","NW","NNW","N"],e=["N","NNE","NE","ENE","E","ESE","SE","SSE","S","SSO","SO","OSO","O","ONO","NO","NNO","N"],i=["N","NNO","NO","ONO","O","OSO","SO","SSO","S","SSW","SW","WSW","W","WNW","NW","NNW","N"],s=["N","NNO","NO","ONO","O","OZO","ZO","ZZO","Z","ZZW","ZW","WZW","W","WNW","NW","NNW","N"],o=["צפון","צ-צ-מז","צפון מזרח","מז-צ-מז","מזרח","מז-ד-מז","דרום מזרח","ד-ד-מז","דרום","ד-ד-מע","דרום מערב","מע-ד-מע","מערב","מע-צ-מע","צפון מערב","צ-צ-מע","צפון"],n=["N","NNØ","NØ","ØNØ","Ø","ØSØ","SØ","SSØ","S","SSV","SV","VSV","V","VNV","NV","NNV","N"],r=["С","ССВ","СВ","ВСВ","В","ВЮВ","ЮВ","ЮЮВ","Ю","ЮЮЗ","ЮЗ","ЗЮЗ","З","ЗСЗ","СЗ","ССЗ","С"],a=["С","ССИ","СИ","ИСИ","И","ИЮИ","ЮИ","ЮЮИ","Ю","ЮЮЗ","ЮЗ","ЗЮЗ","З","ЗСЗ","СЗ","ССЗ","С"];switch(this.locale){case"it":case"fr":return e;case"de":return i;case"nl":return s;case"he":return o;case"ru":return r;case"da":return n;case"bg":return a;default:return t}}get currentBeaufort(){const t=this._config.entity_wind_speed;if(t&&this.hass.states[t]&&!isNaN(Number(this.hass.states[t].state))){const e=Number(this.hass.states[t].state);switch(this.hass.states[t].attributes.unit_of_measurement){case"mph":return e>=73?"12":e>=64?"11":e>=55?"10":e>=47?"9":e>=39?"8":e>=32?"7":e>=25?"6":e>=19?"5":e>=13?"4":e>=8?"3":e>=4?"2":e>=1?"1":"0";case"m/s":return e>=32.7?"12":e>=28.5?"11":e>=24.5?"10":e>=20.8?"9":e>=17.2?"8":e>=13.9?"7":e>=10.8?"6":e>=8?"5":e>=5.5?"4":e>=3.4?"3":e>=1.6?"2":e>=.5?"1":"0";default:return e>=118?"12":e>=103?"11":e>=89?"10":e>=75?"9":e>=62?"8":e>=50?"7":e>=39?"6":e>=29?"5":e>=20?"4":e>=12?"3":e>=6?"2":e>=2?"1":"0"}}return"---"}get currentBeaufortKt(){const t=this._config.entity_wind_speed_kt;if(t&&this.hass.states[t]&&!isNaN(Number(this.hass.states[t].state))){const e=Number(this.hass.states[t].state);return e>=64?"12":e>=56?"11":e>=48?"10":e>=41?"9":e>=34?"8":e>=28?"7":e>=22?"6":e>=17?"5":e>=11?"4":e>=7?"3":e>=4?"2":e>=1?"1":"0"}return"---"}get sunSet(){var t,e;switch(this.timeFormat){case"12hour":t=this._config.entity_sun&&void 0!==this.hass.states[this._config.entity_sun]?new Date(this.hass.states[this._config.entity_sun].attributes.next_setting).toLocaleTimeString(this.locale,{hour:"numeric",minute:"2-digit",hour12:!0}).replace(" am","am").replace(" pm","pm"):"",e=this._config.entity_sun&&void 0!==this.hass.states[this._config.entity_sun]?new Date(this.hass.states[this._config.entity_sun].attributes.next_rising).toLocaleTimeString(this.locale,{hour:"numeric",minute:"2-digit",hour12:!0}).replace(" am","am").replace(" pm","pm"):"";break;case"24hour":t=this._config.entity_sun&&void 0!==this.hass.states[this._config.entity_sun]?new Date(this.hass.states[this._config.entity_sun].attributes.next_setting).toLocaleTimeString(this.locale,{hour:"2-digit",minute:"2-digit",hour12:!1}):"",e=this._config.entity_sun&&void 0!==this.hass.states[this._config.entity_sun]?new Date(this.hass.states[this._config.entity_sun].attributes.next_rising).toLocaleTimeString(this.locale,{hour:"2-digit",minute:"2-digit",hour12:!1}):"";break;case"system":t=this._config.entity_sun&&void 0!==this.hass.states[this._config.entity_sun]?new Date(this.hass.states[this._config.entity_sun].attributes.next_setting).toLocaleTimeString(navigator.language,{timeStyle:"short"}).replace(" am","am").replace(" pm","pm"):"",e=this._config.entity_sun&&void 0!==this.hass.states[this._config.entity_sun]?new Date(this.hass.states[this._config.entity_sun].attributes.next_rising).toLocaleTimeString(navigator.language,{timeStyle:"short"}).replace(" am","am").replace(" pm","pm"):""}var i=new Date;return i.setDate(i.getDate()+1),this._config.entity_sun&&void 0!==this.hass.states[this._config.entity_sun]?"above_horizon"===this.hass.states[this._config.entity_sun].state?(e=i.toLocaleDateString(this.locale,{weekday:"short"})+" "+e,{next:R` +
  • +
    + +
    +
    ${t}
    +
  • `,following:R` +
  • +
    + +
    +
    ${e}
    +
  • `,nextText:t,followingText:e,nextIcon:"mdi:weather-sunset-down",followingIcon:"mdi:weather-sunset-up"}):((new Date).getDate()!=new Date(this.hass.states[this._config.entity_sun].attributes.next_rising).getDate()&&(e=i.toLocaleDateString(this.locale,{weekday:"short"})+" "+e,t=i.toLocaleDateString(this.locale,{weekday:"short"})+" "+t),{next:R` +
  • +
    + +
    +
    ${e}
    +
  • `,following:R` +
  • +
    + +
    +
    ${t}
    +
  • `,nextText:e,followingText:t,nextIcon:"mdi:weather-sunset-up",followingIcon:"mdi:weather-sunset-down"}):{next:R``,following:R``,nextText:"",followingText:"",nextIcon:"",followingIcon:""}}get timeFormat(){return this._config.option_time_format?this._config.option_time_format:"system"}_weatherIcon(t){switch(t){case"sunny":case"clear":return this.iconClear;case"mostly-sunny":case"mostly_sunny":return this.iconMostlySunny;case"partly-cloudy":case"partly_cloudy":case"partlycloudy":return this.iconPartlyCloudy;case"cloudy":return this.iconCloudy;case"hazy":case"hazey":case"haze":return this.iconHazy;case"frost":return this.iconFrost;case"light-rain":case"light_rain":return this.iconLightRain;case"wind":case"windy":return this.iconWindy;case"fog":case"foggy":return this.iconFog;case"showers":case"shower":return this.iconShowers;case"rain":case"rainy":return this.iconRain;case"dust":case"dusty":return this.iconDust;case"snow":case"snowy":return this.iconSnow;case"snowy-rainy":case"snowy_rainy":case"snowyrainy":return this.iconSnowRain;case"storm":case"stormy":return this.iconStorm;case"light-showers":case"light-shower":case"light_showers":case"light_shower":return this.iconLightShowers;case"heavy-showers":case"heavy-shower":case"heavy_showers":case"heavy_shower":case"pouring":return this.iconHeavyShowers;case"tropical-cyclone":case"tropical_cyclone":case"tropicalcyclone":return this.iconCyclone;case"clear-day":case"clear_day":return this.iconClearDay;case"clear-night":case"clear_night":return this.iconClearNight;case"sleet":return this.iconSleet;case"partly-cloudy-day":case"partly_cloudy_day":return this.iconPartlyCloudyDay;case"partly-cloudy-night":case"partly_cloudy_night":return this.iconPartlyCloudyNight;case"hail":return this.iconHail;case"lightning":case"lightning-rainy":case"lightning_rainy":case"thunderstorm":return this.iconLightning;case"windy-variant":case"windy_variant":return this.iconWindyVariant}return"unknown"}get dayOrNight(){return this._config.entity_sun&&void 0!==this.hass.states[this._config.entity_sun]?{below_horizon:"night",above_horizon:"day"}[this.hass.states[this._config.entity_sun].state]:"day"}get iconClear(){return`clear-${this.dayOrNight}`}get iconMostlySunny(){return`cloudy-1-${this.dayOrNight}`}get iconPartlyCloudy(){return`cloudy-2-${this.dayOrNight}`}get iconCloudy(){return"cloudy"}get iconHazy(){return`haze-${this.dayOrNight}`}get iconFrost(){return`frost-${this.dayOrNight}`}get iconLightRain(){return"rainy-2"}get iconWindy(){return"wind"}get iconFog(){return`fog-${this.dayOrNight}`}get iconShowers(){return`rainy-1-${this.dayOrNight}`}get iconRain(){return"rainy-3"}get iconDust(){return"dust"}get iconSnow(){return"snowy-3"}get iconSnowRain(){return"snow-and-sleet-mix"}get iconStorm(){return`scattered-thunderstorms-${this.dayOrNight}`}get iconLightShowers(){return`rainy-1-${this.dayOrNight}`}get iconHeavyShowers(){return`rainy-2-${this.dayOrNight}`}get iconCyclone(){return"tropical-storm"}get iconClearDay(){return"clear-day"}get iconClearNight(){return"clear-night"}get iconSleet(){return"rain-and-sleet-mix"}get iconPartlyCloudyDay(){return"cloudy-1-day"}get iconPartlyCloudyNight(){return"cloudy-1-night"}get iconHail(){return"hail"}get iconLightning(){return`isolated-thunderstorms-${this.dayOrNight}`}get iconWindyVariant(){return"wind"}get locale(){try{return Intl.NumberFormat(this._config.option_locale),this._config.option_locale}catch(t){return}}get localeTextFeelsLike(){switch(this.locale){case"it":return"Percepito";case"fr":return"Ressenti";case"de":return"Gefühlt";case"nl":return"Voelt als";case"pl":return"Odczuwalne";case"he":return"מרגיש כמו";case"da":return"Føles som";case"ru":return"Ощущается как";case"ua":return"Відчувається як";case"bg":return"Усеща се като";default:return"Feels like"}}get localeTextObservedMax(){return this.locale,"Observed Max"}get localeTextObservedMin(){return this.locale,"Observed Min"}get localeTextObsMax(){return this.locale,"Obs Max"}get localeTextObsMin(){return this.locale,"Obs Min"}get localeTextForecastMax(){switch(this.locale){case"it":return"Max oggi";case"fr":return"Max aujourd'hui";case"de":return"Max heute";case"nl":return"Max vandaag";case"pl":return"Maks Temperatura";case"he":return"מקסימלי היום";case"da":return"Højeste i dag";case"ru":return"Макс сегодня";case"ua":return"Макс сьогодні";case"bg":return"Макс днес";default:return"Forecast Max"}}get localeTextForecastMin(){switch(this.locale){case"it":return"Min oggi";case"fr":return"Min aujourd'hui";case"de":return"Min heute";case"nl":return"Min vandaag";case"pl":return"Min Temperatura";case"he":return"דקות היום";case"da":return"Laveste i dag";case"ru":return"Мин сегодня";case"ua":return"Мін сьогодні";case"bg":return"Мин днес";default:return"Forecast Min"}}get localeTextPosToday(){switch(this.locale){case"it":return"Previsione";case"fr":return"Prévoir";case"de":return"Vorhersage";case"nl":return"Prognose";case"pl":return"Prognoza";case"he":return"תַחֲזִית";case"da":return"Vejrudsigt";case"ru":case"ua":return"Прогноз";case"bg":return"Прогноза";default:return"Forecast"}}get localeTextPosTomorrow(){switch(this.locale){case"it":return"Prev per domani";case"fr":return"Prév demain";case"de":case"nl":return"Prog morgen";case"pl":return"Prog jutro";case"he":return"תחזית מחר";case"da":return"Prog i morgen";case"ru":case"ua":return"Прогноз на завтра";case"bg":return"Прогноза за утре";default:return"Fore Tom"}}get localeTextFore(){switch(this.locale){case"it":return"Prev";case"fr":return"Prév";case"de":case"nl":case"pl":case"da":return"Prog";case"he":return"תַחֲזִית";case"ru":case"ua":return"Прогноз";case"bg":return"Прогноза";default:return"Fore"}}get localeTextUVRating(){switch(this.locale){default:return"UV";case"ru":case"ua":return"УФ"}}get localeTextFireDanger(){switch(this.locale){case"it":return"Fuoco";case"fr":return"Feu";case"de":return"Feuer";case"nl":case"da":return"Brand";case"pl":return"Ogień";case"he":return"אֵשׁ";case"ru":return"Огонь";case"ua":return"Вогонь";case"bg":return"Пожар";default:return"Fire"}}getUOM(t){const e=this.hass.config.unit_system.length;switch(t){case"air_pressure":return void 0!==this._config.entity_pressure&&void 0!==this.hass.states[this._config.entity_pressure].attributes.unit_of_measurement?this.hass.states[this._config.entity_pressure].attributes.unit_of_measurement:"km"===e?"hPa":"mbar";case"length":return e;case"precipitation":return"km"===e?"mm":"in";case"intensity":return"km"===e?"mm/h":"in/h";default:return this.hass.config.unit_system[t]||""}}_showConfigWarning(t){return R` + +
    Weather Card
    + ${t.map((t=>R`
    ${t}
    `))} +
    + `}_showWarning(t){return R`${t}`}_showError(t){const e=document.createElement("hui-error-card");return e.setConfig({type:"error",error:t,origConfig:this._config}),R`${e}`}get styles(){const t=this._config.option_tooltips?"visible":"hidden",e=this._config.temp_font_weight||"300",i=this._config.temp_font_size||"4em",s=this._config.forecast_text_font_size||"21px",o=this._config.forecast_text_alignment||"center";return nt` + .card { + padding: 8px 16px 8px 16px; + } + .content { + align-items: center; + } + .card-header { + font-size: 1.5em; + color: var(--primary-text-color); + } + .section { + margin: -1px; + border: 1px solid transparent; + padding-top: 8px; + padding-bottom: 8px; + } + .updated { + font-size: 0.9em; + font-weight: 300; + color: var(--primary-text-color); + } + .overview-top { + display: flex; + justify-content: space-between; + flex-wrap: nowrap; + } + .stacked { + position: absolute; + } + .top-left { + display: flex; + flex-direction: column; + height: 120px; + } + .top-left-obs { + display: flex; + flex-direction: column; + } + .big-icon { + height: 120px; + width: 140px; + position: relative; + } + .unknown-forecast { + position: relative; + top: -30px; + text-align: center; + } + .currentTemps { + display: flex; + align-self: flex-start; + flex-direction: column; + height: 60px; + } + .current-temp { + display: table-row; + margin-left: auto; + padding: 2px 0px; + } + .temp { + display:table-cell; + font-weight: ${ot(e)}; + font-size: ${ot(i)}; + color: var(--primary-text-color); + position: relative; + line-height: 74%; + } + .unit-temp-big { + display: table-cell; + vertical-align: top; + font-weight: ${ot(e)}; + font-size: 1.5em; + color: var(--primary-text-color); + position: relative; + line-height: 74%; + } + .apparent-temp { + display: table-row; + margin-left: auto; + height: 24px; + } + .apparent { + display: table-cell; + color: var(--primary-text-color); + font-weight: 300; + position: relative; + line-height: 24px; + } + .unit-temp-small { + display: table-cell; + vertical-align: top; + font-size: 10.5px; + color: var(--primary-text-color); + position: relative; + line-height: 14px; + padding-top: 3.6px; + padding-left: 1px; + } + .line { + margin-top : 7px; + margin-bottom: -9px; + color: var(--primary-text-color); + } + .forecast-text { + font-size: ${ot(s)}; + text-align: ${ot(o)}; + line-height: 25px; + } + .forecast-text-right { + font-size: ${ot(s)}; + text-align: ${ot(o)}; + width: 100%; + align-items: center; + display: flex; + justify-content: center; + line-height: 25px; + margin-left: -40px; + } + .variations { + display: flex; + flex-flow: row wrap; + font-weight: 300; + color: var(--primary-text-color); + list-style: none; + margin-block-start: 0px; + margin-block-end: 0px; + padding-inline-start: 8px; + } + .slot-list-item-1 { + min-width:50%; + padding-right: 8px; + } + .slot-list { + list-style: none; + padding: 0; + } + .slot-list li { + height:24px; + } + .variations-ugly { + display: flex; + flex-flow: row wrap; + justify-content: space-between; + font-weight: 300; + color: var(--primary-text-color); + list-style: none; + margin-block-start: 0px; + margin-block-end: 0px; + padding-inline-start: 8px; + } + .ha-icon { + height: 24px; + margin-right: 5px; + color: var(--paper-item-icon-color); + } + .unit { + font-size: 0.8em; + display: table-cell; + padding-left: 1px; + } + .slot { + display: table-row; + } + .slot-icon { + display: table-cell; + position: relative; + height: 18px; + padding-right: 5px; + color: var(--paper-item-icon-color); + } + .slot-text { + display: table-cell; + position: relative; + } + .fire-danger-text-color { + display: inline-block; + height: 18px; + line-height: 20px; + text-align: left; + vertical-align: middle; + margin: 0; + padding-left: 4px; + font-weight: 600; + width: 108px; + } + .daily-forecast-horiz-section { + display: flex; + flex-flow: row wrap; + width: 100%; + margin: 0; + clear: both; + } + .daily-forecast-horiz-section .day-horiz:nth-last-child(1) { + border-right: transparent; + } + .day-horiz { + flex: 1; + float: left; + text-align: center; + color: var(--primary-text-color); + border-right: 1px solid #d9d9d9; + margin-right: -1px; + box-sizing: border-box; + } + .daily-forecast-vert-section { + display: flex; + flex-flow: column nowrap; + margin: 0 auto; + clear: both; + } + .day-vert { + flex: 1; + color: var(--primary-text-color); + border-top: 1px solid #d9d9d9; + line-height: 24px; + box-sizing: border-box; + padding-bottom: 8px; + } + .day-vert-top { + display: flex; + width: 100%; + } + .day-vert-middle { + display: flex; + float: left; + width: 100%; + } + .day-vert-bottom { + text-align: left; + float: left; + } + .day-vert-dayicon { + width: 40px; + text-align: left; + float: left; + margin-bottom: -8px; + } + .day-vert-temps { + flex: 1; + text-align: left; + float: left; + padding-left: 1em; + padding-top: 0.5em; + } + .day-vert-rain { + flex: 2; + text-align: left; + float: left; + padding-left: 1em; + padding-top: 0.5em; + } + .dayname { + text-transform: uppercase; + } + .dayname-vert { + min-width: 40px; + max-width: 40px; + text-transform: uppercase; + } + .icon { + width: 49px; + height: 42px; + margin: auto; + display: inline-block; + background-size: contain; + background-position: center center; + background-repeat: no-repeat; + text-indent: -9999px; + } + .f-slot-horiz { + display: inline-block; + text-align: center; + list-style: none; + overflow: hidden; + font-weight: 300; + padding: 0; + margin-block-start: 0; + margin-block-end: -12px; + } + .f-slot-horiz-text { + height:20px; + } + .f-slot-horiz-icon { + height:50px; + } + .f-summary-vert { + padding-left: 1em; + font-weight: 400; + } + .f-firedanger-vert { + text-align: right; + font-weight: 300; + margin-top: -24px; + } + .f-slot-vert { + display: table; + overflow: hidden; + height: 24px; + font-weight: 300; + } + .f-slot-minmax { + width: 100%; + } + .f-extended { + display: inline-table; + font-size: 13px; + font-weight: 300; + padding-top: 8px; + line-height:20px; + } + .extended-section .f-extended { + padding-top: 0; + } + .highTemp { + display: table-cell; + font-weight: bold; + } + .lowTemp { + display: table-cell; + font-weight: 300; + } + .slash { + padding-left: 2px; + padding-right: 2px; + } + .high-temp { + display: table-cell; + font-weight: bold; + width: 21px; + text-align: right; + } + .low-temp { + display: table-cell; + font-weight: 300; + width: 21px; + text-align: right; + } + .temp-label { + display: table-cell; + width: 32px; + font-weight: 300; + } + .f-label { + display: table-cell; + white-space: nowrap; + padding-right: 0.2em; + } + .pop { + display: table-cell; + font-weight: 300; + color: var(--primary-text-color); + } + .pos { + display: table-cell; + font-weight: 300; + color: var(--primary-text-color); + white-space: nowrap; + } + .fcasttooltip { + position: relative; + display: inline-block; + } + .fcasttooltip .fcasttooltipblock { + visibility: hidden; + background-color: #4B9BEF; + color: #FFFFFF; + text-align: center; + border-radius: 6px; + border-style: solid; + border-color: #FFA100; + border-width: 1px; + padding: 5px 0; + position: absolute; + z-index: 1; + bottom: 107%; + margin-left: -2px; + } + .fcasttooltip:hover .fcasttooltipblock { + visibility: ${ot(t)}; + } + .fcasttooltiptext { + padding-left: 8px; + padding-right: 8px; + } + `}};s([wt({attribute:!1})],ge.prototype,"hass",void 0),s([xt()],ge.prototype,"_config",void 0),s([xt()],ge.prototype,"_cardWidth",void 0),ge=s([yt("platinum-weather-card")],ge);export{ge as P,e as _,i as a,s as b,le as c,W as d,wt as e,xt as f,o as g,yt as h,he as i,Ot as l,Pt as n,$t as o,nt as r,vt as s,ce as t,z as x,R as y}; diff --git a/www/community/platinum-weather-card/platinum-weather-card-df6d26e2.js.gz b/www/community/platinum-weather-card/platinum-weather-card-df6d26e2.js.gz new file mode 100644 index 0000000..3398c61 Binary files /dev/null and b/www/community/platinum-weather-card/platinum-weather-card-df6d26e2.js.gz differ diff --git a/www/community/platinum-weather-card/platinum-weather-card.js b/www/community/platinum-weather-card/platinum-weather-card.js new file mode 100644 index 0000000..93c2edf --- /dev/null +++ b/www/community/platinum-weather-card/platinum-weather-card.js @@ -0,0 +1 @@ +export{P as PlatinumWeatherCard}from"./platinum-weather-card-df6d26e2.js"; diff --git a/www/community/platinum-weather-card/platinum-weather-card.js.gz b/www/community/platinum-weather-card/platinum-weather-card.js.gz new file mode 100644 index 0000000..e952a2d Binary files /dev/null and b/www/community/platinum-weather-card/platinum-weather-card.js.gz differ diff --git a/www/community/platinum-weather-card/s-clear-day.svg b/www/community/platinum-weather-card/s-clear-day.svg new file mode 100644 index 0000000..f9dce1c --- /dev/null +++ b/www/community/platinum-weather-card/s-clear-day.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-clear-night.svg b/www/community/platinum-weather-card/s-clear-night.svg new file mode 100644 index 0000000..05878ee --- /dev/null +++ b/www/community/platinum-weather-card/s-clear-night.svg @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-cloudy-1-day.svg b/www/community/platinum-weather-card/s-cloudy-1-day.svg new file mode 100644 index 0000000..c48b575 --- /dev/null +++ b/www/community/platinum-weather-card/s-cloudy-1-day.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-cloudy-1-night.svg b/www/community/platinum-weather-card/s-cloudy-1-night.svg new file mode 100644 index 0000000..b2ac8f5 --- /dev/null +++ b/www/community/platinum-weather-card/s-cloudy-1-night.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-cloudy-2-day.svg b/www/community/platinum-weather-card/s-cloudy-2-day.svg new file mode 100644 index 0000000..9f4ca54 --- /dev/null +++ b/www/community/platinum-weather-card/s-cloudy-2-day.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-cloudy-2-night.svg b/www/community/platinum-weather-card/s-cloudy-2-night.svg new file mode 100644 index 0000000..afe368f --- /dev/null +++ b/www/community/platinum-weather-card/s-cloudy-2-night.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-cloudy-3-day.svg b/www/community/platinum-weather-card/s-cloudy-3-day.svg new file mode 100644 index 0000000..96e73f1 --- /dev/null +++ b/www/community/platinum-weather-card/s-cloudy-3-day.svg @@ -0,0 +1,49 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-cloudy-3-night.svg b/www/community/platinum-weather-card/s-cloudy-3-night.svg new file mode 100644 index 0000000..c4582b4 --- /dev/null +++ b/www/community/platinum-weather-card/s-cloudy-3-night.svg @@ -0,0 +1,39 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-cloudy.svg b/www/community/platinum-weather-card/s-cloudy.svg new file mode 100644 index 0000000..5f542bd --- /dev/null +++ b/www/community/platinum-weather-card/s-cloudy.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-dust.svg b/www/community/platinum-weather-card/s-dust.svg new file mode 100644 index 0000000..a5189f6 --- /dev/null +++ b/www/community/platinum-weather-card/s-dust.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-fog-day.svg b/www/community/platinum-weather-card/s-fog-day.svg new file mode 100644 index 0000000..6817069 --- /dev/null +++ b/www/community/platinum-weather-card/s-fog-day.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-fog-night.svg b/www/community/platinum-weather-card/s-fog-night.svg new file mode 100644 index 0000000..de7be0e --- /dev/null +++ b/www/community/platinum-weather-card/s-fog-night.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-fog.svg b/www/community/platinum-weather-card/s-fog.svg new file mode 100644 index 0000000..0ab8565 --- /dev/null +++ b/www/community/platinum-weather-card/s-fog.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-frost-day.svg b/www/community/platinum-weather-card/s-frost-day.svg new file mode 100644 index 0000000..38a3ae2 --- /dev/null +++ b/www/community/platinum-weather-card/s-frost-day.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-frost-night.svg b/www/community/platinum-weather-card/s-frost-night.svg new file mode 100644 index 0000000..0cbfdcb --- /dev/null +++ b/www/community/platinum-weather-card/s-frost-night.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-frost.svg b/www/community/platinum-weather-card/s-frost.svg new file mode 100644 index 0000000..b61de9b --- /dev/null +++ b/www/community/platinum-weather-card/s-frost.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-hail.svg b/www/community/platinum-weather-card/s-hail.svg new file mode 100644 index 0000000..7aef244 --- /dev/null +++ b/www/community/platinum-weather-card/s-hail.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-haze-day.svg b/www/community/platinum-weather-card/s-haze-day.svg new file mode 100644 index 0000000..85bb64a --- /dev/null +++ b/www/community/platinum-weather-card/s-haze-day.svg @@ -0,0 +1,51 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-haze-night.svg b/www/community/platinum-weather-card/s-haze-night.svg new file mode 100644 index 0000000..0a613ec --- /dev/null +++ b/www/community/platinum-weather-card/s-haze-night.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-haze.svg b/www/community/platinum-weather-card/s-haze.svg new file mode 100644 index 0000000..c5021ce --- /dev/null +++ b/www/community/platinum-weather-card/s-haze.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-hurricane.svg b/www/community/platinum-weather-card/s-hurricane.svg new file mode 100644 index 0000000..6e115d7 --- /dev/null +++ b/www/community/platinum-weather-card/s-hurricane.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-isolated-thunderstorms-day.svg b/www/community/platinum-weather-card/s-isolated-thunderstorms-day.svg new file mode 100644 index 0000000..16635d3 --- /dev/null +++ b/www/community/platinum-weather-card/s-isolated-thunderstorms-day.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-isolated-thunderstorms-night.svg b/www/community/platinum-weather-card/s-isolated-thunderstorms-night.svg new file mode 100644 index 0000000..18c72b6 --- /dev/null +++ b/www/community/platinum-weather-card/s-isolated-thunderstorms-night.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-isolated-thunderstorms.svg b/www/community/platinum-weather-card/s-isolated-thunderstorms.svg new file mode 100644 index 0000000..f2fc09b --- /dev/null +++ b/www/community/platinum-weather-card/s-isolated-thunderstorms.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-rain-and-sleet-mix.svg b/www/community/platinum-weather-card/s-rain-and-sleet-mix.svg new file mode 100644 index 0000000..abcaf56 --- /dev/null +++ b/www/community/platinum-weather-card/s-rain-and-sleet-mix.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-rain-and-snow-mix.svg b/www/community/platinum-weather-card/s-rain-and-snow-mix.svg new file mode 100644 index 0000000..9b2d6c1 --- /dev/null +++ b/www/community/platinum-weather-card/s-rain-and-snow-mix.svg @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-rainy-1-day.svg b/www/community/platinum-weather-card/s-rainy-1-day.svg new file mode 100644 index 0000000..691ab2d --- /dev/null +++ b/www/community/platinum-weather-card/s-rainy-1-day.svg @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-rainy-1-night.svg b/www/community/platinum-weather-card/s-rainy-1-night.svg new file mode 100644 index 0000000..a420db2 --- /dev/null +++ b/www/community/platinum-weather-card/s-rainy-1-night.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-rainy-1.svg b/www/community/platinum-weather-card/s-rainy-1.svg new file mode 100644 index 0000000..ee3a43a --- /dev/null +++ b/www/community/platinum-weather-card/s-rainy-1.svg @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-rainy-2-day.svg b/www/community/platinum-weather-card/s-rainy-2-day.svg new file mode 100644 index 0000000..ac725a2 --- /dev/null +++ b/www/community/platinum-weather-card/s-rainy-2-day.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-rainy-2-night.svg b/www/community/platinum-weather-card/s-rainy-2-night.svg new file mode 100644 index 0000000..87f282a --- /dev/null +++ b/www/community/platinum-weather-card/s-rainy-2-night.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-rainy-2.svg b/www/community/platinum-weather-card/s-rainy-2.svg new file mode 100644 index 0000000..3b919cf --- /dev/null +++ b/www/community/platinum-weather-card/s-rainy-2.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-rainy-3-day.svg b/www/community/platinum-weather-card/s-rainy-3-day.svg new file mode 100644 index 0000000..cc236ea --- /dev/null +++ b/www/community/platinum-weather-card/s-rainy-3-day.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-rainy-3-night.svg b/www/community/platinum-weather-card/s-rainy-3-night.svg new file mode 100644 index 0000000..cb17085 --- /dev/null +++ b/www/community/platinum-weather-card/s-rainy-3-night.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-rainy-3.svg b/www/community/platinum-weather-card/s-rainy-3.svg new file mode 100644 index 0000000..6858669 --- /dev/null +++ b/www/community/platinum-weather-card/s-rainy-3.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-scattered-thunderstorms-day.svg b/www/community/platinum-weather-card/s-scattered-thunderstorms-day.svg new file mode 100644 index 0000000..7309bd8 --- /dev/null +++ b/www/community/platinum-weather-card/s-scattered-thunderstorms-day.svg @@ -0,0 +1,54 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-scattered-thunderstorms-night.svg b/www/community/platinum-weather-card/s-scattered-thunderstorms-night.svg new file mode 100644 index 0000000..fbb9030 --- /dev/null +++ b/www/community/platinum-weather-card/s-scattered-thunderstorms-night.svg @@ -0,0 +1,44 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-scattered-thunderstorms.svg b/www/community/platinum-weather-card/s-scattered-thunderstorms.svg new file mode 100644 index 0000000..4cf2757 --- /dev/null +++ b/www/community/platinum-weather-card/s-scattered-thunderstorms.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-severe-thunderstorm.svg b/www/community/platinum-weather-card/s-severe-thunderstorm.svg new file mode 100644 index 0000000..ebfe5fe --- /dev/null +++ b/www/community/platinum-weather-card/s-severe-thunderstorm.svg @@ -0,0 +1,41 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-snow-and-sleet-mix.svg b/www/community/platinum-weather-card/s-snow-and-sleet-mix.svg new file mode 100644 index 0000000..5fe5b9f --- /dev/null +++ b/www/community/platinum-weather-card/s-snow-and-sleet-mix.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-snowy-1-day.svg b/www/community/platinum-weather-card/s-snowy-1-day.svg new file mode 100644 index 0000000..f877adc --- /dev/null +++ b/www/community/platinum-weather-card/s-snowy-1-day.svg @@ -0,0 +1,57 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-snowy-1-night.svg b/www/community/platinum-weather-card/s-snowy-1-night.svg new file mode 100644 index 0000000..1cfd39a --- /dev/null +++ b/www/community/platinum-weather-card/s-snowy-1-night.svg @@ -0,0 +1,47 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-snowy-1.svg b/www/community/platinum-weather-card/s-snowy-1.svg new file mode 100644 index 0000000..3c3234e --- /dev/null +++ b/www/community/platinum-weather-card/s-snowy-1.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-snowy-2-day.svg b/www/community/platinum-weather-card/s-snowy-2-day.svg new file mode 100644 index 0000000..56bc9f5 --- /dev/null +++ b/www/community/platinum-weather-card/s-snowy-2-day.svg @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-snowy-2-night.svg b/www/community/platinum-weather-card/s-snowy-2-night.svg new file mode 100644 index 0000000..bac3817 --- /dev/null +++ b/www/community/platinum-weather-card/s-snowy-2-night.svg @@ -0,0 +1,55 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-snowy-2.svg b/www/community/platinum-weather-card/s-snowy-2.svg new file mode 100644 index 0000000..722ca42 --- /dev/null +++ b/www/community/platinum-weather-card/s-snowy-2.svg @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-snowy-3-day.svg b/www/community/platinum-weather-card/s-snowy-3-day.svg new file mode 100644 index 0000000..5516109 --- /dev/null +++ b/www/community/platinum-weather-card/s-snowy-3-day.svg @@ -0,0 +1,73 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-snowy-3-night.svg b/www/community/platinum-weather-card/s-snowy-3-night.svg new file mode 100644 index 0000000..ef339dd --- /dev/null +++ b/www/community/platinum-weather-card/s-snowy-3-night.svg @@ -0,0 +1,63 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-snowy-3.svg b/www/community/platinum-weather-card/s-snowy-3.svg new file mode 100644 index 0000000..bed5990 --- /dev/null +++ b/www/community/platinum-weather-card/s-snowy-3.svg @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-thunderstorms.svg b/www/community/platinum-weather-card/s-thunderstorms.svg new file mode 100644 index 0000000..784cbd9 --- /dev/null +++ b/www/community/platinum-weather-card/s-thunderstorms.svg @@ -0,0 +1,35 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-tornado.svg b/www/community/platinum-weather-card/s-tornado.svg new file mode 100644 index 0000000..e68e516 --- /dev/null +++ b/www/community/platinum-weather-card/s-tornado.svg @@ -0,0 +1,43 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-tropical-storm.svg b/www/community/platinum-weather-card/s-tropical-storm.svg new file mode 100644 index 0000000..0c8381a --- /dev/null +++ b/www/community/platinum-weather-card/s-tropical-storm.svg @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-unknown.svg b/www/community/platinum-weather-card/s-unknown.svg new file mode 100644 index 0000000..d610a3e --- /dev/null +++ b/www/community/platinum-weather-card/s-unknown.svg @@ -0,0 +1,17 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/platinum-weather-card/s-wind.svg b/www/community/platinum-weather-card/s-wind.svg new file mode 100644 index 0000000..f99425c --- /dev/null +++ b/www/community/platinum-weather-card/s-wind.svg @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/www/community/scheduler-card/scheduler-card.js b/www/community/scheduler-card/scheduler-card.js new file mode 100644 index 0000000..5b03c94 --- /dev/null +++ b/www/community/scheduler-card/scheduler-card.js @@ -0,0 +1,2917 @@ +!function(e){"use strict";function t(e,t,i,s){var a,o=arguments.length,n=o<3?t:null===s?s=Object.getOwnPropertyDescriptor(t,i):s;if("object"==typeof Reflect&&"function"==typeof Reflect.decorate)n=Reflect.decorate(e,t,i,s);else for(var r=e.length-1;r>=0;r--)(a=e[r])&&(n=(o<3?a(n):o>3?a(t,i,n):a(t,i))||n);return o>3&&n&&Object.defineProperty(t,i,n),n}"function"==typeof SuppressedError&&SuppressedError; +/** + * @license + * Copyright 2019 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const i=window,s=i.ShadowRoot&&(void 0===i.ShadyCSS||i.ShadyCSS.nativeShadow)&&"adoptedStyleSheets"in Document.prototype&&"replace"in CSSStyleSheet.prototype,a=Symbol(),o=new WeakMap;class n{constructor(e,t,i){if(this._$cssResult$=!0,i!==a)throw Error("CSSResult is not constructable. Use `unsafeCSS` or `css` instead.");this.cssText=e,this.t=t}get styleSheet(){let e=this.o;const t=this.t;if(s&&void 0===e){const i=void 0!==t&&1===t.length;i&&(e=o.get(t)),void 0===e&&((this.o=e=new CSSStyleSheet).replaceSync(this.cssText),i&&o.set(t,e))}return e}toString(){return this.cssText}}const r=(e,...t)=>{const i=1===e.length?e[0]:t.reduce((t,i,s)=>t+(e=>{if(!0===e._$cssResult$)return e.cssText;if("number"==typeof e)return e;throw Error("Value passed to 'css' function must be a 'css' function result: "+e+". Use 'unsafeCSS' to pass non-literal values, but take care to ensure page security.")})(i)+e[s+1],e[0]);return new n(i,e,a)},d=(e,t)=>{s?e.adoptedStyleSheets=t.map(e=>e instanceof CSSStyleSheet?e:e.styleSheet):t.forEach(t=>{const s=document.createElement("style"),a=i.litNonce;void 0!==a&&s.setAttribute("nonce",a),s.textContent=t.cssText,e.appendChild(s)})},l=s?e=>e:e=>e instanceof CSSStyleSheet?(e=>{let t="";for(const i of e.cssRules)t+=i.cssText;return(e=>new n("string"==typeof e?e:e+"",void 0,a))(t)})(e):e +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */;var c;const u=window,h=u.trustedTypes,m=h?h.emptyScript:"",p=u.reactiveElementPolyfillSupport,_={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 i=e;switch(t){case Boolean:i=null!==e;break;case Number:i=null===e?null:Number(e);break;case Object:case Array:try{i=JSON.parse(e)}catch(e){i=null}}return i}},g=(e,t)=>t!==e&&(t==t||e==e),v={attribute:!0,type:String,converter:_,reflect:!1,hasChanged:g},y="finalized";class f extends HTMLElement{constructor(){super(),this._$Ei=new Map,this.isUpdatePending=!1,this.hasUpdated=!1,this._$El=null,this._$Eu()}static addInitializer(e){var t;this.finalize(),(null!==(t=this.h)&&void 0!==t?t:this.h=[]).push(e)}static get observedAttributes(){this.finalize();const e=[];return this.elementProperties.forEach((t,i)=>{const s=this._$Ep(i,t);void 0!==s&&(this._$Ev.set(s,i),e.push(s))}),e}static createProperty(e,t=v){if(t.state&&(t.attribute=!1),this.finalize(),this.elementProperties.set(e,t),!t.noAccessor&&!this.prototype.hasOwnProperty(e)){const i="symbol"==typeof e?Symbol():"__"+e,s=this.getPropertyDescriptor(e,i,t);void 0!==s&&Object.defineProperty(this.prototype,e,s)}}static getPropertyDescriptor(e,t,i){return{get(){return this[t]},set(s){const a=this[e];this[t]=s,this.requestUpdate(e,a,i)},configurable:!0,enumerable:!0}}static getPropertyOptions(e){return this.elementProperties.get(e)||v}static finalize(){if(this.hasOwnProperty(y))return!1;this[y]=!0;const e=Object.getPrototypeOf(this);if(e.finalize(),void 0!==e.h&&(this.h=[...e.h]),this.elementProperties=new Map(e.elementProperties),this._$Ev=new Map,this.hasOwnProperty("properties")){const e=this.properties,t=[...Object.getOwnPropertyNames(e),...Object.getOwnPropertySymbols(e)];for(const i of t)this.createProperty(i,e[i])}return this.elementStyles=this.finalizeStyles(this.styles),!0}static finalizeStyles(e){const t=[];if(Array.isArray(e)){const i=new Set(e.flat(1/0).reverse());for(const e of i)t.unshift(l(e))}else void 0!==e&&t.push(l(e));return t}static _$Ep(e,t){const i=t.attribute;return!1===i?void 0:"string"==typeof i?i:"string"==typeof e?e.toLowerCase():void 0}_$Eu(){var e;this._$E_=new Promise(e=>this.enableUpdating=e),this._$AL=new Map,this._$Eg(),this.requestUpdate(),null===(e=this.constructor.h)||void 0===e||e.forEach(e=>e(this))}addController(e){var t,i;(null!==(t=this._$ES)&&void 0!==t?t:this._$ES=[]).push(e),void 0!==this.renderRoot&&this.isConnected&&(null===(i=e.hostConnected)||void 0===i||i.call(e))}removeController(e){var t;null===(t=this._$ES)||void 0===t||t.splice(this._$ES.indexOf(e)>>>0,1)}_$Eg(){this.constructor.elementProperties.forEach((e,t)=>{this.hasOwnProperty(t)&&(this._$Ei.set(t,this[t]),delete this[t])})}createRenderRoot(){var e;const t=null!==(e=this.shadowRoot)&&void 0!==e?e:this.attachShadow(this.constructor.shadowRootOptions);return d(t,this.constructor.elementStyles),t}connectedCallback(){var e;void 0===this.renderRoot&&(this.renderRoot=this.createRenderRoot()),this.enableUpdating(!0),null===(e=this._$ES)||void 0===e||e.forEach(e=>{var t;return null===(t=e.hostConnected)||void 0===t?void 0:t.call(e)})}enableUpdating(e){}disconnectedCallback(){var e;null===(e=this._$ES)||void 0===e||e.forEach(e=>{var t;return null===(t=e.hostDisconnected)||void 0===t?void 0:t.call(e)})}attributeChangedCallback(e,t,i){this._$AK(e,i)}_$EO(e,t,i=v){var s;const a=this.constructor._$Ep(e,i);if(void 0!==a&&!0===i.reflect){const o=(void 0!==(null===(s=i.converter)||void 0===s?void 0:s.toAttribute)?i.converter:_).toAttribute(t,i.type);this._$El=e,null==o?this.removeAttribute(a):this.setAttribute(a,o),this._$El=null}}_$AK(e,t){var i;const s=this.constructor,a=s._$Ev.get(e);if(void 0!==a&&this._$El!==a){const e=s.getPropertyOptions(a),o="function"==typeof e.converter?{fromAttribute:e.converter}:void 0!==(null===(i=e.converter)||void 0===i?void 0:i.fromAttribute)?e.converter:_;this._$El=a,this[a]=o.fromAttribute(t,e.type),this._$El=null}}requestUpdate(e,t,i){let s=!0;void 0!==e&&(((i=i||this.constructor.getPropertyOptions(e)).hasChanged||g)(this[e],t)?(this._$AL.has(e)||this._$AL.set(e,t),!0===i.reflect&&this._$El!==e&&(void 0===this._$EC&&(this._$EC=new Map),this._$EC.set(e,i))):s=!1),!this.isUpdatePending&&s&&(this._$E_=this._$Ej())}async _$Ej(){this.isUpdatePending=!0;try{await this._$E_}catch(e){Promise.reject(e)}const e=this.scheduleUpdate();return null!=e&&await e,!this.isUpdatePending}scheduleUpdate(){return this.performUpdate()}performUpdate(){var e;if(!this.isUpdatePending)return;this.hasUpdated,this._$Ei&&(this._$Ei.forEach((e,t)=>this[t]=e),this._$Ei=void 0);let t=!1;const i=this._$AL;try{t=this.shouldUpdate(i),t?(this.willUpdate(i),null===(e=this._$ES)||void 0===e||e.forEach(e=>{var t;return null===(t=e.hostUpdate)||void 0===t?void 0:t.call(e)}),this.update(i)):this._$Ek()}catch(e){throw t=!1,this._$Ek(),e}t&&this._$AE(i)}willUpdate(e){}_$AE(e){var t;null===(t=this._$ES)||void 0===t||t.forEach(e=>{var t;return null===(t=e.hostUpdated)||void 0===t?void 0:t.call(e)}),this.hasUpdated||(this.hasUpdated=!0,this.firstUpdated(e)),this.updated(e)}_$Ek(){this._$AL=new Map,this.isUpdatePending=!1}get updateComplete(){return this.getUpdateComplete()}getUpdateComplete(){return this._$E_}shouldUpdate(e){return!0}update(e){void 0!==this._$EC&&(this._$EC.forEach((e,t)=>this._$EO(t,this[t],e)),this._$EC=void 0),this._$Ek()}updated(e){}firstUpdated(e){}} +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */ +var b;f[y]=!0,f.elementProperties=new Map,f.elementStyles=[],f.shadowRootOptions={mode:"open"},null==p||p({ReactiveElement:f}),(null!==(c=u.reactiveElementVersions)&&void 0!==c?c:u.reactiveElementVersions=[]).push("1.6.3");const w=window,k=w.trustedTypes,x=k?k.createPolicy("lit-html",{createHTML:e=>e}):void 0,$=`lit$${(Math.random()+"").slice(9)}$`,j="?"+$,O=`<${j}>`,S=document,z=()=>S.createComment(""),C=e=>null===e||"object"!=typeof e&&"function"!=typeof e,A=Array.isArray,T="[ \t\n\f\r]",E=/<(?:(!--|\/[^a-zA-Z])|(\/?[a-zA-Z][^>\s]*)|(\/?$))/g,D=/-->/g,M=/>/g,L=RegExp(`>|${T}(?:([^\\s"'>=/]+)(${T}*=${T}*(?:[^ \t\n\f\r"'\`<>=]|("|')|))|$)`,"g"),P=/'/g,N=/"/g,I=/^(?:script|style|textarea|title)$/i,q=(e=>(t,...i)=>({_$litType$:e,strings:t,values:i}))(1),R=Symbol.for("lit-noChange"),V=Symbol.for("lit-nothing"),H=new WeakMap,U=S.createTreeWalker(S,129,null,!1);function F(e,t){if(!Array.isArray(e)||!e.hasOwnProperty("raw"))throw Error("invalid template strings array");return void 0!==x?x.createHTML(t):t}const B=(e,t)=>{const i=e.length-1,s=[];let a,o=2===t?"":"",n=E;for(let t=0;t"===d[0]?(n=null!=a?a:E,l=-1):void 0===d[1]?l=-2:(l=n.lastIndex-d[2].length,r=d[1],n=void 0===d[3]?L:'"'===d[3]?N:P):n===N||n===P?n=L:n===D||n===M?n=E:(n=L,a=void 0);const u=n===L&&e[t+1].startsWith("/>")?" ":"";o+=n===E?i+O:l>=0?(s.push(r),i.slice(0,l)+"$lit$"+i.slice(l)+$+u):i+$+(-2===l?(s.push(void 0),t):u)}return[F(e,o+(e[i]||"")+(2===t?"":"")),s]};class W{constructor({strings:e,_$litType$:t},i){let s;this.parts=[];let a=0,o=0;const n=e.length-1,r=this.parts,[d,l]=B(e,t);if(this.el=W.createElement(d,i),U.currentNode=this.el.content,2===t){const e=this.el.content,t=e.firstChild;t.remove(),e.append(...t.childNodes)}for(;null!==(s=U.nextNode())&&r.length0){s.textContent=k?k.emptyScript:"";for(let i=0;iA(e)||"function"==typeof(null==e?void 0:e[Symbol.iterator]))(e)?this.T(e):this._(e)}k(e){return this._$AA.parentNode.insertBefore(e,this._$AB)}$(e){this._$AH!==e&&(this._$AR(),this._$AH=this.k(e))}_(e){this._$AH!==V&&C(this._$AH)?this._$AA.nextSibling.data=e:this.$(S.createTextNode(e)),this._$AH=e}g(e){var t;const{values:i,_$litType$:s}=e,a="number"==typeof s?this._$AC(e):(void 0===s.el&&(s.el=W.createElement(F(s.h,s.h[0]),this.options)),s);if((null===(t=this._$AH)||void 0===t?void 0:t._$AD)===a)this._$AH.v(i);else{const e=new K(a,this),t=e.u(this.options);e.v(i),this.$(t),this._$AH=e}}_$AC(e){let t=H.get(e.strings);return void 0===t&&H.set(e.strings,t=new W(e)),t}T(e){A(this._$AH)||(this._$AH=[],this._$AR());const t=this._$AH;let i,s=0;for(const a of e)s===t.length?t.push(i=new G(this.k(z()),this.k(z()),this,this.options)):i=t[s],i._$AI(a),s++;s2||""!==i[0]||""!==i[1]?(this._$AH=Array(i.length-1).fill(new String),this.strings=i):this._$AH=V}get tagName(){return this.element.tagName}get _$AU(){return this._$AM._$AU}_$AI(e,t=this,i,s){const a=this.strings;let o=!1;if(void 0===a)e=Z(this,e,t,0),o=!C(e)||e!==this._$AH&&e!==R,o&&(this._$AH=e);else{const s=e;let n,r;for(e=a[0],n=0;n{var s,a;const o=null!==(s=null==i?void 0:i.renderBefore)&&void 0!==s?s:t;let n=o._$litPart$;if(void 0===n){const e=null!==(a=null==i?void 0:i.renderBefore)&&void 0!==a?a:null;o._$litPart$=n=new G(t.insertBefore(z(),e),e,void 0,null!=i?i:{})}return n._$AI(e),n})(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 R}}oe.finalized=!0,oe._$litElement$=!0,null===(se=globalThis.litElementHydrateSupport)||void 0===se||se.call(globalThis,{LitElement:oe});const ne=globalThis.litElementPolyfillSupport;null==ne||ne({LitElement:oe}),(null!==(ae=globalThis.litElementVersions)&&void 0!==ae?ae:globalThis.litElementVersions=[]).push("3.3.3");const re=e=>t=>"function"==typeof t?((e,t)=>(customElements.define(e,t),t))(e,t):((e,t)=>{const{kind:i,elements:s}=t;return{kind:i,elements:s,finisher(t){customElements.define(e,t)}}})(e,t) +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */,de=(e,t)=>"method"===t.kind&&t.descriptor&&!("value"in t.descriptor)?{...t,finisher(i){i.createProperty(t.key,e)}}:{kind:"field",key:Symbol(),placement:"own",descriptor:{},originalKey:t.key,initializer(){"function"==typeof t.initializer&&(this[t.key]=t.initializer.call(this))},finisher(i){i.createProperty(t.key,e)}}; +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */function le(e){return(t,i)=>void 0!==i?((e,t,i)=>{t.constructor.createProperty(i,e)})(e,t,i):de(e,t) +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */}function ce(e){return le({...e,state:!0})} +/** + * @license + * Copyright 2021 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */var ue;null===(ue=window.HTMLSlotElement)||void 0===ue||ue.prototype.assignedElements;const he=(e,t)=>{if(typeof e!=typeof t)return!1;if("object"==typeof e&&"object"==typeof t&&null!==e&&null!==t){const i=[...new Set([...Object.keys(e),...Object.keys(t)])];return!!i.every(i=>Object.keys(e).includes(i)&&Object.keys(t).includes(i))&&i.every(i=>he(e[i],t[i]))}return e===t};var me,pe,_e,ge,ve,ye,fe,be;!function(e){e.Single="single",e.Scheme="scheme"}(me||(me={})),function(e){e.Daily="daily",e.Workday="workday",e.Weekend="weekend",e.Monday="monday",e.Tuesday="tuesday",e.Wednesday="wednesday",e.Thursday="thursday",e.Friday="friday",e.Saturday="saturday",e.Sunday="sunday"}(pe||(pe={})),function(e){e.Or="or",e.And="and"}(_e||(_e={})),function(e){e.Equal="is",e.Unequal="not",e.Below="below",e.Above="above"}(ge||(ge={})),function(e){e.Name="name",e.RelativeTime="relative-time",e.AdditionalTasks="additional-tasks",e.Time="time",e.Days="days",e.Entity="entity",e.Action="action",e.Tags="tags",e.Default="default"}(ve||(ve={})),function(e){e.ItemCreated="scheduler_item_created",e.ItemUpdated="scheduler_item_updated",e.ItemRemoved="scheduler_item_removed",e.TimerFinished="scheduler_timer_finished",e.TimerUpdated="scheduler_timer_updated"}(ye||(ye={})),function(e){e.Repeat="repeat",e.Pause="pause",e.Single="single"}(fe||(fe={})),function(e){e.Fixed="fixed",e.Sunrise="sunrise",e.Sunset="sunset"}(be||(be={}));const we=e=>({service:e.service,service_data:e.service_data,target:{entity_id:e.entity_id?e.entity_id:void 0}}),ke=e=>({start:e.start,stop:e.stop,actions:je(e.actions.map(we)),conditions:{type:"and"==e.condition_type?_e.And:_e.Or,items:e.conditions||[],track_changes:Boolean(e.track_conditions)}}),xe=e=>{switch(e){case"mon":return pe.Monday;case"tue":return pe.Tuesday;case"wed":return pe.Wednesday;case"thu":return pe.Thursday;case"fri":return pe.Friday;case"sat":return pe.Saturday;case"sun":return pe.Sunday;case"workday":return pe.Workday;case"weekend":return pe.Weekend;default:return pe.Daily}},$e=e=>Object.assign(Object.assign({},Object.fromEntries(Object.entries(e).filter(([e])=>!["slots","weekdays",""].includes(e)))),{entries:[{slots:e.timeslots.map(ke),weekdays:e.weekdays.map(xe)}]}),je=e=>{if(1==e.length)return e;if(e.every(t=>he(Object.assign(Object.assign({},t),{target:void 0}),Object.assign(Object.assign({},e[0]),{target:void 0})))){const t=[...new Set(e.map(e=>{var t;return null===(t=e.target)||void 0===t?void 0:t.entity_id}).filter(e=>void 0!==e))];return[Object.assign(Object.assign({},e[0]),{target:{entity_id:t.length?t:void 0}})]}return e},Oe=e=>e.callWS({type:"scheduler"}).then(e=>e.map($e)),Se=(e,t)=>{let i=t.hours||0,s=t.minutes||0;(i<0||s<0)&&(i=-Math.abs(i),s=-Math.abs(s));let a=e.hours,o=e.minutes;return a<0&&o>0&&(o=-o),a+=i,o+=s,o>=60||o>0&&a<0&&e.mode!=be.Fixed?(a+=1,o-=60):(o<=-60||o<0&&e.mode==be.Fixed||o<0&&a>0&&e.mode!=be.Fixed)&&(a-=1,o+=60),a<0&&e.mode==be.Fixed?a+=24:a>=24&&e.mode==be.Fixed&&(a-=24),{mode:e.mode,hours:a,minutes:o}},ze=(e,t=1)=>{let i=3600*Math.abs(e.hours)+60*Math.abs(e.minutes)+(e.seconds||0);const s=e.hours<0||e.minutes<0?-1:1;let a=Math.floor(i/3600),o=Math.round((i-3600*a)/60);return o%t!=0&&(o=Math.round(o/t)*t),o>=60&&(a++,o-=60),s<0&&(a>0?a=-a:o=-o),Object.assign(Object.assign({},e),{hours:a,minutes:o})},Ce=e=>{if(e.match(/^([0-9:]+)$/)){const t=e.split(":").map(Number),i=ze({hours:t[0],minutes:t[1],seconds:t[2]});return{mode:be.Fixed,hours:i.hours,minutes:i.minutes}}const t=e.match(/^([a-z]+)([\+|-]{1})([0-9:]+)$/);if(t){let e=t[3].split(":").map(Number);const i=ze({hours:e[0],minutes:e[1],seconds:e[2]});let s=i.hours,a=i.minutes;return"-"==t[2]&&(s>0&&(s=-s),a=-a),{mode:"sunrise"==t[1]?be.Sunrise:be.Sunset,hours:s,minutes:a}}const i=new Date(e),s=ze({hours:i.getHours(),minutes:i.getMinutes(),seconds:i.getSeconds()});return{mode:be.Fixed,hours:s.hours,minutes:s.minutes}},Ae=(e,t)=>{if("string"==typeof e&&(e=Ce(e)),e.mode==be.Fixed)return 3600*e.hours+60*e.minutes;if(e.mode==be.Sunrise){const i=Ce(t.states["sun.sun"].attributes.next_rising),s=Se(i,e);return 3600*s.hours+60*s.minutes}{const i=Ce(t.states["sun.sun"].attributes.next_setting),s=Se(i,e);return 3600*s.hours+60*s.minutes}};var Te,Ee;!function(e){e.language="language",e.system="system",e.comma_decimal="comma_decimal",e.decimal_comma="decimal_comma",e.space_comma="space_comma",e.none="none"}(Te||(Te={})),function(e){e.language="language",e.system="system",e.am_pm="12",e.twenty_four="24"}(Ee||(Ee={}));const De=e=>{if(e.time_format===Ee.language||e.time_format===Ee.system){const t=e.time_format===Ee.language?e.language:void 0;return new Date("January 1, 2023 22:00:00").toLocaleString(t).includes("10")}return e.time_format===Ee.am_pm};var Me;!function(e){e.AM="AM",e.PM="PM"}(Me||(Me={}));const Le=e=>({am_pm:e>=12?Me.PM:Me.AM,hours:e%12||12}),Pe=(e,t)=>t==Me.AM?12==e?0:e:12==e?12:e+12,Ne=(e,t={seconds:!0})=>{let i="";if(e.hours>=24&&(e.hours-=24),e.mode==be.Fixed&&t.am_pm){const s=Le(e.hours).am_pm,a=Le(e.hours).hours;i=String(a).padStart(2,"0")+":"+String(e.minutes).padStart(2,"0"),t.seconds&&(i+=":00"),i+=" "+(s===Me.AM?"am":"pm")}else e.mode==be.Fixed?(i=String(e.hours).padStart(2,"0")+":"+String(e.minutes).padStart(2,"0"),t.seconds&&(i+=":00")):(i=e.mode+(e.hours<0||e.minutes<0?"-":"+")+String(Math.abs(e.hours)).padStart(2,"0")+":"+String(Math.abs(e.minutes)).padStart(2,"0"),t.seconds&&(i+=":00"));return i},Ie=(e,t)=>{const i=e=>{(e=(e=e.map(e=>Object.assign(Object.assign({},e),{start:Ae(e.start,t)<0?"00:00:00":e.start,stop:e.stop?Ae(e.stop,t)>86400?"00:00:00":e.stop:void 0}))).map(e=>{if(e.stop&&Ae(e.start,t)>Ae(e.stop,t)){if(0==Ae(e.stop,t))return Object.assign(Object.assign({},e),{stop:Ne(Se(Ce(e.stop),{hours:24}))});e=Object.assign(Object.assign({},e),{start:e.stop,stop:e.start})}return e.stop&&Ae(e.stop,t)-Ae(e.start,t)<60&&(e=Object.assign(Object.assign({},e),{stop:Ne(Se(Ce(e.start),{minutes:1}))})),e})).sort((e,i)=>Ae(e.start,t)>Ae(i.start,t)?1:Ae(e.start,t)Ae(i.stop||i.start,t)?1:-1);let i="00:00:00",s=e.length;for(let a=0;aAe(i,t)?(e.splice(a,0,Object.assign(Object.assign({},o),{start:i,stop:o.start,actions:[],conditions:o.conditions})),s++,a++):Ae(o.start,t)0&&e.push({start:i,stop:Ne({mode:be.Fixed,hours:24,minutes:0}),actions:[],conditions:e[0].conditions}),e};return e=Object.assign(Object.assign({},e),{entries:e.entries.map(e=>Object.assign(Object.assign({},e),{slots:i(e.slots)}))})},qe=["relative-time","state"],Re=["relative-time","additional-tasks"],Ve=["*"],He={actions:[],conditions:{type:_e.Or,items:[],track_changes:!1}},Ue={entries:[{weekdays:[pe.Daily],slots:[Object.assign(Object.assign({},He),{start:"00:00:00",stop:"08:00:00"}),Object.assign(Object.assign({},He),{start:"08:00:00",stop:"16:00:00"}),Object.assign(Object.assign({},He),{start:"16:00:00",stop:"00:00:00"})]}],repeat_type:fe.Repeat,next_entries:[],timestamps:[],enabled:!0},Fe=Object.assign(Object.assign({},Ue),{entries:[{weekdays:[pe.Daily],slots:[Object.assign(Object.assign({},He),{start:"00:00:00",stop:"12:00:00"}),Object.assign(Object.assign({},He),{start:"12:00:00"}),Object.assign(Object.assign({},He),{start:"12:01:00",stop:"00:00:00"})]}]});function Be(e){return null!=e}const We=(e,t)=>Object.keys(e).includes(t),Ze=e=>"boolean"==typeof e,Ke=e=>"number"==typeof e,Ge=e=>"string"==typeof e,Je=e=>"object"==typeof e&&!Array.isArray(e),Ye=e=>Array.isArray(e)&&e.every(e=>"string"==typeof e),Qe=(e,t)=>Je(t)?We(t,"states")&&!(Ye(t.states)||Je(t.states)&&We(t.states,"min")&&Ke(t.states.min)&&We(t.states,"max")&&Ke(t.states.max))?`In 'customize' [${e}].states' must be a list of strings or a range of numbers`:void 0:`'In customize, ${e}' must be a struct`;var Xe={generic:{turn_on:"Включи",turn_off:"изключи",parameter_to_value:"{parameter} на {value}",action_with_parameter:"{action} с {parameter}"},climate:{set_temperature:"задай температура[ на {temperature}]",set_temperature_hvac_mode_heat:"отопление[ на {temperature}]",set_temperature_hvac_mode_cool:"охлаждане[ на {temperature}]",set_temperature_hvac_mode_heat_cool:"отопление/охлаждане[ на {temperature}]",set_temperature_hvac_mode_heat_cool_range:"отопление/охлаждане[ на {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"автоматично[ на {temperature}]",set_hvac_mode:"задай режим[ на {hvac_mode}]",set_preset_mode:"задай предварителна настройка[ на {preset_mode}]",set_fan_mode:"задай режим на вентилатор[ на {fan_mode}]",set_swing_mode:"задай режим на въртене[ на {swing_mode}]"},cover:{close_cover:"затвори",open_cover:"отвори",set_cover_position:"задай позиция[ на {position}]",set_cover_tilt_position:"задай позиция на наклон[ на {tilt_position}]"},fan:{set_percentage:"задай скорост[ на {percentage}]",set_direction:"задай посока[ на {direction}]",oscillate:"задай осцилация[ на {oscillate}]"},humidifier:{set_humidity:"задай влажност[ на {humidity}]",set_mode:"задай режим[ на {mode}]"},input_number:{set_value:"задай стойност[ на {value}]"},input_select:{select_option:"избери опция[ {option}]"},select:{select_option:"избери опция[ {option}]"},light:{turn_on:"включи[ с {brightness} яркост]"},media_player:{select_source:"избери източник[ {source}]"},notify:{send_message:"изпрати известие"},script:{execute:"изпълни"},vacuum:{start_pause:"старт / пауза"},water_heater:{set_operation_mode:"задай режим[ на {operation_mode}]",set_away_mode:"задай режим 'не съм вкъщи'"}},et={components:{date:{day_types_short:{daily:"дневно",workdays:"работни дни",weekend:"уикенд"},day_types_long:{daily:"всеки ден",workdays:"в работни дни",weekend:"през уикенда"},days:"дни",tomorrow:"утре",repeated_days:"всеки {days}",repeated_days_except:"всеки ден освен {excludedDays}",days_range:"от {startDay} до {endDay}",next_week_day:"следващ {weekday}"},time:{absolute:"в {time}",interval:"от {startTime} до {endTime}",at_midnight:"в полунощ",at_noon:"на обяд",at_sun_event:"при {sunEvent}"}},dialog:{enable_schedule:{title:"Завърши промените",description:"Графикът, който сте променили, е деактивиран. Искате ли да го активирате?"},confirm_delete:{title:"Премахни обект?",description:"Сигурни ли сте, че искате да премахнете този обект?"},confirm_migrate:{title:"Актуализирай графика",description:"Някои настройки ще бъдат загубени при тази промяна. Искате ли да продължите?"},weekday_picker:{title:"Повтарящи се дни за графика",choose:"Избери..."},entity_picker:{title:"Изберете обекти",choose:"Избери...",no_results:"Няма намерени съвпадения"},action_picker:{title:"Изберете действие",show_all:"Показване на всички поддържани обекти"}},panel:{common:{title:"Планировчик",new_schedule:"Нов график",default_name:"График #{id}"},overview:{no_entries:"Няма елементи за показване",backend_error:"Не може да се свърже с компонента за планиране. Той трябва да бъде инсталиран като интеграция, преди тази карта да може да се използва.",excluded_items:"{number} изключен {if number is 1} елемент {else} елемента",hide_excluded:"скрий изключените елементи",additional_tasks:"{number} още {if number is 1} задача {else} задачи"},editor:{repeated_days:"Повтарящи се дни",start_time:"Начален час",stop_time:"Краен час",action:"Действие",add_action:"Добави действие",select_timeslot:"Изберете времеви слот",toggle_single_mode:"В единичен режим",toggle_scheme_mode:"В режим на схема",validation_errors:{overlapping_time:"Графикът има припокриващи се времеви слотове",missing_target_entity:"Едно или повече действия нямат целеви обект",missing_service_parameter:"Едно или повече действия нямат задължителна настройка",missing_action:"Графикът няма действия"}},options:{conditions:{header:"Условия",add_condition:"Добави условие",new_condition:"Нов условие",types:{equal_to:"{entity} е равно на {value}",unequal_to:"{entity} не е равно на {value}",above:"{entity} е над {value}",below:"{entity} е под {value}"},options:{logic_and:"Всички условия трябва да са изпълнени",logic_or:"Поне едно условие трябва да е изпълнено",track_changes:"Преоценка при промяна на условията"}},period:{header:"Период",start_date:"От",end_date:"До"},repeat_type:"поведение след завършване",tags:"Етикети"},card_editor:{tabs:{entities:"Обекти",other:"Други"},fields:{title:{heading:"Заглавие на картата",options:{standard:"стандартно",hidden:"скрито",custom:"персонализирано"},custom_title:"Персонализирано заглавие"},discover_existing:{heading:"Покажи всички графици",description:"Това задава параметъра 'discover existing'. Предварително създадени графици ще бъдат автоматично добавени към картата. "},time_step:{heading:"Времева стъпка",description:"Резолюция (в минути) за създаване на графици",unit_minutes:"мин"},default_editor:{heading:"Редактор по подразбиране",options:{single:"Режим на единичен график",scheme:"Режим на времева схема"}},sort_by:{heading:"Опции за сортиране",description:"Ред, в който се показват графиците в картата",options:{relative_time:"Оставащо време до следващото действие",title:"Показано заглавие на графика",state:"Покажи активните графици отгоре"}},display_format_primary:{heading:"Показана основна информация",description:"Конфигурирайте кой етикет се използва за графиците в прегледа",options:{default:"Име на графика",entity_action:"Резюме на задачата"}},display_format_secondary:{heading:"Показана допълнителна информация",description:"Конфигурирайте какви допълнителни свойства са видими в прегледа",options:{relative_time:"Оставащо време до следващото действие",time:"Конфигуриран час за следващо действие",days:"Повтарящи се дни от седмицата",additional_tasks:"Брой допълнителни задачи"}},show_header_toggle:{heading:"Покажи превключвател в заглавието",description:"Покажи превключвател в горната част на картата за активиране/деактивиране на всички обекти"},show_toggle_switches:{heading:"Покажи превключватели",description:"Покажи превключвател за всяко отделно задание в картата"},tags:{heading:"Етикети",description:"Използвайте етикети за разделяне на графиците между множество карти"},entities:{button_label:"Конфигурирай включени обекти",heading:"Включени обекти",description:"Изберете обектите, които искате да контролирате чрез планировчика. Можете да кликнете на група, за да я отворите. Имайте предвид, че някои обекти (като сензори) могат да се използват само за условия, не и за действия.",included_number:"{number}/{total} избрани"}}}}},tt={services:Xe,ui:et},it=Object.freeze({__proto__:null,services:Xe,ui:et,default:tt}),st={generic:{turn_on:"Zapnout",turn_off:"Vypnout",parameter_to_value:"{parameter} na {value}",action_with_parameter:"{action} s {parameter}"},climate:{set_temperature:"nastavit teplotu[ na {temperature}]",set_temperature_hvac_mode_heat:"topení[ na {temperature}]",set_temperature_hvac_mode_cool:"chlazení[ na {temperature}]",set_temperature_hvac_mode_heat_cool:"topení/chlazení[ na {temperature}]",set_temperature_hvac_mode_heat_cool_range:"topení/chlazení[ na {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"automatika[ na {temperature}]",set_hvac_mode:"nastavit režim[ na {hvac_mode}]",set_preset_mode:"nastavit předvolbu[ {preset_mode}]",set_fan_mode:"set fan mode[ to {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"zavřít",open_cover:"otevřít",set_cover_position:"nastavit polohu[ na {position}]",set_cover_tilt_position:"set tilt position[ to {tilt_position}]"},fan:{set_percentage:"nastavit rychlost[ na {speed}]",set_direction:"nastavit směr[ na {direction}]",oscillate:"nastavit oscilaci[ na {oscillate}]"},humidifier:{set_humidity:"nastavit vlhkost[ na {humidity}]",set_mode:"nastavit režim[ na {mode}]"},input_number:{set_value:"nastavit hodnotu[ na {value}]"},input_select:{select_option:"vybrat možnost[ {option}]"},select:{select_option:"vybrat možnost[ {option}]"},light:{turn_on:"zapnout[ na {brightness} jas]"},media_player:{select_source:"vybrat zdroj[ {source}]"},notify:{send_message:"send notification"},script:{execute:"spustit"},vacuum:{start_pause:"start / pauza"},water_heater:{set_operation_mode:"nastavit režim[ na {operation_mode}]",set_away_mode:"vypnout režim"}},at={components:{date:{day_types_short:{daily:"denně",workdays:"pracovní dny",weekend:"víkendy"},day_types_long:{daily:"každý den",workdays:"v pracovní dny",weekend:"o víkendu"},days:"dnů",tomorrow:"zítra",repeated_days:"každý {days}",repeated_days_except:"každý den kromě {excludedDays}",days_range:"od {startDay} do {endDay}",next_week_day:"příští {weekday}"},time:{absolute:"od {time}",interval:"od {startTime} do {endTime}",at_midnight:"od půlnoc",at_noon:"od poledne",at_sun_event:"na {sunEvent}"}},dialog:{enable_schedule:{title:"Dokončete úpravy",description:"Plán, který byl změněn, je aktuálně zakázán, měl by být povolen?"},confirm_delete:{title:"Odebrat entitu?",description:"Opravdu chcete tuto entitu odebrat?"},confirm_migrate:{title:"Aktualizovat plán",description:"Některá nastavení budou touto změnou ztracena. Chceš pokračovat?"},weekday_picker:{title:"Repeated days for schedule",choose:"Choose..."},entity_picker:{title:"Choose entities",choose:"Choose...",no_results:"No matching items found"},action_picker:{title:"Choose action",show_all:"Show all supported entities"}},panel:{common:{title:"Plánovač",new_schedule:"Nový plán",default_name:"Plán #{id}"},overview:{no_entries:"Nejsou žádné položky k zobrazení",backend_error:"Could not connect with the scheduler component. It needs to be installed as integration before this card can be used.",excluded_items:"{number} vyloučeno {if number is 1} položka {else} položek",hide_excluded:"skrýt vyloučené položky",additional_tasks:"{number} a více {if number is 1} úkol {else} úkolů"},editor:{repeated_days:"Repeated days",start_time:"Start time",stop_time:"End time",action:"Action",add_action:"Add action",select_timeslot:"Nejprve vyberte časový úsek",toggle_single_mode:"Do režimu jednoho",toggle_scheme_mode:"Do režimu schématu",validation_errors:{overlapping_time:"Schedule has overlapping timeslots",missing_target_entity:"One or more actions are missing a target entity",missing_service_parameter:"One or more actions are missing a required setting",missing_action:"Schedule has no actions"}},options:{conditions:{header:"Conditions",add_condition:"Add condition",new_condition:"New condition",types:{equal_to:"{entity} is equal to {value}",unequal_to:"{entity} is unequal to {value}",above:"{entity} is above {value}",below:"{entity} is below {value}"},options:{logic_and:"All conditions must be true",logic_or:"Any condition must be true",track_changes:"Re-evaluate when conditions change"}},period:{header:"Období",start_date:"From",end_date:"To"},repeat_type:"behaviour after completion",tags:"Tags"},card_editor:{tabs:{entities:"Entities",other:"Other"},fields:{title:{heading:"Title of the card",options:{standard:"standard",hidden:"hidden",custom:"custom"},custom_title:"Custom title"},discover_existing:{heading:"Show all schedules",description:"This sets the 'discover existing' parameter. Previously created schedules will be automatically added to the card. "},time_step:{heading:"Time step",description:"Resolution (in minutes) for creating schedules",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Single schedule mode",scheme:"Time scheme mode"}},sort_by:{heading:"Sorting options",description:"Order in which the schedules appear in the card",options:{relative_time:"Time remaining until next action",title:"Displayed title of the schedule",state:"Show active schedules on top"}},display_format_primary:{heading:"Displayed primary info",description:"Configure which label is used for schedules in the overview",options:{default:"Schedule name",entity_action:"Summary of task"}},display_format_secondary:{heading:"Displayed secondary info",description:"Configure what additional properties are visible in the overview",options:{relative_time:"Time remaining until next action",time:"Configured time for next action",days:"Repeated days of the week",additional_tasks:"Number of additional tasks"}},show_header_toggle:{heading:"Show header toggle",description:"Show toggle switch at the top of the card for enabling/disabling all entities"},show_toggle_switches:{heading:"Show toggle switches",description:"Show toggle switch for each individual schedule in the card"},tags:{heading:"Tags",description:"Use tags to divide schedules between multiple cards"},entities:{button_label:"Configure included entities",heading:"Included entities",description:"Select the entities that you want to control using the scheduler. You can click on a group to open it. Note that some entities (such as sensors) can only be used for conditions, not for actions.",included_number:"{number}/{total} selected"}}}}},ot={services:st,ui:at},nt=Object.freeze({__proto__:null,services:st,ui:at,default:ot}),rt={generic:{turn_on:"Einschalten",turn_off:"Ausschalten",parameter_to_value:"{parameter} auf {value}",action_with_parameter:"{action} mit {parameter}"},climate:{set_temperature:"Temperatur einstellen[ auf {temperature}]",set_temperature_hvac_mode_heat:"Heizen[ auf {temperature}]",set_temperature_hvac_mode_cool:"Kühlen[ auf {temperature}]",set_temperature_hvac_mode_heat_cool:"Heizen/Kühlen[ auf {temperature}]",set_temperature_hvac_mode_heat_cool_range:"Heizen/Kühlen[ auf {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"Automatisch[ auf {temperature}]",set_hvac_mode:"Modus setzen[ auf {hvac_mode}]",set_preset_mode:"Voreinstellung setzen[ auf {preset_mode}]",set_fan_mode:"Lüftermodus einstellen[ auf {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"Schließen",open_cover:"Öffnen",set_cover_position:"Position setzen[ auf {position}]",set_cover_tilt_position:"Neigungsposition setzen[ auf {tilt_position}]"},fan:{set_percentage:"Geschwindigkeit setzen[ auf {speed}]",set_direction:"Richtung setzen[ auf {direction}]",oscillate:"Oszillation setzen[ auf {oscillate}]"},humidifier:{set_humidity:"Luftfeuchtigkeit setzen[ auf {humidity}]",set_mode:"Modus setzen[ auf {mode}]"},input_number:{set_value:"Wert setzen[ auf {value}]"},input_select:{select_option:"Option[ {option}] auswählen"},select:{select_option:"Option[ {option}] auswählen"},light:{turn_on:"Anschalten[ mit {brightness} Helligkeit]"},media_player:{select_source:"Quelle[ {source}] auswählen"},notify:{send_message:"Nachricht senden"},script:{execute:"Ausführen"},vacuum:{start_pause:"Start/Pause"},water_heater:{set_operation_mode:"Modus setzen[ auf {operation_mode}]",set_away_mode:"Abwesenheitsmodus setzen"}},dt={components:{date:{day_types_short:{daily:"Täglich",workdays:"Werktags",weekend:"Wochenende"},day_types_long:{daily:"Jeden Tag",workdays:"An Werktagen",weekend:"Am Wochenende"},days:"Tage",tomorrow:"Morgen",repeated_days:"Jeden {days}",repeated_days_except:"Täglich außer {excludedDays}",days_range:"von {startDay} bis {endDay}",next_week_day:"nächsten {weekday}"},time:{absolute:"um {time}",interval:"von {startTime} bis {endTime}",at_midnight:"um Mitternacht",at_noon:"zum Mittag",at_sun_event:"beim {sunEvent}"}},dialog:{enable_schedule:{title:"Modifikationen beenden",description:"Der geänderte Zeitplan ist derzeit deaktiviert, soll er aktiviert werden?"},confirm_delete:{title:"Entität entfernen?",description:"Bist du dir sicher, dass du diese Entität löschen möchtest?"},confirm_migrate:{title:"Zeitplan ändern",description:"Einige Einstellungen gehen durch diese Änderung verloren. Möchtest du fortfahren?"},weekday_picker:{title:"Wiederholungen für den Zeitplan",choose:"Auswahl..."},entity_picker:{title:"Entitäten auswählen",choose:"Auswahl...",no_results:"Keine passenden Elemente gefunden"},action_picker:{title:"Aktion auswählen",show_all:"Alle unterstützten Entitäten anzeigen"}},panel:{common:{title:"Zeitplaner",new_schedule:"Neuer Zeitplan",default_name:"Zeitplan #{id}"},overview:{no_entries:"Es gibt keine Einträge, die angezeigt werden können",backend_error:"Es konnte keine Verbindung mit der Scheduler-Komponente hergestellt werden. Es muss als Integration installiert werden, bevor diese Karte verwendet werden kann.",excluded_items:"{number} {if number is 1} ausgeschlossener Eintrag {else} ausgeschlossene Einträge",hide_excluded:"Verstecke ausgeschlossene Einträge",additional_tasks:"{number} weitere {if number is 1} Aufgabe {else} Aufgaben"},editor:{repeated_days:"Wiederholen",start_time:"Startzeit",stop_time:"Endzeit",action:"Aktion",add_action:"Aktion hinzufügen",select_timeslot:"Wähle ein Zeitfenster aus",toggle_single_mode:"Zum Einzelmodus",toggle_scheme_mode:"Zum Schemamodus",validation_errors:{overlapping_time:"Der Zeitplan weist Überschneidungen auf.",missing_target_entity:"Bei einer oder mehreren Aktionen fehlt eine Zielentität.",missing_service_parameter:"Bei einer oder mehreren Aktionen fehlt eine erforderliche Einstellung.",missing_action:"Zeitplan enthält keine Aktionen"}},options:{conditions:{header:"Bedingungen",add_condition:"Bedingung hinzufügen",new_condition:"Neuer Zustand",types:{equal_to:"{entity} ist {value}",unequal_to:"{entity} ist nicht {value}",above:"{entity} ist über {value}",below:"{entity} ist unter {value}"},options:{logic_and:"Alle Bedingungen müssen zutreffen.",logic_or:"Eine Bedingung muss zutreffen.",track_changes:"Erneut prüfen wenn sich die Zustände ändern"}},period:{header:"Zeitraum",start_date:"Von",end_date:"Bis"},repeat_type:"Verhalten nach Abschluss",tags:"Tags"},card_editor:{tabs:{entities:"Entities",other:"Other"},fields:{title:{heading:"Title of the card",options:{standard:"standard",hidden:"hidden",custom:"custom"},custom_title:"Custom title"},discover_existing:{heading:"Show all schedules",description:"This sets the 'discover existing' parameter. Previously created schedules will be automatically added to the card. "},time_step:{heading:"Time step",description:"Resolution (in minutes) for creating schedules",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Single schedule mode",scheme:"Time scheme mode"}},sort_by:{heading:"Sorting options",description:"Order in which the schedules appear in the card",options:{relative_time:"Time remaining until next action",title:"Displayed title of the schedule",state:"Show active schedules on top"}},display_format_primary:{heading:"Displayed primary info",description:"Configure which label is used for schedules in the overview",options:{default:"Schedule name",entity_action:"Summary of task"}},display_format_secondary:{heading:"Displayed secondary info",description:"Configure what additional properties are visible in the overview",options:{relative_time:"Time remaining until next action",time:"Configured time for next action",days:"Repeated days of the week",additional_tasks:"Number of additional tasks"}},show_header_toggle:{heading:"Show header toggle",description:"Show toggle switch at the top of the card for enabling/disabling all entities"},show_toggle_switches:{heading:"Show toggle switches",description:"Show toggle switch for each individual schedule in the card"},tags:{heading:"Tags",description:"Use tags to divide schedules between multiple cards"},entities:{button_label:"Configure included entities",heading:"Included entities",description:"Select the entities that you want to control using the scheduler. You can click on a group to open it. Note that some entities (such as sensors) can only be used for conditions, not for actions.",included_number:"{number}/{total} selected"}}}}},lt={services:rt,ui:dt},ct=Object.freeze({__proto__:null,services:rt,ui:dt,default:lt}),ut={generic:{turn_on:"Ενεργοποίηση",turn_off:"Απενεργοποίηση",parameter_to_value:"{parameter} σε {value}",action_with_parameter:"{action} με {parameter}"},climate:{set_temperature:"ορισμός θερμοκρασίας[ σε {temperature}]",set_temperature_hvac_mode_heat:"θέρμανση[ σε {temperature}]",set_temperature_hvac_mode_cool:"ψύξη[ σε {temperature}]",set_temperature_hvac_mode_heat_cool:"θέρμανση/ψύξη[ σε {temperature}]",set_temperature_hvac_mode_heat_cool_range:"θέρμανση/ψύξη[ σε {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"auto[ σε {temperature}]",set_hvac_mode:"ορισμός λειτουργίας[ σε {hvac_mode}]",set_preset_mode:"ορισμός προκαθορισμένης ρύθμισης[ σε {preset_mode}]",set_fan_mode:"όρισμός ανεμιστήρα[ σε {fan_mode}]",set_swing_mode:"ορισμός κατεύθυνσης[ σε {swing_mode}]"},cover:{close_cover:"κλείσιμο",open_cover:"άνοιγμα",set_cover_position:"ορισμός θέσησ[ σε {position}]",set_cover_tilt_position:"ορισμός κλίσης[ σε {tilt_position}]"},fan:{set_percentage:"ορισμός ταχύτητας[ σε {percentage}]",set_direction:"ορισμός κατεύθυνσης[ σε {direction}]",oscillate:"ορισμός ταλάντωσης[ σε {oscillate}]"},humidifier:{set_humidity:"ορισμός υγρασίας[ σε {humidity}]",set_mode:"ορισμός λειτουργίας[ σε {mode}]"},input_number:{set_value:"ορισμός τιμής[ σε {value}]"},input_select:{select_option:"επιλογή παραμέτρου[ {option}]"},select:{select_option:"επιλογή παραμέτρου[ {option}]"},light:{turn_on:"άναμα[ με φωτεινότητα {brightness} ]"},media_player:{select_source:"επιλογή πηγής[ {source}]"},notify:{send_message:"αποστολή ειδοποίησης"},script:{execute:"εκτέλεση"},vacuum:{start_pause:"έναρξη / παύση"},water_heater:{set_operation_mode:"ορισμός λειτουργίας[ σε {operation_mode}]",set_away_mode:"ορισμός λειτουργίας απουσίας"}},ht={components:{date:{day_types_short:{daily:"ημερήσια",workdays:"εργάσιμες",weekend:"σαββατοκύριακο"},day_types_long:{daily:"κάθε μέρα",workdays:"τις καθημερινές",weekend:"το σαββατοκύριακο"},days:"ημέρες",tomorrow:"αύριο",repeated_days:"κάθε {days}",repeated_days_except:"κάθε μέρα εκτός {excludedDays}",days_range:"από {startDay} έως {endDay}",next_week_day:"επόμενη/-ο {weekday}"},time:{absolute:"στις {time}",interval:"από {startTime} ως {endTime}",at_midnight:"τα μεσάνυχτα",at_noon:"το μεσημέρι",at_sun_event:"το {sunEvent}"}},dialog:{enable_schedule:{title:"Ολοκλήρωση τροποποιήσεων",description:"Το πρόγραμμα που τροποποιήθηκε είναι απενεργοποιημένο, επιθυμείτε να το ενεργοποιήσετε;"},confirm_delete:{title:"Αφαίρεση οντότητας",description:"Είστε σίγουροι ότι θέλετε να αφαιρεθεί αυτή η οντότητα;"},confirm_migrate:{title:"Ενημέρωση προγράμματος",description:"Κάποιες ρυθμίσεις θα χαθούν από αυτή την αλλαγή. Θέλετε να συνεχίσετε;"},weekday_picker:{title:"Επαναλαμβανόμενες ημέρες για το πρόγραμμα",choose:"Επιλογή ημερών..."},entity_picker:{title:"Επιλογή οντοτήτων",choose:"Επιλογή...",no_results:"Δεν βρέθηκε οντότητα"},action_picker:{title:"Επιλογή ενέργειας",show_all:"Εμφάνιση όλων των υποστηριζόμενων οντοτήτων"}},panel:{common:{title:"Χρονοπρογραμματισμός",new_schedule:"Νέο Πρόγραμμα",default_name:"Πρόγραμμα #{id}"},overview:{no_entries:"Δεν βρέθηκαν καταχωρήσεις προς προβολή",backend_error:"Δεν είναι δυνατή η σύνδεη με το scheduler component. Πρέπει να εγκατασταθεί σαν integration πριν την χρήση αυτής της κάρτας.",excluded_items:"{number} {if number is 1} εξαιρούμενο αντικείμενο {else} εξαιρούμενα αντικείμενα",hide_excluded:"απόκρυψη εξαιρούμενων αντικειμένων",additional_tasks:"{number} επιπλέον {if number is 1} εργασία {else} εργασίες"},editor:{repeated_days:"Επαναλαμβανόμενες ημέρες",start_time:"Ώρα έναρξης",stop_time:"Ώρα ολοκλήρωσης",action:"Ενέργεια",add_action:"Προσθήκη ενέργειας",select_timeslot:"Επιλογή μιας ώρας",toggle_single_mode:"Μεμονωμένη λειτουργία",toggle_scheme_mode:"Λειτουργία προγραμματισμού",validation_errors:{overlapping_time:"Το πρόγραμμα παρουσιάζει αλληλοεπικαλύψεις ώρας",missing_target_entity:"Μια ή περισσότερες ενέργειες δεν διαθέτουν οντότητα - στόχο",missing_service_parameter:"Μια ή περισσότερες ενέργειες δεν διαθέτουν μια απαιτούμενη ρύθμιση",missing_action:"Το πρόγραμμα δεν διαθέτει ενέργειες"}},options:{conditions:{header:"Συνθήκες",add_condition:"Προσθήκη συνθήκης",new_condition:"Νέα συνθήκη",types:{equal_to:"{entity} ισούται με {value}",unequal_to:"{entity} δεν ισούται με {value}",above:"{entity} είναι περισσότερο από {value}",below:"{entity} είναι λιγότερο από {value}"},options:{logic_and:"Όλες οι συνθήκες πρέπει να αληθεύουν",logic_or:"Οποιαδήποτε συνθήκη πρέπει να αληθεύει",track_changes:"Επαναξιολόγηση όταν οι συνθήκες αλλάξουν"}},period:{header:"Περίοδος",start_date:"Από",end_date:"Έως"},repeat_type:"συμπεριφορά μετά την ολοκλήρωση",tags:"Tags"},card_editor:{tabs:{entities:"Οντότητες",other:"Λοιπά"},fields:{title:{heading:"Τίτλος της κάρτας",options:{standard:"τυπικό",hidden:"κρυφό",custom:"προσαρμοσμένο"},custom_title:"Προσαρμοσμενος τίτλος"},discover_existing:{heading:"Προβολή όλων των προγραμμάτων",description:"Αυτό ρυθμίζει την παράμμετρο 'ανακάλυψη υπαρχόντων'. Τα ήδη δημιουργημένα προγράμματα θα προστεθούν αυτόματα στην κάρτα. "},time_step:{heading:"Βήμα χρόνου",description:"Ανάλυση (σε λεπτά) για τη δημιουργία προγραμμάτων",unit_minutes:"λεπτά"},default_editor:{heading:"Προεπιλεγμένος επεξεργαστής χρόνου",options:{single:"Λειτουργία μονής χρονικής περιόδου",scheme:"Λειτουργία χρονικού σχήματος"}},sort_by:{heading:"Επιλογές ταξινόμησης",description:"Σειρά με την οποία εμφανίζονται τα προγράμματα στην κάρτα",options:{relative_time:"Υπολειπόμενος χρόνος έως την επόμενη ενέργεια",title:"Εμφανιζόμενος τίτλος του προγράμματος",state:"Εμφάνιση των ενεργών προγραμμάτων στην κορυφή"}},display_format_primary:{heading:"Προβαλλόμενη κύρια πληροφορία",description:"Ρυθμίστε ποια ετικέτα χρησιμοποιείται για τα προγράμματα στην επισκόπηση",options:{default:"Όνομα προγράμματος",entity_action:"Σύνοψη ενέργειας"}},display_format_secondary:{heading:"Προβαλλόμενη δευτερεύουσα πληροφορία",description:"Ρυθμίστε ποιες πρόσθετες ιδιότητες εμφανίζονται στην επισκόπηση",options:{relative_time:"Υπολειπόμενος χρόνος μέχρι την επόμενη ενέργεια",time:"Προγραμματισμένη ώρα για την επόμενη ενέργεια",days:"Επαναλαμβανόμενες ημέρες της εβδομάδας",additional_tasks:"Αριθμός πρόσθετων εργασιών"}},show_header_toggle:{heading:"Εμφάνιση διακόπτη κεφαλίδας",description:"Εμφάνιση διακόπτη στο επάνω μέρος της κάρτας για ενεργοποίηση/απενεργοποίηση όλων των οντοτήτων"},show_toggle_switches:{heading:"Εμφάνιση διακοπτών εναλλαγής",description:"Εμφάνιση διακόπτη εναλλαγής για κάθε μεμονωμένο πρόγραμμα στην κάρτα"},tags:{heading:"Ετικέτες",description:"Χρησιμοποιήστε ετικέτες για να χωρίσετε τα προγράμματα μεταξύ πολλών καρτών"},entities:{button_label:"Ρύθμιση συμπεριλαμβανόμενων οντοτήτων",heading:"Συμπεριλαμβανόμενες οντότητες",description:"Επιλέξτε τις οντότητες που θέλετε να ελέγχετε μέσω του χρονοπρογραμματιστή. Μπορείτε να κάνετε κλικ σε μια ομάδα για να την ανοίξετε. Λάβετε υπόψη ότι ορισμένες οντότητες (όπως αισθητήρες) μπορούν να χρησιμοποιηθούν μόνο για συνθήκες και όχι για ενέργειες.",included_number:"{number}/{total} επιλεγμένες"}}}}},mt={services:ut,ui:ht},pt=Object.freeze({__proto__:null,services:ut,ui:ht,default:mt}),_t={generic:{turn_on:"Turn on",turn_off:"Turn off",parameter_to_value:"{parameter} to {value}",action_with_parameter:"{action} with {parameter}"},climate:{set_temperature:"set temperature[ to {temperature}]",set_temperature_hvac_mode_heat:"heat[ to {temperature}]",set_temperature_hvac_mode_cool:"cool[ to {temperature}]",set_temperature_hvac_mode_heat_cool:"heat/cool[ to {temperature}]",set_temperature_hvac_mode_heat_cool_range:"heat/cool[ to {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"auto[ to {temperature}]",set_hvac_mode:"set mode[ to {hvac_mode}]",set_preset_mode:"set preset[ to {preset_mode}]",set_fan_mode:"set fan mode[ to {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"close",open_cover:"open",set_cover_position:"set position[ to {position}]",set_cover_tilt_position:"set tilt position[ to {tilt_position}]"},fan:{set_percentage:"set speed[ to {percentage}]",set_direction:"set direction[ to {direction}]",oscillate:"set oscillation[ to {oscillate}]"},humidifier:{set_humidity:"set humidity[ to {humidity}]",set_mode:"set mode[ to {mode}]"},input_number:{set_value:"set value[ to {value}]"},input_select:{select_option:"select option[ {option}]"},select:{select_option:"select option[ {option}]"},light:{turn_on:"turn on[ with {brightness} brightness]"},media_player:{select_source:"select source[ {source}]"},notify:{send_message:"send notification"},script:{execute:"execute"},vacuum:{start_pause:"start / pause"},water_heater:{set_operation_mode:"set mode[ to {operation_mode}]",set_away_mode:"set away mode"}},gt={components:{date:{day_types_short:{daily:"daily",workdays:"workdays",weekend:"weekend"},day_types_long:{daily:"every day",workdays:"on workdays",weekend:"in the weekend"},days:"days",tomorrow:"tomorrow",repeated_days:"every {days}",repeated_days_except:"every day except {excludedDays}",days_range:"from {startDay} to {endDay}",next_week_day:"next {weekday}"},time:{absolute:"at {time}",interval:"from {startTime} to {endTime}",at_midnight:"at midnight",at_noon:"at noon",at_sun_event:"at {sunEvent}"}},dialog:{enable_schedule:{title:"Complete modifications",description:"The schedule you have changed is currently disabled, do you want to enable it?"},confirm_delete:{title:"Remove entity?",description:"Are you sure you want to remove this entity?"},confirm_migrate:{title:"Update schedule",description:"Some settings will be lost by this change. Do you want to continue?"},weekday_picker:{title:"Repeated days for schedule",choose:"Choose..."},entity_picker:{title:"Choose entities",choose:"Choose...",no_results:"No matching items found"},action_picker:{title:"Choose action",show_all:"Show all supported entities"}},panel:{common:{title:"Scheduler",new_schedule:"New schedule",default_name:"Schedule #{id}"},overview:{no_entries:"There are no items to show",backend_error:"Could not connect with the scheduler component. It needs to be installed as integration before this card can be used.",excluded_items:"{number} excluded {if number is 1} item {else} items",hide_excluded:"hide excluded items",additional_tasks:"{number} more {if number is 1} task {else} tasks"},editor:{repeated_days:"Repeated days",start_time:"Start time",stop_time:"End time",action:"Action",add_action:"Add action",select_timeslot:"Select a timeslot",toggle_single_mode:"To single mode",toggle_scheme_mode:"To scheme mode",validation_errors:{overlapping_time:"Schedule has overlapping timeslots",missing_target_entity:"One or more actions are missing a target entity",missing_service_parameter:"One or more actions are missing a required setting",missing_action:"Schedule has no actions"}},options:{conditions:{header:"Conditions",add_condition:"Add condition",new_condition:"New condition",types:{equal_to:"{entity} is equal to {value}",unequal_to:"{entity} is unequal to {value}",above:"{entity} is above {value}",below:"{entity} is below {value}"},options:{logic_and:"All conditions must be true",logic_or:"Any condition must be true",track_changes:"Re-evaluate when conditions change"}},period:{header:"Period",start_date:"From",end_date:"To"},repeat_type:"behaviour after completion",tags:"Tags"},card_editor:{tabs:{entities:"Entities",other:"Other"},fields:{title:{heading:"Title of the card",options:{standard:"standard",hidden:"hidden",custom:"custom"},custom_title:"Custom title"},discover_existing:{heading:"Show all schedules",description:"This sets the 'discover existing' parameter. Previously created schedules will be automatically added to the card. "},time_step:{heading:"Time step",description:"Resolution (in minutes) for creating schedules",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Single schedule mode",scheme:"Time scheme mode"}},sort_by:{heading:"Sorting options",description:"Order in which the schedules appear in the card",options:{relative_time:"Time remaining until next action",title:"Displayed title of the schedule",state:"Show active schedules on top"}},display_format_primary:{heading:"Displayed primary info",description:"Configure which label is used for schedules in the overview",options:{default:"Schedule name",entity_action:"Summary of task"}},display_format_secondary:{heading:"Displayed secondary info",description:"Configure what additional properties are visible in the overview",options:{relative_time:"Time remaining until next action",time:"Configured time for next action",days:"Repeated days of the week",additional_tasks:"Number of additional tasks"}},show_header_toggle:{heading:"Show header toggle",description:"Show toggle switch at the top of the card for enabling/disabling all entities"},show_toggle_switches:{heading:"Show toggle switches",description:"Show toggle switch for each individual schedule in the card"},tags:{heading:"Tags",description:"Use tags to divide schedules between multiple cards"},entities:{button_label:"Configure included entities",heading:"Included entities",description:"Select the entities that you want to control using the scheduler. You can click on a group to open it. Note that some entities (such as sensors) can only be used for conditions, not for actions.",included_number:"{number}/{total} selected"}}}}},vt={services:_t,ui:gt},yt=Object.freeze({__proto__:null,services:_t,ui:gt,default:vt}),ft={generic:{turn_on:"Encender",turn_off:"Apagar",parameter_to_value:"{parameter} a {value}",action_with_parameter:"{action} con {parameter}"},climate:{set_temperature:"establecer temperatura[ a {temperature}]",set_temperature_hvac_mode_heat:"calefacción[ a {temperature}]",set_temperature_hvac_mode_cool:"frío[ a {temperature}]",set_temperature_hvac_mode_heat_cool:"calefacción/frío[ a {temperature}]",set_temperature_hvac_mode_heat_cool_range:"calefacción/frío[ a {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"automático[ a {temperature}]",set_hvac_mode:"establecer modo[ a {hvac_mode}]",set_preset_mode:"establecer preajuste[ {preset_mode}]",set_fan_mode:"establecer ventilador[ a {fan_mode}]",set_swing_mode:"establecer oscilación[ a {swing_mode}]"},cover:{close_cover:"cerrado",open_cover:"abierto",set_cover_position:"establecer posición[ a {position}]",set_cover_tilt_position:"establecer inclinación[ a {tilt_position}]"},fan:{set_percentage:"establecer velocidad[ a {speed}]",set_direction:"establecer dirección[ a {direction}]",oscillate:"establecer oscilación[ a {oscillate}]"},humidifier:{set_humidity:"establecer humedad[ a {humidity}]",set_mode:"establecer modo[ a {mode}]"},input_number:{set_value:"establecer valor[ a {value}]"},input_select:{select_option:"seleccionar opción[ a {option}]"},select:{select_option:"seleccionar opción[ {option}]"},light:{turn_on:"encender[ con brillo en {brightness}]"},media_player:{select_source:"seleccionar fuente[ {source}]"},notify:{send_message:"enviar notificación"},script:{execute:"ejecutar"},vacuum:{start_pause:"iniciar / pausar"},water_heater:{set_operation_mode:"establecer modo[ a {operation_mode}]",set_away_mode:"establecer modo fuera de casa"}},bt={components:{date:{day_types_short:{daily:"todos los días",workdays:"días hábiles",weekend:"fin de semana"},day_types_long:{daily:"todos los días",workdays:"días hábiles",weekend:"fin de semana"},days:"días",tomorrow:"mañana",repeated_days:"cada {days}",repeated_days_except:"cada dia excepto {excludedDays}",days_range:"de {startDay} a {endDay}",next_week_day:"próximo {weekday}"},time:{absolute:"a la(s) {time}",interval:"desde la(s) {startTime} hasta la(s) {endTime}",at_midnight:"a la medianoche",at_noon:"al mediodía",at_sun_event:"al {sunEvent}"}},dialog:{enable_schedule:{title:"Completar modificaciones",description:"El horario que ha modificado está actualmente deshabilitado, ¿Desea habilitarlo?"},confirm_delete:{title:"¿Eliminar entidad?",description:"¿Está seguro de que deseas eliminar esta entidad?"},confirm_migrate:{title:"Modificar horario",description:"Algunas configuraciones se perderán con este cambio. ¿Desea proceder?"},weekday_picker:{title:"Días repetidos para el horario",choose:"Elegir..."},entity_picker:{title:"Elegir entidades",choose:"Elegir...",no_results:"No se encontraron artículos coincidentes"},action_picker:{title:"Elija Acción",show_all:"Mostrar todas las entidades admitidas"}},panel:{common:{title:"Planificador",new_schedule:"Nuevo horario",default_name:"Horario #{id}"},overview:{no_entries:"No hay ningún elemento que mostrar",backend_error:"Fallo de conexión con el componente planificador (Scheduler component). Debe ser instalado como integración antes de poder usar este panel.",excluded_items:"{number} {if number is 1} elemento excluido {else} elementos excluidos",hide_excluded:"ocultar elementos excluidos",additional_tasks:"{number} {if number is 1} tarea adicional {else} tareas adicionales"},editor:{repeated_days:"Días repetidos",start_time:"Inicio",stop_time:"Finalización",action:"Acción",add_action:"Agregar acción",select_timeslot:"Seleccione un bloque de tiempo",toggle_single_mode:"Al modo simple",toggle_scheme_mode:"Al modo esquema",validation_errors:{overlapping_time:"El esquema tiene bloques de tiempo sobrepuestos",missing_target_entity:"Una o más acciones carecen de una entidad asociada",missing_service_parameter:"Una o más acciones carecen de una configuración requerida",missing_action:"El horario no tiene acciones"}},options:{conditions:{header:"Condiciones",add_condition:"Agregar condición",new_condition:"Nueva condición",types:{equal_to:"{entity} es igual a {value}",unequal_to:"{entity} es diferente a {value}",above:"{entity} es mayor que {value}",below:"{entity} es menor que {value}"},options:{logic_and:"Todas las condiciones deben ser válidas",logic_or:"Cualquier condición debe ser válida",track_changes:"Reevaluar si una condición cambia"}},period:{header:"Período",start_date:"De",end_date:"A"},repeat_type:"comportamiento despues de finalizar ",tags:"Etiquetas"},card_editor:{tabs:{entities:"Entidades",other:"Otros"},fields:{title:{heading:"Títujo de la tarjeta",options:{standard:"estándar",hidden:"oculta",custom:"personalizada"},custom_title:"Título personalizado"},discover_existing:{heading:"Mostrar todos los horarios",description:"Esto ajustará el parámetro 'descubrir existentes (discover existing)'. Los horarios creados anteriormente deberán de ser agregados automáticamente a la tarjeta."},time_step:{heading:"Paso de tiempo",description:"Resolución (en minutos) para la creación de horarios.",unit_minutes:"min"},default_editor:{heading:"Editor de tiempo por defecto",options:{single:"Modo de horario sencillo",scheme:"Modo de esquema de tiempo"}},sort_by:{heading:"Opciones de clasificación",description:"Orden en que los horarios aparecen en la tarjeta",options:{relative_time:"Tiempo restante hasta la siguiente acción",title:"Título mostrado del horario",state:"Mostrar los horarios activos primero"}},display_format_primary:{heading:"Mostrar información primaria",description:"Configura que etiqueta se utiliza para los horarios en la vista principal",options:{default:"Nombre del horario",entity_action:"Resumen de la tarea"}},display_format_secondary:{heading:"Mostrar información secundaria",description:"Configura que propiedades adicionales son visibles en la vista principal",options:{relative_time:"Tiempo restante hasta la siguiente acción",time:"Tiempo configurado para la siguiente acción",days:"Días repetidos de la semana",additional_tasks:"Número de tareas adicionales"}},show_header_toggle:{heading:"Mostrar el interruptor del encabezado",description:"Muestra el interruptor en la parte alta de la tarjeta para habilitar/desabilitar todas las entidades Show toggle switch at the top of the card for enabling/disabling all entities"},show_toggle_switches:{heading:"Mostrar interruptores",description:"Mostrar el interruptor para cada programación individual en la tarjeta"},tags:{heading:"Etiquetas",description:"Use etiquetas para dividir los horarios entre múltiples tarjetas"},entities:{button_label:"Configurar entidades incluidas",heading:"Entidades incluidas",description:"Seleccione las entidades que desea controlar usando el planificador. Puede hacer click en un grupo para abrirlo. Note que algunas entidades (como los sensores) solo pueden ser usados para condiciones, no para acciones.",included_number:"{number}/{total} seleccionados"}}}}},wt={services:ft,ui:bt},kt=Object.freeze({__proto__:null,services:ft,ui:bt,default:wt}),xt={generic:{turn_on:"Lülita sisse",turn_off:"Lülita välja",parameter_to_value:"{parameter} {value} jaoks",action_with_parameter:"{action} väärtusega {parameter}"},climate:{set_temperature:"vali temperatuur [{temperature}]",set_temperature_hvac_mode_heat:"küte[ @ {temperature}]",set_temperature_hvac_mode_cool:"jahutus [ @ {temperature}]",set_temperature_hvac_mode_heat_cool:"küte/jahutus[ @ {temperature}]",set_temperature_hvac_mode_heat_cool_range:"küte/jahutus[ @ {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"automaatne[ @ {temperature}]",set_hvac_mode:"vali režiim [{hvac_mode}]",set_preset_mode:"eelseade[ {preset_mode}]",set_fan_mode:"set fan mode[ to {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"sulge",open_cover:"ava",set_cover_position:"sea asendisse[{position}]",set_cover_tilt_position:"sea ribide kalle [ asendisse {tilt_position}]"},fan:{set_percentage:"vali kiirus[ @ {speed}]",set_direction:"vali suund[ @ {direction}]",oscillate:"vali hajutus[ @ {oscillate}]"},humidifier:{set_humidity:"sea niiskus[ {humidity}]",set_mode:"vali režiim [{mode}]"},input_number:{set_value:"vali väärtus[ {value}]"},input_select:{select_option:"valik[ {option}]"},select:{select_option:"valik[ {option}]"},light:{turn_on:"lülita sisse[ heledusega {brightness}]"},media_player:{select_source:"vali sisend[ {source}]"},notify:{send_message:"send notification"},script:{execute:"käivita"},vacuum:{start_pause:"alusta/ootele"},water_heater:{set_operation_mode:"vali režiim [{operation_mode}]",set_away_mode:"kodust ära"}},$t={components:{date:{day_types_short:{daily:"iga päev",workdays:"tööpäevadel",weekend:"nädalavahetusel"},day_types_long:{daily:"iga päev",workdays:"tööpäevadel",weekend:"nädalavahetusel"},days:"päeva",tomorrow:"homme",repeated_days:"iga {days} järel",repeated_days_except:"iga päev aga mitte {excludedDays}",days_range:"{startDay} kuni {endDay}",next_week_day:"järgmisel {weekday}"},time:{absolute:"{time}",interval:"{startTime} kuni {endTime}",at_midnight:"keskööl",at_noon:"keskpäeval",at_sun_event:"{sunEvent}"}},dialog:{enable_schedule:{title:"Viige muudatused lõpule",description:"Muudetud ajakava on praegu keelatud, kas see peaks olema lubatud?"},confirm_delete:{title:"Kas eemaldan olemi?",description:"Oled kindel, et soovid selle olemi eemaldada?"},confirm_migrate:{title:"Muutke ajakava",description:"Selle muudatusega lähevad mõned seaded kaotsi. Kas soovite jätkata?"},weekday_picker:{title:"Repeated days for schedule",choose:"Choose..."},entity_picker:{title:"Choose entities",choose:"Choose...",no_results:"No matching items found"},action_picker:{title:"Choose action",show_all:"Show all supported entities"}},panel:{common:{title:"Ajastaja",new_schedule:"Uus ajakava",default_name:"Ajakava #{id}"},overview:{no_entries:"Ajastused puuduvad",backend_error:"Ajastaja sidumine puudub. Sidumine tuleb luua enne selle kaardi kasutamist.",excluded_items:"välja on jäetud {number} {if number is 1} ajastus {else} ajastust",hide_excluded:"peida välja jäetud ajastused",additional_tasks:"veel {number} {if number is 1} ajastus {else} ajastust"},editor:{repeated_days:"Repeated days",start_time:"Start time",stop_time:"End time",action:"Action",add_action:"Add action",select_timeslot:"Alustuseks vali ajavahemik",toggle_single_mode:"Üksikrežiimile",toggle_scheme_mode:"Skeemirežiimile",validation_errors:{overlapping_time:"Schedule has overlapping timeslots",missing_target_entity:"One or more actions are missing a target entity",missing_service_parameter:"One or more actions are missing a required setting",missing_action:"Schedule has no actions"}},options:{conditions:{header:"Conditions",add_condition:"Add condition",new_condition:"New condition",types:{equal_to:"{entity} is equal to {value}",unequal_to:"{entity} is unequal to {value}",above:"{entity} is above {value}",below:"{entity} is below {value}"},options:{logic_and:"All conditions must be true",logic_or:"Any condition must be true",track_changes:"Re-evaluate when conditions change"}},period:{header:"Periood",start_date:"From",end_date:"To"},repeat_type:"toiming peale käivitumist",tags:"Tags"},card_editor:{tabs:{entities:"Entities",other:"Other"},fields:{title:{heading:"Title of the card",options:{standard:"standard",hidden:"hidden",custom:"custom"},custom_title:"Custom title"},discover_existing:{heading:"Show all schedules",description:"This sets the 'discover existing' parameter. Previously created schedules will be automatically added to the card. "},time_step:{heading:"Time step",description:"Resolution (in minutes) for creating schedules",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Single schedule mode",scheme:"Time scheme mode"}},sort_by:{heading:"Sorting options",description:"Order in which the schedules appear in the card",options:{relative_time:"Time remaining until next action",title:"Displayed title of the schedule",state:"Show active schedules on top"}},display_format_primary:{heading:"Displayed primary info",description:"Configure which label is used for schedules in the overview",options:{default:"Schedule name",entity_action:"Summary of task"}},display_format_secondary:{heading:"Displayed secondary info",description:"Configure what additional properties are visible in the overview",options:{relative_time:"Time remaining until next action",time:"Configured time for next action",days:"Repeated days of the week",additional_tasks:"Number of additional tasks"}},show_header_toggle:{heading:"Show header toggle",description:"Show toggle switch at the top of the card for enabling/disabling all entities"},show_toggle_switches:{heading:"Show toggle switches",description:"Show toggle switch for each individual schedule in the card"},tags:{heading:"Tags",description:"Use tags to divide schedules between multiple cards"},entities:{button_label:"Configure included entities",heading:"Included entities",description:"Select the entities that you want to control using the scheduler. You can click on a group to open it. Note that some entities (such as sensors) can only be used for conditions, not for actions.",included_number:"{number}/{total} selected"}}}}},jt={services:xt,ui:$t},Ot=Object.freeze({__proto__:null,services:xt,ui:$t,default:jt}),St={generic:{turn_on:"Laita päälle",turn_off:"Sammuta",parameter_to_value:"{parameter} {value}",action_with_parameter:"{action} {parameter}"},climate:{set_temperature:"aseta lämpötilaksi[ {temperature}]",set_temperature_hvac_mode_heat:"lämmitä[ {temperature} asteeseen]",set_temperature_hvac_mode_cool:"jäähdytä[ {temperature} asteeseen]",set_temperature_hvac_mode_heat_cool:"lämmitä/jäähdytä[ {temperature} asteeseen]",set_temperature_hvac_mode_heat_cool_range:"lämmitä/jäähdytä[ välillä {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"automaatilla[ {temperature} asteeseen]",set_hvac_mode:"aseta tilaksi[ {hvac_mode}]",set_preset_mode:"aseta esivalinta[ {preset_mode}]",set_fan_mode:"aseta tuuletus[ {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"sulje",open_cover:"avaa",set_cover_position:"aseta sijainniksi[ {position}]",set_cover_tilt_position:"aseta kallistus[ {tilt_position}]"},fan:{set_percentage:"aseta nopeus[ {speed}]",set_direction:"asenta suunta[ {direction}]",oscillate:"aseta pyörimisnopeus[ {oscillate}]"},humidifier:{set_humidity:"aseta kosteus[ {humidity}]",set_mode:"aseta tilaksi {mode}"},input_number:{set_value:"aseta arvo {value}"},input_select:{select_option:"valitse[ {option}]"},select:{select_option:"valitse[ {option}]"},light:{turn_on:"kytke päälle[ {brightness} kirkkaudella]"},media_player:{select_source:"valitse lähteeksi[ {source}]"},notify:{send_message:"lähetä ilmoitus"},script:{execute:"suorita"},vacuum:{start_pause:"aloita / keskeytä"},water_heater:{set_operation_mode:"aseta tilaksi[ {operation_mode}]",set_away_mode:"aseta poissa-tila"}},zt={components:{date:{day_types_short:{daily:"päivittäin",workdays:"työpäivisin",weekend:"viikonloppuisin"},day_types_long:{daily:"päivittäin",workdays:"työpäivisin",weekend:"viikonloppuisin"},days:"päivää",tomorrow:"huomenna",repeated_days:"joka {days}",repeated_days_except:"joka päivä paitsi {excludedDays}",days_range:"{startDay} {endDay}",next_week_day:"seuraava {weekday}"},time:{absolute:"{time}",interval:"{startTime} - {endTime}",at_midnight:"keskiyöllä",at_noon:"keskipäivällä",at_sun_event:"{sunEvent}"}},dialog:{enable_schedule:{title:"Suorita muutokset loppuun",description:"Muutettu aikataulu on tällä hetkellä poissa käytöstä, pitäisikö se ottaa käyttöön?"},confirm_delete:{title:"Poistetaanko kohde?",description:"Haluatko varmasti poistaa tämän kohteen?"},confirm_migrate:{title:"Muokkaa aikataulua",description:"Jotkut asetukset menetetään tämän muutoksen seurauksena. Haluatko edetä?"},weekday_picker:{title:"Repeated days for schedule",choose:"Choose..."},entity_picker:{title:"Choose entities",choose:"Choose...",no_results:"No matching items found"},action_picker:{title:"Choose action",show_all:"Show all supported entities"}},panel:{common:{title:"Ajastin",new_schedule:"Uusi aikataulu",default_name:"Aikataulu #{id}"},overview:{no_entries:"Ei näytettäviä kohteita",backend_error:"Ei voitu yhdistää scheduler komponenttiin. Kortin käyttäminen vaatii scheduler integraation asentamisen.",excluded_items:"{number} {if number is 1} poissuljettu kohde {else} poissuljettua kohdetta",hide_excluded:"piilota poissuljetut kohteet",additional_tasks:"{number} {if number is 1} tehtävä {else} tehtävää"},editor:{repeated_days:"Repeated days",start_time:"Start time",stop_time:"End time",action:"Action",add_action:"Add action",select_timeslot:"Valitse aikaikkuna ensin",toggle_single_mode:"To single mode",toggle_scheme_mode:"To scheme mode",validation_errors:{overlapping_time:"Schedule has overlapping timeslots",missing_target_entity:"One or more actions are missing a target entity",missing_service_parameter:"One or more actions are missing a required setting",missing_action:"Schedule has no actions"}},options:{conditions:{header:"Conditions",add_condition:"Add condition",new_condition:"New condition",types:{equal_to:"{entity} is equal to {value}",unequal_to:"{entity} is unequal to {value}",above:"{entity} is above {value}",below:"{entity} is below {value}"},options:{logic_and:"All conditions must be true",logic_or:"Any condition must be true",track_changes:"Re-evaluate when conditions change"}},period:{header:"Ajanjakso",start_date:"From",end_date:"To"},repeat_type:"toiminta tapahtuman jälkeen",tags:"Tags"},card_editor:{tabs:{entities:"Kohteet",other:"Muu"},fields:{title:{heading:"Kortin otsikko",options:{standard:"normaali",hidden:"piilotettu",custom:"muokattu"},custom_title:"Muokattu otsikko"},discover_existing:{heading:"Näytä kaikki ajoitukset",description:"Tämä kytkee käyttöön 'näytä olemassa olevat -attribuutin'. Aiemmin luodut ajastukset lisätään automaattisesti korttiin. "},time_step:{heading:"Ajastusvälit",description:"Ajastusväli minuutteina ajastusten luontiin",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Yksittäistilaan",scheme:"Kaaviotilaan"}},sort_by:{heading:"Lajitteluasetukset",description:"Järjestys miten ajastukset näkyvät kortissa",options:{relative_time:"Aikaa jäljellä seuraavaan toimintoon",title:"Ajastuksen otsikko",state:"Aktiiviset ajastukset ylhäällä"}},display_format_primary:{heading:"Ensisijainen tieto",description:"Valitse mitä näytetään yhteenvedossa",options:{default:"Ajastuksen nimi",entity_action:"Ajastuksen yhteenveto"}},display_format_secondary:{heading:"Toissijainen tieto",description:"Valitse mitkä lisätiedot näkyvät yhteenvedossa",options:{relative_time:"Aikaa jäljellä seuraavaan toimintoon",time:"Seuraavalle toiminnolle määritetty aika",days:"Toistuvat viikonpäivät",additional_tasks:"Lisätoimintojen määrä"}},show_header_toggle:{heading:"Näytä otsikkokytkin",description:"Näytä kytkin kortin yläreunassa usean ajastuksen päälle/pois kytkemiseen"},show_toggle_switches:{heading:"Näytä kytkimet",description:"Näytä kytkin jokaiselle yksittäiselle ajastukselle kortissa"},tags:{heading:"Tunniste",description:"Käytä tunnisteita ajastusten jakamiseen korttien välillä"},entities:{button_label:"Sisällytettyjen entiteettien määrittäminen",heading:"Ajastettavat kohteet",description:"Valitse kohteet, joille haluat luoda ajastuksia. Voit klikata ryhmään laajentaaksesi sen. Huom: joitain kohteita voi käyttää vain ehtoina (esim. sensorit), ei toimintoihin",included_number:"{number} / {total} valittu"}}}}},Ct={services:St,ui:zt},At=Object.freeze({__proto__:null,services:St,ui:zt,default:Ct}),Tt={generic:{turn_on:"Allumer",turn_off:"Éteindre",parameter_to_value:"{parameter} vers {value}",action_with_parameter:"{action} avec {parameter}"},climate:{set_temperature:"ajuster la température[ à {temperature}]",set_temperature_hvac_mode_heat:"chauffe[ à {temperature}]",set_temperature_hvac_mode_cool:"refroidit[ à {temperature}]",set_temperature_hvac_mode_heat_cool:"chauffe/refroidit[ à {temperature}]",set_temperature_hvac_mode_heat_cool_range:"chauffe/refroidit[ à {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"auto[ à {temperature}]",set_hvac_mode:"ajuster le mode[ à {hvac_mode}]",set_preset_mode:"choisir le pré-réglage[ {preset_mode}]",set_fan_mode:"ajuster le mode de ventilation[ à {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"fermer",open_cover:"ouvrir",set_cover_position:"ajuster la position[ à {position}]",set_cover_tilt_position:"régler les volets[ à {tilt_position}]"},fan:{set_percentage:"ajuster la vitesse[ à {speed}]",set_direction:"ajuster l'orientation[ vers {direction}]",oscillate:"ajuster l'oscillation[ à {oscillate}]"},humidifier:{set_humidity:"ajuster l'humidité[ à {humidity}]",set_mode:"ajuster le mode[ à {mode}]"},input_number:{set_value:"ajuster la valeur[ à {value}]"},input_select:{select_option:"choisir l'option[ {option}]"},select:{select_option:"choisir l'option[ {option}]"},light:{turn_on:"allumer[ avec une luminosité de {brightness}]"},media_player:{select_source:"choisir la source[ {source}]"},notify:{send_message:"envoyer une notification"},script:{execute:"exécuter"},vacuum:{start_pause:"démarrer / pause"},water_heater:{set_operation_mode:"ajuster le mode[ à {operation_mode}]",set_away_mode:"choisir le mode absent"}},Et={components:{date:{day_types_short:{daily:"quotidien",workdays:"jours de travail",weekend:"weekend"},day_types_long:{daily:"chaque jour",workdays:"en semaine",weekend:"le weekend"},days:"jours",tomorrow:"demain",repeated_days:"chaque {days}",repeated_days_except:"chaque jour sauf {excludedDays}",days_range:"de {startDay} à {endDay}",next_week_day:"{weekday} prochain"},time:{absolute:"à {time}",interval:"de {startTime} à {endTime}",at_midnight:"à minuit",at_noon:"à midi",at_sun_event:"au {sunEvent}"}},dialog:{enable_schedule:{title:"Compléter les modifications",description:"Le planning qui a été modifié est actuellement désactivé, doit-il être activé ?"},confirm_delete:{title:"Supprimer l'entité ?",description:"Voulez-vous vraiment supprimer cette entité ?"},confirm_migrate:{title:"Modifier l'horaire",description:"Certains paramètres seront perdus par ce changement. Voulez-vous poursuivre?"},weekday_picker:{title:"Jours de répétition",choose:"Choisir les jours..."},entity_picker:{title:"Choisir les entités",choose:"Choisir...",no_results:"Aucune entité trouvée"},action_picker:{title:"Choisir une action",show_all:"Afficher toutes les entités prises en charge"}},panel:{common:{title:"Planificateur",new_schedule:"Nouvel horaire",default_name:"Horaire #{id}"},overview:{no_entries:"il n'y a pas d'entrée à montrer",backend_error:"Impossible de se connecter au composant du planificateur. Il doit être installé en tant qu'intégration avant que cette carte ne puisse être utilisée.",excluded_items:"{number} {if number is 1}entrée exclue{else}entrées exclues",hide_excluded:"cacher les entrées exclues",additional_tasks:"{number} {if number is 1}tâche à venir{else}tâches à venir"},editor:{repeated_days:"Jours de répétition",start_time:"Heure de début",stop_time:"Heure de fin",action:"Action",add_action:"Ajouter une action",select_timeslot:"Choisir d'abord une plage horaire",toggle_single_mode:"Vers mode simple",toggle_scheme_mode:"Vers mode schéma",validation_errors:{overlapping_time:"Certaines plages horaires se chevauchent",missing_target_entity:"Certaines actions n'ont pas d'entité sélectionnée",missing_service_parameter:"Certaines actions ne sont pas totalement configurées",missing_action:"Le planning n'a aucune action définie"}},options:{conditions:{header:"Conditions",add_condition:"Ajouter une condition",new_condition:"Nouvelle condition",types:{equal_to:"{entity} est égal à {value}",unequal_to:"{entity} n'est pas égal à {value}",above:"{entity} est supérieur à {value}",below:"{entity} est inférieur à {value}"},options:{logic_and:"Toutes les conditions doivent être valides",logic_or:"Au moins une condition doit être valide",track_changes:"Ré-évaluer lorsque la condition change"}},period:{header:"Période",start_date:"Du",end_date:"Au"},repeat_type:"Comportement après l'achèvement",tags:"Tags"},card_editor:{tabs:{entities:"Entities",other:"Other"},fields:{title:{heading:"Title of the card",options:{standard:"standard",hidden:"hidden",custom:"custom"},custom_title:"Custom title"},discover_existing:{heading:"Show all schedules",description:"This sets the 'discover existing' parameter. Previously created schedules will be automatically added to the card. "},time_step:{heading:"Time step",description:"Resolution (in minutes) for creating schedules",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Single schedule mode",scheme:"Time scheme mode"}},sort_by:{heading:"Sorting options",description:"Order in which the schedules appear in the card",options:{relative_time:"Time remaining until next action",title:"Displayed title of the schedule",state:"Show active schedules on top"}},display_format_primary:{heading:"Displayed primary info",description:"Configure which label is used for schedules in the overview",options:{default:"Schedule name",entity_action:"Summary of task"}},display_format_secondary:{heading:"Displayed secondary info",description:"Configure what additional properties are visible in the overview",options:{relative_time:"Time remaining until next action",time:"Configured time for next action",days:"Repeated days of the week",additional_tasks:"Number of additional tasks"}},show_header_toggle:{heading:"Show header toggle",description:"Show toggle switch at the top of the card for enabling/disabling all entities"},show_toggle_switches:{heading:"Show toggle switches",description:"Show toggle switch for each individual schedule in the card"},tags:{heading:"Tags",description:"Use tags to divide schedules between multiple cards"},entities:{button_label:"Configure included entities",heading:"Included entities",description:"Select the entities that you want to control using the scheduler. You can click on a group to open it. Note that some entities (such as sensors) can only be used for conditions, not for actions.",included_number:"{number}/{total} selected"}}}}},Dt={services:Tt,ui:Et},Mt=Object.freeze({__proto__:null,services:Tt,ui:Et,default:Dt}),Lt={generic:{turn_on:"הפעלה",turn_off:"כיבוי",parameter_to_value:"{parameter} ל {value}",action_with_parameter:"{action} עם {parameter}"},climate:{set_temperature:"קבע טמפרטורה[ ל {temperature}]",set_temperature_hvac_mode_heat:"חימום[ ל {temperature}]",set_temperature_hvac_mode_cool:"קירור[ ל {temperature}]",set_temperature_hvac_mode_heat_cool:"חימום/קירור[ ל {temperature}]",set_temperature_hvac_mode_heat_cool_range:"חימום/קירור[ ל {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"אוטומטי[ ל {temperature}]",set_hvac_mode:"קבע מצב עבודה[ ל {hvac_mode}]",set_preset_mode:"קבע הגדרה[ ל {preset_mode}]",set_fan_mode:"הגדר מצב מאוורר[ ל {fan_mode}]",set_swing_mode:"הגדר מצב תנודת תריס[ ל {swing_mode}]"},cover:{close_cover:"סגירה",open_cover:"פתיחה",set_cover_position:"קבע מיקום[ ל {position}]",set_cover_tilt_position:"קבע הטיה[ ל {tilt_position}]"},fan:{set_percentage:"קבע מהירות[ ל {speed}]",set_direction:"קבע כיוון[ ל {direction}]",oscillate:"קבע תנודת תריס[ ל {oscillate}]"},humidifier:{set_humidity:"קבע לחות[ ל {humidity}]",set_mode:"קבע מצב עבודה[ ל {mode}]"},input_number:{set_value:"קבע ערך[ ל {value}]"},input_select:{select_option:"בחר אפשרות[ {option}]"},select:{select_option:"בחר אפשרות[ {option}]"},light:{turn_on:"הדלקה[ בעוצמה של {brightness}]"},media_player:{select_source:"בחר מקור[ {source}]"},notify:{send_message:"שלח התראה"},script:{execute:"בצע"},vacuum:{start_pause:"התחל / הפסק"},water_heater:{set_operation_mode:"קבע מצב עבודה[ ל {operation_mode}]",set_away_mode:"קבע מצב מוץ לבית"}},Pt={components:{date:{day_types_short:{daily:"כל יום",workdays:"ימי חול",weekend:"סוף שבוע"},day_types_long:{daily:"כל יום",workdays:"בימי חול",weekend:"בסוף השבוע"},days:"ימים",tomorrow:"מחר",repeated_days:"בכל {days}",repeated_days_except:"בכל יום פרט ל {excludedDays}",days_range:"מ- {startDay} ועד- {endDay}",next_week_day:"הבא {weekday}"},time:{absolute:"בשעה {time}",interval:"משעה {startTime} עד שעה {endTime}",at_midnight:"בחצות הלילה",at_noon:"בחצות היום",at_sun_event:"ב {sunEvent}"}},dialog:{enable_schedule:{title:"השלם את השינויים",description:"לוח הזמנים ששונה מושבת כעת, האם צריך להפעיל אותו?"},confirm_delete:{title:"להסיר את הישות?",description:"האם בוודאות ברצונך להסיר ישות זו?"},confirm_migrate:{title:"שנה את לוח הזמנים",description:"חלק מההגדרות יאבדו על ידי פעולה זו. האם אתה רוצה להמשיך?"},weekday_picker:{title:"ימים לחזרה עבור לוח זמנים",choose:"בחר..."},entity_picker:{title:"בחר ישויות",choose:"בחר...",no_results:"לא נמצאו פריטים תואמים"},action_picker:{title:"בחר פעולה",show_all:"הצג את כל הישויות הנתמכות"}},panel:{common:{title:"לוח זמנים",new_schedule:"לוח זמנים חדש",default_name:"לוח זמנים #{id}"},overview:{no_entries:"אין פריטים להצגה",backend_error:"אין אפשרות להתחבר לרכיב התזמונים. נדרש להתקין את הרכיב באינטגרציה לפני השימוש בכרטיס.",excluded_items:"{number} לא נכלל {if number is 1} פריט {else} פריטים",hide_excluded:"הסתר פריטים לא כלולים",additional_tasks:"{number} נוסף {if number is 1} משימה {else} משימות"},editor:{repeated_days:"ימים לחזרה",start_time:"זמן התחלה",stop_time:"זמן סיום",action:"פעולה",add_action:"הוספת פעולה",select_timeslot:"בחר משבצת זמן קודם",toggle_single_mode:"למצב פשוט",toggle_scheme_mode:"למצב דיאגרמה",validation_errors:{overlapping_time:"לוח הזמנים כולל משבצות זמן חופפות",missing_target_entity:"אחת או יותר מהפעולות חסרות ישות יעד",missing_service_parameter:"אחת או יותר מהפעולות חסרות הגדרה נדרשת",missing_action:"לוח הזמנים אינו כולל פעולות"}},options:{conditions:{header:"תנאים",add_condition:"הוספת תנאי",new_condition:"תנאי חדש",types:{equal_to:"{entity} שווה ל-{value}",unequal_to:"{entity} לא שווה ל-{value}",above:"{entity} מעל {value}",below:"{entity} מתחת {value}"},options:{logic_and:"כל התנאים חייבים להיות נכונים",logic_or:"כל תנאי חייב להיות נכון",track_changes:"הערכה מחדש כאשר התנאים משתנים"}},period:{header:"פרק זמן",start_date:"מ",end_date:"ל"},repeat_type:"התנהגות לאחר הפעלה",tags:"תגים"},card_editor:{tabs:{entities:"ישויות",other:"אחר"},fields:{title:{heading:"כותרת הכרטיס",options:{standard:"רגילה",hidden:"מוסתרת",custom:"מותאמת אישית"},custom_title:"כותרת מותאמת אישית"},discover_existing:{heading:"הצג את כל לוחות הזמנים",description:"הגדרה זו קובעת את הפרמטר 'discover existing'. לוחות זמנים שנוצרו בעבר יתווספו אוטומטית לכרטיס"},time_step:{heading:"מרווח זמן",description:"רזולוציה (בדקות) ליצירת לוחות זמנים",unit_minutes:"דק'"},default_editor:{heading:"עורך זמן ברירת מחדל",options:{single:"מצב לוח זמנים בודד",scheme:"מצב תבנית זמנים"}},sort_by:{heading:"אפשרויות מיון",description:"סדר הופעת לוחות הזמנים בכרטיס",options:{relative_time:"זמן שנותר עד הפעולה הבאה",title:"כותרת לוח הזמנים המוצגת",state:"הצג לוחות זמנים פעילים בראש"}},display_format_primary:{heading:"מידע ראשי מוצג",description:"הגדר איזו תווית תשמש עבור לוחות הזמנים בסקירה הכללית",options:{default:"שם לוח הזמנים",entity_action:"סיכום המשימה"}},display_format_secondary:{heading:"מידע משני מוצג",description:"הגדר אילו מאפיינים נוספים יהיו גלויים בסקירה הכללית",options:{relative_time:"זמן שנותר עד הפעולה הבאה",time:"זמן מוגדר לפעולה הבאה",days:"ימים חוזרים בשבוע",additional_tasks:"מספר משימות נוספות"}},show_header_toggle:{heading:"הצג מתג בכותרת",description:"הצג מתג הפעלה/כיבוי בראש הכרטיס להפעלה/השבתה של כל הישויות"},show_toggle_switches:{heading:"הצג מתגים",description:"הצג מתג עבור כל לוח זמנים בודד בכרטיס"},tags:{heading:"תגיות",description:"השתמש בתגיות כדי לחלק לוחות זמנים בין כרטיסים שונים"},entities:{button_label:"הגדר ישויות כלולות",heading:"ישויות כלולות",description:"בחר את הישויות שברצונך לשלוט בהן באמצעות המתזמן. ניתן ללחוץ על קבוצה כדי לפתוח אותה. שים לב שחלק מהישויות (כמו חיישנים) יכולות לשמש רק לתנאים, ולא לפעולות.",included_number:"{number}/{total} נבחרו"}}}}},Nt={services:Lt,ui:Pt},It=Object.freeze({__proto__:null,services:Lt,ui:Pt,default:Nt}),qt={generic:{turn_on:"Bekapcsolás",turn_off:"Kikapcsolás",parameter_to_value:"{parameter} to {value}",action_with_parameter:"{action} with {parameter}"},climate:{set_temperature:"hőmérséklet[ to {temperature}]",set_temperature_hvac_mode_heat:"melegíteni[ to {temperature}]",set_temperature_hvac_mode_cool:"hűtés[ to {temperature}]",set_temperature_hvac_mode_heat_cool:"melegíteni/hűtés[ to {temperature}]",set_temperature_hvac_mode_heat_cool_range:"melegíteni/hűtés[ to {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"automatikus[ to {temperature}]",set_hvac_mode:"mód beállítása[ to {hvac_mode}]",set_preset_mode:"preset beállítása[ {preset_mode}]",set_fan_mode:"set fan mode[ to {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"zárás",open_cover:"nyitás",set_cover_position:"változtass pozíciót[ to {position}]",set_cover_tilt_position:"set tilt position[ to {tilt_position}]"},fan:{set_percentage:"set speed[ to {speed}]",set_direction:"set direction[ to {direction}]",oscillate:"set oscillation[ to {oscillate}]"},humidifier:{set_humidity:"set humidity[ to {humidity}]",set_mode:"mód beállítása[ to {mode}]"},input_number:{set_value:"érték beállítása[ to {value}]"},input_select:{select_option:"opció kiválasztása[ {option}]"},select:{select_option:"opció kiválasztása[ {option}]"},light:{turn_on:"bekapcsolás[ with {brightness} brightness]"},media_player:{select_source:"forrás kiválasztása[ {source}]"},notify:{send_message:"send notification"},script:{execute:"kezdés"},vacuum:{start_pause:"start / pause"},water_heater:{set_operation_mode:"mód beállítása[ to {operation_mode}]",set_away_mode:"set away mode"}},Rt={components:{date:{day_types_short:{daily:"minden nap",workdays:"munkanapokon",weekend:"hétvégén"},day_types_long:{daily:"minden nap",workdays:"munkanapokon",weekend:"hétvégén"},days:"Napokon",tomorrow:"tomorrow",repeated_days:"every {days}",repeated_days_except:"every day except {excludedDays}",days_range:"from {startDay} to {endDay}",next_week_day:"következő {weekday}"},time:{absolute:"{time}-kor",interval:"{startTime} - {endTime}",at_midnight:"éjfélkor",at_noon:"délben",at_sun_event:"{sunEvent}kor"}},dialog:{enable_schedule:{title:"Végezze el a módosításokat",description:"A módosított ütemezés jelenleg le van tiltva, engedélyezni kell?"},confirm_delete:{title:"Biztos benne, hogy eltávolítja az entitást?",description:"Biztos benne, hogy el szeretné távolítani ezt az entitást?"},confirm_migrate:{title:"Ütemezés módosítása",description:"Ezzel a művelettel bizonyos beállítások elvesznek. Akarod folytatni?"},weekday_picker:{title:"Repeated days for schedule",choose:"Choose..."},entity_picker:{title:"Choose entities",choose:"Choose...",no_results:"No matching items found"},action_picker:{title:"Choose action",show_all:"Show all supported entities"}},panel:{common:{title:"Időzítések",new_schedule:"Új ütemezés",default_name:"Ütemterv #{id}"},overview:{no_entries:"Nincs megjeleníthető elem",backend_error:"Could not connect with the scheduler component. It needs to be installed as integration before this card can be used.",excluded_items:"{number} excluded {if number is 1} item {else} items",hide_excluded:"hide excluded items",additional_tasks:"még {number} feladat"},editor:{repeated_days:"Repeated days",start_time:"Start time",stop_time:"End time",action:"Action",add_action:"Add action",select_timeslot:"Select a timeslot",toggle_single_mode:"Egyszerű módba",toggle_scheme_mode:"Diagram módba",validation_errors:{overlapping_time:"Schedule has overlapping timeslots",missing_target_entity:"One or more actions are missing a target entity",missing_service_parameter:"One or more actions are missing a required setting",missing_action:"Schedule has no actions"}},options:{conditions:{header:"Conditions",add_condition:"Add condition",new_condition:"New condition",types:{equal_to:"{entity} is equal to {value}",unequal_to:"{entity} is unequal to {value}",above:"{entity} is above {value}",below:"{entity} is below {value}"},options:{logic_and:"All conditions must be true",logic_or:"Any condition must be true",track_changes:"Re-evaluate when conditions change"}},period:{header:"Időszak",start_date:"From",end_date:"To"},repeat_type:"behaviour after completion",tags:"Tags"},card_editor:{tabs:{entities:"Entities",other:"Other"},fields:{title:{heading:"Title of the card",options:{standard:"standard",hidden:"hidden",custom:"custom"},custom_title:"Custom title"},discover_existing:{heading:"Show all schedules",description:"This sets the 'discover existing' parameter. Previously created schedules will be automatically added to the card. "},time_step:{heading:"Time step",description:"Resolution (in minutes) for creating schedules",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Single schedule mode",scheme:"Time scheme mode"}},sort_by:{heading:"Sorting options",description:"Order in which the schedules appear in the card",options:{relative_time:"Time remaining until next action",title:"Displayed title of the schedule",state:"Show active schedules on top"}},display_format_primary:{heading:"Displayed primary info",description:"Configure which label is used for schedules in the overview",options:{default:"Schedule name",entity_action:"Summary of task"}},display_format_secondary:{heading:"Displayed secondary info",description:"Configure what additional properties are visible in the overview",options:{relative_time:"Time remaining until next action",time:"Configured time for next action",days:"Repeated days of the week",additional_tasks:"Number of additional tasks"}},show_header_toggle:{heading:"Show header toggle",description:"Show toggle switch at the top of the card for enabling/disabling all entities"},show_toggle_switches:{heading:"Show toggle switches",description:"Show toggle switch for each individual schedule in the card"},tags:{heading:"Tags",description:"Use tags to divide schedules between multiple cards"},entities:{button_label:"Configure included entities",heading:"Included entities",description:"Select the entities that you want to control using the scheduler. You can click on a group to open it. Note that some entities (such as sensors) can only be used for conditions, not for actions.",included_number:"{number}/{total} selected"}}}}},Vt={services:qt,ui:Rt},Ht=Object.freeze({__proto__:null,services:qt,ui:Rt,default:Vt}),Ut={generic:{turn_on:"Accendi",turn_off:"Spegni",parameter_to_value:"{parameter} a {value}",action_with_parameter:"{action} con {parameter}"},climate:{set_temperature:"imposta temperatura[ a {temperature}]",set_temperature_hvac_mode_heat:"riscaldamento[ a {temperature}]",set_temperature_hvac_mode_cool:"raffrescamento[ a {temperature}]",set_temperature_hvac_mode_heat_cool:"riscaldamento/raffrescamento[ a {temperature}]",set_temperature_hvac_mode_heat_cool_range:"riscaldamento/raffrescamento[ a {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"auto[ a {temperature}]",set_hvac_mode:"imposta modalità[ a {hvac_mode}]",set_preset_mode:"imposta programmazione[ {preset_mode}]",set_fan_mode:"set fan mode[ to {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"chiuso",open_cover:"aperto",set_cover_position:"imposta posizione[ su {position}]",set_cover_tilt_position:"imposta inclinazione[ su {tilt_position}]"},fan:{set_percentage:"imposta velocità[ a {speed}]",set_direction:"imposta direzione[ a {direction}]",oscillate:"imposta oscillazione[ a {oscillate}]"},humidifier:{set_humidity:"imposta umidità[ a {humidity}]",set_mode:"imposta modalità[ a {mode}]"},input_number:{set_value:"imposta valore[ a {value}]"},input_select:{select_option:"seleziona opzione[ {option}]"},select:{select_option:"seleziona opzione[ {option}]"},light:{turn_on:"accendi[ con il {brightness} di luminosità]"},media_player:{select_source:"seleziona sorgente[ {source}]"},notify:{send_message:"invia notifica"},script:{execute:"esegui"},vacuum:{start_pause:"avvia / pausa"},water_heater:{set_operation_mode:"imposta modalità[ a {operation_mode}]",set_away_mode:"imposta fuori casa"}},Ft={components:{date:{day_types_short:{daily:"giornaliero",workdays:"giorni lavorativi",weekend:"weekend"},day_types_long:{daily:"ogni giorno",workdays:"nei giorni lavorativi",weekend:"nel weekend"},days:"giorni",tomorrow:"domani",repeated_days:"ogni {days}",repeated_days_except:"ogni giorno tranne {excludedDays}",days_range:"{if startDay is domenica} dalla domenica {else} dal {startDay} {if endDay is domenica} alla domenica {else} al {endDay}",next_week_day:"prossimo {weekday}"},time:{absolute:"alle {time}",interval:"dalle {startTime} alle {endTime}",at_midnight:"a mezzanotte",at_noon:"a mezzogiorno",at_sun_event:"al {sunEvent}"}},dialog:{enable_schedule:{title:"Completa le modifiche",description:"La pianificazione modificata è attualmente disabilitata, dovrebbe essere abilitata?"},confirm_delete:{title:"Vuoi rimuovere l'entità?",description:"Sei sicuro di voler rimuovere questa entità?"},confirm_migrate:{title:"Modifica orario",description:"Alcune impostazioni andranno perse con questa azione. Vuoi procedere?"},weekday_picker:{title:"Giorni ripetuti per la pianificazione",choose:"Scegli..."},entity_picker:{title:"Scegli entità",choose:"Scegli...",no_results:"Nessun elemento corrispondente trovato"},action_picker:{title:"Scegli azione",show_all:"Mostra tutte le entità supportate"}},panel:{common:{title:"Schedulatore",new_schedule:"Nuovo orario",default_name:"Orario #{id}"},overview:{no_entries:"Non ci sono oggetti da visualizzare",backend_error:"Impossibile connettersi con il componente scheduler. Deve essere installato come integrazione prima di poter utilizzare questa card.",excluded_items:"{number} {if number is 1} oggetto escluso {else} oggetti esclusi",hide_excluded:"Nascondi oggetti esclusi",additional_tasks:"{number} attività in più"},editor:{repeated_days:"Giorni ripetuti",start_time:"Ora di inizio",stop_time:"Ora di fine",action:"Azione",add_action:"Aggiungi azione",select_timeslot:"Seleziona una fascia oraria",toggle_single_mode:"Alla modo semplice",toggle_scheme_mode:"Alla modo schema",validation_errors:{overlapping_time:"Il programma ha fasce orarie sovrapposte",missing_target_entity:"Una o più azioni mancano di un'entità target",missing_service_parameter:"Una o più azioni mancano di un'impostazione richiesta",missing_action:"Il programma non ha azioni"}},options:{conditions:{header:"Condizioni",add_condition:"Aggiungi condizione",new_condition:"Nuova condizione",types:{equal_to:"{entity} è uguale a {value}",unequal_to:"{entity} è diverso da {value}",above:"{entity} è superiore a {value}",below:"{entity} è inferiore a {value}"},options:{logic_and:"Tutte le condizioni devono essere vere",logic_or:"Qualsiasi condizione deve essere vera",track_changes:"Rivaluta quando cambiano le condizioni"}},period:{header:"Periodo",start_date:"From",end_date:"To"},repeat_type:"comportamento dopo il completamento",tags:"Tags"},card_editor:{tabs:{entities:"Entities",other:"Other"},fields:{title:{heading:"Title of the card",options:{standard:"standard",hidden:"hidden",custom:"custom"},custom_title:"Custom title"},discover_existing:{heading:"Show all schedules",description:"This sets the 'discover existing' parameter. Previously created schedules will be automatically added to the card. "},time_step:{heading:"Time step",description:"Resolution (in minutes) for creating schedules",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Single schedule mode",scheme:"Time scheme mode"}},sort_by:{heading:"Sorting options",description:"Order in which the schedules appear in the card",options:{relative_time:"Time remaining until next action",title:"Displayed title of the schedule",state:"Show active schedules on top"}},display_format_primary:{heading:"Displayed primary info",description:"Configure which label is used for schedules in the overview",options:{default:"Schedule name",entity_action:"Summary of task"}},display_format_secondary:{heading:"Displayed secondary info",description:"Configure what additional properties are visible in the overview",options:{relative_time:"Time remaining until next action",time:"Configured time for next action",days:"Repeated days of the week",additional_tasks:"Number of additional tasks"}},show_header_toggle:{heading:"Show header toggle",description:"Show toggle switch at the top of the card for enabling/disabling all entities"},show_toggle_switches:{heading:"Show toggle switches",description:"Show toggle switch for each individual schedule in the card"},tags:{heading:"Tags",description:"Use tags to divide schedules between multiple cards"},entities:{button_label:"Configure included entities",heading:"Included entities",description:"Select the entities that you want to control using the scheduler. You can click on a group to open it. Note that some entities (such as sensors) can only be used for conditions, not for actions.",included_number:"{number}/{total} selected"}}}}},Bt={services:Ut,ui:Ft},Wt=Object.freeze({__proto__:null,services:Ut,ui:Ft,default:Bt}),Zt={generic:{turn_on:"Įjungti",turn_off:"Išjungti",parameter_to_value:"{parameter} uz {value}",action_with_parameter:"{action} ar {parameter}"},climate:{set_temperature:"uzstādīt temperatūru[ uz {temperature}]",set_temperature_hvac_mode_heat:"sildīt[ līdz {temperature}]",set_temperature_hvac_mode_cool:"atdzesēt[ līdz {temperature}]",set_temperature_hvac_mode_heat_cool:"sildīt/atdzesēt[ līdz {temperature}]",set_temperature_hvac_mode_heat_cool_range:"sildīt/atdzesēt[ uz {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"auto[ uz {temperature}]",set_hvac_mode:"iestatīt[ uz {hvac_mode}]",set_preset_mode:"iestatīt režīmu[ uz {preset_mode}]",set_fan_mode:"iestatīt ventilatora režīmu[ uz {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"aizvērt",open_cover:"atvērt",set_cover_position:"uzstādīt pozīciju[ uz {position}]",set_cover_tilt_position:"uzstādīt slīpuma stāvokli[ uz {tilt_position}]"},fan:{set_percentage:"iestatīt ātrumu[ uz {speed}]",set_direction:"iestatīt virzienu[ uz {direction}]",oscillate:"iestatīt oscilāciju[ uz {oscillate}]"},humidifier:{set_humidity:"iestatīt mitrumu[ uz {humidity}]",set_mode:"iestatīt režīmu[ uz {mode}]"},input_number:{set_value:"iestatīt vērtību[ uz {value}]"},input_select:{select_option:"izvēlēties opciju[ {option}]"},select:{select_option:"izvēlēties opciju[ {option}]"},light:{turn_on:"ieslēgt[ ar {brightness} spilgtumu]"},media_player:{select_source:"izvēlēties avotu[ {source}]"},notify:{send_message:"nosūtīt paziņojumu"},script:{execute:"izpildīt"},vacuum:{start_pause:"sākt / pauze"},water_heater:{set_operation_mode:"iestatīt režīmu[ uz {operation_mode}]",set_away_mode:"iestatīt prombūtnes režīmu"}},Kt={components:{date:{day_types_short:{daily:"ikdienišķs",workdays:"darba dienas",weekend:"nedēļas nogale"},day_types_long:{daily:"katru dienu",workdays:"darba dienās",weekend:"nedēļas nogalē"},days:"dienas",tomorrow:"rītdiena",repeated_days:"katras {days}",repeated_days_except:"katru dienu, izņemot {excludedDays}",days_range:"no {startDay} līdz {endDay}",next_week_day:"nākošo {weekday}"},time:{absolute:"kad {time}",interval:"no {startTime} līdz {endTime}",at_midnight:"kad midnight",at_noon:"kad noon",at_sun_event:"kad {sunEvent}"}},dialog:{enable_schedule:{title:"Pabeigt modificēšanu",description:"Izmainītais grafiks pašlaik ir atspējots, vai vēlaties to iespējot?"},confirm_delete:{title:"Vai dzēst vienību?",description:"Vai tiešām vēlaties dzēst šo vienību?"},confirm_migrate:{title:"Atjaunināt grafiku",description:"Šīs izmaiņas rezultātā daži iestatījumi tiks zaudēti. Vai vēlaties turpināt?"},weekday_picker:{title:"Repeated days for schedule",choose:"Choose..."},entity_picker:{title:"Choose entities",choose:"Choose...",no_results:"No matching items found"},action_picker:{title:"Choose action",show_all:"Show all supported entities"}},panel:{common:{title:"Plānotājs",new_schedule:"Jauns grafiks",default_name:"Grafiks #{id}"},overview:{no_entries:"Nav parādāmu vienību",backend_error:"Nevar izveidot savienojumu ar plānotāja komponenti. Pirms šīs kartes izmantošanas tā ir jāinstalē kā integrācija.",excluded_items:"{number} izslēgtas {if number is 1} vienība {else} vienības",hide_excluded:"paslēpt izslēgtās vienības",additional_tasks:"{number} papildu {if number is 1} uzdevums {else} uzdevumi"},editor:{repeated_days:"Repeated days",start_time:"Start time",stop_time:"End time",action:"Action",add_action:"Add action",select_timeslot:"Vispirms izvēlieties laika slotu",toggle_single_mode:"Uz vienkāršo režīmu",toggle_scheme_mode:"Uz shēmas režīmu",validation_errors:{overlapping_time:"Schedule has overlapping timeslots",missing_target_entity:"One or more actions are missing a target entity",missing_service_parameter:"One or more actions are missing a required setting",missing_action:"Schedule has no actions"}},options:{conditions:{header:"Conditions",add_condition:"Add condition",new_condition:"New condition",types:{equal_to:"{entity} is equal to {value}",unequal_to:"{entity} is unequal to {value}",above:"{entity} is above {value}",below:"{entity} is below {value}"},options:{logic_and:"All conditions must be true",logic_or:"Any condition must be true",track_changes:"Pārvērtēt, kad mainās nosacījumi"}},period:{header:"Periods",start_date:"From",end_date:"To"},repeat_type:"uzvedība pēc beigām",tags:"Tags"},card_editor:{tabs:{entities:"Vienības",other:"Cits"},fields:{title:{heading:"Kartes nosaukums",options:{standard:"standarta",hidden:"paslēpts",custom:"pielāgots"},custom_title:"Pielāgots nosaukums"},discover_existing:{heading:"Rādīt visus grafikus",description:"Šis iestata 'atklāt esošo' parametru. Iepriekš izveidotie grafiki automātiski tiks pievienoti kartei."},time_step:{heading:"Laika solis",description:"Izšķirtspēja (minūtēs) grafiku izveidei",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Single schedule mode",scheme:"Time scheme mode"}},sort_by:{heading:"Kārtošanas opcijas",description:"Kārtība, kādā grafiki parādās kartē",options:{relative_time:"Atlikušais laiks līdz nākamajai darbībai",title:"Grafika nosaukums",state:"Rādīt aktīvos grafikus augšā"}},display_format_primary:{heading:"Rādītā primārā informācija",description:"Konfigurējiet, kura informācija tiek izmantota grafiku pārskatā",options:{default:"Grafika nosaukums",entity_action:"Uzdevuma kopsavilkums"}},display_format_secondary:{heading:"Rādītā sekundārā informācija",description:"Konfigurējiet, kuras papildu īpašības ir redzamas pārskatā",options:{relative_time:"Atlikušais laiks līdz nākamajai darbībai",time:"Konfigurētais laiks nākamajai darbībai",days:"Atkārtotas nedēļas dienas",additional_tasks:"Papildu uzdevumu skaits"}},show_header_toggle:{heading:"Rādīt galvenes pārslēgšanu",description:"Rādīt pārslēgšanas slēdzi kartes augšdaļā, lai iespējotu/atspējotu visas vienības"},show_toggle_switches:{heading:"Rādīt pārslēgšanas slēdžus",description:"Rādīt pārslēgšanas slēdzi katram atsevišķam grafikam kartē"},tags:{heading:"Tagi",description:"Izmantojiet tagus, lai sadalītu grafikus starp vairākām kartēm"},entities:{button_label:"Iekļauto elementu konfigurēšana",heading:"Iekļautās vienības",description:"Izvēlieties vienības, kuras vēlaties kontrolēt, izmantojot plānotāju. Jūs varat noklikšķināt uz grupas, lai to atvērtu. Ņemiet vērā, ka dažas vienības (piemēram, sensori) var tikt izmantotas tikai nosacījumiem, nevis darbībām.",included_number:"{number}/{total} izvēlēts"}}}}},Gt={services:Zt,ui:Kt},Jt=Object.freeze({__proto__:null,services:Zt,ui:Kt,default:Gt}),Yt={generic:{turn_on:"Aanzetten",turn_off:"Uitzetten",parameter_to_value:"{parameter} naar {value}",action_with_parameter:"{action} met {parameter}"},climate:{set_temperature:"temperatuur instellen[ naar {temperature}]",set_temperature_hvac_mode_heat:"verwarmen[ naar {temperature}]",set_temperature_hvac_mode_cool:"koelen[ naar {temperature}]",set_temperature_hvac_mode_heat_cool:"verwarmen/koelen[ naar {temperature}]",set_temperature_hvac_mode_heat_cool_range:"verwarmen/koelen[ naar {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"automatisch[ naar {temperature}]",set_hvac_mode:"modus aanpassen[ naar {hvac_mode}]",set_preset_mode:"programma[ {preset_mode}] instellen",set_fan_mode:"ventilatiemodus aanpassen[ naar {fan_mode}]",set_swing_mode:"oscillatiemodus aanpassen[ naar {swing_mode}]"},cover:{close_cover:"sluiten",open_cover:"openen",set_cover_position:"positie instellen[ naar {position}]",set_cover_tilt_position:"hellingshoek instellen[ naar {tilt_position}]"},fan:{set_percentage:"snelheid instellen[ op {percentage}]",set_direction:"richting instellen[ naar {direction}]",oscillate:"zet oscillatie[ naar {oscillate}]"},humidifier:{set_humidity:"luchtvochtigheid instellen [ op {humidity}]",set_mode:"modus aanpassen[ naar {mode}]"},input_number:{set_value:"waarde aanpassen[ naar {value}]"},input_select:{select_option:"selecteer optie[ {option}]"},select:{select_option:"selecteer optie[ {option}]"},light:{turn_on:"inschakelen[ met {brightness} helderheid]"},media_player:{select_source:"kies ingang[ {source}]"},notify:{send_message:"notificatie sturen"},script:{execute:"uitvoeren"},vacuum:{start_pause:"start / pauzeer"},water_heater:{set_operation_mode:"modus aanpassen[ naar {operation_mode}]",set_away_mode:"stel afwezigheidsmode in"}},Qt={components:{date:{day_types_short:{daily:"dagelijks",workdays:"werkdagen",weekend:"weekend"},day_types_long:{daily:"iedere dag",workdays:"doordeweeks",weekend:"in het weekend"},days:"dagen",tomorrow:"morgen",repeated_days:"elke {days}",repeated_days_except:"elke dag behalve {excludedDays}",days_range:"van {startDay} tot {endDay}",next_week_day:"volgende week {weekday}"},time:{absolute:"om {time}",interval:"van {startTime} tot {endTime}",at_midnight:"om middernacht",at_noon:"om 12:00",at_sun_event:"bij {sunEvent}"}},dialog:{enable_schedule:{title:"Wijzigingen voltooid",description:"Deze planning is momenteel gedeactiveerd. Dient deze te worden ingeschakeld?"},confirm_delete:{title:"Entiteit verwijderen?",description:"Weet je zeker dat je dit item wilt verwijderen?"},confirm_migrate:{title:"Schema bijwerken",description:"Door deze actie gaan vorige instellingen verloren. Wil je doorgaan?"},weekday_picker:{title:"Herhaalde dagen voor tijdschema",choose:"Kies..."},entity_picker:{title:"Kies entiteiten",choose:"Kies...",no_results:"Geen overeenkomstige items gevonden"},action_picker:{title:"Kies actie",show_all:"Toon alle ondersteunde entiteiten"}},panel:{common:{title:"Tijdplanner",new_schedule:"Nieuw schema",default_name:"Schema #{id}"},overview:{no_entries:"Er zijn geen items aangemaakt",backend_error:"Er kon geen verbinding worden gemaakt met het Scheduler component. Deze moet als integratie zijn geinstalleerd voordat deze kaart gebruikt kan worden.",excluded_items:"{number} uitgesloten {if number is 1} item {else} items",hide_excluded:"verberg uitgesloten items",additional_tasks:"{number} overige {if number is 1} taak {else} taken"},editor:{repeated_days:"Herhaling",start_time:"Starttijd",stop_time:"Eindtijd",action:"Actie",add_action:"Actie toevoegen",select_timeslot:"Selecteer een tijdslot...",toggle_single_mode:"Naar enkele modus",toggle_scheme_mode:"Naar schema modus",validation_errors:{overlapping_time:"Schedule has overlapping timeslots",missing_target_entity:"One or more actions are missing a target entity",missing_service_parameter:"One or more actions are missing a required setting",missing_action:"Schedule has no actions"}},options:{conditions:{header:"Condities",add_condition:"Conditie toevoegen",new_condition:"Nieuwe conditie",types:{equal_to:"{entity} is gelijk aan {value}",unequal_to:"{entity} is ongelijk aan {value}",above:"{entity} is hoger dan {value}",below:"{entity} is lager dan {value}"},options:{logic_and:"Alle condities moeten zijn voldaan",logic_or:"Een van de condities moet zijn voldaan",track_changes:"Herevalueer als de condities veranderen"}},period:{header:"Periode",start_date:"Vanaf",end_date:"Tot"},repeat_type:"Gedrag na voltooiing",tags:"Tags"},card_editor:{tabs:{entities:"Entiteiten",other:"Overig"},fields:{title:{heading:"Titel van de kaart",options:{standard:"standaard",hidden:"verborgen",custom:"anders"},custom_title:"Eigen titel"},discover_existing:{heading:"Alle schema's tonen",description:"Hiermee wordt de 'discover existing' instelling geactiveerd. Eerder aangemaakte schema's zullen automatisch worden weergegeven."},time_step:{heading:"Stapgrootte voor tijd",description:"Resolutie (in minuten)",unit_minutes:"min"},default_editor:{heading:"Standaardmodus voor tijdsinvoer",options:{single:"Enkele modus",scheme:"Tijdschema-modus"}},sort_by:{heading:"Sorteer-opties",description:"Volgorde waarin de schema's worden weergegeven in de kaart",options:{relative_time:"Resterende tijd tot volgende actie",title:"Weergegeven titel van de schema's",state:"Actieve schema's eerst"}},display_format_primary:{heading:"Weergegeven primaire info",description:"Kies welk label wordt gebruikt in de weergave",options:{default:"Schema naam",entity_action:"Samenvatting van de actie"}},display_format_secondary:{heading:"Weergegeven secondaire info",description:"Kies welke aanvullende informatie zichtbaar is in de weergave",options:{relative_time:"Resterende tijd tot volgende actie",time:"Ingestelde tijd voor de volgende actie",days:"Herhaalde dagen van de week",additional_tasks:"Aantal resterende acties"}},show_header_toggle:{heading:"Hoofdschakelaar weergeven",description:"Schakelaar weergeven bovenin de kaart om alle schema's te (de)activeren"},show_toggle_switches:{heading:"Schakelknoppen weergeven",description:"Schakelknop weergeven voor elk individueel schema in de kaart"},tags:{heading:"Tags",description:"Tags kunnen gebruikt worden om schema's te verdelen over meerdere kaarten"},entities:{button_label:"Configureer zichtbare entiteiten",heading:"Zichtbare entiteiten",description:"Kies de entiteiten die je wilt bedienen vanuit schema's. Klik op een categorie om deze te openen. Merk op dat sommige entiteiten gebruikt worden om condities toe te wijzen.",included_number:"{number}/{total} geselecteerd"}}}}},Xt={services:Yt,ui:Qt},ei=Object.freeze({__proto__:null,services:Yt,ui:Qt,default:Xt}),ti={generic:{turn_on:"Skru på",turn_off:"Slå av",parameter_to_value:"{parameter} til {value}",action_with_parameter:"{action} med {parameter}"},climate:{set_temperature:"sett temperatur[ til {temperature}]",set_temperature_hvac_mode_heat:"oppvarming[ til {temperature}]",set_temperature_hvac_mode_cool:"kjøling[ til {temperature}]",set_temperature_hvac_mode_heat_cool:"oppvarming/kjøling[ til {temperature}]",set_temperature_hvac_mode_heat_cool_range:"oppvarming/kjøling[ til {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"auto[ til {temperature}]",set_hvac_mode:"sett modus[ til {hvac_mode}]",set_preset_mode:"sett forhåndsvalg[ {preset_mode}]",set_fan_mode:"set fan mode[ to {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"lukk",open_cover:"åpne",set_cover_position:"sett posisjon[ til {position}]",set_cover_tilt_position:"sett vippestilling[ til {tilt_position}]"},fan:{set_percentage:"sett hastighet[ til {speed}]",set_direction:"sett retning[ til {direction}]",oscillate:"sett svingning[ til {oscillate}]"},humidifier:{set_humidity:"sett luftfuktighet[ til {humidity}]",set_mode:"sett modus[ til {mode}]"},input_number:{set_value:"sett verdi[ til {value}]"},input_select:{select_option:"velg[ {option}]"},select:{select_option:"velg[ {option}]"},light:{turn_on:"slå på[ med {brightness} lysstyrke]"},media_player:{select_source:"velg kilde[ {source}]"},notify:{send_message:"send notifikasjon"},script:{execute:"utfør"},vacuum:{start_pause:"start / pause"},water_heater:{set_operation_mode:"sett modus[ til {operation_mode}]",set_away_mode:"sett bortemodus"}},ii={components:{date:{day_types_short:{daily:"hver dag",workdays:"ukedager",weekend:"helg"},day_types_long:{daily:"hver dag",workdays:"ukedager",weekend:"helg"},days:"Dager",tomorrow:"imorgen",repeated_days:"hver {days}",repeated_days_except:"hver dag unntatt {excludedDays}",days_range:"fra {startDay} til {endDay}",next_week_day:"neste {weekday}"},time:{absolute:"kl. {time}",interval:"fra {startTime} til {endTime}",at_midnight:"ved midnatt",at_noon:"kl. 12.00",at_sun_event:"ved {sunEvent}"}},dialog:{enable_schedule:{title:"Fullfør endringene",description:"Tidsplanen som er endret er for øyeblikket deaktivert, bør den være aktivert?"},confirm_delete:{title:"Vil du fjerne entiteten?",description:"Er du sikker på at du vil fjerne denne entiteten?"},confirm_migrate:{title:"Endre tidsplanen",description:"Noen innstillinger vil gå tapt ved denne handlingen. Vil du fortsette?"},weekday_picker:{title:"Repeated days for schedule",choose:"Choose..."},entity_picker:{title:"Choose entities",choose:"Choose...",no_results:"No matching items found"},action_picker:{title:"Choose action",show_all:"Show all supported entities"}},panel:{common:{title:"Tidsplan",new_schedule:"Ny tidsplan",default_name:"Tidsplan #{id}"},overview:{no_entries:"Det er ingen definerte tidsplaner å vise",backend_error:"Kunne ikke koble til tidsplankomponenten. Den må installeres som en integrasjon før dette kortet kan benyttes.",excluded_items:"{number} ekskludert {if number is 1} element {else} elementer",hide_excluded:"skjul ekskluderte elementer",additional_tasks:"{number} flere {if number is 1} oppgaver {else} oppgaver"},editor:{repeated_days:"Repeated days",start_time:"Start time",stop_time:"End time",action:"Action",add_action:"Add action",select_timeslot:"Velg tidsluke",toggle_single_mode:"Til enkel modus",toggle_scheme_mode:"Til skjemamodus",validation_errors:{overlapping_time:"Schedule has overlapping timeslots",missing_target_entity:"One or more actions are missing a target entity",missing_service_parameter:"One or more actions are missing a required setting",missing_action:"Schedule has no actions"}},options:{conditions:{header:"Conditions",add_condition:"Add condition",new_condition:"New condition",types:{equal_to:"{entity} is equal to {value}",unequal_to:"{entity} is unequal to {value}",above:"{entity} is above {value}",below:"{entity} is below {value}"},options:{logic_and:"All conditions must be true",logic_or:"Any condition must be true",track_changes:"Re-evaluate when conditions change"}},period:{header:"Periode",start_date:"From",end_date:"To"},repeat_type:"oppførsel etter fullføring",tags:"Tags"},card_editor:{tabs:{entities:"Entities",other:"Other"},fields:{title:{heading:"Title of the card",options:{standard:"standard",hidden:"hidden",custom:"custom"},custom_title:"Custom title"},discover_existing:{heading:"Show all schedules",description:"This sets the 'discover existing' parameter. Previously created schedules will be automatically added to the card. "},time_step:{heading:"Time step",description:"Resolution (in minutes) for creating schedules",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Single schedule mode",scheme:"Time scheme mode"}},sort_by:{heading:"Sorting options",description:"Order in which the schedules appear in the card",options:{relative_time:"Time remaining until next action",title:"Displayed title of the schedule",state:"Show active schedules on top"}},display_format_primary:{heading:"Displayed primary info",description:"Configure which label is used for schedules in the overview",options:{default:"Schedule name",entity_action:"Summary of task"}},display_format_secondary:{heading:"Displayed secondary info",description:"Configure what additional properties are visible in the overview",options:{relative_time:"Time remaining until next action",time:"Configured time for next action",days:"Repeated days of the week",additional_tasks:"Number of additional tasks"}},show_header_toggle:{heading:"Show header toggle",description:"Show toggle switch at the top of the card for enabling/disabling all entities"},show_toggle_switches:{heading:"Show toggle switches",description:"Show toggle switch for each individual schedule in the card"},tags:{heading:"Tags",description:"Use tags to divide schedules between multiple cards"},entities:{button_label:"Configure included entities",heading:"Included entities",description:"Select the entities that you want to control using the scheduler. You can click on a group to open it. Note that some entities (such as sensors) can only be used for conditions, not for actions.",included_number:"{number}/{total} selected"}}}}},si={services:ti,ui:ii},ai=Object.freeze({__proto__:null,services:ti,ui:ii,default:si}),oi={generic:{turn_on:"Włącz",turn_off:"Wyłącz",parameter_to_value:"{parameter} na {value}",action_with_parameter:"{action} z parametrem {parameter}"},climate:{set_temperature:"ustaw temperaturę[ na {temperature}]",set_temperature_hvac_mode_heat:"ogrzewanie[ na {temperature}]",set_temperature_hvac_mode_cool:"chłodzenie[ na {temperature}]",set_temperature_hvac_mode_heat_cool:"ogrzewanie/chłodzenie[ na {temperature}]",set_temperature_hvac_mode_heat_cool_range:"ogrzewanie/chłodzenie[ {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"auto[ na {temperature}]",set_hvac_mode:"ustaw tryb[ na {hvac_mode}]",set_preset_mode:"ustaw tryb[ na {preset_mode}]",set_fan_mode:"ustaw tryb wentylatora[ na {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"zamknij",open_cover:"otwórz",set_cover_position:"ustaw pozycję[ na {position}]",set_cover_tilt_position:"ustaw kąt nachylenia[ na {tilt_position}]"},fan:{set_percentage:"ustaw prędkość[ na {percentage}]",set_direction:"ustaw kierunek[ na {direction}]",oscillate:"ustaw oscylację[ na {oscillate}]"},humidifier:{set_humidity:"ustaw wilgotność[ na {humidity}]",set_mode:"ustaw tryb[ na {mode}]"},input_number:{set_value:"ustaw wartość[ na {value}]"},input_select:{select_option:"wybierz opcję[ {option}]"},select:{select_option:"wybierz opcję[ {option}]"},light:{turn_on:"włącz[ z jasnością {brightness}]"},media_player:{select_source:"wybierz źródło[ {source}]"},notify:{send_message:"wyślij powiadomienie"},script:{execute:"wykonaj"},vacuum:{start_pause:"start / pauza"},water_heater:{set_operation_mode:"ustaw tryb[ na {operation_mode}]",set_away_mode:"ustaw tryb poza domem"}},ni={components:{date:{day_types_short:{daily:"codziennie",workdays:"dni robocze",weekend:"weekend"},day_types_long:{daily:"każdego dnia",workdays:"w dni robocze",weekend:"w weekend"},days:"dni",tomorrow:"jutro",repeated_days:"każde {days}",repeated_days_except:"każdego dnia oprócz {excludedDays}",days_range:"od {startDay} do {endDay}",next_week_day:"w {weekday}"},time:{absolute:"o {time}",interval:"od {startTime} do {endTime}",at_midnight:"o północy",at_noon:"w południe",at_sun_event:"o {sunEvent}"}},dialog:{enable_schedule:{title:"Zakończ modyfikacje",description:"Harmonogram, który zmieniłeś, jest obecnie wyłączony. Czy chcesz go włączyć?"},confirm_delete:{title:"Usunąć encję?",description:"Czy na pewno chcesz usunąć tę encję?"},confirm_migrate:{title:"Aktualizuj harmonogram",description:"Niektóre ustawienia zostaną utracone przy tej zmianie. Czy chcesz kontynuować?"},weekday_picker:{title:"Dni powtórzeń dla harmonogramu",choose:"Wybierz..."},entity_picker:{title:"Wybierz encje",choose:"Wybierz...",no_results:"Nie znaleziono pasujących elementów"},action_picker:{title:"Wybierz akcję",show_all:"Pokaż wszystkie obsługiwane jednostki"}},panel:{common:{title:"Harmonogram",new_schedule:"Nowy harmonogram",default_name:"Harmonogram #{id}"},overview:{no_entries:"Brak elementów do wyświetlenia",backend_error:"Nie można połączyć się z komponentem harmonogramu. Musi być zainstalowany jako integracja, zanim ta karta będzie mogła być używana.",excluded_items:"{number} wykluczonych {if number is 1} element {else} elementów",hide_excluded:"ukryj wykluczone elementy",additional_tasks:"{number} dodatkowych {if number is 1} zadanie {else} zadań"},editor:{repeated_days:"Powtarzane dni",start_time:"Czas rozpoczęcia",stop_time:"Czas zakończenia",action:"Akcja",add_action:"Dodaj akcję",select_timeslot:"Wybierz przedział czasowy",toggle_single_mode:"Do trybu prostego",toggle_scheme_mode:"Do trybu schematu",validation_errors:{overlapping_time:"Harmonogram ma nachodzące na siebie przedziały czasowe",missing_target_entity:"Jednej lub więcej akcji brakuje docelowej encji",missing_service_parameter:"Jednej lub więcej akcji brakuje wymaganego parametru",missing_action:"Harmonogram nie ma żadnych akcji"}},options:{conditions:{header:"Warunki",add_condition:"Dodaj warunek",new_condition:"Nowy warunek",types:{equal_to:"{entity} równa się {value}",unequal_to:"{entity} różni się od {value}",above:"{entity} jest powyżej {value}",below:"{entity} jest poniżej {value}"},options:{logic_and:"Wszystkie warunki muszą być spełnione",logic_or:"Dowolny warunek musi być spełniony",track_changes:"Ponownie sprawdzaj przy zmianie warunków"}},period:{header:"Okres",start_date:"Od",end_date:"Do"},repeat_type:"zachowanie po zakończeniu",tags:"Tagi"},card_editor:{tabs:{entities:"Encje",other:"Inne"},fields:{title:{heading:"Tytuł karty",options:{standard:"standardowy",hidden:"ukryty",custom:"niestandardowy"},custom_title:"Niestandardowy tytuł"},discover_existing:{heading:"Pokaż wszystkie harmonogramy",description:"Ustawia parametr 'discover existing'. Wcześniej utworzone harmonogramy będą automatycznie dodane do karty."},time_step:{heading:"Krok czasu",description:"Rozdzielczość (w minutach) przy tworzeniu harmonogramów",unit_minutes:"min"},default_editor:{heading:"Domyślny edytor czasu",options:{single:"Tryb prostego harmonogramu",scheme:"Tryb schematu czasu"}},sort_by:{heading:"Opcje sortowania",description:"Kolejność, w jakiej harmonogramy pojawiają się na karcie",options:{relative_time:"Czas do następnej akcji",title:"Wyświetlana nazwa harmonogramu",state:"Pokaż aktywne harmonogramy na górze"}},display_format_primary:{heading:"Wyświetlana główna informacja",description:"Określ, która etykieta jest używana dla harmonogramów w podglądzie",options:{default:"Nazwa harmonogramu",entity_action:"Podsumowanie zadania"}},display_format_secondary:{heading:"Wyświetlana dodatkowa informacja",description:"Skonfiguruj, które dodatkowe właściwości są widoczne w podglądzie",options:{relative_time:"Czas do następnej akcji",time:"Skonfigurowany czas następnej akcji",days:"Powtarzane dni tygodnia",additional_tasks:"Liczba dodatkowych zadań"}},show_header_toggle:{heading:"Pokaż przełącznik w nagłówku",description:"Pokaż przełącznik w nagłówku karty do włączania/wyłączania wszystkich encji"},show_toggle_switches:{heading:"Pokaż przełączniki",description:"Pokaż przełącznik dla każdego harmonogramu w karcie"},tags:{heading:"Tagi",description:"Używaj tagów do podziału harmonogramów pomiędzy wieloma kartami"},entities:{button_label:"Konfiguruj zawarte encje",heading:"Zawarte encje",description:"Wybierz encje, którymi chcesz sterować za pomocą harmonogramu. Możesz kliknąć na grupę, aby ją otworzyć. Zauważ, że niektóre encje (np. sensory) mogą być używane tylko jako warunki, a nie akcje.",included_number:"{number}/{total} wybrano"}}}}},ri={services:oi,ui:ni},di=Object.freeze({__proto__:null,services:oi,ui:ni,default:ri}),li={generic:{turn_on:"Ligar",turn_off:"Desligar",parameter_to_value:"{parameter} para {value}",action_with_parameter:"{action} com {parameter}"},climate:{set_temperature:"definir temperatura[ para {temperature}]",set_temperature_hvac_mode_heat:"aquecimento[ para {temperature}]",set_temperature_hvac_mode_cool:"arrefecimento[ para {temperature}]",set_temperature_hvac_mode_heat_cool:"aquecimento/arrefecimento[ para {temperature}]",set_temperature_hvac_mode_heat_cool_range:"aquecimento/arrefecimento[ para {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"auto[ para {temperature}]",set_hvac_mode:"definir modo[ para {hvac_mode}]",set_preset_mode:"definir predefinição[ {preset_mode}]",set_fan_mode:"definir modo ventoinha[ para {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"fechar",open_cover:"abrir",set_cover_position:"definir posição[ para {position}]",set_cover_tilt_position:"definir inclinação[ como {tilt_position}]"},fan:{set_percentage:"definir velocidade[ para {speed}]",set_direction:"definir direção[ para {direction}]",oscillate:"definir oscilação[ para {oscillate}]"},humidifier:{set_humidity:"definir humidade[ para {humidity}]",set_mode:"definir modo[ para {mode}]"},input_number:{set_value:"definir valor[ para {value}]"},input_select:{select_option:"selecionar opção[ {option}]"},select:{select_option:"selecionar opção[ {option}]"},light:{turn_on:"ligar[ com {brightness} brightness]"},media_player:{select_source:"selecionar origem[ {source}]"},notify:{send_message:"enviar notificação"},script:{execute:"executar"},vacuum:{start_pause:"iniciar / pausar"},water_heater:{set_operation_mode:"definir modo[ para {operation_mode}]",set_away_mode:"definir modo ausente"}},ci={components:{date:{day_types_short:{daily:"todos",workdays:"semana de trabalho",weekend:"fim-de-semana"},day_types_long:{daily:"todos os dias",workdays:"em dias de semana",weekend:"no fim-de-semana"},days:"dias",tomorrow:"amanhã",repeated_days:"a cada {days}",repeated_days_except:"a cada dia exceto {excludedDays}",days_range:"até {startDay} até {endDay}",next_week_day:"próximo {weekday}"},time:{absolute:"Às {time}",interval:"das {startTime} às {endTime}",at_midnight:"ao meia-noite",at_noon:"ao meio-dia",at_sun_event:"ao {sunEvent}"}},dialog:{enable_schedule:{title:"Conclua as modificações",description:"A programação que foi alterada está atualmente desabilitada, deveria ser habilitada?"},confirm_delete:{title:"Remover a entidade?",description:"Tem a certeza que deseja remover esta entidade?"},confirm_migrate:{title:"Modificar horário",description:"Algumas configurações serão perdidas por esta ação. Você quer prosseguir?"},weekday_picker:{title:"Repetições semanais",choose:"Escolha..."},entity_picker:{title:"Escolha entidades",choose:"Escolha...",no_results:"Sem resultados"},action_picker:{title:"Escolha a acção",show_all:"Mostrar todas as entidades suportadas"}},panel:{common:{title:"Agenda",new_schedule:"Novo horário",default_name:"Horário #{id}"},overview:{no_entries:"Não existem itens a mostrar",backend_error:"Não consegui ligar ao componente de agendamento. Essa integração tem que ser instalada antes da utilização deste cartão.",excluded_items:"{number}{if number is 1} item excluído {else} itens excluídos",hide_excluded:"ocultar itens excluídos",additional_tasks:"Mais {number} {if number is 1} tarefa {else} tarefas"},editor:{repeated_days:"Repetições semanais",start_time:"Hora início",stop_time:"Hora fim",action:"Acção",add_action:"Acrescentar acção",select_timeslot:"Selecionar um período horário",toggle_single_mode:"Para modo simples",toggle_scheme_mode:"Para modo esquema",validation_errors:{overlapping_time:"O Horário tem sobreposições",missing_target_entity:"Uma ou mais acções sem entidade definida",missing_service_parameter:"Unma ou mais acções sem uma definição obrigatória",missing_action:"O Horário não tem acções"}},options:{conditions:{header:"Condições",add_condition:"Acrescentar condição",new_condition:"Nova condição",types:{equal_to:"{entity} igual a {value}",unequal_to:"{entity} diferente de {value}",above:"{entity} acima de {value}",below:"{entity} abaixo de {value}"},options:{logic_and:"Todas as condições têm de ser verdadeiras",logic_or:"Uma das condições tem de ser verdadeira",track_changes:"Reavaliar em caso de alterações"}},period:{header:"Período",start_date:"Desde",end_date:"Até"},repeat_type:"Comportamento após a conclusão",tags:"Etiquetas"},card_editor:{tabs:{entities:"Entities",other:"Other"},fields:{title:{heading:"Title of the card",options:{standard:"standard",hidden:"hidden",custom:"custom"},custom_title:"Custom title"},discover_existing:{heading:"Show all schedules",description:"This sets the 'discover existing' parameter. Previously created schedules will be automatically added to the card. "},time_step:{heading:"Time step",description:"Resolution (in minutes) for creating schedules",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Single schedule mode",scheme:"Time scheme mode"}},sort_by:{heading:"Sorting options",description:"Order in which the schedules appear in the card",options:{relative_time:"Time remaining until next action",title:"Displayed title of the schedule",state:"Show active schedules on top"}},display_format_primary:{heading:"Displayed primary info",description:"Configure which label is used for schedules in the overview",options:{default:"Schedule name",entity_action:"Summary of task"}},display_format_secondary:{heading:"Displayed secondary info",description:"Configure what additional properties are visible in the overview",options:{relative_time:"Time remaining until next action",time:"Configured time for next action",days:"Repeated days of the week",additional_tasks:"Number of additional tasks"}},show_header_toggle:{heading:"Show header toggle",description:"Show toggle switch at the top of the card for enabling/disabling all entities"},show_toggle_switches:{heading:"Show toggle switches",description:"Show toggle switch for each individual schedule in the card"},tags:{heading:"Tags",description:"Use tags to divide schedules between multiple cards"},entities:{button_label:"Configure included entities",heading:"Included entities",description:"Select the entities that you want to control using the scheduler. You can click on a group to open it. Note that some entities (such as sensors) can only be used for conditions, not for actions.",included_number:"{number}/{total} selected"}}}}},ui={services:li,ui:ci},hi=Object.freeze({__proto__:null,services:li,ui:ci,default:ui}),mi={generic:{turn_on:"Ligar",turn_off:"Desligar",parameter_to_value:"{parameter} para {value}",action_with_parameter:"{action} com {parameter}"},climate:{set_temperature:"definir temperatura[ para {temperature}]",set_temperature_hvac_mode_heat:"aquecimento[ para {temperature}]",set_temperature_hvac_mode_cool:"arrefecimento[ para {temperature}]",set_temperature_hvac_mode_heat_cool:"aquecimento/arrefecimento[ para {temperature}]",set_temperature_hvac_mode_heat_cool_range:"aquecimento/arrefecimento[ para {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"auto[ para {temperature}]",set_hvac_mode:"definir modo[ para {hvac_mode}]",set_preset_mode:"definir predefinição[ {preset_mode}]",set_fan_mode:"definir modo do ventilador[ para {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"fechar",open_cover:"abrir",set_cover_position:"definir posição[ para {position}]",set_cover_tilt_position:"definir a posição de inclinação[ para {tilt_position}]"},fan:{set_percentage:"definir velocidade[ para {speed}]",set_direction:"definir direção[ para {direction}]",oscillate:"definir oscilação[ para {oscillate}]"},humidifier:{set_humidity:"definir humidade[ para {humidity}]",set_mode:"definir modo[ para {mode}]"},input_number:{set_value:"definir valor[ para {value}]"},input_select:{select_option:"selecionar opção[ {option}]"},select:{select_option:"selecionar opção[ {option}]"},light:{turn_on:"ligar[ com {brightness} brightness]"},media_player:{select_source:"selecionar origem[ {source}]"},notify:{send_message:"enviar notificação"},script:{execute:"executar"},vacuum:{start_pause:"iniciar / pausar"},water_heater:{set_operation_mode:"definir modo[ para {operation_mode}]",set_away_mode:"definir modo ausente"}},pi={components:{date:{day_types_short:{daily:"diário",workdays:"semana de trabalho",weekend:"fim-de-semana"},day_types_long:{daily:"todos os dias",workdays:"em dias de semana",weekend:"no fim-de-semana"},days:"dias",tomorrow:"amanhã",repeated_days:"a cada {days}",repeated_days_except:"a cada dia exceto {excludedDays}",days_range:"de {startDay} até {endDay}",next_week_day:"próximo {weekday}"},time:{absolute:"à {time}",interval:"das {startTime} às {endTime}",at_midnight:"ao meia-noite",at_noon:"ao meio-dia",at_sun_event:"ao {sunEvent}"}},dialog:{enable_schedule:{title:"Conclua as modificações",description:"A programação que foi alterada está atualmente desabilitada, deveria ser habilitada?"},confirm_delete:{title:"Remover entidade?",description:"Tem certeza de que deseja remover esta entidade?"},confirm_migrate:{title:"Modificar horário",description:"Algumas configurações serão perdidas por esta ação. Você quer prosseguir?"},weekday_picker:{title:"Repeated days for schedule",choose:"Choose..."},entity_picker:{title:"Choose entities",choose:"Choose...",no_results:"No matching items found"},action_picker:{title:"Choose action",show_all:"Show all supported entities"}},panel:{common:{title:"Agenda",new_schedule:"Novo horário",default_name:"Horário #{id}"},overview:{no_entries:"Não existem itens para mostrar",backend_error:"Não foi possível conectar com o componente agendador. Ele precisa ser instalado como integração antes que este cartão possa ser usado.",excluded_items:"{number}{if number is 1} item excluído {else} itens excluídos",hide_excluded:"ocultar itens excluídos",additional_tasks:"Mais {number} {if number is 1} tarefa {else} tarefas"},editor:{repeated_days:"Repeated days",start_time:"Start time",stop_time:"End time",action:"Action",add_action:"Add action",select_timeslot:"Selecionar um período horário",toggle_single_mode:"Para modo simples",toggle_scheme_mode:"Para modo esquema",validation_errors:{overlapping_time:"Schedule has overlapping timeslots",missing_target_entity:"One or more actions are missing a target entity",missing_service_parameter:"One or more actions are missing a required setting",missing_action:"Schedule has no actions"}},options:{conditions:{header:"Conditions",add_condition:"Add condition",new_condition:"New condition",types:{equal_to:"{entity} is equal to {value}",unequal_to:"{entity} is unequal to {value}",above:"{entity} is above {value}",below:"{entity} is below {value}"},options:{logic_and:"All conditions must be true",logic_or:"Any condition must be true",track_changes:"Reavaliar quando as condições mudarem"}},period:{header:"Período",start_date:"From",end_date:"To"},repeat_type:"comportamento após a conclusão",tags:"Tags"},card_editor:{tabs:{entities:"Entities",other:"Other"},fields:{title:{heading:"Title of the card",options:{standard:"standard",hidden:"hidden",custom:"custom"},custom_title:"Custom title"},discover_existing:{heading:"Show all schedules",description:"This sets the 'discover existing' parameter. Previously created schedules will be automatically added to the card. "},time_step:{heading:"Time step",description:"Resolution (in minutes) for creating schedules",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Single schedule mode",scheme:"Time scheme mode"}},sort_by:{heading:"Sorting options",description:"Order in which the schedules appear in the card",options:{relative_time:"Time remaining until next action",title:"Displayed title of the schedule",state:"Show active schedules on top"}},display_format_primary:{heading:"Displayed primary info",description:"Configure which label is used for schedules in the overview",options:{default:"Schedule name",entity_action:"Summary of task"}},display_format_secondary:{heading:"Displayed secondary info",description:"Configure what additional properties are visible in the overview",options:{relative_time:"Time remaining until next action",time:"Configured time for next action",days:"Repeated days of the week",additional_tasks:"Number of additional tasks"}},show_header_toggle:{heading:"Show header toggle",description:"Show toggle switch at the top of the card for enabling/disabling all entities"},show_toggle_switches:{heading:"Show toggle switches",description:"Show toggle switch for each individual schedule in the card"},tags:{heading:"Tags",description:"Use tags to divide schedules between multiple cards"},entities:{button_label:"Configure included entities",heading:"Included entities",description:"Select the entities that you want to control using the scheduler. You can click on a group to open it. Note that some entities (such as sensors) can only be used for conditions, not for actions.",included_number:"{number}/{total} selected"}}}}},_i={services:mi,ui:pi},gi=Object.freeze({__proto__:null,services:mi,ui:pi,default:_i}),vi={generic:{turn_on:"Pornește",turn_off:"Oprește",parameter_to_value:"{parameter} la {value}",action_with_parameter:"{action} cu {parameter}"},climate:{set_temperature:"setare temperatură[ la {temperature}]",set_temperature_hvac_mode_heat:"încălzire[ la {temperature}]",set_temperature_hvac_mode_cool:"răcire[ la {temperature}]",set_temperature_hvac_mode_heat_cool:"încălzire/răcire[ la {temperature}]",set_temperature_hvac_mode_heat_cool_range:"încălzire/răcire[ la {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"auto[ la {temperature}]",set_hvac_mode:"setare mod[ la {hvac_mode}]",set_preset_mode:"setare preset[ {preset_mode}]",set_fan_mode:"set fan mode[ to {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"închidere",open_cover:"deschidere",set_cover_position:"setare poziție[ la {position}]",set_cover_tilt_position:"set tilt position[ to {tilt_position}]"},fan:{set_percentage:"setare viteză[ la {speed}]",set_direction:"setare direcție[ la {direction}]",oscillate:"setare oscilare[ la {oscillate}]"},humidifier:{set_humidity:"setare umiditate[ la {humidity}]",set_mode:"setare mod[ la {mode}]"},input_number:{set_value:"setare valoare[ la {value}]"},input_select:{select_option:"selectare opțiune[ {option}]"},select:{select_option:"selectare opțiune[ {option}]"},light:{turn_on:"pornire[ cu luminozitate {brightness}]"},media_player:{select_source:"selectare sursă[ {source}]"},notify:{send_message:"send notification"},script:{execute:"executare"},vacuum:{start_pause:"start / pauză"},water_heater:{set_operation_mode:"setare mod[ la {operation_mode}]",set_away_mode:"setare mod plecat"}},yi={components:{date:{day_types_short:{daily:"zilnic",workdays:"zile lucrătoare",weekend:"sfârșit de săptămână"},day_types_long:{daily:"zilnic",workdays:"în timpul săptămânii",weekend:"la sfârșit de săptămână"},days:"zile",tomorrow:"mâine",repeated_days:"la fiecare {days} zile",repeated_days_except:"zilnic cu excepția {excludedDays}",days_range:"din {startDay} până în {endDay}",next_week_day:"{weekday} viitoare"},time:{absolute:"la {time}",interval:"de la {startTime} până la {endTime}",at_midnight:"la miezul nopții",at_noon:"la amiază",at_sun_event:"la {sunEvent}"}},dialog:{enable_schedule:{title:"Completați modificările",description:"Programul care a fost modificat este momentan dezactivat, ar trebui activat?"},confirm_delete:{title:"Ștergeți entitatea?",description:"Sigur doriți să eliminați această entitate?"},confirm_migrate:{title:"Modificați programul",description:"Unele setări se vor pierde prin această acțiune. Vrei sa continui?"},weekday_picker:{title:"Repeated days for schedule",choose:"Choose..."},entity_picker:{title:"Choose entities",choose:"Choose...",no_results:"No matching items found"},action_picker:{title:"Choose action",show_all:"Show all supported entities"}},panel:{common:{title:"Planificator",new_schedule:"Noul program",default_name:"Program #{id}"},overview:{no_entries:"Nu există elemente de afișat",backend_error:"Could not connect with the scheduler component. It needs to be installed as integration before this card can be used.",excluded_items:"{if number is 1}un element exclus {else}{number} elemente excluse",hide_excluded:"ascunde elementele excluse",additional_tasks:"{if number is 1}o sarcină suplimentară {else}{number} sarcini suplimentare"},editor:{repeated_days:"Repeated days",start_time:"Start time",stop_time:"End time",action:"Action",add_action:"Add action",select_timeslot:"Selectați un interval orar",toggle_single_mode:"Te lokho modo",toggle_scheme_mode:"Te sxeme modo",validation_errors:{overlapping_time:"Schedule has overlapping timeslots",missing_target_entity:"One or more actions are missing a target entity",missing_service_parameter:"One or more actions are missing a required setting",missing_action:"Schedule has no actions"}},options:{conditions:{header:"Conditions",add_condition:"Add condition",new_condition:"New condition",types:{equal_to:"{entity} is equal to {value}",unequal_to:"{entity} is unequal to {value}",above:"{entity} is above {value}",below:"{entity} is below {value}"},options:{logic_and:"All conditions must be true",logic_or:"Any condition must be true",track_changes:"Re-evaluate when conditions change"}},period:{header:"Perioadă",start_date:"From",end_date:"To"},repeat_type:"comportament după declanșare",tags:"Tags"},card_editor:{tabs:{entities:"Entities",other:"Other"},fields:{title:{heading:"Title of the card",options:{standard:"standard",hidden:"hidden",custom:"custom"},custom_title:"Custom title"},discover_existing:{heading:"Show all schedules",description:"This sets the 'discover existing' parameter. Previously created schedules will be automatically added to the card. "},time_step:{heading:"Time step",description:"Resolution (in minutes) for creating schedules",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Single schedule mode",scheme:"Time scheme mode"}},sort_by:{heading:"Sorting options",description:"Order in which the schedules appear in the card",options:{relative_time:"Time remaining until next action",title:"Displayed title of the schedule",state:"Show active schedules on top"}},display_format_primary:{heading:"Displayed primary info",description:"Configure which label is used for schedules in the overview",options:{default:"Schedule name",entity_action:"Summary of task"}},display_format_secondary:{heading:"Displayed secondary info",description:"Configure what additional properties are visible in the overview",options:{relative_time:"Time remaining until next action",time:"Configured time for next action",days:"Repeated days of the week",additional_tasks:"Number of additional tasks"}},show_header_toggle:{heading:"Show header toggle",description:"Show toggle switch at the top of the card for enabling/disabling all entities"},show_toggle_switches:{heading:"Show toggle switches",description:"Show toggle switch for each individual schedule in the card"},tags:{heading:"Tags",description:"Use tags to divide schedules between multiple cards"},entities:{button_label:"Configure included entities",heading:"Included entities",description:"Select the entities that you want to control using the scheduler. You can click on a group to open it. Note that some entities (such as sensors) can only be used for conditions, not for actions.",included_number:"{number}/{total} selected"}}}}},fi={services:vi,ui:yi},bi=Object.freeze({__proto__:null,services:vi,ui:yi,default:fi}),wi={generic:{turn_on:"Включить",turn_off:"Выключить",parameter_to_value:"{parameter} к {value}",action_with_parameter:"{action} с {parameter}"},climate:{set_temperature:"установить температуру[ {temperature}]",set_temperature_hvac_mode_heat:"обогрев[ {temperature}]",set_temperature_hvac_mode_cool:"охлаждение[ {temperature}]",set_temperature_hvac_mode_heat_cool:"обогрев/охлаждение[ {temperature}]",set_temperature_hvac_mode_heat_cool_range:"обогрев/охлаждение[ {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"aвто[ {temperature}]",set_hvac_mode:"установить режим[ {hvac_mode}]",set_preset_mode:"выбрать набор настроек[ {preset_mode}]",set_fan_mode:"set fan mode[ to {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"закрыть",open_cover:"открыть",set_cover_position:"установить позицию[ {position}]",set_cover_tilt_position:"установить наклон[ {tilt_position}]"},fan:{set_percentage:"установить скорость[ {speed}]",set_direction:"установить направление[ {direction}]",oscillate:"установить колебание[ {oscillate}]"},humidifier:{set_humidity:"установить влажность[ {humidity}]",set_mode:"установить режим[ {mode}]"},input_number:{set_value:"установить значение[ в {value}]"},input_select:{select_option:"установить опцию[ {option}]"},select:{select_option:"установить опцию[ {option}]"},light:{turn_on:"включить[ с {brightness} яркостью]"},media_player:{select_source:"выбрать источник[ {source}]"},notify:{send_message:"послать сообщение"},script:{execute:"запустить"},vacuum:{start_pause:"старт / пауза"},water_heater:{set_operation_mode:"установить режим[ {operation_mode}]",set_away_mode:"установить режим вне дома"}},ki={components:{date:{day_types_short:{daily:"ежедневно",workdays:"рабочие дни",weekend:"выходные"},day_types_long:{daily:"каждый день",workdays:"по рабочим дням",weekend:"в выходные"},days:"дни",tomorrow:"завтра",repeated_days:"каждый {days}",repeated_days_except:"каждый день кроме {excludedDays}",days_range:"с {startDay} до {endDay}",next_week_day:"в следующую {weekday}"},time:{absolute:"в {time}",interval:"с {startTime} до {endTime}",at_midnight:"в полночь",at_noon:"в полдень",at_sun_event:"в {sunEvent}"}},dialog:{enable_schedule:{title:"Завершите модификации",description:"Расписание, которое было изменено, в настоящее время отключено, следует ли его включить?"},confirm_delete:{title:"Удалить объект?",description:"Вы уверены, что хотите удалить этот объект?"},confirm_migrate:{title:"Изменить расписание",description:"При этом некоторые настройки будут потеряны. Вы хотите продолжить?"},weekday_picker:{title:"Repeated days for schedule",choose:"Choose..."},entity_picker:{title:"Choose entities",choose:"Choose...",no_results:"No matching items found"},action_picker:{title:"Choose action",show_all:"Show all supported entities"}},panel:{common:{title:"Планировщик",new_schedule:"Новое расписание",default_name:"Расписание #{id}"},overview:{no_entries:"Отсутствуют элементы",backend_error:"Нет соединенияс scheduler component. Scheduler component должен быть установлен до применения этой карты.",excluded_items:"{number} исключено {if number is 1} элемент {else} элементов",hide_excluded:"скрыть исключенные элементы",additional_tasks:"{number} больше {if number is 1} задача {else} задач"},editor:{repeated_days:"Repeated days",start_time:"Start time",stop_time:"End time",action:"Action",add_action:"Add action",select_timeslot:"Выберите временной слот",toggle_single_mode:"В простой режим",toggle_scheme_mode:"В режим схемы",validation_errors:{overlapping_time:"Schedule has overlapping timeslots",missing_target_entity:"One or more actions are missing a target entity",missing_service_parameter:"One or more actions are missing a required setting",missing_action:"Schedule has no actions"}},options:{conditions:{header:"Conditions",add_condition:"Add condition",new_condition:"New condition",types:{equal_to:"{entity} is equal to {value}",unequal_to:"{entity} is unequal to {value}",above:"{entity} is above {value}",below:"{entity} is below {value}"},options:{logic_and:"All conditions must be true",logic_or:"Any condition must be true",track_changes:"Re-evaluate when conditions change"}},period:{header:"период",start_date:"From",end_date:"To"},repeat_type:"поведение после срабатывания",tags:"Tags"},card_editor:{tabs:{entities:"Entities",other:"Other"},fields:{title:{heading:"Title of the card",options:{standard:"standard",hidden:"hidden",custom:"custom"},custom_title:"Custom title"},discover_existing:{heading:"Show all schedules",description:"This sets the 'discover existing' parameter. Previously created schedules will be automatically added to the card. "},time_step:{heading:"Time step",description:"Resolution (in minutes) for creating schedules",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Single schedule mode",scheme:"Time scheme mode"}},sort_by:{heading:"Sorting options",description:"Order in which the schedules appear in the card",options:{relative_time:"Time remaining until next action",title:"Displayed title of the schedule",state:"Show active schedules on top"}},display_format_primary:{heading:"Displayed primary info",description:"Configure which label is used for schedules in the overview",options:{default:"Schedule name",entity_action:"Summary of task"}},display_format_secondary:{heading:"Displayed secondary info",description:"Configure what additional properties are visible in the overview",options:{relative_time:"Time remaining until next action",time:"Configured time for next action",days:"Repeated days of the week",additional_tasks:"Number of additional tasks"}},show_header_toggle:{heading:"Show header toggle",description:"Show toggle switch at the top of the card for enabling/disabling all entities"},show_toggle_switches:{heading:"Show toggle switches",description:"Show toggle switch for each individual schedule in the card"},tags:{heading:"Tags",description:"Use tags to divide schedules between multiple cards"},entities:{button_label:"Configure included entities",heading:"Included entities",description:"Select the entities that you want to control using the scheduler. You can click on a group to open it. Note that some entities (such as sensors) can only be used for conditions, not for actions.",included_number:"{number}/{total} selected"}}}}},xi={services:wi,ui:ki},$i=Object.freeze({__proto__:null,services:wi,ui:ki,default:xi}),ji={generic:{turn_on:"Zapnúť",turn_off:"Vypnúť",parameter_to_value:"{parameter} na {value}",action_with_parameter:"{action} s {parameter}"},climate:{set_temperature:"nastaviť teplotu[ na {temperature}]",set_temperature_hvac_mode_heat:"vykurovanie[ na {temperature}]",set_temperature_hvac_mode_cool:"chladenie[ na {temperature}]",set_temperature_hvac_mode_heat_cool:"vykurovanie/chladenie[ na {temperature}]",set_temperature_hvac_mode_heat_cool_range:"vykurovanie/chladenie[ na {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"automatika[ na {temperature}]",set_hvac_mode:"nastaviť režim[ na {hvac_mode}]",set_preset_mode:"nastaviť predvoľbu[ {preset_mode}]",set_fan_mode:"nastaviť režim ventilátora[ to {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"zatvoriť",open_cover:"otvoriť",set_cover_position:"nastaviť polohu[ na {position}]",set_cover_tilt_position:"nastaviť naklonenie[ na {tilt_position}]"},fan:{set_percentage:"nastaviť rýchlosť[ na {speed}]",set_direction:"nastaviť smer[ na {direction}]",oscillate:"nastaviť osciláciu[ na {oscillate}]"},humidifier:{set_humidity:"nastaviť vlhkosť[ na {humidity}]",set_mode:"nastaviť režim[ na {mode}]"},input_number:{set_value:"nastaviť hodnotu[ na {value}]"},input_select:{select_option:"vybrať možnosť[ {option}]"},select:{select_option:"vybrať možnosť[ {option}]"},light:{turn_on:"zapnúť[ na {brightness} jas]"},media_player:{select_source:"vybrať zdroj[ {source}]"},notify:{send_message:"poslať notifikáciu"},script:{execute:"spustiť"},vacuum:{start_pause:"štart / pauza"},water_heater:{set_operation_mode:"nastaviť režim[ na {operation_mode}]",set_away_mode:"nastaviť režim neprítomnosti"}},Oi={components:{date:{day_types_short:{daily:"denne",workdays:"pracovné dni",weekend:"víkendy"},day_types_long:{daily:"každý deň",workdays:"cez pracovné dni",weekend:"cez víkend"},days:"dni",tomorrow:"zajtra",repeated_days:"každý {days}",repeated_days_except:"každý deň okrem {excludedDays}",days_range:"od {startDay} do {endDay}",next_week_day:"budúcu {weekday}"},time:{absolute:"od {time}",interval:"od {startTime} do {endTime}",at_midnight:"od polnoci",at_noon:"od obeda",at_sun_event:"na {sunEvent}"}},dialog:{enable_schedule:{title:"Dokončite úpravy",description:"Plán, ktorý sa zmenil, je momentálne zakázaný, chcete ho povoliť?"},confirm_delete:{title:"Odstrániť entitu?",description:"Naozaj chcete odstrániť túto entitu?"},confirm_migrate:{title:"Aktualizovať plán",description:"Touto akciou sa stratia niektoré nastavenia. Chcete pokračovať?"},weekday_picker:{title:"Dni opakovania pre plán",choose:"Vyberte..."},entity_picker:{title:"Vyberte entity",choose:"Vyberte...",no_results:"Žiadne vyhovujúce položky neboli nájdené"},action_picker:{title:"Vyberte akciu",show_all:"Zobraziť všetky podporované entity"}},panel:{common:{title:"Plánovač",new_schedule:"Nový plán",default_name:"Plán #{id}"},overview:{no_entries:"Žiadne položky na zobrazenie",backend_error:"Nepodarilo sa pripojiť ku komponentu Scheduler. Pred tým, ako použijete túto kartu ho musíte nainštalovať ako integráciu.",excluded_items:"Vylúčené položky: {number}",hide_excluded:"skryť vylúčené položky",additional_tasks:"Ďalšie úlohy: {number}"},editor:{repeated_days:"Dni opakovania",start_time:"Čas začiatku",stop_time:"Čas konca",action:"Akcia",add_action:"Pridať akciu",select_timeslot:"Najprv vyberte časový úsek",toggle_single_mode:"Do režimu jedného",toggle_scheme_mode:"Do režimu schémy",validation_errors:{overlapping_time:"Plán obsahuje prekrývajúce sa časové intervaly",missing_target_entity:"Pre aspoň jednu akciu nie je zadaná cieľová entita",missing_service_parameter:"Pre aspoň jednu akciu nie je zadané vyžadované nastavenie",missing_action:"Plán neobsahuje žiadne akcie"}},options:{conditions:{header:"Podmienky",add_condition:"Pridať podmienku",new_condition:"Nová podmienka",types:{equal_to:"{entity} sa rovná {value}",unequal_to:"{entity} sa nerovná {value}",above:"{entity} je nad {value}",below:"{entity} je pod {value}"},options:{logic_and:"Všetky podmienky musia platiť",logic_or:"Akákoľvek podmienka musí platiť",track_changes:"Prehodnoťte, keď sa zmenia podmienky"}},period:{header:"Obdobie",start_date:"Od",end_date:"Do"},repeat_type:"správanie sa po spustení",tags:"Štítky"},card_editor:{tabs:{entities:"Entity",other:"Iné"},fields:{title:{heading:"Názov karty",options:{standard:"štandardné",hidden:"skryté",custom:"vlastné"},custom_title:"Vlastný názov"},discover_existing:{heading:"Zobraziť všetky plány",description:"Tým sa nastaví parameter „objaviť existujúce“. Predtým vytvorené plány sa automaticky pridajú na kartu."},time_step:{heading:"Časový krok",description:"Rozlíšenie (v minútach) pre vytváranie plánov",unit_minutes:"min"},default_editor:{heading:"Predvolený editor času",options:{single:"Režim jedného plánu",scheme:"Režim schémy"}},sort_by:{heading:"Možnosti triedenia",description:"Poradie, v akom sa rozvrhy zobrazujú na karte",options:{relative_time:"Zostávajúci čas do ďalšej akcie",title:"Zobrazený názov rozvrhu",state:"Zobraziť aktívne plány navrchu"}},display_format_primary:{heading:"Zobrazené primárne informácie",description:"V prehľade nakonfigurujte, ktorý štítok sa použije pre plány",options:{default:"Názov rozvrhu",entity_action:"Zhrnutie úlohy"}},display_format_secondary:{heading:"Zobrazené sekundárne informácie",description:"Nakonfigurujte, ktoré ďalšie vlastnosti sú viditeľné v prehľade",options:{relative_time:"Zostávajúci čas do ďalšej akcie",time:"Nastavený čas pre ďalšiu akciu",days:"Opakované dni v týždni",additional_tasks:"Počet dodatočných úloh"}},show_header_toggle:{heading:"Zobraziť prepínač hlavičky",description:"Zobraziť prepínač v hornej časti karty na povolenie/zakázanie všetkých entít"},show_toggle_switches:{heading:"Zobraziť prepínače",description:"Zobraziť prepínač pre každý jednotlivý harmonogram na karte"},tags:{heading:"Štítky",description:"Použite štítky na rozdelenie plánov medzi viacero kariet"},entities:{button_label:"Konfigurácia zahrnutých entít",heading:"Zahrnuté entity",description:"Vyberte entity, ktoré chcete ovládať pomocou plánovača. Kliknutím na skupinu ju otvoríte. Upozorňujeme, že niektoré entity (napríklad senzory) možno použiť iba na podmienky, nie na akcie.",included_number:"{number}/{total} vybraný"}}}}},Si={services:ji,ui:Oi},zi={generic:{turn_on:"Vklopi",turn_off:"Izklopi",parameter_to_value:"{parameter} v {value}",action_with_parameter:"{action} z {parameter}"},climate:{set_temperature:"nastavi temperaturo[ na {temperature}]",set_temperature_hvac_mode_heat:"ogrej[ na {temperature}]",set_temperature_hvac_mode_cool:"ohladi[ na {temperature}]",set_temperature_hvac_mode_heat_cool:"ogrej/ohladi[ na {temperature}]",set_temperature_hvac_mode_heat_cool_range:"ogrej/ohladi[ na {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"samodejno[ na {temperature}]",set_hvac_mode:"nastavi način[ na {hvac_mode}]",set_preset_mode:"nastavi preset[ na {preset_mode}]",set_fan_mode:"nastavi ventilator[ na {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"zapri",open_cover:"odpri",set_cover_position:"nastavi pozicijo[ na {position}]",set_cover_tilt_position:"nastavi naklon[ na {tilt_position}]"},fan:{set_percentage:"nastavi hitrost[ na {speed}]",set_direction:"nastavi smer[ na {direction}]",oscillate:"nastavi obračanje[ na {oscillate}]"},humidifier:{set_humidity:"nastavi vlažnost[ na {humidity}]",set_mode:"nastavi način[ na {mode}]"},input_number:{set_value:"nastavi vrednost[ na {value}]"},input_select:{select_option:"izberi možnost[ {option}]"},select:{select_option:"izberi možnost[ {option}]"},light:{turn_on:"vključi[ z {brightness} brightness]"},media_player:{select_source:"Izberi vir[ {source}]"},notify:{send_message:"pošlji sporočilo"},script:{execute:"izvedi"},vacuum:{start_pause:"zaženi / ustavi"},water_heater:{set_operation_mode:"nastavi način[ na {operation_mode}]",set_away_mode:"nastavi način odsoten"}},Ci={components:{date:{day_types_short:{daily:"dnevno",workdays:"delovniki",weekend:"vikend"},day_types_long:{daily:"vsak dan",workdays:"ob delovnikih",weekend:"ob vikendih"},days:"dni",tomorrow:"jutri",repeated_days:"vsakih {days}",repeated_days_except:"vsak dan razen {excludedDays}",days_range:"od {startDay} do {endDay}",next_week_day:"naslednji {weekday}"},time:{absolute:"ob {time}",interval:"od {startTime} do {endTime}",at_midnight:"opolnoči",at_noon:"opoldne",at_sun_event:"ob {sunEvent}"}},dialog:{enable_schedule:{title:"Zaključi spremembe",description:"Urnik, katerega ste spremenili je trenutno izključen, ali ga želite omogočiti?"},confirm_delete:{title:"Ali želite odstraniti entiteto?",description:"Ali ste prepričani, da želite odstraniti to entiteto?"},confirm_migrate:{title:"Spremenite urnik",description:"Nekatere nastavitve bodo s tem dejanjem izgubljene. Želite nadaljevati?"},weekday_picker:{title:"Repeated days for schedule",choose:"Choose..."},entity_picker:{title:"Choose entities",choose:"Choose...",no_results:"No matching items found"},action_picker:{title:"Choose action",show_all:"Show all supported entities"}},panel:{common:{title:"Urnik",new_schedule:"Nov urnik",default_name:"Urnik #{id}"},overview:{no_entries:"Ni vpisov za prikaz",backend_error:"Ni povezave s komponento urnika. Komponenta mora biti nameščena kot integracija, preden lahko uporabite to kartico.",excluded_items:"{number}{if number is 1} izločen predmet {else} izločenih predmetov",hide_excluded:"skrij izločene predmete",additional_tasks:"še {number}{if number is 1} opravilo {else} opravil"},editor:{repeated_days:"Repeated days",start_time:"Start time",stop_time:"End time",action:"Action",add_action:"Add action",select_timeslot:"Najprej izberi časovni okvir",toggle_single_mode:"To single mode",toggle_scheme_mode:"To scheme mode",validation_errors:{overlapping_time:"Schedule has overlapping timeslots",missing_target_entity:"One or more actions are missing a target entity",missing_service_parameter:"One or more actions are missing a required setting",missing_action:"Schedule has no actions"}},options:{conditions:{header:"Conditions",add_condition:"Add condition",new_condition:"New condition",types:{equal_to:"{entity} is equal to {value}",unequal_to:"{entity} is unequal to {value}",above:"{entity} is above {value}",below:"{entity} is below {value}"},options:{logic_and:"All conditions must be true",logic_or:"Any condition must be true",track_changes:"Ponovno preglej ko se pogoji spremenijo"}},period:{header:"Perioda",start_date:"From",end_date:"To"},repeat_type:"obnašanje, ko končaš",tags:"Tags"},card_editor:{tabs:{entities:"Entitete",other:"Ostalo"},fields:{title:{heading:"Ime kartice",options:{standard:"standardno",hidden:"skrito",custom:"po meri"},custom_title:"Ime po želji"},discover_existing:{heading:"Prikaži vse urnike",description:"Tole nastavi parameter 'discover existing'. Prej kreirani urniki bodo samodejno dodani v kartico. "},time_step:{heading:"Časovni korak",description:"Ločljivost (v minutah) za kreiranje urnikov",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"V enojni način",scheme:"V shematski način"}},sort_by:{heading:"Možnosti sortiranja",description:"Zaporedje, po katerem se prikažejo urniki na kartici",options:{relative_time:"Preostali čas do naslednje akcije",title:"Prikazano ime urnika",state:"Prikaži aktivne urnike na vrhu"}},display_format_primary:{heading:"Prikazane primarne informacije",description:"Nastavite, kaj se prikazuje v pregledu urnikov",options:{default:"Ime urnika",entity_action:"Povzetek operacije"}},display_format_secondary:{heading:"Prikazane sekundarne informacije",description:"Nastavite, katere dodatne informacije želite imeti prikazane v pregledu",options:{relative_time:"Preostali čas do naslednje akcije",time:"Nastavljen čas za naslednjo akcijo",days:"Katere dni/kdaj se sproži akcija",additional_tasks:"Število dodatnih opravil"}},show_header_toggle:{heading:"Prikaži glavo",description:"Na vrhu prikaže stikalo, s katerim lahko omogočite/izključite vse entitete naenkrat"},show_toggle_switches:{heading:"Prikaži stikala",description:"Prikaži stikalo za vsak posamezen urnik na kartici"},tags:{heading:"Tag-i",description:"Uporabite tag-e za ločevanje urnikov med več karticami"},entities:{button_label:"Konfiguriraj vključene entitete",heading:"Vključene entitete",description:"Izberite entitete, katere želike krmiliti s tem urnikom. Lahko kliknete na skupino, da jo odprete. Vedite, da lahko nekatere entitete (npr. senzorje) uporabite samo v pogojih, ne pa za akcije.",included_number:"{number}/{total} izbranih"}}}}},Ai={services:zi,ui:Ci},Ti={generic:{turn_on:"Slå på",turn_off:"Stäng av",parameter_to_value:"{parameter} till {value}",action_with_parameter:"{action} med {parameter}"},climate:{set_temperature:"ställ in temperaturen[ på {temperature}]",set_temperature_hvac_mode_heat:"värma[ till {temperature}]",set_temperature_hvac_mode_cool:"kyla[ till {temperature}]",set_temperature_hvac_mode_heat_cool:"värma/kyla[ till {temperature}]",set_temperature_hvac_mode_heat_cool_range:"värma/kyla[ till {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"auto[ till {temperature}]",set_hvac_mode:"ställ in läget[ till {hvac_mode}]",set_preset_mode:"ställ in förinställningen[ till {preset_mode}]",set_fan_mode:"ställ in fläktläge[ till {fan_mode}]",set_swing_mode:"ställ in svängningsläge[ till {swing_mode}]"},cover:{close_cover:"stäng",open_cover:"öppna",set_cover_position:"ställ in positionen[ till {position}]",set_cover_tilt_position:"ställ in lutningsposition[ till {tilt_position}]"},fan:{set_percentage:"ställ in hastighet[ till {percentage}]",set_direction:"ställ in riktning[ till {direction}]",oscillate:"ställ in oscillation[ till {oscillate}]"},humidifier:{set_humidity:"ställ in luftfuktighet[ till {humidity}]",set_mode:"ställ in läge[ till {mode}]"},input_number:{set_value:"ställ in den[ till {value}]"},input_select:{select_option:"välj alternativ[ {option}]"},select:{select_option:"välj alternativ[ {option}]"},light:{turn_on:"slå på[ med {brightness} brightness]"},media_player:{select_source:"välj källa[ {source}]"},notify:{send_message:"send notification"},script:{execute:"utföra"},vacuum:{start_pause:"starta / pausa"},water_heater:{set_operation_mode:"ställ in läget[ till {operation_mode}]",set_away_mode:"ställ in borta läget"}},Ei={components:{date:{day_types_short:{daily:"daglig",workdays:"arbetsdagar",weekend:"helgen"},day_types_long:{daily:"varje dag",workdays:"på arbetsdagar",weekend:"i helgen"},days:"dagar",tomorrow:"imorgon",repeated_days:"varje {days}",repeated_days_except:"varje dag utom {excludedDays}",days_range:"från {startDay} till {endDay}",next_week_day:"nästa {weekday}"},time:{absolute:"kl. {time}",interval:"från {startTime} till {endTime}",at_midnight:"vid midnatt",at_noon:"vid middagstid",at_sun_event:"vid {sunEvent}"}},dialog:{enable_schedule:{title:"Slutför modifieringar",description:"Schemat du har ändrat är för närvarande inaktiverat, vill du aktivera den?"},confirm_delete:{title:"Ta bort enheten?",description:"Är du säker på att du vill ta bort den här enheten?"},confirm_migrate:{title:"Uppdatera schema",description:"Vissa inställningar kommer att gå förlorade genom den här ändringen. Vill du fortsätta?"},weekday_picker:{title:"Upprepade dagar för schema",choose:"Välj..."},entity_picker:{title:"Välj enheter",choose:"Välj...",no_results:"Inga matchande objekt hittades"},action_picker:{title:"Välj åtgärd",show_all:"Visa alla enheter som stöds"}},panel:{common:{title:"Schemaläggare",new_schedule:"Ny schema",default_name:"Schema #{id}"},overview:{no_entries:"Det finns inga objekt att visa",backend_error:"Kunde inte ansluta till schemaläggarkomponenten. Den måste installeras som integration innan det här kortet kan användas.",excluded_items:"{number} utesluten {if number is 1} artikel {else} artiklar",hide_excluded:"dölj uteslutna objekt",additional_tasks:"{number} mer {if number is 1} uppgift {else} uppgifter"},editor:{repeated_days:"Upprepade dagar",start_time:"Starttid",stop_time:"Sluttid",action:"Åtgärd",add_action:"Lägg till åtgärd",select_timeslot:"Välj en tidslucka",toggle_single_mode:"Till enkelläge",toggle_scheme_mode:"Till schemaläge",validation_errors:{overlapping_time:"Schemat har överlappande tidsluckor",missing_target_entity:"En eller flera åtgärder saknar en målentitet",missing_service_parameter:"En eller flera åtgärder saknar en obligatorisk inställning",missing_action:"Schemat har inga åtgärder"}},options:{conditions:{header:"Villkor",add_condition:"Lägg till villkor",new_condition:"Nytt villkor",types:{equal_to:"{entity} är lika med {value}",unequal_to:"{entity} är ojämlik med {value}",above:"{entity} är över {value}",below:"{entity} är under {value}"},options:{logic_and:"Alla villkor måste vara sanna",logic_or:"Något av villkoren måste vara sant",track_changes:"Omvärdera när förutsättningarna förändras"}},period:{header:"Period",start_date:"Från",end_date:"Till"},repeat_type:"beteende efter avslutad",tags:"Taggar"},card_editor:{tabs:{entities:"Enheter",other:"Andra"},fields:{title:{heading:"Kortets titel",options:{standard:"standard",hidden:"dold",custom:"anpassad"},custom_title:"Anpassad titel"},discover_existing:{heading:"Visa alla scheman",description:"Detta ställer in parametern ''upptäck befintliga''. Tidigare skapade scheman läggs automatiskt till på kortet."},time_step:{heading:"Tidssteg",description:"Upplösning (i minuter) för att skapa scheman",unit_minutes:"min"},default_editor:{heading:"Standard tidsredigerare",options:{single:"Enkelt schemalagt läge",scheme:"Tidschemaläge"}},sort_by:{heading:"Sorteringsalternativ",description:"Ordning i vilken scheman visas på kortet",options:{relative_time:"Tid kvar till nästa åtgärd",title:"Visad titel på schemat",state:"Visa aktiva scheman överst"}},display_format_primary:{heading:"Visad primär information",description:"Konfigurera vilken etikett som används för scheman i översikten",options:{default:"Schemanamn",entity_action:"Sammanfattning av uppgiften"}},display_format_secondary:{heading:"Visad sekundär information",description:"Konfigurera vilka ytterligare egenskaper som ska synas i översikten",options:{relative_time:"Tid kvar till nästa åtgärd",time:"Konfigurerad tid för nästa åtgärd",days:"Upprepade dagar i veckan",additional_tasks:"Antal ytterligare uppgifter"}},show_header_toggle:{heading:"Visa rubrikväxling",description:"Visa växlingsknappen högst upp på kortet för att aktivera/inaktivera alla enheter"},show_toggle_switches:{heading:"Visa växlingsknappar",description:"Visa växlingsknappen för varje enskilt schema i kortet"},tags:{heading:"Taggar",description:"Använd taggar för att dela upp scheman mellan flera kort"},entities:{button_label:"Konfigurera inkluderade entiteter",heading:"Inkluderade enheter",description:"Välj de enheter som du vill styra med hjälp av schemaläggaren. Du kan klicka på en grupp för att öppna den. Observera att vissa enheter (t.ex. sensorer) bara kan användas för villkor, inte för åtgärder.",included_number:"{number}/{total} vald"}}}}},Di={services:Ti,ui:Ei},Mi={generic:{turn_on:"Ввімкнути",turn_off:"Вимкнути",parameter_to_value:"{parameter} до {value}",action_with_parameter:"{action} з {parameter}"},climate:{set_temperature:"встановити температуру[ to {temperature}]",set_temperature_hvac_mode_heat:"нагрів[ to {temperature}]",set_temperature_hvac_mode_cool:"охолодження[ to {temperature}]",set_temperature_hvac_mode_heat_cool:"нагрів/охолодження[ to {temperature}]",set_temperature_hvac_mode_heat_cool_range:"нагрів/охолодження[ to {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"aвтоматичний[ to {temperature}]",set_hvac_mode:"встановити режим[ to {hvac_mode}]",set_preset_mode:"вибрати пресет[ to {preset_mode}]",set_fan_mode:"set fan mode[ to {fan_mode}]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"закрити",open_cover:"відкрити",set_cover_position:"встановити позицію[ to {position}]",set_cover_tilt_position:"set tilt position[ to {tilt_position}]"},fan:{set_percentage:"встановити швидкість[ to {speed}]",set_direction:"встановити напрямок[ to {direction}]",oscillate:"встановити коливання[ to {oscillate}]"},humidifier:{set_humidity:"встановити вологість[ to {humidity}]",set_mode:"встановити режим[ to {mode}]"},input_number:{set_value:"встановити значення[ to {value}]"},input_select:{select_option:"встановити опцію[ {option}]"},select:{select_option:"встановити опцію[ {option}]"},light:{turn_on:"увімкнути[ з {brightness} якскравістю]"},media_player:{select_source:"вибрати джерело[ {source}]"},notify:{send_message:"send notification"},script:{execute:"виконати"},vacuum:{start_pause:"старт / пауза"},water_heater:{set_operation_mode:"встановити режим[ to {operation_mode}]",set_away_mode:"встановити режим Не вдома"}},Li={components:{date:{day_types_short:{daily:"щоденно",workdays:"робочі дні",weekend:"вихідні"},day_types_long:{daily:"кожного дня",workdays:"в робочі дні",weekend:"по вихідних"},days:"дні",tomorrow:"завтра",repeated_days:"кожні {days}",repeated_days_except:"кожного дня окрім {excludedDays}",days_range:"з {startDay} до {endDay}",next_week_day:"наступної {weekday}"},time:{absolute:"о {time}",interval:"з {startTime} до {endTime}",at_midnight:"опівночі",at_noon:"опівдні",at_sun_event:"о {sunEvent}"}},dialog:{enable_schedule:{title:"Завершіть зміни",description:"Розклад, який було змінено, наразі вимкнено, чи потрібно його ввімкнути?"},confirm_delete:{title:"Видалити сутність?",description:"Ви дійсно бажаєте видалити цю сутність?"},confirm_migrate:{title:"Змінити розклад",description:"У результаті цієї дії деякі налаштування буде втрачено. Ви бажаєте продовжити?"},weekday_picker:{title:"Repeated days for schedule",choose:"Choose..."},entity_picker:{title:"Choose entities",choose:"Choose...",no_results:"No matching items found"},action_picker:{title:"Choose action",show_all:"Show all supported entities"}},panel:{common:{title:"Планувальник",new_schedule:"Новий розклад",default_name:"Розклад #{id}"},overview:{no_entries:"Елементи відсутні",backend_error:"Не вдалося підключитися до компонента планувальника. Перш ніж використовувати цю карту, її потрібно встановити як інтеграцію.",excluded_items:"{number} виключено {if number is 1} елемент {else} елементів",hide_excluded:"сховати виключені елементи",additional_tasks:"{number} більше {if number is 1} завдання {else} завдань"},editor:{repeated_days:"Repeated days",start_time:"Start time",stop_time:"End time",action:"Action",add_action:"Add action",select_timeslot:"Спершу виберіть часовий проміжок",toggle_single_mode:"До одиночного режиму",toggle_scheme_mode:"До схемного режиму",validation_errors:{overlapping_time:"Schedule has overlapping timeslots",missing_target_entity:"One or more actions are missing a target entity",missing_service_parameter:"One or more actions are missing a required setting",missing_action:"Schedule has no actions"}},options:{conditions:{header:"Conditions",add_condition:"Add condition",new_condition:"New condition",types:{equal_to:"{entity} is equal to {value}",unequal_to:"{entity} is unequal to {value}",above:"{entity} is above {value}",below:"{entity} is below {value}"},options:{logic_and:"All conditions must be true",logic_or:"Any condition must be true",track_changes:"Re-evaluate when conditions change"}},period:{header:"період",start_date:"From",end_date:"To"},repeat_type:"поведінка після спрацювання",tags:"Tags"},card_editor:{tabs:{entities:"Entities",other:"Other"},fields:{title:{heading:"Title of the card",options:{standard:"standard",hidden:"hidden",custom:"custom"},custom_title:"Custom title"},discover_existing:{heading:"Show all schedules",description:"This sets the 'discover existing' parameter. Previously created schedules will be automatically added to the card. "},time_step:{heading:"Time step",description:"Resolution (in minutes) for creating schedules",unit_minutes:"min"},default_editor:{heading:"Default time editor",options:{single:"Single schedule mode",scheme:"Time scheme mode"}},sort_by:{heading:"Sorting options",description:"Order in which the schedules appear in the card",options:{relative_time:"Time remaining until next action",title:"Displayed title of the schedule",state:"Show active schedules on top"}},display_format_primary:{heading:"Displayed primary info",description:"Configure which label is used for schedules in the overview",options:{default:"Schedule name",entity_action:"Summary of task"}},display_format_secondary:{heading:"Displayed secondary info",description:"Configure what additional properties are visible in the overview",options:{relative_time:"Time remaining until next action",time:"Configured time for next action",days:"Repeated days of the week",additional_tasks:"Number of additional tasks"}},show_header_toggle:{heading:"Show header toggle",description:"Show toggle switch at the top of the card for enabling/disabling all entities"},show_toggle_switches:{heading:"Show toggle switches",description:"Show toggle switch for each individual schedule in the card"},tags:{heading:"Tags",description:"Use tags to divide schedules between multiple cards"},entities:{button_label:"Configure included entities",heading:"Included entities",description:"Select the entities that you want to control using the scheduler. You can click on a group to open it. Note that some entities (such as sensors) can only be used for conditions, not for actions.",included_number:"{number}/{total} selected"}}}}},Pi={services:Mi,ui:Li},Ni={generic:{turn_on:"پر سوئچ کریں",turn_off:"بند کر دیں",parameter_to_value:"{parameter} کو {value} پر سیٹ کریں",action_with_parameter:"{parameter} کے ساتھ {action}"},climate:{set_temperature:"درجہ حرارت سیٹ کریں[ {temperature} پر]",set_temperature_hvac_mode_heat:"ہیٹ[ {temperature} پر]",set_temperature_hvac_mode_cool:"کول[ {temperature} پر]",set_temperature_hvac_mode_heat_cool:"ہیٹ/کول[ {temperature} پر]",set_temperature_hvac_mode_heat_cool_range:"ہیٹ/کول[ {target_temp_low} سے {target_temp_high} تک]",set_temperature_hvac_mode_auto:"آٹو[ {temperature} پر]",set_hvac_mode:"موڈ سیٹ کریں[ {hvac_mode} پر]",set_preset_mode:"پری سیٹ موڈ سیٹ کریں[ {preset_mode} پر]",set_fan_mode:"فین موڈ سیٹ کریں[ {fan_mode} پر]",set_swing_mode:"set swing mode[ to {swing_mode}]"},cover:{close_cover:"بند کریں",open_cover:"کھولیں",set_cover_position:"پوزیشن سیٹ کریں[ {position} پر]",set_cover_tilt_position:"جھکاؤ پوزیشن سیٹ کریں[ {tilt_position} پر]"},fan:{set_percentage:"رفتار سیٹ کریں[ {percentage} پر]",set_direction:"سمت سیٹ کریں[ {direction} پر]",oscillate:"آسیلیشن سیٹ کریں[ {oscillate} پر]"},humidifier:{set_humidity:"نمی سیٹ کریں[ {humidity} پر]",set_mode:"موڈ سیٹ کریں[ {mode} پر]"},input_number:{set_value:"ویلیو سیٹ کریں[ {value} پر]"},input_select:{select_option:"آپشن منتخب کریں[ {option}]"},select:{select_option:"آپشن منتخب کریں[ {option}]"},light:{turn_on:"آن کریں[ {brightness} چمک کے ساتھ]"},media_player:{select_source:"سورس منتخب کریں[ {source}]"},notify:{send_message:"نوٹیفکیشن بھیجیں"},script:{execute:"چلائیں"},vacuum:{start_pause:"شروع / روکیں"},water_heater:{set_operation_mode:"موڈ سیٹ کریں[ {operation_mode} پر]",set_away_mode:"غیر موجودگی کا موڈ سیٹ کریں"}},Ii={components:{date:{day_types_short:{daily:"روزانہ",workdays:"کام کے دن",weekend:"ہفتہ اختتام"},day_types_long:{daily:"ہر دن",workdays:"کام کے دنوں میں",weekend:"ہفتے کے آخر میں"},days:"دن",tomorrow:"کل",repeated_days:"ہر {days}",repeated_days_except:"ہر دن سوائے {excludedDays}",days_range:"{startDay} سے {endDay} تک",next_week_day:"اگلا {weekday}"},time:{absolute:"{time} پر",interval:"{startTime} سے {endTime} تک",at_midnight:"آدھی رات کو",at_noon:"دوپہر کو",at_sun_event:"{sunEvent} کے وقت"}},dialog:{enable_schedule:{title:"ترمیم مکمل کریں",description:"جو شیڈول آپ نے بدلا ہے وہ اس وقت غیر فعال ہے، کیا آپ اسے فعال کرنا چاہتے ہیں؟"},confirm_delete:{title:"اینٹیٹی حذف کریں؟",description:"کیا آپ واقعی اس اینٹیٹی کو حذف کرنا چاہتے ہیں؟"},confirm_migrate:{title:"شیڈول اپ ڈیٹ کریں",description:"اس تبدیلی سے کچھ سیٹنگز ضائع ہو سکتی ہیں۔ کیا آپ جاری رکھنا چاہتے ہیں؟"},weekday_picker:{title:"شیڈول کے لیے دہرائے گئے دن",choose:"منتخب کریں..."},entity_picker:{title:"اینٹیٹیز منتخب کریں",choose:"منتخب کریں...",no_results:"کوئی مماثل آئٹمز نہیں ملے"},action_picker:{title:"عمل منتخب کریں",show_all:"تمام تعاون یافتہ اداروں کو دکھائیں۔"}},panel:{common:{title:"شیڈولر",new_schedule:"نیا شیڈول",default_name:"شیڈول #{id}"},overview:{no_entries:"دکھانے کے لیے کوئی آئٹمز نہیں ہیں",backend_error:"شیڈولر کمپوننٹ سے کنکشن نہیں ہو سکا۔ اسے کارڈ استعمال کرنے سے پہلے انٹیگریشن کے طور پر انسٹال کرنا ضروری ہے۔",excluded_items:"{number} خارج شدہ {if number is 1} آئٹم {else} آئٹمز",hide_excluded:"خارج شدہ آئٹمز چھپائیں",additional_tasks:"{number} مزید {if number is 1} کام {else} کام"},editor:{repeated_days:"دہرائے گئے دن",start_time:"آغاز کا وقت",stop_time:"اختتامی وقت",action:"عمل",add_action:"عمل شامل کریں",select_timeslot:"ٹائم سلاٹ منتخب کریں",toggle_single_mode:"سنگل موڈ پر جائیں",toggle_scheme_mode:"اسکیم موڈ پر جائیں",validation_errors:{overlapping_time:"شیڈول میں وقتوں کا اوورلیپ ہے",missing_target_entity:"ایک یا زیادہ اعمال میں ہدف اینٹیٹی غائب ہے",missing_service_parameter:"ایک یا زیادہ اعمال میں مطلوبہ سیٹنگ غائب ہے",missing_action:"شیڈول میں کوئی عمل موجود نہیں"}},options:{conditions:{header:"شرائط",add_condition:"شرط شامل کریں",new_condition:"نئی شرط",types:{equal_to:"{entity} {value} کے برابر ہے",unequal_to:"{entity} {value} کے برابر نہیں ہے",above:"{entity} {value} سے زیادہ ہے",below:"{entity} {value} سے کم ہے"},options:{logic_and:"تمام شرائط درست ہونی چاہئیں",logic_or:"کوئی ایک شرط درست ہونی چاہیے",track_changes:"جب شرائط بدلیں تو دوبارہ جانچ کریں"}},period:{header:"مدت",start_date:"سے",end_date:"تک"},repeat_type:"مکمل ہونے کے بعد کا برتاؤ",tags:"ٹیگز"},card_editor:{tabs:{entities:"اینٹیٹیز",other:"دیگر"},fields:{title:{heading:"کارڈ کا عنوان",options:{standard:"معیاری",hidden:"چھپا ہوا",custom:"حسبِ ضرورت"},custom_title:"اپنا عنوان"},discover_existing:{heading:"تمام شیڈولز دکھائیں",description:"یہ 'discover existing' پیرامیٹر سیٹ کرتا ہے۔ پہلے سے بنائے گئے شیڈولز خود بخود کارڈ میں شامل ہو جائیں گے۔"},time_step:{heading:"وقت کا وقفہ",description:"شیڈول بنانے کے لیے وقت کی ریزولوشن (منٹ میں)",unit_minutes:"منٹ"},default_editor:{heading:"ڈیفالٹ ٹائم ایڈیٹر",options:{single:"سنگل شیڈول موڈ",scheme:"ٹائم اسکیم موڈ"}},sort_by:{heading:"ترتیب کے اختیارات",description:"کارڈ میں شیڈولز کی ترتیب",options:{relative_time:"اگلے عمل تک باقی وقت",title:"شیڈول کا دکھایا گیا عنوان",state:"فعال شیڈولز اوپر دکھائیں"}},display_format_primary:{heading:"مرکزی معلومات کی نمائش",description:"اوورویو میں شیڈولز کے لیے لیبل کنفیگر کریں",options:{default:"شیڈول کا نام",entity_action:"کام کا خلاصہ"}},display_format_secondary:{heading:"ثانوی معلومات کی نمائش",description:"اوورویو میں اضافی خصوصیات دکھانے کا انتخاب کریں",options:{relative_time:"اگلے عمل تک باقی وقت",time:"اگلے عمل کا وقت",days:"ہفتے کے دہرائے گئے دن",additional_tasks:"اضافی کاموں کی تعداد"}},show_header_toggle:{heading:"ہیڈر ٹوگل دکھائیں",description:"کارڈ کے اوپر تمام اینٹیٹیز کو فعال/غیر فعال کرنے کے لیے سوئچ دکھائیں"},show_toggle_switches:{heading:"ٹوگل سوئچز دکھائیں",description:"کارڈ میں ہر انفرادی شیڈول کے لیے ٹوگل سوئچ دکھائیں"},tags:{heading:"ٹیگز",description:"شیڈولز کو مختلف کارڈز میں تقسیم کرنے کے لیے ٹیگز استعمال کریں"},entities:{button_label:"شامل اینٹیٹیز ترتیب دیں",heading:"شامل اینٹیٹیز",description:"وہ اینٹیٹیز منتخب کریں جنہیں آپ شیڈولر کے ذریعے کنٹرول کرنا چاہتے ہیں۔ آپ گروپ پر کلک کر کے اسے کھول سکتے ہیں۔ یاد رکھیں کہ کچھ اینٹیٹیز (جیسے سینسرز) صرف شرائط کے لیے استعمال ہو سکتی ہیں، اعمال کے لیے نہیں۔",included_number:"{number}/{total} منتخب شدہ"}}}}},qi={services:Ni,ui:Ii},Ri={generic:{turn_on:"打开",turn_off:"关闭",parameter_to_value:"{parameter} 至 {value}",action_with_parameter:"{action} 使用 {parameter}"},climate:{set_temperature:"设定温度[ 至 {temperature}]",set_temperature_hvac_mode_heat:"制热模式[ 至 {temperature}]",set_temperature_hvac_mode_cool:"制冷模式[ 至 {temperature}]",set_temperature_hvac_mode_heat_cool:"制热模式/制冷模式[ 至 {temperature}]",set_temperature_hvac_mode_heat_cool_range:"制热模式/制冷模式[ 至 {target_temp_low} - {target_temp_high}]",set_temperature_hvac_mode_auto:"自动[ 至 {temperature}]",set_hvac_mode:"设定模式[ 为 {hvac_mode}]",set_preset_mode:"设定预设模式[ 为 {preset_mode}]",set_fan_mode:"设置风扇模式[ 为 {fan_mode}]",set_swing_mode:"设置摆动模式[ 为 {swing_mode}]"},cover:{close_cover:"关闭",open_cover:"打开",set_cover_position:"设置位置[ 为 {position}]",set_cover_tilt_position:"设置倾斜位置[ 为 {tilt_position}]"},fan:{set_percentage:"设定风速[ 为 {speed}]",set_direction:"设定方向[ 为 {direction}]",oscillate:"设置摇摆[ 为 {oscillate}]"},humidifier:{set_humidity:"设定湿度[ 至 {humidity}]",set_mode:"设定模式[ 为 {mode}]"},input_number:{set_value:"设定数值[ 至 {value}]"},input_select:{select_option:"选择选项[ {option}]"},select:{select_option:"选择选项[ {option}]"},light:{turn_on:"打开[ 并设定亮度为 {brightness}]"},media_player:{select_source:"选择播放源[ {source}]"},notify:{send_message:"发送通知"},script:{execute:"执行"},vacuum:{start_pause:"开始 / 暂停"},water_heater:{set_operation_mode:"设定模式[ 为 {operation_mode}]",set_away_mode:"设定离开模式"}},Vi={components:{date:{day_types_short:{daily:"每天",workdays:"工作日",weekend:"周末"},day_types_long:{daily:"每天",workdays:"工作日",weekend:"周末"},days:"天",tomorrow:"明天",repeated_days:"每 {days}",repeated_days_except:"每天,除了 {excludedDays}",days_range:"从 {startDay} 至 {endDay}",next_week_day:"下{weekday}"},time:{absolute:"{time}",interval:"从 {startTime} 至 {endTime}",at_midnight:"午夜",at_noon:"中午",at_sun_event:"{sunEvent}"}},dialog:{enable_schedule:{title:"完成修改",description:"您修改的计划任务当前已禁用,是否需要启用?"},confirm_delete:{title:"是否删除实体?",description:"您确定要删除此实体吗?"},confirm_migrate:{title:"修改任务",description:"此操作将丢失某些设置。 你想继续吗?"},weekday_picker:{title:"重复周期",choose:"选择..."},entity_picker:{title:"选择实体",choose:"选择...",no_results:"未找到匹配项"},action_picker:{title:"选择动作",show_all:"显示所有受支持的实体"}},panel:{common:{title:"计划任务",new_schedule:"新建任务",default_name:"任务 #{id}"},overview:{no_entries:"无事项",backend_error:"计划任务组件关联失败。使用本卡片前,需先安装计划任务组件(Scheduler component)集成.",excluded_items:"其他{number}项{if number is 1}任务{else}任务",hide_excluded:"隐藏其他任务",additional_tasks:"另有{number}项{if number is 1}任务{else}任务"},editor:{repeated_days:"重复周期",start_time:"开始时间",stop_time:"结束时间",action:"动作",add_action:"添加动作",select_timeslot:"选择时间段",toggle_single_mode:"切换为单次模式",toggle_scheme_mode:"切换为方案模式",validation_errors:{overlapping_time:"计划任务存在重叠的时间段",missing_target_entity:"一个或多个动作缺少目标实体",missing_service_parameter:"一个或多个操作缺少必要设置",missing_action:"计划任务未设置任何动作"}},options:{conditions:{header:"条件",add_condition:"添加条件",new_condition:"新建条件",types:{equal_to:"{entity} 等于 {value}",unequal_to:"{entity} 不等于 {value}",above:"{entity} 大于 {value}",below:"{entity} 小于 {value}"},options:{logic_and:"所有条件必须同时满足(AND)",logic_or:"任意一个条件满足(OR)",track_changes:"当条件变化时重新判断"}},period:{header:"生效时段",start_date:"从",end_date:"到"},repeat_type:"任务完成后的行为",tags:"标签"},card_editor:{tabs:{entities:"实体",other:"其他"},fields:{title:{heading:"卡片标题",options:{standard:"标准",hidden:"隐藏",custom:"自定义"},custom_title:"自定义标题"},discover_existing:{heading:"显示所有计划任务",description:"这将设置‘发现已有任务(discover existing)’参数。已创建的任务会自动添加到卡片中。 "},time_step:{heading:"时间调整步长",description:"创建计划任务时,时间选择器每次点击增加或减少的分钟数",unit_minutes:"分钟"},default_editor:{heading:"新建任务默认模式",options:{single:"单次任务模式",scheme:"时间方案模式"}},sort_by:{heading:"排序方式",description:"计划任务在卡片中的显示顺序",options:{relative_time:"按距离下次执行时间排序",title:"按任务标题排序",state:"优先显示已启用的任务"}},display_format_primary:{heading:"显示的主要信息",description:"设置卡片中显示任务的主要信息",options:{default:"任务名称",entity_action:"任务概要"}},display_format_secondary:{heading:"显示的次要信息",description:"设置卡片中显示任务的次要信息",options:{relative_time:"下次执行的剩余时间",time:"下次执行的设定时间",days:"重复周期(星期)",additional_tasks:"额外任务数量"}},show_header_toggle:{heading:"显示标题开关",description:"在卡片顶部显示切换开关,用于启用/禁用所有实体"},show_toggle_switches:{heading:"显示切换开关",description:"为卡片中的每个单独计划显示切换开关"},tags:{heading:"标签",description:"使用标签可将不同的计划任务分配到多个卡片中"},entities:{button_label:"配置包含的实体(配置后未选择的实体相关任务将会隐藏)",heading:"包含的实体",description:"选择您希望通过计划任务控制的实体。您可以点击一个分组将其展开。请注意,部分实体(例如传感器)只能用作触发条件,而不能作为执行动作。",included_number:"已选择 {number}/{total}"}}}}},Hi={services:Ri,ui:Vi};const Ui={bg:it,cs:nt,de:ct,el:pt,en:yt,es:kt,et:Ot,es_419:kt,fi:At,fr:Mt,he:It,hu:Ht,it:Wt,lv:Jt,nb:ai,nl:ei,nn:ai,no:ai,pl:di,pt:hi,"pt-BR":gi,ro:bi,sk:Object.freeze({__proto__:null,services:ji,ui:Oi,default:Si}),sl:Object.freeze({__proto__:null,services:zi,ui:Ci,default:Ai}),sv:Object.freeze({__proto__:null,services:Ti,ui:Ei,default:Di}),ru:$i,uk:Object.freeze({__proto__:null,services:Mi,ui:Li,default:Pi}),ur:Object.freeze({__proto__:null,services:Ni,ui:Ii,default:qi}),"zh-Hans":Object.freeze({__proto__:null,services:Ri,ui:Vi,default:Hi})};function Fi(e,t,i=[],s=[]){let a;try{a=e.split(".").reduce((e,t)=>e[t],Ui[t.locale.language]),a||(a=e.split(".").reduce((e,t)=>e[t],Ui.en))}catch(t){try{a=e.split(".").reduce((e,t)=>e[t],Ui.en)}catch(e){a=""}}if(i=[i||[]].flat(),s=[s||[]].flat(),i.length&&s.length&&a)for(let e=0;ee.split(".")[1]||"",Wi=e=>e.split(".")[0]||"",Zi=(e,t)=>{var i;return void 0===(null==t?void 0:t.friendly_name)?Bi(e).replace(/_/g," "):(null!==(i=null==t?void 0:t.friendly_name)&&void 0!==i?i:"").toString()};function Ki(e,t){let i=!1;if(!t)return!1;if(e.match(/^[a-z0-9_\.]+$/))i=!e.includes(".")&&t.includes(".")?e==Wi(t):e==t;else try{if(e.startsWith("/")&&e.endsWith("/")||-1!==e.indexOf("*")){e.startsWith("/")||(e=`/^${e=e.replace(/\./g,".").replace(/\*/g,".*")}$/`);i=new RegExp(e.slice(1,-1)).test(t)}}catch(e){}return i}const Gi=(e,t)=>e.includes(".")?((t.include||Ve).some(t=>Ki(t,e))||Object.keys(t.customize||{}).some(t=>Ki(t,e)))&&!(t.exclude||[]).some(t=>Ki(t,e)):((t.include||Ve).map(Wi).some(t=>Ki(t,e))||Object.keys(t.customize||{}).map(Wi).some(t=>Ki(t,e)))&&!(t.exclude||[]).some(t=>Ki(t,e)),Ji=(e,t)=>{var i,s;let a=[],o=!0;if(e.entries.forEach(e=>{e.slots.forEach(e=>{e.actions.forEach(e=>{var t;let i=(null===(t=e.target)||void 0===t?void 0:t.entity_id)?[e.target.entity_id].flat():[e.service];a=[...a,...i]})})}),![...new Set(a)].every(e=>Gi(e,t)))return!1;const n=[t.tags||[]].flat();n.length&&(o=!1,((e.tags||[]).some(e=>n.includes(e))||n.includes("none")&&!(null===(i=e.tags)||void 0===i?void 0:i.length)||n.includes("enabled")&&e.enabled||n.includes("disabled")&&!e.enabled)&&(o=!0));const r=[t.exclude_tags||[]].flat();return r.length&&o&&((e.tags||[]).some(e=>r.includes(e))||r.includes("none")&&!(null===(s=e.tags)||void 0===s?void 0:s.length)||r.includes("enabled")&&e.enabled||r.includes("disabled")&&!e.enabled)&&(o=!1),o},Yi=(e,t)=>((e,t)=>et?1:0)(e.toLowerCase(),t.toLowerCase()),Qi=e=>{let t=e.trim();return t.charAt(0).toUpperCase()+t.slice(1)},Xi=(e,t,i=!0)=>{let s=t.localize(e);return s||!i?s:`{${e.split(".").pop()}}`},es=(e,t)=>{let i=e.mode==be.Sunrise?Xi("ui.panel.config.automation.editor.conditions.type.sun.sunrise",t):Xi("ui.panel.config.automation.editor.conditions.type.sun.sunset",t);"de"!=t.language&&(i=i.toLowerCase());const s=3600*e.hours+60*e.minutes;if(Math.abs(s)<=60)return Fi("ui.components.time.at_sun_event",t,"{sunEvent}",i);let a=Xi(s<0?"ui.panel.config.automation.editor.conditions.type.sun.before":"ui.panel.config.automation.editor.conditions.type.sun.after",t);return a=a.replace(/[^a-z]/gi,"").toLowerCase(),`${Ne(e,{seconds:!1}).split(/\+|\-/).pop()} ${a} ${i}`},ts={alarm_control_panel:{alarm_disarm:{target:{}},alarm_arm_home:{supported_features:1,target:{}},alarm_arm_away:{supported_features:2,target:{}},alarm_arm_night:{supported_features:4,target:{}},alarm_arm_custom_bypass:{supported_features:16,target:{}},alarm_arm_vacation:{supported_features:32,target:{}}},automation:{turn_on:{translation_key:"services.generic.turn_on",target:{}},turn_off:{translation_key:"services.generic.turn_off",target:{}},trigger:{target:{}}},button:{press:{target:{}}},climate:{turn_off:{translation_key:"services.generic.turn_off",target:{},supported_features:128},turn_on:{translation_key:"services.generic.turn_on",target:{},supported_features:256},set_hvac_mode:{translation_key:"services.climate.set_hvac_mode",target:{},fields:{hvac_mode:{}}},set_temperature:{translation_key:["services.climate.set_temperature","services.climate.set_temperature_hvac_mode_heat","services.climate.set_temperature_hvac_mode_cool","services.climate.set_temperature_hvac_mode_heat_cool"],target:{},fields:{temperature:{supported_features:1},target_temp_high:{supported_features:2},target_temp_low:{supported_features:2},hvac_mode:{optional:!0}}},set_preset_mode:{translation_key:"services.climate.set_preset_mode",supported_features:16,target:{},fields:{preset_mode:{}}},set_fan_mode:{translation_key:"services.climate.set_fan_mode",supported_features:8,target:{},fields:{fan_mode:{}}},set_swing_mode:{translation_key:"services.climate.set_swing_mode",supported_features:32,target:{},fields:{swing_mode:{}}}},cover:{close_cover:{translation_key:"services.cover.close_cover",supported_features:2,target:{}},open_cover:{translation_key:"services.cover.open_cover",supported_features:1,target:{}},set_cover_position:{translation_key:"services.cover.set_cover_position",supported_features:4,target:{},fields:{position:{}}},close_cover_tilt:{supported_features:32,target:{}},open_cover_tilt:{supported_features:16,target:{}},set_cover_tilt_position:{translation_key:"services.cover.set_cover_tilt_position",supported_features:128,target:{},fields:{tilt_position:{}}}},fan:{turn_on:{translation_key:"services.generic.turn_on",target:{}},turn_off:{translation_key:"services.generic.turn_off",target:{}},set_percentage:{translation_key:"services.fan.set_percentage",supported_features:1,target:{},fields:{percentage:{}}},oscillate:{translation_key:"services.fan.oscillate",supported_features:2,target:{},fields:{oscillating:{}}},set_direction:{translation_key:"services.fan.set_direction",supported_features:4,target:{},fields:{direction:{}}},set_preset_mode:{translation_key:"services.climate.set_preset_mode",supported_features:8,target:{},fields:{preset_mode:{}}}},humidifier:{turn_on:{translation_key:"services.generic.turn_on",target:{}},turn_off:{translation_key:"services.generic.turn_off",target:{}},set_humidity:{translation_key:"services.humidifier.set_humidity",target:{},fields:{humidity:{}}},set_mode:{translation_key:"services.humidifier.set_mode",supported_features:1,target:{},fields:{mode:{}}}},input_boolean:{turn_on:{translation_key:"services.generic.turn_on",target:{}},turn_off:{translation_key:"services.generic.turn_off",target:{}}},input_button:{press:{target:{}}},input_number:{set_value:{translation_key:"services.input_number.set_value",target:{},fields:{value:{}}}},input_select:{select_option:{translation_key:"services.input_select.select_option",target:{},fields:{option:{}}}},lawn_mower:{start_mowing:{target:{},supported_features:1},pause:{target:{},supported_features:2},dock:{target:{},supported_features:4}},light:{turn_on:{translation_key:"services.light.turn_on",target:{},fields:{brightness:{optional:!0}}},turn_off:{translation_key:"services.generic.turn_off",target:{}}},lock:{lock:{target:{}},unlock:{target:{}}},media_player:{turn_on:{translation_key:"services.generic.turn_on",target:{}},turn_off:{translation_key:"services.generic.turn_off",target:{}},select_source:{translation_key:"services.media_player.select_source",supported_features:2048,target:{},fields:{source:{}}}},notify:{"{entity_id}":{translation_key:"services.notify.send_message",fields:{title:{optional:!0},message:{}}}},number:{set_value:{translation_key:"services.input_number.set_value",target:{},fields:{value:{}}}},scene:{turn_on:{target:{}}},script:{"{entity_id}":{translation_key:"services.script.execute"}},select:{select_option:{translation_key:"services.input_select.select_option",target:{},fields:{option:{}}}},switch:{turn_on:{translation_key:"services.generic.turn_on",target:{}},turn_off:{translation_key:"services.generic.turn_off",target:{}}},vacuum:{turn_on:{translation_key:"services.generic.turn_on",supported_features:1,target:{}},start:{supported_features:8192,target:{}},play_pause:{target:{}}},valve:{open_valve:{supported_features:1,target:{}},close_valve:{supported_features:2,target:{}},set_valve_position:{translation_key:"services.cover.set_cover_position",supported_features:4,target:{},fields:{position:{}}}},water_heater:{set_temperature:{translation_key:"services.climate.set_temperature",supported_features:1,target:{},fields:{temperature:{}}},set_operation_mode:{translation_key:"services.water_heater.set_operation_mode",supported_features:2,target:{},fields:{operation_mode:{}}},set_away_mode:{translation_key:"services.water_heater.set_away_mode",supported_features:4,target:{},fields:{away_mode:{}}},turn_off:{translation_key:"services.generic.turn_off",target:{},supported_features:8},turn_on:{translation_key:"services.generic.turn_on",target:{},supported_features:8}}},is=e=>{if("object"!=typeof e)return null;if(!Object.keys(e).length||!Object.keys(e).every(e=>"string"==typeof e))return null;let t={value:"",label:""};return Object.keys(e).includes("name")?t=Object.assign(Object.assign({},t),{label:String(e.name)}):Object.keys(e).includes("label")?t=Object.assign(Object.assign({},t),{label:String(e.label)}):Object.keys(e).includes("value")&&(t=Object.assign(Object.assign({},t),{label:String(e.value)})),Object.keys(e).includes("value")?t=Object.assign(Object.assign({},t),{value:String(e.value)}):Object.keys(e).includes("name")?t=Object.assign(Object.assign({},t),{value:String(e.name)}):Object.keys(e).includes("label")&&(t=Object.assign(Object.assign({},t),{value:String(e.label)})),Object.keys(e).includes("icon")&&Be(e.icon)&&(t=Object.assign(Object.assign({},t),{icon:String(e.icon)})),t.value.length&&t.label.length?t:null},ss=e=>{let t={select:{options:Array.isArray(e.options)?e.options.every(e=>"string"==typeof e)?e.options:e.options.map(is).filter(Be):[]}};return e.translation_key&&(t=Object.assign(Object.assign({},t),{select:Object.assign(Object.assign({},t.select),{translation_key:e.translation_key})})),t},as=(e,t)=>{if(e.hasOwnProperty("service")&&e.service!==t.service)return!1;const i=e.service_data||{},s=t.service_data||{},a=e.hasOwnProperty("variables")?e.variables||{}:e.hasOwnProperty("fields")&&e.fields||{};let o=[...new Set([...Object.keys(i),...Object.keys(s),...Object.keys(a)])];return o=o.filter(e=>"entity_id"!=e),!!o.every(e=>{var t;if(Object.keys(i).includes(e)&&Object.keys(s).includes(e))return he(i[e],s[e]);if(Object.keys(a).includes(e)){let i=a[e],o=s[e];if(Object.keys(i).includes("options")){let e=ss({options:i.options});return!Be(o)||(null===(t=e.select)||void 0===t?void 0:t.options.find(e=>"string"==typeof e?e==o:e.value==o))}return!Object.keys(i).includes("min")||!Object.keys(i).includes("max")||(!(Be(o)||!Object.keys(i).includes("optional")||!i.optional)||"number"==typeof o)}return!1})},os=(e,t)=>{let i=[];return Object.keys(e).filter(t=>{var i;return null===(i=e[t].actions)||void 0===i?void 0:i.length}).filter(i=>!t||!t.includes(".")&&Ki(Wi(i),t)||Ki(i,t)||t.includes(".")&&e[i].actions.find(e=>e.service==t)).forEach(s=>{Object.values(e[s].actions).forEach(e=>{if(e.service.includes(".")||(e=Object.assign(Object.assign({},e),{service:`${Wi(s)}.${e.service}`})),s.includes(".")&&"script"!=Wi(s)&&(e=Object.assign(Object.assign({},e),{target:{entity_id:s}})),"script"!=Wi(s)&&(!s.includes(".")||"script"==Wi(t||""))){if((null==t?void 0:t.includes("."))&&e.service!=t)return;e=Object.assign(Object.assign({},e),{target:Object.assign(Object.assign({},e.target),{domain:s})})}i.push({service:e.service,service_data:e.service_data||{},target:e.target?e.target:void 0,name:e.name||"",icon:e.icon||"",variables:e.variables})})}),i},ns=(e,t)=>{var i;const s=Wi(e.service),a=Bi(e.service);let o,n={};if(Object.keys(ts).includes(s)&&(Object.keys(ts[s]).includes(a)?n=Object.assign(Object.assign({},n),ts[s][a]):Object.keys(ts[s]).includes("{entity_id}")&&(n=Object.assign(Object.assign({},n),ts[s]["{entity_id}"]))),!t)return n;o=["script","notify"].includes(s)?e.service:null===(i=e.target)||void 0===i?void 0:i.entity_id,o||(o=e.service);const r=os(t,[o].flat().pop());if(r.length){let t=r.map(t=>{if(!as(t,e))return null;let i={};return Object.keys(t.variables||{}).forEach(e=>{i=Object.assign(Object.assign({},i),{fields:Object.assign(Object.assign({},i.fields||{}),{[e]:{}})})}),Object.assign(Object.assign({},i),{name:t.name||n.name,icon:t.icon||n.icon,target:t.target||n.target})}).filter(Be);if(t.length&&!as(n,e))return t[0]}return n},rs=e=>{const t=Math.pow(10,5);return e=Math.round(e*t)/t},ds=(e,t,i)=>{if(!t)return e;if(Object.keys(t).includes("select")&&t.select){const s=t.select;let a=s.options.map(e=>"string"==typeof e?Object({value:e,label:e}):e),o=null==a?void 0:a.find(t=>t.value==e);s.translation_key?e=Xi(s.translation_key.replace("${value}",e),i,!1)||o?null==o?void 0:o.label:e:o&&(e=o.label)}if(Object.keys(t).includes("number")&&t.number){const i=t.number;if(e=Number(e),"number"==typeof(null==i?void 0:i.scale_factor)&&(e/=i.scale_factor),"number"==typeof(null==i?void 0:i.step)&&(e=Math.round(e/i.step)*i.step),e=rs(e),null==i?void 0:i.unit)return`${e}${i.unit}`}return Object.keys(t).includes("boolean")&&t.boolean&&(e=Boolean(e)?"True":"False"),e},ls=e=>{let t={number:{}};return Object.keys(e).includes("min")&&!isNaN(Number(e.min))&&(t=Object.assign(Object.assign({},t),{number:Object.assign(Object.assign({},t.number),{min:Number(e.min)})})),Object.keys(e).includes("max")&&!isNaN(Number(e.max))&&(t=Object.assign(Object.assign({},t),{number:Object.assign(Object.assign({},t.number),{max:Number(e.max)})})),Object.keys(e).includes("step")&&!isNaN(Number(e.step))&&e.step>0&&(t=Object.assign(Object.assign({},t),{number:Object.assign(Object.assign({},t.number),{step:Number(e.step)})})),Object.keys(e).includes("mode")&&["box","slider"].includes(e.mode)&&(t=Object.assign(Object.assign({},t),{number:Object.assign(Object.assign({},t.number),{mode:e.mode})})),Object.keys(e).includes("unit")&&e.unit&&(t=Object.assign(Object.assign({},t),{number:Object.assign(Object.assign({},t.number),{unit:e.unit})})),Object.keys(e).includes("optional")&&(t=Object.assign(Object.assign({},t),{number:Object.assign(Object.assign({},t.number),{optional:Boolean(e.optional)})})),Object.keys(e).includes("scale_factor")&&!isNaN(Number(e.scale_factor))&&(t=Object.assign(Object.assign({},t),{number:Object.assign(Object.assign({},t.number),{scale_factor:Number(e.scale_factor)})})),t},cs={alarm_control_panel:{services:{alarm_arm_away:"mdi:shield-lock",alarm_arm_home:"mdi:shield-home",alarm_arm_night:"mdi:shield-moon",alarm_custom_bypass:"mdi:security",alarm_disarm:"mdi:shield-off",alarm_trigger:"mdi:bell-ring",alarm_arm_vacation:"mdi:shield-airplane"}},automation:{services:{turn_on:"mdi:robot",turn_off:"mdi:robot-off",trigger:"mdi:play"}},button:{services:{press:"mdi:gesture-tap-button"}},climate:{services:{set_temperature:"mdi:thermometer",set_hvac_mode:"mdi:cog-transfer-outline",set_preset_mode:"mdi:cloud-download-outline",set_fan_mode:"mdi:fan",set_humidity:"mdi:water-percent",set_swing_mode:"mdi:arrow-oscillating"},attributes:{hvac_mode:{auto:"mdi:autorenew",cool:"mdi:snowflake",dry:"mdi:water-percent",fan_only:"mdi:fan",heat:"mdi:fire",heat_cool:"mdi:thermometer",off:"mdi:power"},preset_mode:{activity:"mdi:motion-sensor",away:"mdi:account-arrow-right",boost:"mdi:rocket-launch",comfort:"mdi:sofa",eco:"mdi:leaf",home:"mdi:home",sleep:"mdi:bed"},fan_mode:{diffuse:"mdi:weather-windy",focus:"mdi:target",high:"mdi:speedometer",low:"mdi:speedometer-slow",medium:"mdi:speedometer-medium",middle:"mdi:speedometer-medium",off:"mdi:fan-off",on:"mdi:fan"},swing_mode:{both:"mdi:arrow-all",horizontal:"mdi:arrow-left-right",off:"mdi:arrow-oscillating-off",on:"mdi:arrow-oscillating",vertical:"mdi:arrow-up-down"}}},cover:{services:{close_cover:"mdi:arrow-down-box",close_cover_tilt:"mdi:arrow-bottom-left",open_cover:"mdi:arrow-up-box",open_cover_tilt:"mdi:arrow-top-right",set_cover_position:"mdi:arrow-down-box",set_cover_tilt_position:"mdi:arrow-top-right"}},fan:{services:{oscillate:"mdi:arrow-oscillating",set_percentage:"mdi:fan",set_preset_mode:"mdi:fan-auto",turn_off:"mdi:fan-off",turn_on:"mdi:fan"}},humidifier:{services:{set_humidity:"mdi:water-percent",set_mode:"mdi:air-humidifier",turn_off:"mdi:air-humidifier-off",turn_on:"mdi:air-humidifier"},attributes:{mode:{auto:"mdi:refresh-auto",away:"mdi:account-arrow-right",baby:"mdi:baby-carriage",boost:"mdi:rocket-launch",comfort:"mdi:sofa",eco:"mdi:leaf",home:"mdi:home",normal:"mdi:water-percent",sleep:"mdi:power-sleep"}}},input_boolean:{services:{turn_off:"mdi:toggle-switch-off",turn_on:"mdi:toggle-switch"}},input_button:{services:{press:"mdi:gesture-tap-button"}},input_number:{services:{set_value:"mdi:counter"}},input_select:{services:{select_option:"mdi:counter"}},lawn_mower:{services:{dock:"mdi:home-import-outline",start_mowing:"mdi:play",pause:"mdi:pause"}},light:{services:{turn_off:"mdi:lightbulb-off",turn_on:"mdi:lightbulb-on"}},lock:{services:{lock:"mdi:lock",unlock:"mdi:lock-open"}},media_player:{services:{media_play:"mdi:play",media_stop:"mdi:stop",play_media:"mdi:play",select_source:"mdi:import",turn_off:"mdi:power",turn_on:"mdi:power"}},notify:{services:{"{entity_id}":"mdi:message-alert"}},scene:{services:{turn_on:"mdi:play"}},script:{services:{turn_on:"mdi:flash",turn_off:"mdi:flash-off","{entity_id}":"mdi:play"}},select:{services:{select_option:"mdi:counter"}},switch:{services:{turn_off:"mdi:toggle-switch-variant-off",turn_on:"mdi:toggle-switch-variant"}},vacuum:{services:{send_command:"mdi:send",start:"mdi:play",turn_off:"mdi:stop",turn_on:"mdi:play"}},valve:{services:{open_valve:"mdi:valve-open",close_valve:"mdi:valve-closed",set_valve_position:"mdi:valve"}},water_heater:{services:{set_away_mode:"mdi:account-arrow-right",set_operation_mode:"mdi:water-boiler",set_temperature:"mdi:thermometer",turn_off:"mdi:water-boiler-off",turn_on:"mdi:water-boiler"},attributes:{operation_mode:{eco:"mdi:leaf",electric:"mdi:lightning-bolt",gas:"mdi:fire-circle",heat_pump:"mdi:heat-wave",high_demand:"mdi:finance",off:"mdi:power",performance:"mdi:rocket-launch"}}}},us=(e,t,i,s,a)=>{const o=Wi(e),n=["script","notify"].includes(o)?[e]:[t||[]].flat();let r=n.map(e=>hs(e,i,s)),d=_s(r),l=n.map(t=>ms(e,t,i,a));return _s(l)||d},hs=(e,t,i)=>{const s=Object.keys(i.states).includes(e)?i.states[e]:null,a=(null==s?void 0:s.attributes)||{},o=Wi(e),n=`${o}.${t}`,r=e=>{var i,s;const a=null===(s=null===(i=cs[o])||void 0===i?void 0:i.attributes)||void 0===s?void 0:s[t],n=!!a&&(e||[]).every(e=>e in a);return(e||[]).map(e=>({value:e,label:e,icon:n?a[e]:void 0}))};switch(n){case"climate.temperature":case"climate.target_temp_low":case"climate.target_temp_high":{const e="climate.temperature"==n?(2&(a.supported_features||0))>0:(1&(a.supported_features||0))>0,t=i.config.unit_system.temperature.includes("F")?1:.5;return ls({min:a.min_temp,max:a.max_temp,step:a.target_temp_step||t,unit:""+i.config.unit_system.temperature,optional:e})}case"climate.hvac_mode":return ss({options:r(a.hvac_modes),translation_key:"component.climate.entity_component._.state.${value}"});case"climate.preset_mode":return ss({options:r(a.preset_modes),translation_key:"state_attributes.climate.preset_mode.${value}"});case"climate.fan_mode":return ss({options:r(a.fan_modes)});case"climate.swing_mode":return ss({options:r(a.swing_modes)});case"cover.position":case"cover.tilt_position":case"fan.percentage":case"valve.position":return ls({min:0,max:100,step:1,unit:"%"});case"fan.oscillating":return{boolean:{}};case"fan.direction":return ss({options:r(["forward","reverse"]),translation_key:"ui.card.fan.${value}"});case"fan.preset_mode":return ss({options:r(a.preset_modes)});case"humidifier.humidity":return ls({min:a.min_humidity,max:a.max_humidity,step:1,unit:"%"});case"humidifier.mode":return ss({options:r(a.available_modes),translation_key:"component.humidifier.entity_component._.state_attributes.mode.state.${value}"});case"input_number.value":case"number.value":return ls({min:a.min,max:a.max,step:a.step,mode:a.mode,unit:a.unit_of_measurement});case"input_select.option":case"select.option":return ss({options:r(a.options)});case"light.brightness":return ls({min:0,max:100,step:1,unit:"%",scale_factor:2.55});case"media_player.source":case"notify.title":case"notify.message":return{text:{}};case"water_heater.temperature":{const e=i.config.unit_system.temperature.includes("F")?1:.5;return ls({min:a.min_temp,max:a.max_temp,step:a.target_temp_step||e,unit:""+i.config.unit_system.temperature})}case"water_heater.operation_mode":return ss({options:r(a.operation_list)});case"water_heater.away_mode":return{boolean:{}}}return null},ms=(e,t,i,s)=>{const a=os(s||{},t);if(a.length){let t=a.map(t=>{if(t.service!=e||!Object.keys(t.variables||{}).includes(i))return null;let s=(t.variables||{})[i];return ps(s)}).filter(e=>void 0!==e);return _s(t)}return null},ps=e=>Object.keys(e).includes("options")?ss({options:e.options}):Object.keys(e).includes("min")&&Object.keys(e).includes("max")?ls(e):{text:{}},_s=e=>{const t=e=>1==new Set(e).size;if(e.some(e=>null===e)||!e.length)return null;if(e.every(e=>e.hasOwnProperty("select"))){const i=e.map(e=>e.select.options).filter(e=>void 0!==e);let s=[];if(i.every(e=>e.every(e=>"string"==typeof e)))s=i.length?i.reduce((e,t)=>e.filter(e=>t.includes(e))):[];else{let e=i.map(e=>e.map(e=>is("object"==typeof e?e:{value:e})).filter(Be));s=e.length?e.reduce((e,t)=>e.filter(e=>t.find(t=>t.value===e.value))):[]}const a=e.map(e=>e.select.translation_key).filter(e=>void 0!==e);return{select:{options:s.length?s:[],translation_key:a.length&&t(a)?a[0]:void 0}}}if(e.every(e=>e.hasOwnProperty("number"))){const i=e.map(e=>e.number.min).filter(e=>void 0!==e),s=e.map(e=>e.number.max).filter(e=>void 0!==e),a=e.map(e=>e.number.step).filter(e=>void 0!==e),o=e.map(e=>e.number.mode).filter(e=>void 0!==e),n=e.map(e=>e.number.unit).filter(e=>void 0!==e),r=e.map(e=>e.number.optional),d=e.map(e=>e.number.scale_factor).filter(e=>void 0!==e);return{number:{min:i.length?Math.max(...i):void 0,max:s.length?Math.min(...s):void 0,step:a.length?Math.max(...a):void 0,mode:o.length&&t(o)?o[0]:void 0,unit:n.length&&t(n)?n[0]:void 0,optional:r.every(e=>e),scale_factor:d.length&&t(d)?d[0]:void 0}}}return e.every(e=>e.hasOwnProperty("boolean"))?{boolean:{}}:e.every(e=>e.hasOwnProperty("text"))?{text:{}}:null},gs=/\[([^\]]+)\]/,vs=/\{([^\}]+)\}/,ys=(e,t)=>{const i=Bi(t.service);return-1!=e.indexOf(i)&&(e=e.substring(e.indexOf(i)+i.length+1)),Object.keys(t.service_data).reduce((i,s)=>{if(-1==e.indexOf(s))return i;let a=e.substring(e.indexOf(s)+s.length+1);return a==t.service_data[s]?i+e.length+a.length+1:i},0)},fs=(e,t,i,s=!1,a=!1)=>{const o=ns(e,i);let n,r=o.name||"",d=Object.fromEntries(Object.entries(e.service_data).filter(([e,t])=>Be(t)).map(([s,a])=>{var o;const n=us(e.service,null===(o=e.target)||void 0===o?void 0:o.entity_id,s,t,i);return n?[s,ds(a,n,t)]:[s,null]}).filter(([e,t])=>Be(t)));if(s){if(Object.keys(d).length>1){const e=(e,t)=>{const i=!!o.fields&&o.fields[e]||{},s=!!o.fields&&o.fields[t]||{};return(null==i?void 0:i.optional)&&!s.optional?1:(null==s?void 0:s.optional)&&!i.optional||et?1:0};return d=Object.fromEntries(Object.entries(d).sort(([t],[i])=>e(t,i))),Object.values(d).join(", ")}if(Object.keys(d).length)return Object.values(d)[0]}if((null==o?void 0:o.translation_key)&&!r){let i="";if(Array.isArray(o.translation_key)){let t=o.translation_key;t.sort((t,i)=>{let s=ys(t,e),a=ys(i,e);return s!=a?a-s:t.length-i.length}),i=t[0]}else i=o.translation_key;r=Fi(i,t,Object.keys(d).map(e=>`{${e}}`),Object.values(d))}else{const i=Wi(e.service),s=Bi(e.service);r||(r=Xi(`component.${i}.services.${s}.name`,t,!1)),!r&&Object.keys(t.services[i]||{}).includes(s)&&(r=t.services[i][s].name||""),r||(r=s.replace(/_/g," "))}let l,c=0;for(;(n=gs.exec(r))&&c<100;){c++;let t=n[1].match(vs);r=t&&Object.keys(e.service_data||{}).includes(t[1])&&Object.keys(d).includes(t[1])?r.replace(n[0],n[1].replace(t[0],d[t[1]])):r.replace(n[0],"")}for(c=0;(l=vs.exec(r))&&c<100;)c++,r=Object.keys(d).includes(l[1])?r.replace(l[0],d[l[1]]):r.replace(l[0],"");if(a&&null!==/<.+?>/g.exec(r)){r=(new DOMParser).parseFromString(r,"text/html").body.textContent||""}return r},bs=()=>{try{(new Date).toLocaleDateString("i")}catch(e){return"RangeError"===e.name}return!1},ws=["sunday","monday","tuesday","wednesday","thursday","friday","saturday"],ks=(e,t,i)=>{let s;if(e instanceof Date){e.getDay();if(s=pe.Friday,bs())return e.toLocaleDateString(i.locale.language,{weekday:t});e.getDay();s=pe.Friday}else s=e;switch(s){case pe.Daily:return Fi(`ui.components.date.day_types_${t}.daily`,i);case pe.Workday:return Fi(`ui.components.date.day_types_${t}.workdays`,i);case pe.Weekend:return Fi(`ui.components.date.day_types_${t}.weekend`,i);case pe.Monday:case pe.Tuesday:case pe.Wednesday:case pe.Thursday:case pe.Friday:case pe.Saturday:case pe.Sunday:let e=new Date(2017,1,26),a=ws.findIndex(e=>e==s);return bs()?(e.setDate(e.getDate()+a),e.toLocaleDateString(i.locale.language,{weekday:t})):ws[a];default:return""}},xs=e=>{let t=e.locale.first_weekday;if(t&&"language"!=t)return $s.map(e=>e.toLowerCase()).findIndex(e=>e==t);if("weekInfo"in Intl.Locale.prototype)return new Intl.Locale(e.locale.language).weekInfo.firstDay%7;{const t="AEAFBHDJDZEGIQIRJOKWLYOMQASDSY".match(/../g),i="AGARASAUBDBRBSBTBWBZCACNCODMDOETGTGUHKHNIDILINJMJPKEKHKRLAMHMMMOMTMXMZNINPPAPEPHPKPRPTPYSASGSVTHTTTWUMUSVEVIWSYEZAZW".match(/../g),s=["ar","arq","arz","fa"],a="amasbndzengnguhehiidjajvkmknkolomhmlmrmtmyneomorpapssdsmsnsutatethtnurzhzu".match(/../g),o=e.locale.language.match(/^([a-z]{2,3})(?:-([a-z]{3})(?=$|-))?(?:-([a-z]{4})(?=$|-))?(?:-([a-z]{2}|\d{3})(?=$|-))?/i);return o[1]?i.includes(o[4])?0:t.includes(o[4])?6:1:a.includes(o[1])?0:s.includes(o[1])?6:1}},$s=[pe.Sunday,pe.Monday,pe.Tuesday,pe.Wednesday,pe.Thursday,pe.Friday,pe.Saturday],js=(e,t,i)=>{let s="";const a=xs(i);let o=((e,t)=>e.concat(e).slice(t,t+e.length))($s,a);e.sort((e,t)=>o.findIndex(t=>t==e)-o.findIndex(e=>e==t));const n=e.filter(e=>o.includes(e)).map(e=>o.findIndex(t=>t==e)),r=(e=>{const t=[];for(let i=0;in.length&&(s+=e.filter(e=>!o.includes(e)).map(e=>ks(e,t,i)).join(", "),s+=", "),6==n.length){const e=[0,1,2,3,4,5,6].filter(e=>!n.includes(e)),a=ks(o[e.pop()],t,i);s+=Fi("ui.components.date.repeated_days_except",i,"{excludedDays}",a)}else{const e=n.map(e=>ks(o[e],t,i));if(n.length>=3&&d>=3){const t=r.reduce((e,t,i)=>t==d?i:e,0);e.splice(t,d,Fi("ui.components.date.days_range",i,["{startDay}","{endDay}"],[e[t],e[t+d-1]]))}const a=e.length>1?`${e.slice(0,-1).join(", ")} ${Xi("ui.common.and",i)} ${e.pop()}`:""+e.pop();s+=n.length>=3&&d>=3?a:Fi("ui.components.date.repeated_days",i,"{days}",a)}return s}return e.map(e=>ks(e,t,i)).join(", ")},Os=(e,t,i)=>{const s=Object.entries(i||{}).filter(([t,i])=>Ki(t,e)&&i.name).map(([e,t])=>t.name);return s.filter(Be).length?s.filter(Be)[0]:Object.keys(t.states).includes(e)&&t.states[e].attributes.friendly_name?t.states[e].attributes.friendly_name:Bi(e).replace(/_/g," ")},Ss=(e,t,i,s)=>{const a=t=>{var o,n;switch(t){case ve.Action:const r=e.entries[0].slots[e.next_entries[0]||0].actions[0];return Qi(fs(r,i,s));case ve.Days:return Qi(js(e.entries[0].weekdays,"long",i));case ve.Name:return Qi(e.name||"");case ve.AdditionalTasks:return e.entries[0].slots.length>1?"+"+Fi("ui.panel.overview.additional_tasks",i,"{number}",String(e.entries[0].slots.length-1)):"";case ve.Entity:const d=e.entries[0].slots[e.next_entries[0]||0].actions[0];let l=[(null===(o=d.target)||void 0===o?void 0:o.entity_id)||[]].flat();!l.length&&["script","notify"].includes(Wi(d.service))&&(l=[d.service]);const c=l.map(e=>Os(e,i,s)).join(", ");return Qi(c);case ve.RelativeTime:return"";case ve.Tags:return null===(n=e.tags)||void 0===n?void 0:n.map(e=>`${e}`).join("");case ve.Time:const u=e.entries[0].slots[e.next_entries[0]||0],h=((e,t,i)=>{const s=De(i.locale);if(t){const a=Ce(e),o=Ce(t),n=a.mode==be.Fixed?Ne(a,{am_pm:s}):es(a,i),r=o.mode==be.Fixed?Ne(o,{am_pm:s}):es(o,i);return Qi(Fi("ui.components.time.interval",i,["{startTime}","{endTime}"],[n,r]))}{const t=Ce(e),a=t.mode==be.Fixed?Ne(t,{am_pm:s}):es(t,i);return Qi(Fi("ui.components.time.absolute",i,"{time}",a))}})(u.start,u.stop,i);if(h&&h.trim())return Qi(h);const m=e=>{try{const t=e.split(":").map(Number);return`${String(t[0]).padStart(2,"0")}:${String(t[1]).padStart(2,"0")}`}catch(t){return String(e)}};return Qi(u.stop?`${m(u.start)} - ${m(u.stop)}`:""+m(u.start));case ve.Default:const p=a(ve.Name);return p||`${a(ve.Entity)}: ${a(ve.Action)}`;default:const _=/\{([^\}]+)\}/;let g;for(;g=_.exec(t);)t=t.replace(g[0],String(a(String(g[1]))));return t}};return[...[t].flat()].map(e=>{let t=a(e);return t||""})},zs=(e,t)=>{const i=new Date(e.timestamps[e.next_entries[0]]).valueOf(),s=new Date(t.timestamps[t.next_entries[0]]).valueOf(),a=(new Date).valueOf(),o=i=a?1:i>=a&&ss?o?-1:1:i{const s=[t.sort_by].flat();return s.includes("relative-time")&&(e=e.sort(zs)),s.includes("title")&&(e=e.sort((e,s)=>{var a;return((e,t,i,s,a)=>{try{const o=Ss(e,i,s,a).join(),n=Ss(t,i,s,a).join();return Yi(o,n)}catch(e){return 0}})(e,s,(null===(a=t.display_options)||void 0===a?void 0:a.primary_info)||"default",i,t.customize)})),s.includes("state")&&(e=e.sort((e,t)=>((e,t,i,s)=>{var a,o;const n=null===(a=i.states[e.entity_id])||void 0===a?void 0:a.state,r=null===(o=i.states[t.entity_id])||void 0===o?void 0:o.state,d=["on","triggered"].includes(n),l=["on","triggered"].includes(r);if(d&&!l)return-1;if(!d&&l)return 1;if(s){if("off"!=n&&"off"==r)return 1;if("off"==n&&"off"!=r)return-1}return 0})(e,t,i,s.includes("relative-time")))),e},As=(e,t)=>e.callWS({type:"scheduler/item",schedule_id:t}).then(e=>$e(e)),Ts=(e,t,i,s)=>{s=s||{},i=null==i?{}:i;const a=new Event(t,{bubbles:void 0===s.bubbles||s.bubbles,cancelable:Boolean(s.cancelable),composed:void 0===s.composed||s.composed});return a.detail=i,e.dispatchEvent(a),a},Es=async e=>{let t={};const i=Object.keys(e.states).filter(e=>"script"==Wi(e));return i.length&&await e.callWS({type:"config/entity_registry/get_entries",entity_ids:i}).then(e=>{t=Object.fromEntries(Object.entries(e).map(([,e])=>(e=>{let t={};const i=`${e.platform}.${e.unique_id}`;return e.name&&(t=Object.assign(Object.assign({},t),{name:e.name})),e.icon&&(t=Object.assign(Object.assign({},t),{icon:e.icon})),[i,t]})(e)).filter(([,e])=>Object.keys(e).length))}),t},Ds=e=>e.callWS({type:"scheduler/tags"});var Ms="M7.41,8.58L12,13.17L16.59,8.58L18,10L12,16L6,10L7.41,8.58Z",Ls="M15.41,16.58L10.83,12L15.41,7.41L14,6L8,12L14,18L15.41,16.58Z",Ps="M19,6.41L17.59,5L12,10.59L6.41,5L5,6.41L10.59,12L5,17.59L6.41,19L12,13.41L17.59,19L19,17.59L13.41,12L19,6.41Z",Ns="M12,16A2,2 0 0,1 14,18A2,2 0 0,1 12,20A2,2 0 0,1 10,18A2,2 0 0,1 12,16M12,10A2,2 0 0,1 14,12A2,2 0 0,1 12,14A2,2 0 0,1 10,12A2,2 0 0,1 12,10M12,4A2,2 0 0,1 14,6A2,2 0 0,1 12,8A2,2 0 0,1 10,6A2,2 0 0,1 12,4Z",Is="M20.71,7.04C21.1,6.65 21.1,6 20.71,5.63L18.37,3.29C18,2.9 17.35,2.9 16.96,3.29L15.12,5.12L18.87,8.87M3,17.25V21H6.75L17.81,9.93L14.06,6.18L3,17.25Z",qs="M11,13.5V21.5H3V13.5H11M12,2L17.5,11H6.5L12,2M17.5,13C20,13 22,15 22,17.5C22,20 20,22 17.5,22C15,22 13,20 13,17.5C13,15 15,13 17.5,13Z";const Rs={alarm_control_panel:"mdi:alarm-light-outline",air_quality:"mdi:air-filter",alert:"mdi:alert",automation:"mdi:robot",binary_sensor:"mdi:radiobox-blank",button:"mdi:gesture-tap-button",camera:"mdi:camera",calendar:"mdi:calendar",cover:"mdi:window-shutter",climate:"mdi:thermostat",configurator:"mdi:cog",conversation:"mdi:microphone-message",counter:"mdi:counter",date:"mdi:calendar",datetime:"mdi:calendar-clock",demo:"mdi:home-assistant",device_tracker:"mdi:account",fan:"mdi:fan",google_assistant:"mdi:google-assistant",group:"mdi:google-circles-communities",homeassistant:"mdi:home-assistant",homekit:"mdi:home-automation",humidifier:"mdi:air-humidifier",image_processing:"mdi:image-filter-frames",image:"mdi:image",input_boolean:"mdi:toggle-switch",input_button:"mdi:button-pointer",input_datetime:"mdi:calendar-clock",input_number:"mdi:ray-vertex",input_select:"mdi:format-list-bulleted",input_text:"mdi:form-textbox",lawn_mower:"mdi:robot-mower",light:"mdi:lightbulb",lock:"mdi:lock-open-outline",media_player:"mdi:cast-connected",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",schedule:"mdi:calendar-clock",script:"mdi:script-text",select:"mdi:format-list-bulleted",sensor:"mdi:eye",simple_alarm:"mdi:bell",siren:"mdi:bullhorn",stt:"mdi:microphone-message",sun:"mdi:white-balance-sunny",switch:"mdi:flash",text:"mdi:form-textbox",time:"mdi:clock",timer:"mdi:timer-outline",todo:"mdi:clipboard-list",tts:"mdi:speaker-message",vacuum:"mdi:robot-vacuum",valve:"mdi:valve-closed",wake_word:"mdi:chat-sleep",water_heater:"mdi:water-boiler",weather:"mdi:weather-partly-cloudy",zone:"mdi:map-marker-radius"},Vs=e=>Object.keys(Rs).includes(e)?Rs[e]:"mdi:help",Hs=(e,t)=>{let i=Object.keys(e.services).filter(e=>((e,t)=>{let i=Object.keys(ts).includes(e);return!i&&t?Object.keys(t).map(Wi).includes(e):i})(e,t.customize));return i=i.filter(e=>Gi(e,t)),i.map(t=>({key:t,name:Xi(`component.${t}.title`,e,!1)||t.replace(/_/g," "),description:"",icon:Vs(t)}))},Us=1,Fs=2,Bs=e=>(...t)=>({_$litDirective$:e,values:t});class Ws{constructor(e){}get _$AU(){return this._$AM._$AU}_$AT(e,t,i){this._$Ct=e,this._$AM=t,this._$Ci=i}_$AS(e,t){return this.update(e,t)}update(e,t){return this.render(...t)}} +/** + * @license + * Copyright 2018 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */const Zs=Bs(class extends Ws{constructor(e){var t;if(super(e),e.type!==Us||"style"!==e.name||(null===(t=e.strings)||void 0===t?void 0:t.length)>2)throw Error("The `styleMap` directive must be used in the `style` attribute and must be the only part in the attribute.")}render(e){return Object.keys(e).reduce((t,i)=>{const s=e[i];return null==s?t:t+`${i=i.includes("-")?i:i.replace(/(?:^(webkit|moz|ms|o)|)(?=[A-Z])/g,"-$&").toLowerCase()}:${s};`},"")}update(e,[t]){const{style:i}=e.element;if(void 0===this.ht){this.ht=new Set;for(const e in t)this.ht.add(e);return this.render(t)}this.ht.forEach(e=>{null==t[e]&&(this.ht.delete(e),e.includes("-")?i.removeProperty(e):i[e]="")});for(const e in t){const s=t[e];if(null!=s){this.ht.add(e);const t="string"==typeof s&&s.endsWith(" !important");e.includes("-")||t?i.setProperty(e,t?s.slice(0,-11):s,t?"important":""):i[e]=s}}return R}}),Ks={alarm_control_panel:{disarmed:"mdi:lock-open-variant-outline",armed_away:"mdi:exit-run",armed_home:"mdi:home-outline",armed_night:"mdi:power-sleep",armed_custom_bypass:"mdi:security",armed_vacation:"mdi:shield-airplane",triggered:"mdi:alarm-light-outline"},binary_sensor:{battery:{on:"mdi:battery-outline",off:"mdi:battery"},battery_charging:{on:"mdi:battery-charging",off:"mdi:battery"},cold:{on:"mdi:snowflake",off:"mdi:thermometer"},connectivity:{on:"mdi:server-network",off:"mdi:server-network-off"},door:{on:"mdi:door-open",off:"mdi:door-closed"},garage_door:{on:"mdi:garage-open",off:"mdi:garage"},power:{on:"mdi:power-plug",off:"mdi:power-plug-off"},gas:{on:"mdi:alert-circle",off:"mdi:check-circle"},problem:{on:"mdi:alert-circle",off:"mdi:check-circle"},safety:{on:"mdi:alert-circle",off:"mdi:check-circle"},tamper:{on:"mdi:alert-circle",off:"mdi:check-circle"},smoke:{on:"mdi:smoke",off:"mdi:check-circle"},heat:{on:"mdi:fire",off:"mdi:thermometer"},light:{on:"mdi:brightness-7",off:"mdi:brightness-5"},lock:{on:"mdi:lock-open",off:"mdi:lock"},moisture:{on:"mdi:water",off:"mdi:water-off"},motion:{on:"mdi:run",off:"mdi:walk"},occupancy:{on:"mdi:home",off:"mdi:home-outline"},opening:{on:"mdi:square-outline",off:"mdi:square"},plug:{on:"mdi:power-plug",off:"mdi:power-plug-off"},presence:{on:"mdi:home",off:"mdi:home-outline"},running:{on:"mdi:play",off:"mdi:stop"},sound:{on:"mdi:music-note",off:"mdi:music-note-off"},update:{on:"mdi:package-up",off:"mdi:package"},vibration:{on:"mdi:vibrate",off:"mdi:crop-portrait"},window:{on:"mdi:window-open",off:"mdi:window-closed"},_:{on:"mdi:checkbox-marked-circle",off:"mdi:radiobox-blank"}},calendar:{on:"mdi:flash",off:"mdi:flash-off"},cover:{garage:{closed:"mdi:garage",open:"mdi:garage-open"},door:{closed:"mdi:door-closed",open:"mdi:door-open"},blind:{closed:"mdi:blinds",open:"mdi:blinds-open"},window:{closed:"mdi:window-closed",open:"mdi:window-open"},_:{closed:"mdi:window-shutter",open:"mdi:window-shutter-open"}},climate:{off:"mdi:power-off",heat:"mdi:fire",cool:"mdi:snowflake",heat_cool:"mdi:thermometer",auto:"mdi:autorenew",dry:"mdi:water-percent",fan_only:"mdi:fan"},device_tracker:{home:"mdi:home-outline",not_home:"mdi:exit-run"},fan:{on:"mdi:power",off:"mdi:power-off"},humidifier:{on:"mdi:power",off:"mdi:power-off"},input_boolean:{on:"mdi:flash",off:"mdi:flash-off"},light:{on:"mdi:lightbulb",off:"mdi:lightbulb-off"},lawn_mower:{mowing:"mdi:play",paused:"mdi:pause",docked:"mdi:home-import-outline"},lock:{unlocked:"mdi:lock-open-variant-outline",locked:"mdi:lock-outline"},person:{home:"mdi:home-outline",not_home:"mdi:exit-run"},sun:{below_horizon:"mdi:weather-sunny-off",above_horizon:"mdi:weather-sunny"},switch:{on:"mdi:flash",off:"mdi:flash-off"},timer:{active:"mdi:play",paused:"mdi:pause",idle:"mdi:sleep"},valve:{open:"mdi:valve-open",closed:"mdi:valve-closed"},weather:{"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"},water_heater:{off:"mdi:power-off",eco:"mdi:leaf",electric:"mdi:lightning-bolt",gas:"mdi:fire",heat_pump:"mdi:hvac",high_demand:"mdi:water-plus-outline",performance:"mdi:rocket-launch-outline"}},Gs=(e,t,i)=>{const s=Wi(e);if(!Object.keys(Ks).includes(s))return;let a=Ks[s];if("object"==typeof Object.values(a)[0]){const t=i.states[e],s=null==t?void 0:t.attributes.device_class;a=s&&Object.keys(a).includes(s)?a[s]:a._}return Object.keys(a).includes(t)?a[t]:void 0},Js=["alarm_control_panel","binary_sensor","climate","calendar","cover","device_tracker","fan","humidifier","input_boolean","input_number","input_select","lawn_mower","light","lock","number","person","proximity","select","sensor","sun","switch","timer","valve","weather","water_heater"],Ys=["clear-night","cloudy","exceptional","fog","hail","lightning","lightning-rainy","partlycloudy","pouring","rainy","snowy","snowy-rainy","sunny","windy","windy-variant"],Qs=(e,t,i)=>{let s=((e,t)=>{const i=Object.keys(t.states).includes(e)?t.states[e]:void 0,s=Wi(e),a=(null==i?void 0:i.attributes)||{},o=i=>null==i?void 0:i.map(i=>Object({value:i,icon:Gs(e,i,t)}));switch(s){case"alarm_control_panel":let e=["disarmed","triggered"];return 2&(a.supported_features||0)&&(e=[...e,"armed_away"]),1&(a.supported_features||0)&&(e=[...e,"armed_home"]),4&(a.supported_features||0)&&(e=[...e,"armed_night"]),16&(a.supported_features||0)&&(e=[...e,"armed_custom_bypass"]),32&(a.supported_features||0)&&(e=[...e,"armed_vacation"]),ss({options:o(e),translation_key:"component.alarm_control_panel.entity_component._.state.${value}"});case"binary_sensor":return ss({options:o(["on","off"]),translation_key:"component.binary_sensor.entity_component.${deviceClass}.state.${value}"});case"climate":return ss({options:o(a.hvac_modes),translation_key:"component.climate.entity_component._.state.${value}"});case"calendar":case"fan":case"humidifier":case"input_boolean":case"light":case"switch":return ss({options:o(["on","off"]),translation_key:"component.switch.entity_component._.state.${value}"});case"cover":return ss({options:o(["open","closed"]),translation_key:"component.cover.entity_component._.state.${value}"});case"device_tracker":return ss({options:o(["home","not_home"]),translation_key:"component.device_tracker.entity_component._.state.${value}"});case"input_number":case"number":return ls({min:a.min,max:a.max,step:a.step,mode:a.mode,unit:a.unit_of_measurement});case"input_select":case"select":return ss({options:a.options});case"lawn_mower":return ss({options:o(["mowing","paused","docked"]),translation_key:"component.lawn_mower.entity_component._.state.${value}"});case"lock":return ss({options:o(["locked","unlocked"]),translation_key:"component.lock.entity_component._.state.${value}"});case"person":const s=Object.keys(t.states).filter(e=>"zone"==Wi(e)).map(Bi);return ss({options:[...new Set(["home","not_home",...s])]});case"proximity":return ls({mode:"box",unit:a.unit_of_measurement});case"sensor":return!isNaN(Number(null==i?void 0:i.state))||Be(a.unit_of_measurement)?ls({mode:"box",unit:a.unit_of_measurement,min:"%"==a.unit_of_measurement?0:void 0,max:"%"==a.unit_of_measurement?100:void 0}):{text:{}};case"sun":return ss({options:o(["above_horizon","below_horizon"]),translation_key:"component.sun.entity_component._.state.${value}"});case"timer":return ss({options:o(["active","paused","idle"]),translation_key:"component.timer.entity_component._.state.${value}"});case"valve":return ss({options:o(["open","closed"]),translation_key:"component.valve.entity_component._.state.${value}"});case"weather":return ss({options:o(Ys),translation_key:"component.weather.entity_component._.state.${value}"});case"water_heater":case"climate":return ss({options:o(a.operation_list),translation_key:"component.climate.entity_component._.state.${value}"});default:return{text:{}}}})(e,t),a=Object.keys(i||{}).filter(t=>Ki(t,Wi(e))||Ki(t,e)).filter(e=>Object.keys(i[e]).includes("states")).sort((e,t)=>e.length-t.length).map(e=>i[e].states).shift();return a&&(Array.isArray(a)?s=ss({options:a}):"object"==typeof a&&"min"in a&&"max"in a&&(s=ls(a))),s},Xs=(e,t)=>{let i=Object.keys(e.states).map(e=>Wi(e)).reduce((e,t)=>e.includes(t)?e:[...e,t],[]).filter(e=>((e,t)=>{let i=Js.includes(e);return!i&&t?Object.keys(t).map(Wi).includes(e):i})(e,t.customize||{}));return i=i.filter(e=>Gi(e,t)),i.map(t=>({key:t,name:Xi(`component.${t}.title`,e,!1)||t.replace(/_/g," "),description:"",icon:Vs(t)}))},ea=(e,t,i)=>{let s=Object.entries(t||{}).filter(([t,i])=>Ki(t,e)&&i.icon).map(([e,t])=>t);if(s.length)return s.map(e=>{return(t=e.icon).match(/^[a-z]+\:[a-zA-Z\-]+$/)?t:"mdi:"+t;var t}).shift();if(!Object.keys(i.states).includes(e))return"mdi:help";const a=i.states[e];if(a.attributes.icon)return a.attributes.icon;const o=Wi(e);return Vs(o)},ta=(e,t,i)=>{if(["script","notify"].includes(e)){let s=Object.keys(i.services[e]);"script"==e&&(s=s.filter(e=>!["turn_on","turn_off","reload","toggle","test"].includes(e)));let a=s.map(s=>({key:`${e}.${s}`,name:Os(`${e}.${s}`,i,t),description:"",icon:ea(`${e}.${s}`,t,i)}));return a.sort((e,t)=>Yi(e.name,t.name)),a}{let s=Object.keys(i.states).filter(t=>Wi(t)==e).map(e=>{var s;return{key:e,name:Zi(e,null===(s=i.states[e])||void 0===s?void 0:s.attributes),description:"",icon:ea(e,t,i)}});return s.sort((e,t)=>Yi(e.name,t.name)),s}},ia=(e,t)=>t.every(t=>e.name.toLowerCase().includes(t))||t.every(t=>e.key.toLowerCase().includes(t));let sa=class extends oe{constructor(){super(...arguments),this._search="",this._filter="",this.timer=0,this.expandedGroups=[],this.scheduleEntities=[]}async showDialog(e){this._params=e,this.loadOptions(),await this.updateComplete}async closeDialog(){this._params&&this._params.confirm({domains:this._params.domains,entities:this._params.entities}),this._params=void 0,this._clearSearch(),this._height=void 0,this._width=void 0}loadOptions(){if(!this._params)return;let e=(e=>{let t=Hs(e,{include:["*"]}),i=Xs(e,{include:["*"]});return i=i.filter(e=>!t.map(e=>e.key).includes(e.key)),t=[...t,...i],t.sort((e,t)=>Yi(e.name,t.name)),t})(this.hass);this.options=e.map(e=>Object.assign(Object.assign({},e),{entities:ta(e.key,this._params.cardConfig.customize,this.hass)})).filter(e=>e.entities.length)}shouldUpdate(e){return!!(e.has("_params")||e.has("expandedGroups")||e.has("_filter")||e.has("scheduleEntities"))}async firstUpdated(){this.scheduleEntities=Object.entries(await Oe(this.hass)).map(([,e])=>e.entity_id)}async willUpdate(){var e;if(!this._width||!this._height){const t=null===(e=this.shadowRoot.querySelector("ha-list"))||void 0===e?void 0:e.getBoundingClientRect();this._width=null==t?void 0:t.width,this._height=null==t?void 0:t.height}}render(){return this._params?q` + +
    + + +
    + ${Fi("ui.dialog.entity_picker.title",this.hass)} +
    +
    + + +
    + ${this._search&&q` + + `} + +
    +
    +
    + + + ${this._renderOptions()} + +
    + `:q``}_opened(){var e;const t=null===(e=this.shadowRoot.querySelector("ha-list"))||void 0===e?void 0:e.getBoundingClientRect();this._width=null==t?void 0:t.width,this._height=null==t?void 0:t.height}_handleSearchChange(e){const t=e.currentTarget.value;this._search=t,clearTimeout(this.timer),this.timer=window.setTimeout(()=>{this._filter=this._search},100)}_clearSearch(){this._search="",this._filter=""}_toggleSelectEntity(e){let t=e.target;for(;"HA-LIST-ITEM"!=t.tagName;)t=t.parentElement;t.querySelector("ha-checkbox");const i=t.getAttribute("key");this._params.entities.includes(i)?this._params=Object.assign(Object.assign({},this._params),{entities:this._params.entities.filter(e=>e!=i)}):this._params=Object.assign(Object.assign({},this._params),{entities:[...this._params.entities,i]})}_toggleSelectDomain(e,t){var i;let s=e.target;for(;"HA-LIST-ITEM"!=s.tagName;)s=s.parentElement;const a=s.getAttribute("key"),o=null===(i=this.options)||void 0===i?void 0:i.find(e=>e.key==a).entities.map(e=>e.key);this._params=t?Object.assign(Object.assign({},this._params),{domains:this._params.domains.filter(e=>e!=a),entities:this._params.entities.filter(e=>!(null==o?void 0:o.includes(e)))}):Object.assign(Object.assign({},this._params),{domains:[...this._params.domains,a]}),e.stopPropagation()}closeGroupByKey(e){this.shadowRoot.querySelector("ha-list").childNodes.forEach(t=>{if(t.nodeType==Node.ELEMENT_NODE&&"HA-LIST-ITEM"==t.tagName&&t.getAttribute("key")==e){const e=t,i=e.nextElementSibling,s=e.querySelector("ha-icon-button");i.style.height="0px",e.removeAttribute("expanded"),s.classList.remove("expanded")}})}async _toggleExpandGroup(e){let t=e.target;for(;"HA-LIST-ITEM"!=t.tagName;)t=t.parentElement;const i=t.querySelector("ha-icon-button"),s=t.getAttribute("key");this.expandedGroups.includes(s)||(this.expandedGroups.forEach(e=>this.closeGroupByKey(e)),this.expandedGroups=[s],await this.requestUpdate());const a=t.nextElementSibling,o=a.scrollHeight;t.hasAttribute("expanded")?(t.removeAttribute("expanded"),i.classList.remove("expanded"),a.style.height="0px",setTimeout(()=>{this.expandedGroups=this.expandedGroups.filter(e=>e!=s)},300)):(t.setAttribute("expanded","true"),i.classList.add("expanded"),a.style.height=o+"px")}_renderOptions(){if(!this.options)return;let e=[...this.options];const t=this._filter&&this._filter.trim().length;if(t){const t=this._filter.toLowerCase().trim().split(" ");e=e.map(e=>ia(e,t)||(e=Object.assign(Object.assign({},e),{entities:(e.entities||[]).filter(e=>ia(e,t))})).entities.length?e:void 0).filter(e=>void 0!==e)}return e.length?Object.keys(e).map(i=>{var s,a;const o=e[i].key,n=null===(s=this._params)||void 0===s?void 0:s.domains.includes(o);let r=[...e[i].entities];"switch"==o&&(r=r.filter(e=>!this.scheduleEntities.includes(e.key)));const d=n?r.length:r.filter(e=>{var t;return null===(t=this._params)||void 0===t?void 0:t.entities.includes(e.key)}).length,l=(null===(a=this._params)||void 0===a?void 0:a.domains.includes(o))||e[i].entities.every(e=>{var t;return null===(t=this._params)||void 0===t?void 0:t.entities.includes(e.key)});return q` + + +
    + this._toggleSelectDomain(e,l)} + size="small" + > + ${Xi(l?"ui.components.media-browser.file_management.deselect_all":"ui.components.subpage-data-table.select_all",this.hass)} + + {e.target.blur()}} class="chevron"> +
    + ${e[i].name} + ${Fi("ui.panel.card_editor.fields.entities.included_number",this.hass,["{number}","{total}"],[d,r.length])} +
    + ${this.expandedGroups.includes(o)||t?q` +
    +
  • + ${r.map(e=>{var t,i;return q` + + ${Object.keys(this.hass.states).includes(e.key)?q``:q``} + + + ${e.name} + ${e.key} + + `})} +
  • +
    + `:""} + `}):q` + + ${Xi("ui.components.entity.entity-picker.no_match",this.hass)} + + `}static get styles(){return r` + ha-dialog { + --dialog-content-padding: 0; + --ha-dialog-width-md: 480px; + } + ha-input { + display: block; + margin: 0 16px; + } + ha-list { + min-height: 300px; + } + ha-list-item { + --mdc-ripple-hover-opacity: 0.04; + --mdc-ripple-focus-opacity: 0.04; + --mdc-ripple-press-opacity: 0.12; + --mdc-list-item-meta-size: 180px; + } + ha-list-item.nested { + --mdc-list-item-meta-size: 48px; + --mdc-list-side-padding: 32px; + } + ha-list-item.nested ha-icon { + display: flex; + justify-content: flex-end; + } + ha-list-item ha-checkbox, ha-list-item ha-icon-button, ha-list-item ha-button { + display: flex; + align-items: center; + justify-content: center; + } + div.group { + height: 0px; + overflow: hidden; + transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1); + box-sizing: border-box; + } + div.group.open { + height: auto; + } + ha-list-item .chevron { + transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); + } + ha-list-item .chevron.expanded { + transform: rotate(180deg); + } + div.group li { + width: 100%; + height: 1px; + display: block; + background: var(--divider-color); + margin: 0px 10px; + } + div.meta { + display: flex; + justify-content: flex-end; + align-items: center; + } + `}};t([le({attribute:!1})],sa.prototype,"hass",void 0),t([ce()],sa.prototype,"_params",void 0),t([ce()],sa.prototype,"_search",void 0),t([ce()],sa.prototype,"_filter",void 0),t([ce()],sa.prototype,"_width",void 0),t([ce()],sa.prototype,"_height",void 0),t([ce()],sa.prototype,"expandedGroups",void 0),t([ce()],sa.prototype,"options",void 0),t([ce()],sa.prototype,"scheduleEntities",void 0),sa=t([re("dialog-select-entities")],sa);var aa=Object.freeze({__proto__:null,get DialogSelectEntities(){return sa}});let oa=class extends oe{constructor(){super(...arguments),this.active=!1}render(){return q` +
    +
    + ${this.renderIcon()} + + ${this.renderTrailingIcon()} +
    + `}renderIcon(){if(!this.icon&&!this.toggleable&&!this.useStateIcon)return V;if(this.toggleable)return q` +
    + +
    + `;if(this.useStateIcon){let e=this.hass.states[this.value||""];return q` +
    + ${e?q``:q` + `} +
    + `}return q` +
    + +
    + `}renderTrailingIcon(){const e="icn_"+Math.random().toString(36).substring(2,9);return this.removable||this.badge?this.badge?q` +
    + ${this.badge} +
    + `:q` +
    + + ${this.disabled?V:q`${Xi("ui.common.remove",this.hass)}`} +
    + `:V}_handleClick(e){if(!this.disabled){if(this.toggleable){this.active=!this.active;const e=new CustomEvent("click",{detail:{active:this.active,value:this.value}});this.dispatchEvent(e)}else if(this.selectable){const e=new CustomEvent("click",{detail:{value:this.value}});this.dispatchEvent(e)}e.stopPropagation()}}_iconClick(e){if(e.stopPropagation(),this.disabled)return;const t=new CustomEvent("icon-clicked",{detail:{value:this.value}});this.dispatchEvent(t)}static get styles(){return r` + :host { + margin: 4px; + } + .chip { + display: inline-flex; + position: relative; + height: var(--chip-height, 32px); + background: none; + user-select: none; + z-index: 0; + align-items: center; + justify-content: center; + } + .chip:before { + position: absolute; + pointer-events: none; + top: 0; + right: 0; + bottom: 0; + left: 0; + content: ''; + border: 1px solid var(--chip-color, rgb(168, 225, 251)); + border-radius: var(--chip-border-radius, 32px); + background: rgba(0, 0, 0, 0); + opacity: var(--background-opacity, 1); + z-index: -2; + } + .chip.active:before { + background: var(--chip-color, rgb(168, 225, 251)); + } + .icon { + position: relative; + width: 32px; + height: 32px; + border-radius: 16px; + display: flex; + align-items: center; + justify-content: center; + --mdc-icon-size: 20px; + margin-right: -8px; + color: rgba(0, 0, 0, 0.54); + } + .icon.filled:before { + position: absolute; + pointer-events: none; + top: 0; + right: 0; + bottom: 0; + left: 0; + content: ''; + background: var(--chip-color, rgb(168, 225, 251)); + border-radius: 32px; + z-index: -2; + } + .value { + color: var(--primary-text-color); + font-size: var(--chip-font-size, 0.875rem); + font-weight: 400; + display: flex; + align-items: center; + padding: 0px 12px; + opacity: 0.9; + } + .trailing-icon { + position: relative; + width: 26px; + height: 26px; + border-radius: 13px; + display: flex; + align-items: center; + justify-content: center; + --mdc-icon-size: 16px; + margin: 0px 3px 0px -8px; + color: var(--secondary-text-color); + cursor: pointer; + } + .trailing-icon:before { + position: absolute; + pointer-events: none; + top: 0; + right: 0; + bottom: 0; + left: 0; + content: ''; + background: var(--chip-color, var(--secondary-text-color)); + border-radius: 26px; + z-index: -2; + opacity: 0; + transition: opacity 0.1s ease-in-out; + } + .trailing-icon:hover:before { + opacity: 0.15; + } + .trailing-icon:active:before { + opacity: 0.3; + } + :host([disabled]) .trailing-icon:hover:before, :host([disabled]) .trailing-icon:active:before { + opacity: 0; + } + :host([disabled]) .trailing-icon { + cursor: not-allowed; + } + :host([selectable]) .chip, :host([toggleable]) .chip { + cursor: pointer; + } + .overlay { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: -1; + background: rgba(0, 0, 0, 0); + border-radius: var(--chip-border-radius, 32px); + transition: background-color 0.1s ease-in-out, border 0.1s ease-in-out; + border: 1px solid rgba(0, 0, 0, 0); + } + :host([selectable]) .chip:hover .overlay, :host([toggleable]) .chip:hover .overlay { + border: 1px solid rgba(0, 0, 0, 0.05); + background: rgba(0, 0, 0, 0.05); + } + :host([selectable]) .chip:active .overlay, :host([toggleable]) .chip:active .overlay { + border: 1px solid rgba(0, 0, 0, 0.1); + background: rgba(0, 0, 0, 0.1); + } + :host([selectable]) .chip:hover .value, :host([toggleable]) .chip:hover .value { + opacity: 1; + } + :host([active]):host([selectable]) .chip:hover .overlay, :host([active]):host([toggleable]) .chip:hover .overlay { + background: rgba(0, 0, 0, 0.1); + border: 1px solid rgba(0, 0, 0, 0); + } + :host([active]):host([selectable]) .chip:active .overlay, :host([active]):host([toggleable]) .chip:active .overlay { + background: rgba(0, 0, 0, 0.2); + border: 1px solid rgba(0, 0, 0, 0); + } + + :host([toggleable]) .icon { + width: 0px; + transition: width 0.1s ease-in-out; + overflow: hidden; + display: flex; + align-items: center; + margin-left: 12px; + } + :host([toggleable]) .active .icon { + width: 20px; + } + .badge { + position: relative; + display: flex; + height: 26px; + min-width: 26px; + border-radius: 13px; + font-size: var(--chip-font-size, 0.875rem); + align-items: center; + justify-content: center; + margin: 0px 3px 0px -8px; + } + .badge:before { + position: absolute; + pointer-events: none; + top: 0; + right: 0; + bottom: 0; + left: 0; + content: ''; + background: var(--chip-color, var(--secondary-text-color)); + border-radius: 26px; + z-index: -2; + transition: opacity 0.1s ease-in-out; + opacity: 0.1; + } + `}};t([le({attribute:!1})],oa.prototype,"hass",void 0),t([le({type:String})],oa.prototype,"icon",void 0),t([le({type:Boolean})],oa.prototype,"useStateIcon",void 0),t([le({type:Boolean})],oa.prototype,"selectable",void 0),t([le({type:Boolean})],oa.prototype,"removable",void 0),t([le({type:Boolean})],oa.prototype,"toggleable",void 0),t([le({type:Boolean})],oa.prototype,"active",void 0),t([le({type:String})],oa.prototype,"badge",void 0),t([le({type:String})],oa.prototype,"value",void 0),t([le({type:Boolean})],oa.prototype,"disabled",void 0),oa=t([re("scheduler-chip")],oa);let na=class extends oe{constructor(){super(...arguments),this.value=[]}render(){return this.items?q` + ${Object.values(this.items).map(e=>this.renderChipitem(e))} + `:q``}renderChipitem(e){const t=e.useStateIcon&&!Object.keys(this.hass.states).includes(e.value||"");return q` + + ${e.name} + + `}_handleClick(e){if(!this.disabled)if(this.toggleable){const t=e.detail.value,i=e.detail.active;this.value.includes(t)&&!i?this.value=this.value.filter(e=>e!=t):!this.value.includes(t)&&t&&(this.value=[...this.value,t]);const s=new CustomEvent("value-changed",{detail:this.value});this.dispatchEvent(s)}else{const t=new CustomEvent("value-changed",{detail:e.detail.value});this.dispatchEvent(t)}}static get styles(){return r` + :host { + display: flex; + flex-direction: row; + flex: 1; + margin: 0px -4px; + flex-wrap: wrap; + } + scheduler-chip { + display: inline-flex; + margin-bottom: 4px; + } + `}};t([le({attribute:!1})],na.prototype,"hass",void 0),t([le({attribute:!1})],na.prototype,"items",void 0),t([le({attribute:!1})],na.prototype,"value",void 0),t([le({type:Boolean})],na.prototype,"selectable",void 0),t([le({type:Boolean})],na.prototype,"toggleable",void 0),t([le({type:Boolean})],na.prototype,"removable",void 0),t([le({type:Boolean})],na.prototype,"disabled",void 0),na=t([re("scheduler-chip-set")],na);const ra=[{name:"primary",weight:10},{name:"secondary",weight:8}];let da=class extends oe{constructor(){super(...arguments),this.value=[],this.multiple=!1,this.disabled=!1,this.multipleMode=!1,this.scheduleEntities=[],this._valueRenderer=e=>{Array.isArray(e)&&(e=e.length?[...e].pop():"");const t=e||"",i=this.hass.states[t];if(!i)return q` + + ${t} + `;const s=this._parseEntityItem(t);return q` + ${s.icon?q` + + `:q` + + `} + ${s.primary} + ${s.secondary} + `},this._rowRenderer=e=>{const t=e.id||"",i=this.hass.states[t];return q` + + ${e.icon?q` + + `:i?q` + + `:q` + + `} + ${e.primary} + ${e.secondary?q`${e.secondary}`:V} + + `},this._filteredItems=()=>{let e=Object.keys(this.hass.states);return this.domain&&(e=e.filter(e=>Wi(e)==this.domain)),this.multiple&&(e=e.filter(e=>{var t;return!(null===(t=this.value)||void 0===t?void 0:t.includes(e))})),this.config&&(e=e.filter(e=>((this.config.include||Ve).some(t=>Ki(t,e))||Object.keys(this.config.customize||{}).some(t=>Ki(t,e)))&&!(this.config.exclude||[]).some(t=>Ki(t,e)))),e=e.filter(e=>!this.scheduleEntities.includes(e)),this.filterFunc&&(e=e.filter(e=>this.filterFunc(this.hass.states[e]))),e.map(e=>this._parseEntityItem(e))}}async firstUpdated(){this.scheduleEntities=Object.entries(await Oe(this.hass)).map(([,e])=>e.entity_id),this.domain&&this.config&&!Gi(this.domain,this.config)&&(this.config=Object.assign(Object.assign({},this.config),{include:[...this.config.include||[],this.domain],exclude:[...(this.config.exclude||[]).filter(e=>!e.startsWith(this.domain))]})),this._autoSelectIfSingleEntity()}updated(e){super.updated(e),e.has("domain")&&this._autoSelectIfSingleEntity()}_autoSelectIfSingleEntity(){if(this.value&&this.value.length>0)return;const e=this._filteredItems();1===e.length&&(this.value=[e[0].id],Ts(this,"value-changed",{value:this.value}))}render(){var e,t;return q` + ${this.renderChips()} + + ${(null===(e=this.value)||void 0===e?void 0:e.length)&&!this.multipleMode&&this.multiple?V:q` + + + + `} + `}renderChips(){if(!this.multiple)return V;let e=(this.value||[]).map(e=>{const t=this._parseEntityItem(e);return{name:t.primary,value:e,useStateIcon:!t.icon,icon:t.icon}});return q` +
    + + +
    + ${e.length?q` + {this.multipleMode=!this.multipleMode,e.target.blur()}} + .path=${this.multipleMode?"M7.41,15.41L12,10.83L16.59,15.41L18,14L12,8L6,14L7.41,15.41Z":Ms} + slot="end" + > + `:V} +
    +
    + `}_valueChanged(e){let t=e.detail.value;const i=e.currentTarget;t&&(this.value=[...this.value||[],t],this.multiple&&(i.value=""),Ts(this,"value-changed",{value:this.value}),e.stopPropagation())}_removeClick(e){const t=e.detail;this.value=(this.value||[]).filter(e=>e!==t),Ts(this,"value-changed",{value:this.value})}_parseEntityItem(e){var t,i,s,a;const o=Object.entries((null===(t=this.config)||void 0===t?void 0:t.customize)||{}).filter(([t,i])=>Ki(t,e)).map(([e,t])=>t),n=null===(i=o.find(e=>"name"in e))||void 0===i?void 0:i.name,r=null===(s=o.find(e=>"icon"in e))||void 0===s?void 0:s.icon;return{id:e,primary:n||Zi(e,null===(a=this.hass.states[e])||void 0===a?void 0:a.attributes),secondary:e,icon:r}}};da.styles=r` + :host { + display: flex; + flex-direction: column; + width: 100%; + } + :host > * { + display: block; + width: 100%; + } + div.wrapper { + display: flex; + } + scheduler-chip-set { + display: flex; + } + div.column-right { + display: flex; + } + div.column-right ha-icon-button { + display: flex; + align-self: flex-end; + } + `,t([le({attribute:!1})],da.prototype,"hass",void 0),t([le()],da.prototype,"domain",void 0),t([le()],da.prototype,"config",void 0),t([le({type:Array})],da.prototype,"value",void 0),t([le({type:Boolean})],da.prototype,"multiple",void 0),t([le({type:Boolean})],da.prototype,"disabled",void 0),t([ce()],da.prototype,"multipleMode",void 0),t([ce()],da.prototype,"scheduleEntities",void 0),da=t([re("scheduler-entity-picker")],da);let la=class extends oe{constructor(){super(...arguments),this.expanded=!1,this.disabled=!1,this.idx=-1,this.openClose=new CustomEvent("open-close",{detail:{},bubbles:!0,composed:!0})}render(){return q` +
    + ${this.disabled?"":q` + + `} + +
    + + +
    +
    + +
    + +
    + `}_toggleContent(){this.disabled||this.dispatchEvent(this.openClose)}attributeChangedCallback(e,t,i){let s=void 0;if(null!==this.shadowRoot)for(const e of this.shadowRoot.children)if("container"==e.className){s=e;break}if(s)if(this.hasAttribute("expanded")){const e=s.scrollHeight;s.style.height=e+"px"}else s.style.height="0px";super.attributeChangedCallback(e,t,i)}_focusChanged(e){this.disabled||this.shadowRoot.querySelector(".header").classList.toggle("focused","focus"===e.type)}static get styles(){return r` + :host { + display: block; + border-radius: 12px; + border: 1px solid var(--divider-color); + box-sizing: border-box; + margin: 8px 0px; + position: relative; + } + .header { + display: flex; + width: 100%; + border-radius: 12px; + padding: 12px; + box-sizing: border-box; + cursor: pointer; + } + :host([disabled]) .header { + cursor: default; + } + .header .title { + font-weight: 600; + padding: 0px 8px; + } + .header ::slotted(div) { + flex: 1; + margin-right: 32px; + } + .header.focused { + background: var(--input-fill-color); + } + .header.expanded { + border-radius: 12px 12px 0px 0px; + } + #contextMenu { + position: absolute; + right: 0px; + top: 0px; + } + + .chevron { + transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); + direction: var(--direction); + margin-left: 0px; + } + .chevron.expanded { + transform: rotate(180deg); + } + .container { + overflow: hidden; + transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1); + padding: 0px 12px; + box-sizing: border-box; + height: 0px; + } + :host([disabled]) .container { + height: auto; + } + .container.expanded { + height: auto; + } + @media all and (max-width: 450px) { + .container { + padding: 0px; + } + } + `}};t([le({type:Boolean,reflect:!0})],la.prototype,"expanded",void 0),t([le({type:Boolean,reflect:!0})],la.prototype,"disabled",void 0),t([le({attribute:!0})],la.prototype,"idx",void 0),t([le({type:CustomEvent})],la.prototype,"openClose",void 0),la=t([re("scheduler-collapsible-section")],la);let ca=class extends oe{set openedItem(e){e!==this._openedItem&&void 0!==e&&setTimeout(()=>{this.updateOpenedItem(e)},50)}constructor(){super(),this.disabled=!1,this._openedItem=-1,this._numItems=0,this.addEventListener("open-close",this.toggleActiveSection)}firstUpdated(){const e=this.querySelectorAll("scheduler-collapsible-section");this._numItems=e.length}toggleActiveSection(e){if(this.disabled)return;const t=e.target,i=Number(t.getAttribute("idx"));"true"===t.getAttribute("expanded")?this.updateOpenedItem(-1):this.updateOpenedItem(i)}updateOpenedItem(e){this.querySelectorAll("scheduler-collapsible-section").forEach((function(t){const i=Number(t.getAttribute("idx"));i!==e&&t.getAttribute("expanded")?t.removeAttribute("expanded"):i!==e||t.getAttribute("expanded")||t.setAttribute("expanded","true")})),this._openedItem=e;const t=new CustomEvent("openclose-changed",{detail:{item:e}});this.dispatchEvent(t)}render(){return q` + + `}};t([le()],ca.prototype,"disabled",void 0),t([ce()],ca.prototype,"_openedItem",void 0),t([ce()],ca.prototype,"_numItems",void 0),ca=t([re("scheduler-collapsible-group")],ca);const ua="__NONE_OPTION__";let ha=class extends oe{constructor(){super(...arguments),this.disabled=!1}render(){var e;if(this.config.select){const e=this.config.select,t=[this.value||[]].flat().map(String),i=e=>{const i=e.detail;this.value=t.filter(e=>e!=i),Ts(this,"value-changed",{value:this.value})},s=()=>{let e=t.map(e=>Object({name:e,value:e}));return q` + + `},a=e=>{var t;let i=null===(t=this.config.select)||void 0===t?void 0:t.translation_key,s="";return i&&(s=Xi(i.replace("${value}",e),this.hass,!1)),s||(s=e),s},o=e=>"object"==typeof e?{id:e.value,primary:a(e.label),icon:e.icon}:{id:e,primary:a(e)};let n=[...null==e?void 0:e.options].map(o),r=[this.value||[]].flat().map(String);n=[...n,...r.filter(e=>!n.find(t=>t.id==e)).map(o)],Array.isArray(this.value)&&(n=n.filter(e=>"object"==typeof e?!t.includes(e.id):!t.includes(e)));const d=()=>{if(!n.length)return q` + + ${this.hass.localize("ui.components.combo-box.no_match")} + + `;n.some(e=>e.icon);return n.map(e=>q` + + ${e.icon?q``:V} + ${e.primary} + + `)},l=e=>{let t=e.target,i=t.querySelector("ha-picker-field");this.style.setProperty("--select-menu-width",i.offsetWidth+"px"),t.classList.add("opened")},c=e=>{e.target.classList.remove("opened")},u=e=>{const t=e.detail.item.value;(t!=ua&&!Array.isArray(this.value)||(e.target.value=void 0,t!=ua))&&this._valueChanged(new CustomEvent("value-changed",{detail:{value:t}}))},h=()=>{this._valueChanged(new CustomEvent("value-changed",{detail:{value:void 0}}))},m=Be(this.value)&&!Array.isArray(this.value)?n.find(e=>e.id===this.value):void 0,p=m?m.primary||m.id:Be(this.value)&&!Array.isArray(this.value)?this.value:void 0;return q` +
    + ${e.multiple?q` +
    + ${s()} +
    + `:""} + + + + ${d()} + +
    + `}if(this.config.number){const t=this.config.number,i="box"==t.mode||!Be(t.min)||!Be(t.max);let s=this.value;i||"number"==typeof s||(s=t.min),"number"==typeof t.scale_factor&&(s=Number(s)/t.scale_factor),"number"==typeof(null==t?void 0:t.step)&&(s=Math.round(Number(s)/t.step)*t.step),Be(s)&&(s=rs(Number(s)));const a=e=>{let i=Number(e.target.value);"number"==typeof t.scale_factor&&(i*=t.scale_factor),"number"==typeof(null==t?void 0:t.step)&&(i=Math.round(i/t.step)*t.step),i=rs(i),this._valueChanged(new CustomEvent("value-changed",{detail:{value:i}})),e.stopPropagation()},o=e=>{e.stopPropagation();let t=e.target.value;const i=""===t||isNaN(Number(t))?void 0:Number(t);this._valueChanged(new CustomEvent("value-changed",{detail:{value:i}}))},n=(e,i)=>{let s=t.step&&Number(t.step)%1==0?null!==e.match(/^-?\d+$/):null!==e.match(/^[+-]?\d+([.,]\d+)?$/);return s&&Be(t.min)&&(s=Number(e)>=t.min),s&&Be(t.max)&&(s=Number(e)<=t.max),{valid:s,customError:!s}};return q` +
    + ${i?q` + + + `:q` + + ${s} ${t.unit||""} + `} +
    + `}if(this.config.text){this.config.text;return q` +
    + +
    + `}if(this.config.boolean){let e={select:{options:[{value:"true",label:"True",icon:"mdi:check"},{value:"false",label:"False",icon:"mdi:close"}]}};const t=e=>{let t=Be(e.detail.value)?"true"===e.detail.value:void 0;e.stopPropagation(),this._valueChanged(new CustomEvent("value-changed",{detail:{value:t}}))};return q` + + + `}return q``}_valueChanged(e){if(e.stopPropagation(),Array.isArray(this.value)){let t=e.detail.value;if(!t)return;this.value=[...this.value,t]}else if(e.detail){let t=e.detail.value;void 0===t&&(t=null),this.value=t}else this.value=e.target.value;Ts(this,"value-changed",{value:this.value})}};ha.styles=r` + :host { + display: flex; + width: 100%; + } + div.slider-wrapper { + display: flex; + flex-direction: row; + width: 100%; + align-items: center; + gap: 4px; + } + div.slider-wrapper > * { + display: flex; + } + div.slider-wrapper ha-slider { + flex: 1; + } + div.slider-wrapper span { + justify-content: center; + align-self: center; + min-width: 45px; + text-align: end; + } + div.slider-wrapper ha-input { + --ha-input-input-width: 100px; + } + div.select-wrapper { + display: flex; + flex-direction: column; + width: 100%; + } + div.textfield-wrapper { + display: flex; + width: 100%; + } + div.textfield-wrapper ha-input { + display: flex; + width: 100%; + } + ha-dropdown::part(menu) { + min-width: var(--select-menu-width); + } + `,t([le({attribute:!1})],ha.prototype,"hass",void 0),t([le({attribute:!1})],ha.prototype,"config",void 0),t([le()],ha.prototype,"value",void 0),t([le({type:Boolean})],ha.prototype,"disabled",void 0),ha=t([re("scheduler-combo-selector")],ha);let ma=class extends oe{constructor(){super(...arguments),this._config={},this.title="",this.tagOptions=[],this.customTagValue=""}setConfig(e){this._config=Object.assign({},e)}async firstUpdated(){this.title="string"==typeof this._config.title?this._config.title:"";const e=(await Ds(this.hass)).map(e=>e.name);e.sort(Yi),this.tagOptions=e}render(){var e,t,i,s,a,o;const n={number:{min:0,max:30,step:1,unit_of_measurement:Fi("ui.panel.card_editor.fields.time_step.unit_minutes",this.hass)}},r={select:{options:this.tagOptions,multiple:!0,custom_value:!0}};return q` +
    + + + ${Fi("ui.panel.card_editor.fields.entities.button_label",this.hass)} + + + + + + + ${Fi("ui.panel.card_editor.fields.title.heading",this.hass)} + + + + + +
    +
    + + {this._updateConfig({discover_existing:e.target.checked})}} + > + +
    +
    + + {this._updateConfig({show_header_toggle:e.target.checked})}} + > + +
    +
    + + {this._updateConfig({show_toggle_switches:e.target.checked})}} + > + +
    +
    + + + ${Fi("ui.panel.card_editor.fields.time_step.heading",this.hass)} + + {this._updateConfig({time_step:e.detail.value})}} + > + + + + ${Fi("ui.panel.card_editor.fields.default_editor.heading",this.hass)} +
    +
    + + {this._updateConfig({default_editor:me.Single})}} + ?checked=${this._config.default_editor!=me.Scheme} + > + + +
    +
    + + {this._updateConfig({default_editor:me.Scheme})}} + ?checked=${this._config.default_editor==me.Scheme} + > + + +
    +
    + + ${Fi("ui.panel.card_editor.fields.sort_by.heading",this.hass)} + +
    +
    + + + + + +
    +
    + + + + +
    +
    + + + ${Fi("ui.panel.card_editor.fields.display_format_primary.heading",this.hass)} + + +
    +
    + + + + + + +
    +
    + + + + + + +
    + +
    + + ${Fi("ui.panel.card_editor.fields.display_format_secondary.heading",this.hass)} + +
    +
    + + + + + + + + + + +
    +
    + + + + + + + + + +
    + +
    + + + ${Fi("ui.panel.card_editor.fields.tags.heading",this.hass)} +
    + {this._updateConfig({tags:e.detail.value})}} + > + + + {e.stopPropagation(),e.target.querySelector("ha-button").blur()}} + @click=${e=>{e.preventDefault(),e.stopImmediatePropagation()}} + @wa-after-show=${e=>{e.target.querySelector("ha-input").focus()}} + placement="bottom-start" + > + + + ${Xi("ui.panel.config.tag.add_tag",this.hass)} + + +
    + {this.customTagValue=e.currentTarget.value}} + .placeholder="" + > + + ${Xi("ui.common.ok",this.hass)} + +
    +
    +
    +
    + +
    + `}_setEnableTitle(e){e.target.checked?this.title.length?this._updateConfig({title:this.title}):this._updateConfig({title:!0}):this._updateConfig({title:!1})}_setTitle(e){const t=e.target.value;this.title=t,t!==Fi("ui.panel.common.title",this.hass)&&t.length?this._updateConfig({title:t}):this._updateConfig({title:!0})}_setSortBy(e){var t;const i=e.target.value;let s=[(null===(t=this._config)||void 0===t?void 0:t.sort_by)||qe].flat();s=s.filter(e=>"state"==e),s.includes(i)||(s=[...s,i]),this._updateConfig({sort_by:s})}_setDisplayOptionsPrimary(e){var t;const i=e.target.value,s=Object.assign(Object.assign({},null===(t=this._config)||void 0===t?void 0:t.display_options),{primary_info:i});this._updateConfig({display_options:s})}_setDisplayOptionsSecondary(e){var t;const i=e.target.value,s=e.target.checked;let a=Object.assign({},null===(t=this._config)||void 0===t?void 0:t.display_options),o=[a.secondary_info||[]].flat();o=s?Array.from(new Set([...o,i])):o.filter(e=>e!==i),o.sort((e,t)=>{const i={"relative-time":1,time:o.includes("relative-time")?3:2,days:o.includes("relative-time")?2:3,"additional-tasks":4},s=Object.keys(i).includes(e)?i[e]:5,a=Object.keys(i).includes(t)?i[t]:5;return s>a?1:s!e.includes(".")),i=(this._config.include||[]).filter(e=>e.includes("."));const s=await Es(this.hass);let a=Object.assign(Object.assign({},this._config),{customize:Object.assign(Object.assign({},s),this._config.customize||{})});await new Promise(s=>{const o={cancel:()=>s(null),confirm:e=>s(e),domains:t,entities:i,cardConfig:a};Ts(e.target,"show-dialog",{dialogTag:"dialog-select-entities",dialogImport:()=>Promise.resolve().then((function(){return aa})),dialogParams:o})}).then(e=>{e&&this._updateConfig({include:[...e.domains,...e.entities]})})}_customTagConfirmClick(e){let t=e.target;t=t.parentElement,t=t.parentElement;t.querySelector("ha-button").click();let i=String(this.customTagValue).trim();if(i.length){let e=[this._config.tags||[]].flat();e=[...new Set([...e,i])],this._updateConfig({tags:e})}this.customTagValue=""}_updateConfig(e){this._config&&(this._config=Object.assign(Object.assign({},this._config),e),Ts(this,"config-changed",{config:this._config}))}};ma.styles=r` + div.entities-list { + max-height: 500px; + overflow: auto; + } + div.row { + display: flex; + align-items: center; + flex-direction: row; + cursor: pointer; + margin: 10px 0px; + } + div.row ha-icon { + padding: 8px; + color: var(--state-icon-color); + } + div.row ha-switch { + padding: 13px 5px; + } + .info { + margin-left: 16px; + flex: 1 0 60px; + } + .info, + .info > * { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + ha-input { + width: 100%; + } + div.two-columns { + display: flex; + flex-direction: row; + } + div.two-columns .column { + flex: 50%; + + } + div.two-columns .column > * { + display: flex; + flex-direction: column; + } + scheduler-combo-selector { + min-width: 240px; + } + ha-dropdown { + display: block; + } + `,t([le({attribute:!1})],ma.prototype,"hass",void 0),t([le()],ma.prototype,"_config",void 0),t([le()],ma.prototype,"title",void 0),t([le()],ma.prototype,"tagOptions",void 0),t([ce()],ma.prototype,"customTagValue",void 0),ma=t([re("scheduler-card-editor")],ma);const pa=r` + ha-dialog { + --justify-action-buttons: space-between; + --dialog-container-padding: var(--safe-area-inset-top, 0) + var(--safe-area-inset-right, 0) var(--safe-area-inset-bottom, 0) + var(--safe-area-inset-left, 0); + --dialog-surface-padding: 0px; + --dialog-content-padding: 0px; + } + /* make dialog fullscreen on small screens */ + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-dialog { + --dialog-container-padding: 0px; + --dialog-surface-padding: var(--safe-area-inset-top, 0) + var(--safe-area-inset-right, 0) var(--safe-area-inset-bottom, 0) + var(--safe-area-inset-left, 0); + --vertical-align-dialog: flex-end; + --ha-dialog-border-radius: var(--ha-border-radius-square); + } + } + @media all and (min-width: 600px) and (min-height: 501px) { + ha-dialog { + --dialog-surface-margin-top: 40px; + --vertical-align-dialog: flex-start; + } + } + .buttons { + box-sizing: border-box; + display: flex; + padding: 16px 24px; + justify-content: space-between; + background-color: var(--mdc-theme-surface, #fff); + border-top: 1px solid var(--divider-color); + } + .content { + padding: 0px 24px 16px 24px; + } +`,_a=(r` + .error { + color: red; + } + .dl { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + } + .dt { + display: flex; + align-content: center; + flex-wrap: wrap; + } + .dd { + display: grid; + grid-template-columns: repeat(2, minmax(0, auto) minmax(0, 2fr)); + margin: 0; + } + .toggle { + padding: 0.6em; + border: grey; + border-radius: 50%; + } + .toggle.on { + background-color: green; + } + .toggle.off { + background-color: red; + } + .button { + display: block; + border: outset 0.2em; + border-radius: 50%; + border-color: silver; + background-color: silver; + width: 1.4em; + height: 1.4em; + } + .value { + padding-left: 0.5em; + display: flex; + align-content: center; + flex-wrap: wrap; + } + + +`,(e,t,i,s)=>{var a;const o=e.service,n=null===(a=e.target)||void 0===a?void 0:a.entity_id,r=ns(e,s);if(!r||!r.fields||!Object.keys(r.fields).includes(t))return!1;const d=r.fields[t];if(null===us(o,n,t,i,s))return!1;if(Object.keys(e.service_data||{}).includes(t))return!0;if(d.supported_features){if(![n||[]].flat().every(e=>{if(!Object.keys(i.states).includes(e))return!1;return((i.states[e].attributes.supported_features||0)&d.supported_features)>0}))return!1}return!("light"==Wi(o)&&![n||[]].flat().every(e=>{if(!Object.keys(i.states).includes(e))return!1;const s=i.states[e].attributes.supported_color_modes||[];return"brightness"!=t||s.filter(e=>"onoff"!=e).length}))});var ga;!function(e){e.OverlappingTime="overlapping_time",e.MissingTargetEntity="missing_target_entity",e.MissingServiceParameter="missing_service_parameter",e.MissingAction="missing_action"}(ga||(ga={}));const va=(e,t,i)=>{let s=[];s=[...s,...e.entries.map(e=>((e,t)=>e.every((i,s)=>{const a=Ae(i.start,t),o=void 0===i.stop?a:Ae(i.stop,t)||86400;return!(a>o)&&e.every((e,i)=>{if(i<=s)return!0;return Ae(e.start,t)>=o})})?null:ga.OverlappingTime)(e.slots,t)).filter(e=>null!==e)];let a=e.entries.map(e=>e.slots.map(e=>e.actions)).flat().flat();return a.length||(s=[...s,ga.MissingAction]),s=[...s,...a.map(e=>((e,t,i)=>{var s;const a=ns(e,i);return(null==a?void 0:a.target)&&!(null===(s=e.target)||void 0===s?void 0:s.entity_id)?ga.MissingTargetEntity:(null==a?void 0:a.fields)&&!Object.entries(a.fields).filter(([s])=>_a(e,s,t,i)).every(([s,a])=>{var o,n;let r=us(e.service,null===(o=e.target)||void 0===o?void 0:o.entity_id,s,t,i);const d=!(!r.number||!(null===(n=r.number)||void 0===n?void 0:n.optional))||a.optional;return!(!Object.keys(e.service_data).includes(s)&&!d)&&!(!Be(e.service_data[s])&&!d)})?ga.MissingServiceParameter:null})(e,t,i)).filter(e=>null!==e)],s.length?s.shift():null},ya=e=>{const t=e=>e.actions.length?(e.stop||(e=Object.fromEntries(Object.entries(e).filter(([e])=>"stop"!=e))),e):null;let i={weekdays:(e=Object.assign(Object.assign({},e),{entries:e.entries.map(e=>Object.assign(Object.assign({},e),{slots:e.slots.map(t).filter(e=>null!==e)}))})).entries[0].weekdays.map(fa),timeslots:e.entries[0].slots.map(ba),name:e.name,start_date:e.start_date,end_date:e.end_date,repeat_type:e.repeat_type,tags:e.tags||[]};return e.schedule_id&&(i=Object.assign(Object.assign({},i),{schedule_id:e.schedule_id})),i},fa=e=>{switch(e){case pe.Monday:return"mon";case pe.Tuesday:return"tue";case pe.Wednesday:return"wed";case pe.Thursday:return"thu";case pe.Friday:return"fri";case pe.Saturday:return"sat";case pe.Sunday:return"sun";case pe.Workday:return"workday";case pe.Weekend:return"weekend";default:return"daily"}},ba=e=>({start:e.start,stop:e.stop,actions:e.actions.map(e=>wa(e)).flat(),condition_type:e.conditions.items.length?e.conditions.type==_e.And?"and":"or":void 0,conditions:e.conditions.items.length?e.conditions.items:void 0,track_conditions:e.conditions.track_changes}),wa=e=>{var t,i;const s=e=>Object.keys(e).filter(t=>Be(e[t])).reduce((t,i)=>(t[i]=e[i],t),{});if(e.target){if(Array.isArray(null===(t=e.target)||void 0===t?void 0:t.entity_id)){return null==e?void 0:e.target.entity_id.map(t=>({service:e.service,service_data:s(e.service_data),entity_id:t}))}return{service:e.service,service_data:s(e.service_data),entity_id:null===(i=e.target)||void 0===i?void 0:i.entity_id}}return{service:e.service,service_data:s(e.service_data)}},ka=(e,t,i)=>{const s={title:Xi("state_badge.default.error",i),description:q` + Something went wrong!
    + ${e.body.message}

    + ${e.error}

    + Please report the bug. + `,primaryButtonLabel:Xi("ui.common.ok",i),confirm:()=>{},cancel:()=>{}};Ts(t,"show-dialog",{dialogTag:"scheduler-generic-dialog",dialogImport:()=>Promise.resolve().then((function(){return Ya})),dialogParams:s})},xa=e=>{if(!Object.keys(e).includes("select")||!e.select){if(Object.keys(e).includes("number")&&e.number){const t=e.number.min;return void 0!==t?t:0}return Object.keys(e).includes("boolean")&&e.boolean?void 0:(Object.keys(e).includes("text")&&e.text,"")}e.select.options},$a=(e,t)=>{const i=ns(e,t),s=Wi(e.service),a=Bi(e.service);if(i.icon)return(o=i.icon).match(/^[a-z]+\:[a-zA-Z\-]+$/)?o:"mdi:"+o;if(Object.keys(cs).includes(s)&&Object.keys(cs[s].services).includes(a)){if(void 0!==cs[s].attributes){let t=cs[s].attributes;const i=Object.keys(t).find(t=>Object.keys(e.service_data).includes(t));if(i&&Object.keys(t[i]).includes(e.service_data[i]))return t[i][e.service_data[i]]}return cs[s].services[a]}return Object.keys(cs).includes(s)&&Object.keys(cs[s].services).includes("{entity_id}")?cs[s].services["{entity_id}"]:"mdi:flash";var o},ja=(e,t,i)=>{let s=Ae("string"==typeof e?Ce(e):e,i),a=Ae("string"==typeof t?Ce(t):t,i);return a>s?1:aCe(e.start),Sa=e=>{if(Be(e.stop)){const t=Ce(e.stop);return 0==t.hours&&0==t.minutes&&t.mode==be.Fixed?Object.assign(Object.assign({},t),{hours:24}):t}return Se(Oa(e),{minutes:1})},za=(e,t,i,s)=>{let a=t;if(Be(i.stop))return[e,a]=za(e,t+1,{start:i.stop},s),[e,a-1];if(!Be(i.start))return[e,a];let o=Oa(e[t]),n=i.start,r=Oa(e[t]);for(let i=t-1;i>=0;i--){if(e[i].actions.length){r=i==t-1?Se(Oa(e[i]),{minutes:1}):Sa(e[i]);break}r=Oa(e[i])}let d=Se(Sa(e[t]),{minutes:-1});if(!Be(e[t].stop))for(let i=t+1;i0&&Ae(d,s)>0&&(n=d),e=Object.assign(e,{[t]:Object.assign(Object.assign({},e[t]),{start:Ne(n)})}),ja(o,n,s)<=0)for(let i=t-1;i>=0;i--){let t=ja(Oa(e[i]),n,s),o=ja(Sa(e[i]),n,s);if(t>0&&o<=0){e=Object.assign(e,{[i]:Object.assign(Object.assign({},e[i]),{stop:Ne(n)})});break}if(o>=0)break;t<=0&&(e=Object.assign(e,{[i]:null}),a-=1)}if(ja(o,n,s)<0&&!Be(e[t].stop)&&(e=Be(e[t+1].stop)&&!e[t+1].actions.length?Object.assign(e,{[t+1]:Object.assign(Object.assign({},e[t+1]),{start:Ne(Sa(e[t]))})}):[...e.slice(0,t+1),Object.assign(Object.assign({},e[t]),{start:Ne(Sa(e[t])),stop:Ne(Oa(e[t+1])),actions:[]}),...e.slice(t+1)]),ja(o,n,s)>=0)for(let t=a+1;t=0&&n<0)e=Object.assign(e,{[t]:Object.assign(Object.assign({},e[t]),{start:Ne(i)})});else{if(o<0)break;n>=0&&(e=Object.assign(e,{[t]:null}))}}return ja(o,n,s)>0&&(t>0&&Be(e[t-1].stop)?e=Object.assign(e,{[t-1]:Object.assign(Object.assign({},e[t-1]),{stop:Ne(n)})}):(e=[...e.slice(0,t),Object.assign(Object.assign({},e[t]),{start:t>0?Ne(Sa(e[t-1])):"00:00:00",stop:Ne(n),actions:[]}),...e.slice(t)],a=t+1)),[e=e.filter(Be),a]},Ca=(e,t)=>{const i=new Date(t),s=3600*i.getHours()+60*i.getMinutes()+i.getSeconds();let a=3600*e.hours+60*e.minutes-s;const o=a<0?-1:1;a=Math.abs(a);let n=Math.floor(a/3600),r=Math.round((a-3600*n)/60);return o<0&&(n>0&&(n=-n),r=-r),ze({hours:n,minutes:r})};let Aa=class extends oe{constructor(){super(...arguments),this.selectedSlot=null,this._width=0,this.large=!1}connectedCallback(){super.connectedCallback(),this._resizeObserver=new ResizeObserver(e=>{for(const t of e){const e=t.contentRect.width;e!==this._width&&(this._width=e)}}),this._resizeObserver.observe(this)}disconnectedCallback(){var e;super.disconnectedCallback(),null===(e=this._resizeObserver)||void 0===e||e.disconnect()}render(){return q` +
    + ${this.renderTimeslots()} +
    +
    + ${this.renderTimebar()} +
    + `}renderTimebar(){const e=this._width,t=[1,2,3,4,6,8,12],i=De(this.hass.locale),s=i?130:100;if(!e)return q``;let a=Math.ceil(24/(e/s));for(;!t.includes(a);)a++;const o=[0,...Array.from(Array(24/a-1).keys()).map(e=>(e+1)*a),24];return o.map((e,t)=>{let s=a/24*100;s=Math.floor(100*s)/100;let n={mode:be.Fixed,hours:e,minutes:0};return 0==t?q` + ${Ne(n,{seconds:!1,am_pm:i})} + `:t==o.length-1?q` + ${Ne(n,{seconds:!1,am_pm:i})} + `:q` + ${Ne(n,{seconds:!1,am_pm:i})} + `})}renderTimeslots(){const e=this.schedule.slots,t=this.computeSlotWidths();return e.map((i,s)=>{const a=i.actions.length?fs(i.actions[0],this.hass,this.config.customize,!0,!0):"",o=5*a.length+10,n=s>0?15:0,r=s + ${i.stop,""} + ${i.actions.length?a&&(d>o/3||d>50)&&d>30?q`${a}`:d>16?q``:"":""} + + ${s + + + + + + `:""} + `})}computeSlotWidths(){const e=this._width,t=this.schedule.slots,i=e-3*(t.length-1),s=t.map((e,i)=>{const s=Ae(e.start,this.hass);let a;if(void 0!==e.stop)a=Ae(e.stop,this.hass),!a&&s&&(a=86400);else{const e=t[i+1];a=e&&Ae(e.start,this.hass)||86400}return(a-s)/86400}),a=5/i,o=i-5*s.filter(e=>ee0?Ae(this.schedule.slots[s-1].stop||this.schedule.slots[s-1].start,this.hass)+o:o,r=(Ae(this.schedule.slots[s+1].stop||this.schedule.slots[s+1].start,this.hass)||86400)-o;void 0===this.schedule.slots[s+1].stop&&(r=(Ae(this.schedule.slots[s+2].stop||this.schedule.slots[s+2].start,this.hass)||86400)-o);const d=Ce(this.schedule.slots[s+1].start).mode;if([be.Sunrise,be.Sunset].includes(d)){let e=Ce(this.schedule.slots[s+1].start),t=Ae(Object.assign(Object.assign({},e),{hours:4,minutes:0}),this.hass),i=Ae(Object.assign(Object.assign({},e),{hours:-4,minutes:0}),this.hass);i>n&&(n=i),t{let t;e.preventDefault(),t="undefined"!=typeof TouchEvent&&e instanceof TouchEvent?e.changedTouches[0].pageX:e.pageX,t-=i.left,t>i.width&&(t=i.width),t<0&&(t=0);let o=Math.round(t/i.width*86400);or&&(o=r);const l=Math.floor(o/3600),c=Math.round((o-3600*l)/60);let u={mode:be.Fixed,hours:l,minutes:c};if([be.Sunrise,be.Sunset].includes(d)){const e=d==be.Sunrise?this.hass.states["sun.sun"].attributes.next_rising:this.hass.states["sun.sun"].attributes.next_setting,t=Ca(u,e);u={mode:d,hours:t.hours,minutes:t.minutes}}u=ze(u,a);const h=Ne(u);let m=[...this.schedule.slots];if(m=Object.assign(m,{[s]:Object.assign(Object.assign({},m[s]),{stop:h}),[s+1]:Object.assign(Object.assign({},m[s+1]),{start:Ne(u)})}),void 0===m[s+1].stop){const e=Ne(Se(u,{minutes:1}));m=Object.assign(m,{[s+2]:Object.assign(Object.assign({},m[s+2]),{start:e})})}this.schedule=Object.assign(Object.assign({},this.schedule),{slots:m});const p=new CustomEvent("update",{detail:{slots:m}});this.dispatchEvent(p)};const c=e=>{e.preventDefault()},u=()=>{window.removeEventListener("mousemove",l),window.removeEventListener("touchmove",l),window.removeEventListener("mouseup",u),window.removeEventListener("touchend",u),window.removeEventListener("blur",u),window.removeEventListener("dragstart",c),l=()=>{}};window.addEventListener("mouseup",u),window.addEventListener("touchend",u),window.addEventListener("blur",u),window.addEventListener("dragstart",c),window.addEventListener("mousemove",l),window.addEventListener("touchmove",l)}static get styles(){return r` + :host { + display: block; + max-width: 100%; + overflow: hidden; + } + .bar { + width: 100%; + height: 60px; + display: flex; + } + .time-bar { + width: 100%; + height: 18px; + display: flex; + } + .time-bar span { + display: flex; + justify-content: center; + white-space: nowrap; + } + .time-bar span.left { + justify-content: left; + } + .time-bar span.right { + justify-content: right; + } + + .slot { + display: flex; + height: 100%; + box-sizing: border-box; + cursor: pointer; + background: rgba(var(--rgb-primary-color), 0.7); + color: var(--text-primary-color); + font-weight: 500; + align-items: center; + justify-content: center; + word-break: break-all; + white-space: normal; + margin-right: 3px; + } + .slot:hover { + background: rgba(var(--rgb-primary-color), 0.85); + } + .slot.selected { + border: 3px solid rgba(var(--rgb-primary-color), 0.85); + } + .slot.selected:hover { + border: 3px solid rgba(var(--rgb-primary-color), 1); + } + .slot:first-child { + border-radius: 10px 0px 0px 10px; + } + .slot:last-child { + border-radius: 0px 10px 10px 0px; + margin-right: 0px; + } + .slot.empty { + background: rgba(var(--rgb-secondary-text-color), 0.5); + } + .slot.empty:hover { + background: rgba(var(--rgb-secondary-text-color), 0.65); + } + .slot.empty.selected { + border: 3px solid rgba(var(--rgb-secondary-text-color), 0.65); + } + .slot.empty.selected:hover { + border: 3px solid rgba(var(--rgb-secondary-text-color), 1); + } + .slot .marker { + width: 24px; + height: 24px; + background: rgba(var(--rgb-primary-color), 0.85); + margin-top: -80px; + position: absolute; + transform: rotate(45deg); + border-radius: 12px 12px 0px 12px; + } + .slot .marker:hover { + background: rgba(var(--rgb-primary-color), 1); + } + .slot.empty .marker { + background: rgba(var(--rgb-secondary-text-color), 0.85); + } + .slot.empty .marker:hover { + background: rgba(var(--rgb-secondary-text-color), 1); + } + .handle { + display: flex; + width: 36px; + height: 100%; + align-content: center; + align-items: center; + justify-content: center; + margin-left: -18px; + margin-right: -18px; + visibility: visible; + } + .handle.hidden { + visibility: hidden; + } + .handle span { + background: var(--card-background-color); + border-radius: 50%; + width: 24px; + height: 24px; + display: flex; + z-index: 5; + } + .handle ha-icon-button { + --mdc-icon-button-size: 36px; + margin-top: -12px; + margin-left: -12px; + } + .handle.center span { + margin-right: -2px; + } + `}};t([le({attribute:!1})],Aa.prototype,"config",void 0),t([ce()],Aa.prototype,"schedule",void 0),t([ce()],Aa.prototype,"selectedSlot",void 0),t([ce()],Aa.prototype,"_width",void 0),t([le({type:Boolean})],Aa.prototype,"large",void 0),Aa=t([re("scheduler-timeslot-editor")],Aa);const Ta=e=>{let t=60*e.hours+e.minutes;return t>240?e=Object.assign(Object.assign({},e),{hours:4,minutes:0}):t<-240&&(e=Object.assign(Object.assign({},e),{hours:-4,minutes:0})),e};let Ea=class extends oe{constructor(){super(...arguments),this.hours=0,this.minutes=0,this.mode=be.Fixed,this.autoValidate=!0,this.required=!0,this.disabled=!1,this.label="",this.useAmPm=!1,this.large=!1,this.stepSize=10}set time(e){const t=Ce(e);this.mode=t.mode,this.hours=t.hours,this.minutes=t.minutes}render(){return q` +
    +
    + ${this.label?q`${this.label}`:V} + ${this.large?V:this._renderTimeMode()} +
    + ${this.large?q` + this._addTimeOffset({hours:1})} + ?disabled=${this.mode!=be.Fixed&&4==this.hours} + > + + + `:V} + {let i=null!==e.match(/^[1|2]?[0-9]$/);return{valid:i,customError:!i}}} + > + + ${this.large?q` + this._addTimeOffset({hours:-1})} + ?disabled=${this.mode!=be.Fixed&&-4==this.hours} + > + + + `:V} +
    +
    :
    +
    + ${this.large?q` + this._addTimeOffset({minutes:this.stepSize})} + ?disabled=${this.mode!=be.Fixed&&4==this.hours} + > + + + `:V} + {let i=null!==e.match(/^[0-5]?[0-9]$/);return{valid:i,customError:!i}}} + > + + ${this.large?q` + this._addTimeOffset({minutes:-this.stepSize})} + ?disabled=${this.mode!=be.Fixed&&-4==this.hours} + > + + + `:V} +
    + ${this._renderSuffix()} + ${this.large?this._renderTimeMode():V} +
    +
    + `}_renderTimeMode(){if(!this.hass.states["sun.sun"])return V;if(this.large){const e=()=>{let e=this._convertTimeMode();e.mode!=be.Fixed&&(e=Ta(e)),this.mode=e.mode,this.hours=e.hours,this.minutes=e.minutes,this._valueChanged()};return q` +
    + ${this.hass.states["sun.sun"]?q` + + + + `:V} +
    + `}{let e=[be.Fixed,be.Sunrise,be.Sunset];const t={[be.Fixed]:Xi("ui.components.selectors.selector.types.time",this.hass),[be.Sunrise]:Xi("ui.panel.config.automation.editor.triggers.type.sun.sunrise",this.hass),[be.Sunset]:Xi("ui.panel.config.automation.editor.triggers.type.sun.sunset",this.hass)},i={[be.Fixed]:"mdi:clock-outline",[be.Sunrise]:"mdi:weather-sunset-up",[be.Sunset]:"mdi:weather-sunset-down"},s={[be.Fixed]:"M12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22C6.47,22 2,17.5 2,12A10,10 0 0,1 12,2M12.5,7V12.25L17,14.92L16.25,16.15L11,13V7H12.5Z",[be.Sunrise]:"M3,12H7A5,5 0 0,1 12,7A5,5 0 0,1 17,12H21A1,1 0 0,1 22,13A1,1 0 0,1 21,14H3A1,1 0 0,1 2,13A1,1 0 0,1 3,12M15,12A3,3 0 0,0 12,9A3,3 0 0,0 9,12H15M12,2L14.39,5.42C13.65,5.15 12.84,5 12,5C11.16,5 10.35,5.15 9.61,5.42L12,2M3.34,7L7.5,6.65C6.9,7.16 6.36,7.78 5.94,8.5C5.5,9.24 5.25,10 5.11,10.79L3.34,7M20.65,7L18.88,10.79C18.74,10 18.47,9.23 18.05,8.5C17.63,7.78 17.1,7.15 16.5,6.64L20.65,7M12.71,16.3L15.82,19.41C16.21,19.8 16.21,20.43 15.82,20.82C15.43,21.21 14.8,21.21 14.41,20.82L12,18.41L9.59,20.82C9.2,21.21 8.57,21.21 8.18,20.82C7.79,20.43 7.79,19.8 8.18,19.41L11.29,16.3C11.5,16.1 11.74,16 12,16C12.26,16 12.5,16.1 12.71,16.3Z",[be.Sunset]:"M3,12H7A5,5 0 0,1 12,7A5,5 0 0,1 17,12H21A1,1 0 0,1 22,13A1,1 0 0,1 21,14H3A1,1 0 0,1 2,13A1,1 0 0,1 3,12M15,12A3,3 0 0,0 12,9A3,3 0 0,0 9,12H15M12,2L14.39,5.42C13.65,5.15 12.84,5 12,5C11.16,5 10.35,5.15 9.61,5.42L12,2M3.34,7L7.5,6.65C6.9,7.16 6.36,7.78 5.94,8.5C5.5,9.24 5.25,10 5.11,10.79L3.34,7M20.65,7L18.88,10.79C18.74,10 18.47,9.23 18.05,8.5C17.63,7.78 17.1,7.15 16.5,6.64L20.65,7M12.71,20.71L15.82,17.6C16.21,17.21 16.21,16.57 15.82,16.18C15.43,15.79 14.8,15.79 14.41,16.18L12,18.59L9.59,16.18C9.2,15.79 8.57,15.79 8.18,16.18C7.79,16.57 7.79,17.21 8.18,17.6L11.29,20.71C11.5,20.9 11.74,21 12,21C12.26,21 12.5,20.9 12.71,20.71Z"},a=e=>{if(e==be.Fixed)return!1;const t=this._convertTimeMode(e);return Math.abs(60*t.hours+t.minutes)>240};return q` + {const t=e.detail.item.value;if(this.mode==t)return;const i=this._convertTimeMode(t);this.hours=i.hours,this.minutes=i.minutes,this.mode=t,this._valueChanged()}} + @wa-after-hide=${e=>{e.target.firstElementChild.blur()}} + ?disabled=${this.disabled} + > + + + ${e.map(e=>q` + + + ${t[e]} + + `)} + + `}}_renderSuffix(){if(this.large){const e=()=>{let e=Le(this.hours).am_pm;const t=Le(this.hours).hours;this.hours=Pe(t,"AM"==e?Me.PM:Me.AM),this._valueChanged()},t=()=>{0!=this.hours?this.hours=-this.hours:this.minutes=-this.minutes,this._valueChanged()},i=()=>{this.mode=this.mode==be.Sunrise?be.Sunset:be.Sunrise,this._valueChanged()};return q` +
    + ${this.useAmPm&&this.mode==be.Fixed?q` + + + ${Le(this.hours).am_pm==Me.AM?"AM":"PM"} + + + `:V} + ${this.mode!=be.Fixed?q` + + + ${this.hours<0||this.minutes<0?this.hass.localize("ui.panel.config.automation.editor.conditions.type.sun.before").trim().toLowerCase():this.hass.localize("ui.panel.config.automation.editor.conditions.type.sun.after").trim().toLowerCase()} + + + + + + `:V} +
    + `}return this.useAmPm&&this.mode==be.Fixed?q` + {e.stopPropagation()}} + > + AM + PM + + `:V}_convertTimeMode(e){const t=this.hass.states["sun.sun"].attributes.next_rising,i=this.hass.states["sun.sun"].attributes.next_setting;if(e&&e!=be.Fixed||this.mode==be.Fixed){const s=Ca({hours:this.hours,minutes:this.minutes},t),a=Ca({hours:this.hours,minutes:this.minutes},i),o=60*s.hours+s.minutes,n=60*a.hours+a.minutes;let r=e||(Math.abs(o)<=Math.abs(n)?be.Sunrise:be.Sunset),d=r==be.Sunrise?s:a;return{mode:r,hours:d.hours,minutes:d.minutes}}{let e=this.mode==be.Sunrise?Ce(t):Ce(i);return e=Se(e,{hours:this.hours,minutes:this.minutes}),{mode:be.Fixed,hours:e.hours,minutes:e.minutes}}}_hoursChanged(e){let t=Number(e.target.value);if(this.useAmPm){const e=Le(this.hours).am_pm;t=Pe(t,e)}this.hours=t,this._valueChanged()}_minutesChanged(e){const t=Number(e.target.value);this.minutes=t,this._valueChanged()}_amPmChanged(e){const t=e.detail.value;if(Le(this.hours).am_pm==t)return;const i=Le(this.hours).hours;this.hours=Pe(i,"AM"==t?Me.AM:Me.PM),this._valueChanged()}_addTimeOffset(e){let t={mode:this.mode,hours:this.hours,minutes:this.minutes};t=Se(t,e),e.minutes&&(t=ze(t,this.stepSize)),this.mode!=be.Fixed&&(t=Ta(t)),this.hours=t.hours,this.minutes=t.minutes,this._valueChanged()}_valueChanged(){const e={mode:this.mode,hours:this.hours,minutes:this.minutes};Ts(this,"value-changed",{value:e})}_onFocus(e){e.currentTarget.select()}formatHours(){const e=this.hours<0||this.minutes<0;let t=this.useAmPm&&this.mode==be.Fixed?Le(this.hours).hours:this.hours;return e&&!this.large?"-"+Math.abs(t).toFixed():this.mode==be.Fixed||this.large?this.large?Math.abs(t):t.toFixed():"+"+Math.abs(t).toFixed()}formatMinutes(){return Math.abs(this.minutes).toString().padStart(2,"0")}};Ea.styles=r` + :host { + display: flex; + flex-direction: row; + flex-wrap: wrap; + column-gap: 4px; + align-items: center; + } + .time-input-wrap { + display: flex; + border-radius: var(--mdc-shape-small, 4px) var(--mdc-shape-small, 4px) 0 0; + overflow: hidden; + position: relative; + direction: ltr; + } + :host([large]) .time-input-wrap { + width: 100%; + } + div.input { + display: flex; + } + :host([large]) div.input { + width: 100%; + } + div.hours, div.minutes { + display: flex; + flex-direction: column; + width: min-content; + } + div.hours ha-icon, div.minutes ha-icon { + --mdc-icon-size: 42px; + } + .time-separator { + background-color: var(--ha-color-form-background); + color: var(--ha-color-text-secondary); + border-bottom: 1px solid var(--ha-color-border-neutral-loud); + box-sizing: border-box; + height: 56px; + width: 9px; + display: flex; + align-items: center; + align-self: center; + justify-content: center; + font-size: 16px; + font-weight: 600; + } + :host([disabled]) .time-separator { + background-color: var(--ha-color-form-background-disabled); + opacity: 0.5; + } + :host([large]) .time-separator { + background: none; + border: none; + font-size: 36px; + } + ha-input { + width: 40px; + --mdc-shape-small: 0; + --text-field-appearance: none; + --text-field-padding-top: 0; + --text-field-padding-bottom: 0; + --text-field-padding-start: 4px; + --text-field-padding-end: 4px; + --text-field-suffix-padding-left: 2px; + --text-field-suffix-padding-right: 0; + --ha-input-text-align: center; + --ha-input-padding-top: 0px; + --ha-input-padding-bottom: 0px; + } + ha-input::part(wa-input) { + text-align: center; + } + ha-input::part(wa-hint) { + height: 0; + min-height: 0; + } + ha-input::part(wa-base) { + padding: var(--ha-space-1); + } + ha-input#hour::part(wa-base) { + border-top-right-radius: 0px; + } + ha-input#min::part(wa-base) { + border-top-left-radius: 0px; + } + :host([large]) ha-input#hour::part(wa-base), + :host([large]) ha-input#min::part(wa-base) { + border-top-right-radius: var(--ha-border-radius-sm); + border-top-left-radius: var(--ha-border-radius-sm); + } + :host([large]) ha-input { + width: 75px; + --wa-form-control-value-font-size: 42px; + } + div.suffix { + display: flex; + flex-direction: row; + flex-grow: 1; + align-items: center; + flex-wrap: wrap; + align-content: center; + } + div.mode { + display: flex; + align-items: center; + } + :host([large]) div.suffix ha-icon, :host([large]) div.mode ha-icon { + --mdc-icon-size: 32px; + } + ha-select { + --mdc-shape-small: 0; + width: 90px; + } + .label { + display: flex; + justify-content: center; + align-self: center; + white-space: nowrap; + } + ha-dropdown-menu { + display: flex; + align-items: flex-end; + margin-right: 4px; + padding-bottom: 4px; + } + ha-dropdown-menu ha-icon-button { + color: var(--secondary-text-color); + } + ha-dropdown-item[disabled] ha-icon { + color: var(--disabled-text-color); + } + ha-dropdown-item[noninteractive] { + background-color: rgba(var(--rgb-primary-color), 0.12); + color: var(--sidebar-selected-text-color); + } + ha-dropdown-item[noninteractive] ha-icon { + color: var(--sidebar-selected-text-color); + } + ha-button { + --ha-button-border-radius: 8px; + --button-color-fill-loud-hover: var(--ha-color-primary-50); + } + ha-button span.large { + font-size: 16px; + text-transform: uppercase; + } + @media all and (max-width: 450px), all and (max-height: 500px) { + ha-button { + --wa-form-control-padding-inline: 10px; + } + } + `,t([le({attribute:!1})],Ea.prototype,"hass",void 0),t([ce()],Ea.prototype,"hours",void 0),t([ce()],Ea.prototype,"minutes",void 0),t([ce()],Ea.prototype,"mode",void 0),t([le({type:Boolean})],Ea.prototype,"autoValidate",void 0),t([le({type:Boolean})],Ea.prototype,"required",void 0),t([le({type:Boolean})],Ea.prototype,"disabled",void 0),t([le({type:String})],Ea.prototype,"label",void 0),t([le({type:Boolean})],Ea.prototype,"useAmPm",void 0),t([le({type:Boolean})],Ea.prototype,"large",void 0),t([le({attribute:!1})],Ea.prototype,"stepSize",void 0),Ea=t([re("scheduler-time-picker")],Ea);let Da=class extends oe{constructor(){super(...arguments),this.weekdayTypeCustomSelected=!1,this.selectedWeekdays=[]}async showDialog(e){this._params=e,await this.updateComplete,this.selectedWeekdays=this._params.weekdays.filter(e=>![pe.Daily,pe.Weekend,pe.Workday].includes(e)),this.weekdayTypeCustomSelected=this.selectedWeekdays.length>0&&this._params.weekdays.length==this.selectedWeekdays.length}async closeDialog(){this._params&&this._params.cancel(),this._params=void 0}render(){return this._params?q` + + + ${this.weekdayTypeCustomSelected?q` + + `:q` + + `}; +
    + ${Fi("ui.dialog.weekday_picker.title",this.hass)} +
    +
    +
    + + ${this._renderWeekdayOptions()} + +
    + + + + ${Xi("ui.common.cancel",this.hass)} + + + ${Xi("ui.common.ok",this.hass)} + + +
    + `:q``}_renderWeekdayOptions(){let e=[];if(this.weekdayTypeCustomSelected){e=[pe.Sunday,pe.Monday,pe.Tuesday,pe.Wednesday,pe.Thursday,pe.Friday,pe.Saturday];e=((e,t)=>e.concat(e).slice(t,t+e.length))(e,xs(this.hass))}else e=[pe.Daily,pe.Workday,pe.Weekend,"Custom"];const t=e=>{var t,i;return"Custom"==e?null===(t=this._params)||void 0===t?void 0:t.weekdays.some(e=>![pe.Daily,pe.Weekend,pe.Workday].includes(e)):null===(i=this._params)||void 0===i?void 0:i.weekdays.includes(e)};return e.map(e=>q` + + ${t(e)?q``:""} + ${"Custom"==e?q` + ${Qi(Fi("ui.dialog.weekday_picker.choose",this.hass))} + ${this.selectedWeekdays.length?q`${this.selectedWeekdays.length}`:""} + `:Qi(ks(e,"long",this.hass))} + + ${"Custom"==e?q``:""} + + `)}_toggleSelectOption(e){const t=e.target.getAttribute("option");let i=[...this._params.weekdays];"Custom"==t?(i=this.selectedWeekdays,this.weekdayTypeCustomSelected=!0):[pe.Daily,pe.Weekend,pe.Workday].includes(t)?(i.includes(t)?i.length>1&&(i=i.filter(e=>e!=t)):i=[t],this.weekdayTypeCustomSelected=!1):i=i.includes(t)?i.filter(e=>e!=t):[...i,t],this._params=Object.assign(this._params,{weekdays:i}),e.target.blur(),this.requestUpdate()}confirmClick(){const e=Array.from(new Set(this._params.weekdays));this._params.confirm(e)}cancelClick(){this._params.cancel()}backClick(){this.weekdayTypeCustomSelected=!1,this.selectedWeekdays=this._params.weekdays.filter(e=>![pe.Daily,pe.Weekend,pe.Workday].includes(e))}static get styles(){return r` + div.wrapper { + color: var(--primary-text-color); + padding: 0px 12px; + } + ha-list { + --mdc-list-vertical-padding: 0px; + } + ha-list-item[disabled] { + color: var(--disabled-text-color); + } + ha-list-item.nested { + --mdc-list-side-padding: 36px; + } + .badge { + height: 24px; + border-radius: 12px; + background: rgba(var(--rgb-primary-color), 0.3); + line-height: 1.25rem; + font-size: 0.875rem; + font-weight: 400; + padding: 0px 12px; + display: inline-flex; + align-items: center; + box-sizing: border-box; + margin: 0px 16px; + } + `}};t([le({attribute:!1})],Da.prototype,"hass",void 0),t([ce()],Da.prototype,"_params",void 0),t([ce()],Da.prototype,"weekdayTypeCustomSelected",void 0),t([ce()],Da.prototype,"selectedWeekdays",void 0),Da=t([re("dialog-select-weekdays")],Da);var Ma=Object.freeze({__proto__:null,get DialogSelectWeekdays(){return Da}});const La=(e,t)=>((e,t)=>et?1:0)(e.toLowerCase(),t.toLowerCase()),Pa=(e,t,i)=>{const s=Object.keys(e.services).includes(t)?Object.keys(e.services[t]).filter(e=>{if(!Object.keys(ts).includes(t))return!1;let s=Object.keys(ts[t]).includes(e);if(!s&&Object.keys(ts[t]).includes("{entity_id}")){if("script"==t&&["turn_on","turn_off","reload","toggle","test"].includes(e))return!1;s=Gi(`${t}.${e}`,i)}return s}):[],a=t=>Xi(`component.${t}.title`,e,!1)||t.replace(/_/g," "),o=s=>{let o=Qi(fs({service:`${t}.${s}`,service_data:{}},e,i.customize));return"script"==t?Object.keys(i.customize||{}).includes(`${t}.${s}`)&&Be(i.customize[`${t}.${s}`].name)?i.customize[`${t}.${s}`].name:`${Qi(Os(`${t}.${s}`,e,i.customize))}: ${o}`:`${a(t)}: ${o}`},n=i=>{let s=Xi(`component.${t}.services.${i}.description`,e,!1);return s||(s=e.services[t][i].description),s||"script"!=t||(s=Xi(`component.${t}.services.turn_on.description`,e,!1)),s};let r=s.map(s=>{return{key:s,name:o(s),description:n(s),icon:(a=s,"script"==t&&Object.keys(i.customize||{}).includes(`${t}.${a}`)&&Be(i.customize[`${t}.${a}`].icon)?i.customize[`${t}.${a}`].icon:Object.keys(cs).includes(t)&&Object.keys(cs[t].services).includes(a)?cs[t].services[a]:Vs(t)),action:{service:s.includes(".")?s:`${t}.${s}`,service_data:{},target:e.services[t][s].target?{}:void 0}};var a}),d=(l=i.customize||{},c=t,Object.keys(l).filter(e=>{var t;return null===(t=l[e].exclude_actions)||void 0===t?void 0:t.length}).filter(e=>!c||!c.includes(".")&&Ki(Wi(e),c)||Ki(e,c)).map(e=>l[e].exclude_actions).flat().filter(Be));var l,c;return d.length&&(r=r.filter(t=>!d.some(s=>La(Bi(t.action.service),s)>0||La(fs(t.action,e,i.customize),s)>0))),os(i.customize||{},t).forEach(e=>{let i=e.service;for(;r.find(e=>e.key==i);)i+="_2";e.variables&&Object.entries(e.variables).forEach(([t,i])=>{let s=ps(i),a=xa(s);!Be(e.service_data[t])&&Be(a)?e=Object.assign(Object.assign({},e),{service_data:Object.assign(Object.assign({},e.service_data),{[t]:a})}):Be(e.service_data[t])||(e=Object.assign(Object.assign({},e),{service_data:Object.assign(Object.assign({},e.service_data),{[t]:null})}))}),r.push({key:i,name:`${a(t)}: ${Na(e.name||o(Bi(e.service)))}`,description:Na(e.name||""),icon:e.icon||Vs(t),action:{service:e.service.includes(".")?e.service:`${t}.${e.service}`,service_data:e.service_data||{},target:e.target?e.target:void 0,name:e.name,icon:e.icon}})}),r},Na=e=>{if(null!==/<.+?>/g.exec(e)){e=(new DOMParser).parseFromString(e,"text/html").body.textContent||""}let t;for(;t=/\[([^\]]+)\]/.exec(e);)e=e.replace(t[0],"");for(;t=/\{([^\}]+)\}/.exec(e);)e=e.replace(t[0],"");return e};let Ia=class extends oe{constructor(){super(...arguments),this._search="",this._filter="",this.timer=0,this.lockDomain=!1,this.showAll=!1}async showDialog(e){this._params=e,this.lockDomain=void 0!==e.domainFilter,this.showAll=!1,await this.updateComplete}async closeDialog(){this._params&&this._params.cancel(),this._params=void 0,this._clearSearch(),this._height=void 0,this._width=void 0}async willUpdate(){this.hass.loadBackendTranslation("title"),this.hass.loadBackendTranslation("services")}render(){return this._params?q` + +
    + + ${void 0===this._params.domainFilter||this.lockDomain?q` + + `:q` + + `} +
    + ${Fi("ui.dialog.action_picker.title",this.hass)} +
    + ${!this.lockDomain&&Be(this._params.cardConfig.include)?q` + {e.target.firstElementChild.blur()}} + > + + + + + ${Fi("ui.dialog.action_picker.show_all",this.hass)} + + `:""} +
    + + +
    + ${this._search&&q` + + `} + +
    +
    +
    + + + ${this._renderOptions()} + +
    + `:q``}_opened(){var e;const t=null===(e=this.shadowRoot.querySelector("ha-list"))||void 0===e?void 0:e.getBoundingClientRect();this._width=null==t?void 0:t.width,this._height=null==t?void 0:t.height}_handleSearchChange(e){const t=e.currentTarget.value;this._search=t,clearTimeout(this.timer),this.timer=window.setTimeout(()=>{this._filter=this._search},100)}_renderOptions(){var e,t;if(null===(e=this._params)||void 0===e?void 0:e.domainFilter)return this._renderDomainActions();let i=Object.assign({},null===(t=this._params)||void 0===t?void 0:t.cardConfig);this.showAll&&(i=Object.assign(Object.assign({},i),{include:void 0,exclude:void 0}));const s=Hs(this.hass,i);return 1===s.length?(this._params=Object.assign(Object.assign({},this._params),{domainFilter:[s[0].key]}),this._renderDomainActions()):this._renderDomainList(s)}_renderDomainList(e){e.sort((e,t)=>Yi(e.name,t.name)),this._filter&&(e=e.filter(e=>{const t=this._filter.toLowerCase().trim().split(" ");return t.every(t=>e.name.toLowerCase().includes(t))||t.every(t=>e.key.toLowerCase().includes(t))}));let t=[];for(var i=e.length;i<7;i++)t.push(0);return Object.keys(e).length?q` + ${Object.keys(e).map(t=>q` + this._handleDomainClick(e[t].key)} + > + + + ${e[t].name} + `)} + ${t.map(e=>q` + + + `)} + `:q` + + ${Xi("ui.components.combo-box.no_match",this.hass)} + + `}_renderDomainActions(){var e,t;let i=Object.assign({},null===(e=this._params)||void 0===e?void 0:e.cardConfig);this.showAll&&(i=Object.assign(Object.assign({},i),{include:void 0,exclude:void 0}));let s=this._params.domainFilter.map(e=>Pa(this.hass,e,i)).flat();return(null===(t=this._params.entityFilter)||void 0===t?void 0:t.length)&&(s=s.filter(e=>{var t;return null===(t=this._params.entityFilter)||void 0===t?void 0:t.every(t=>{const i=ns(e.action,this._params.cardConfig.customize),s=this.hass.states[t];return!(i.supported_features&&!((s.attributes.supported_features||0)&i.supported_features))&&((!Object.keys(e.action.service_data).includes("entity_id")||e.action.service_data.entity_id==t)&&(!Object.keys(e.action.target||{}).includes("entity_id")||(e.action.target||{}).entity_id==t))})})),this._filter&&(s=s.filter(e=>{const t=this._filter.toLowerCase().trim().split(" ");return t.every(t=>e.name.toLowerCase().includes(t))||t.every(t=>e.key.toLowerCase().includes(t))})),Object.keys(s).length?Object.keys(s).map(e=>q` + this._handleActionClick(s[e])} + twoline + > + + ${s[e].name} + ${s[e].description} + + `):q` + + ${Xi("ui.components.combo-box.no_match",this.hass)} + + `}_handleDomainClick(e){this._params=Object.assign(Object.assign({},this._params),{domainFilter:[e]}),this._clearSearch()}_clearDomain(){this._params=Object.assign(Object.assign({},this._params),{domainFilter:void 0}),this._clearSearch()}_handleActionClick(e){this._params.confirm(e.action),this._params=void 0,this._clearSearch()}_clearSearch(){this._search="",this._filter=""}_toggleShowAll(){this.showAll?this.showAll=!1:(this.showAll=!0,this.lockDomain||this._clearDomain())}static get styles(){return r` + ha-dialog { + --dialog-content-padding: 0; + --ha-dialog-width-md: 480px; + } + ha-input { + display: block; + margin: 0 16px; + } + ha-list { + min-height: 300px; + } + ha-list-item:not([twoline]) { + height: 56px; + } + `}};t([le({attribute:!1})],Ia.prototype,"hass",void 0),t([ce()],Ia.prototype,"_params",void 0),t([ce()],Ia.prototype,"_search",void 0),t([ce()],Ia.prototype,"_filter",void 0),t([ce()],Ia.prototype,"_width",void 0),t([ce()],Ia.prototype,"_height",void 0),t([ce()],Ia.prototype,"lockDomain",void 0),t([ce()],Ia.prototype,"showAll",void 0),Ia=t([re("dialog-select-action")],Ia);var qa=Object.freeze({__proto__:null,get DialogSelectAction(){return Ia}});let Ra=class extends oe{constructor(){super(...arguments),this.showPrefix=!1}render(){return q` + ${this.showPrefix?q` +
    +
    +
    +
    +
    +
    +
    + `:q` +
    +
    +
    +
    + `} +
    + `}static get styles(){return r` + + :host { + display: flex; + padding: 0px; + align-content: normal; + align-self: auto; + align-items: center; + } + .body { + padding-top: 0px; + padding-bottom: 0px; + padding-left: 0; + padding-inline-start: 0; + padding-right: 16x; + padding-inline-end: 16px; + overflow: hidden; + align-content: center; + } + .body > * { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + .content { + display: flex; + justify-content: flex-end; + flex: 1; + padding: 8px 0; + } + // .content ::slotted(*) { + // width: var(--settings-row-content-width); + // } + .prefix-wrap { + display: flex; + flex-direction: row; + } + .prefix { + display: flex; + width: 48px; + } + .heading, .heading ::slotted(*) { + display: flex; + align-items: center; + width: 150px; + } + @media all and (max-width: 450px) { + :host { + align-items: normal; + flex-direction: column; + border-top: 1px solid var(--divider-color); + padding: 8px 16px; + } + .prefix-wrap { + display: flex; + align-items: center; + } + .content ::slotted(*) { + width: 100%; + } + `}};t([le({type:Boolean})],Ra.prototype,"showPrefix",void 0),Ra=t([re("scheduler-settings-row")],Ra);let Va=class extends oe{constructor(){super(...arguments),this.selectedSlot=null,this.large=!1,this.selectedEntry=0}shouldUpdate(e){return e.get("schedule")&&this.dispatchEvent(new CustomEvent("change",{detail:{schedule:this.schedule}})),!0}render(){return q` + ${this.schedule.entries.map((e,t)=>q` + +
    +
    + + ${Fi("ui.panel.editor.repeated_days",this.hass)}: + ${js(e.weekdays,"short",this.hass)} + + this._showWeekdayDialog(e,t)}> +
    +
    + + ${this.viewMode==me.Scheme?Fi("ui.panel.editor.toggle_single_mode",this.hass):Fi("ui.panel.editor.toggle_scheme_mode",this.hass)} + + +
    +
    + + ${this.viewMode==me.Scheme?q` +
    +
    + ${this.hass.localize("ui.dialogs.helper_settings.input_datetime.time")}: +
    + ${this.renderActionButtons()} +
    + this._handleUpdate(e,t)} + .large=${this.large} + > + + `:q` + ${this.hass.localize("ui.dialogs.helper_settings.input_datetime.time")}: + + + `} + `)} + + ${this.renderSlot()} + `}toggleViewMode(){const e=this.viewMode==me.Scheme?me.Single:me.Scheme;this.dispatchEvent(new CustomEvent("setViewMode",{detail:e}))}renderActionButtons(){if(null===this.selectedSlot||null===this.selectedEntry)return q``;const e=this.schedule.entries[this.selectedEntry].slots[this.selectedSlot].start,t=this.schedule.entries[this.selectedEntry].slots[this.selectedSlot].stop||e,i=Ae(e,this.hass),s=(Ae(t,this.hass)||86400)-i;return q` +
    + {this._updateSelectedSlot(this.selectedSlot-1),e.target.blur()}} ?disabled=${null===this.selectedSlot||this.selectedSlot<1}> + + {this._updateSelectedSlot(this.selectedSlot+1),e.target.blur()}} ?disabled=${null===this.selectedSlot||this.selectedSlot>this.schedule.entries[this.selectedEntry].slots.length-2}> + + + + + +
    + `}renderSlot(){if(null===this.selectedEntry||null===this.selectedSlot)return q` +
    + ${Fi("ui.panel.editor.select_timeslot",this.hass)} +
    + `;const e=this.schedule.entries[this.selectedEntry].slots[this.selectedSlot],t=this.selectedSlot===this.schedule.entries[this.selectedEntry].slots.length-1;let i=e.stop;return!i&&this.selectedSlot +
    + + +
    +
    + + +
    + `:""} + + ${Fi("ui.panel.editor.action",this.hass)}: + ${this._renderActionConfig()} + `}_renderActionConfig(){var e,t,i,s;const a=Object.assign({},this.schedule.entries[this.selectedEntry].slots[this.selectedSlot]),o=a.actions.length?a.actions[0]:void 0;if(!o)return q` +
    + + + ${Fi("ui.panel.editor.add_action",this.hass)} + +
    + `;const n=ns(o,this.config.customize),r=(null===(e=n.target)||void 0===e?void 0:e.domain)||Wi(o.service),d=Be(null===(t=null==n?void 0:n.target)||void 0===t?void 0:t.entity_id)||this.schedule.entries[this.selectedEntry].slots.some(e=>{var t,i;return e.actions.length&&Be(null===(i=null===(t=ns(e.actions[0],this.config.customize))||void 0===t?void 0:t.target)||void 0===i?void 0:i.entity_id)});if(void 0===n)return q``;const l=Object.keys(n.fields||{}).filter(e=>_a(o,e,this.hass,this.config.customize));let c="",u=[(null===(i=o.target)||void 0===i?void 0:i.entity_id)||[]].flat();return!u.length&&["notify","script"].includes(r)&&(u=[o.service]),u.length&&(c+=u.map(e=>Os(e,this.hass,this.config.customize)).join(", "),c+=": "),c+=fs(o,this.hass,this.config.customize,!1,!0),q` + +
    + + ${Qi(c)} +
    + + {e.target.firstElementChild.blur()}} + placement="bottom-end" + > + + + + + ${Xi("ui.panel.lovelace.editor.card.conditional.change_type",this.hass)} + + + + ${Xi("ui.common.delete",this.hass)} + + + +
    + + ${n.target?q` + + ${Xi("ui.components.entity.entity-picker.entity",this.hass)} + !n.supported_features||((e.attributes.supported_features||0)&n.supported_features)>0} + @value-changed=${this._selectEntity} + .value=${[(null===(s=o.target)||void 0===s?void 0:s.entity_id)||[]].flat()} + ?multiple=${!0} + ?disabled=${d} + > + + + `:""} + + ${l.map(e=>{var t;const i=us(o.service,null===(t=o.target)||void 0===t?void 0:t.entity_id,e,this.hass,this.config.customize);if(null===i)return"";let s=n.fields[e].optional||(i.number||{}).optional;const a=!s||Object.keys(o.service_data).includes(e);return q` + + ${s?q` + this._toggleOptionalField(t,e,i)} + > + + `:""} + + ${((e,t,i,s)=>{var a;const o=Wi(e.service),n=Bi(e.service);let r=Xi(`component.${o}.services.${n}.fields.${t}.name`,i,!1);!r&&i.services[o]&&i.services[o][e.service]&&i.services[o][e.service].fields&&i.services[o][e.service].fields[t]&&(r=String(i.services[o][e.service].fields[t].name));const d=["script","notify"].includes(o)?[e.service]:[(null===(a=e.target)||void 0===a?void 0:a.entity_id)||[]].flat(),l=d.length?d[0]:e.service;let c=os(s||{},l);if(c.length){let i=c.map(i=>{if(i.service!=e.service||!Object.keys(i.variables||{}).includes(t))return null;return(i.variables||{})[t].name}).filter(Be);if(i.length)return i[0]}return r||(r=t.replace(/_/g," ")),r})(o,e,this.hass,this.config.customize)} + + this._selectField(e,t)} + > + + + `})} +
    +
    + `}_selectField(e,t){const i=t.detail.value,s=Object.assign({},this.schedule.entries[this.selectedEntry].slots[this.selectedSlot]);let a=void 0!==i?Object.assign(Object.assign({},s.actions[0]),{service_data:Object.assign(Object.assign({},s.actions[0].service_data),{[e]:i})}):Object.assign(Object.assign({},s.actions[0]),{service_data:Object.fromEntries(Object.entries(s.actions[0].service_data).filter(([t])=>t!=e))});this._updateSlot({actions:[a]})}_toggleOptionalField(e,t,i){const s=e.target.checked,a=s?xa(i):void 0;s?this._selectField(t,new CustomEvent("value-changed",{detail:{value:Be(a)?a:null}})):this._selectField(t,new CustomEvent("value-changed",{detail:{value:void 0}}))}_selectEntity(e){const t=e.detail.value;t&&this.schedule.entries[this.selectedEntry].slots.forEach((e,i)=>{if(!e.actions.length)return;let s=Object.assign(Object.assign({},e.actions[0]),{target:{entity_id:t}});this._updateSlot({actions:[s]},i)})}_handleUpdate(e,t){this.selectedEntry=t,e.detail.hasOwnProperty("selectedSlot")?(this._updateSelectedSlot(e.detail.selectedSlot),this.selectedSlot=e.detail.selectedSlot):e.detail.hasOwnProperty("slots")&&this._updateEntry({slots:e.detail.slots})}_updateSelectedSlot(e){this.dispatchEvent(new CustomEvent("change",{detail:{selectedSlot:e}}))}_updateEntry(e){let t=Object.assign({},this.schedule.entries[this.selectedEntry]);t=Object.assign(Object.assign({},t),e),this.schedule=Object.assign(Object.assign({},this.schedule),{entries:Object.assign(this.schedule.entries,{[this.selectedEntry]:t})})}_updateSlot(e,t=this.selectedSlot){let i=Object.assign({},this.schedule.entries[this.selectedEntry].slots[t]);i=Object.assign(Object.assign({},i),e),this._updateEntry({slots:Object.assign(this.schedule.entries[this.selectedEntry].slots,{[t]:i})})}async _showWeekdayDialog(e,t){this.selectedEntry=t,await new Promise(i=>{const s={weekdays:[...this.schedule.entries[t].weekdays],cancel:()=>i(null),confirm:e=>i(e)};Ts(e.target,"show-dialog",{dialogTag:"dialog-select-weekdays",dialogImport:()=>Promise.resolve().then((function(){return Ma})),dialogParams:s})}).then(e=>{e&&this._updateEntry({weekdays:e})})}async _showActionDialog(e){let t=[],i=[];this.schedule.entries.forEach(e=>{e.slots.forEach(e=>{e.actions.forEach(e=>{var s,a;i=[...i,...[(null===(s=e.target)||void 0===s?void 0:s.entity_id)||[]].flat()],t=[...t,...[Wi(e.service),...[(null===(a=e.target)||void 0===a?void 0:a.entity_id)||[]].flat()].map(Wi)]})})}),t=[...new Set(t)],i=[...new Set(i)],await new Promise(s=>{const a={cancel:()=>s(null),confirm:e=>s(e),domainFilter:t.length?t:void 0,entityFilter:i.length?i:void 0,cardConfig:this.config};Ts(e.target,"show-dialog",{dialogTag:"dialog-select-action",dialogImport:()=>Promise.resolve().then((function(){return qa})),dialogParams:a})}).then(e=>{if(!e)return;Object.assign({},this.schedule.entries[this.selectedEntry].slots[this.selectedSlot]);const t=this.schedule.entries[this.selectedEntry].slots.find(e=>{var t;return e.actions.length?null===(t=e.actions[0].target)||void 0===t?void 0:t.entity_id:void 0});let i=Object.assign({},e);t&&i.target&&(i=Object.assign(Object.assign({},i),{target:t.actions[0].target})),this._updateSlot({actions:[i]})})}_actionItemOptionsClick(e){switch(e.detail.item.value){case"change_type":this._showActionDialog(e);break;case"delete":this._updateSlot({actions:[]})}}_stopTimeChanged(e){let t=e.detail.value,[i,s]=za([...this.schedule.entries[this.selectedEntry].slots],Number(this.selectedSlot),{stop:t},this.hass);this._updateEntry({slots:i}),s!=this.selectedSlot&&this._updateSelectedSlot(s)}_startTimeChanged(e){let t=e.detail.value,[i,s]=za([...this.schedule.entries[this.selectedEntry].slots],Number(this.selectedSlot),{start:t},this.hass);this._updateEntry({slots:i}),s!=this.selectedSlot&&this._updateSelectedSlot(s)}_addTimeslot(e){null!==this.selectedEntry&&null!==this.selectedSlot&&(this.schedule=((e,t,i,s)=>{let a=[...e.entries[t].slots],o=Ce(a[i].start),n=void 0===a[i].stop?o:Ce(a[i].stop);if(n.mode!==be.Fixed||n.hours||n.minutes||(n=Object.assign(Object.assign({},n),{hours:24})),[be.Sunrise,be.Sunset].includes(o.mode)){const e=o.mode==be.Sunrise?s.states["sun.sun"].attributes.next_rising:s.states["sun.sun"].attributes.next_setting;let t=Ce(e);o=Se(t,{hours:o.hours,minutes:o.minutes})}const r=Ae(o,s),d=(Ae(n,s)-r)/2,l=Math.floor(d/3600),c=Math.round((d-3600*l)/60);let u=Se(o,{hours:l,minutes:c});return u=ze(u,15),a=[...a.slice(0,i),Object.assign(Object.assign({},a[i]),{stop:Ne(u)}),Object.assign(Object.assign({},a[i]),{start:Ne(u),stop:Ne(n),actions:[]}),...a.slice(i+1)],e=Object.assign(Object.assign({},e),{entries:Object.assign(e.entries,{[t]:Object.assign(Object.assign({},e.entries[t]),{slots:a})})})})(this.schedule,this.selectedEntry,this.selectedSlot,this.hass),e.target.blur())}_removeTimeslot(e){null!==this.selectedEntry&&null!==this.selectedSlot&&(this.schedule=((e,t,i)=>{let s=[...e.entries[t].slots];const a=i==s.length-1?i-1:i;return s=[...s.slice(0,a),Object.assign(Object.assign({},s[a+1]),{start:s[a].start,stop:s[a+1].stop}),...s.slice(a+2)],e=Object.assign(Object.assign({},e),{entries:Object.assign(e.entries,{[t]:Object.assign(Object.assign({},e.entries[t]),{slots:s})})})})(this.schedule,this.selectedEntry,this.selectedSlot),this.selectedSlot>=this.schedule.entries[this.selectedEntry].slots.length&&(this.selectedSlot=this.schedule.entries[this.selectedEntry].slots.length-1),e.target.blur())}static get styles(){return r` + :host { + position: relative; + } + .two-column { + display: flex; + flex-direction: row; + margin: 16px 0px; + flex-wrap: wrap; + gap: 10px; + } + .two-column .column { + display: flex; + flex-direction: column; + flex: 0 0 215px; + } + div.editor-header { + display: flex; + flex-direction: row; + flex-wrap: wrap; + } + .weekdays { + display: flex; + flex: 1; + align-items: center; + white-space: nowrap; + } + .weekdays-actions { + display: flex; + align-items: center; + } + div.actions { + display: flex; + align-items: end; + margin: -4px 0px 0px 0px; + } + @media all and (max-width: 150px) { + div.editor-header { + flex-direction: column; + margin-top: 0px; + } + div.actions { + align-self: flex-end; + } + } + div.slot-placeholder { + padding: 20px 0px 0px 0px; + } + scheduler-collapsible-section .header ha-icon { + margin-right: 6px; + } + scheduler-collapsible-section .header span { + flex: 1; + } + ha-list-item.warning, ha-list-item.warning ha-icon { + color: var(--error-color); + } + `}};t([le({attribute:!1})],Va.prototype,"hass",void 0),t([le({attribute:!1})],Va.prototype,"config",void 0),t([le({attribute:!1})],Va.prototype,"viewMode",void 0),t([le({attribute:!1})],Va.prototype,"selectedSlot",void 0),t([le({type:Boolean})],Va.prototype,"large",void 0),t([ce()],Va.prototype,"schedule",void 0),t([ce()],Va.prototype,"selectedEntry",void 0),Va=t([re("scheduler-main-panel")],Va);const Ha=["January","February","March","April","May","June","July","August","September","October","November","December"];function Ua(e){return e.toISOString().split("T")[0]}function Fa(e){let t=new Date;const i=(e||"").match(/^([0-9]{4})-([0-9]{2})-([0-9]{2})/);null!==i&&t.setFullYear(Number(i[1]),Number(i[2])-1,Number(i[3]));const s=(e||"").match(/([0-9]{2}):([0-9]{2})(:([0-9]{2}))?$/);return null!==s&&t.setHours(Number(s[1]),Number(s[2]),s.length>4?Number(s[4]):t.getSeconds()),t}const Ba=(e,t)=>"select"in t&&null!==t.select?((e,t)=>{var i;return((null===(i=t.select)||void 0===i?void 0:i.options)||[]).some(t=>"object"==typeof t?t.value==e:t==e)})(String(e),t):"number"in t&&null!==t.number?((e,t)=>{var i,s;return!isNaN(e)&&(!(void 0!==(null===(i=t.number)||void 0===i?void 0:i.min)&&et.number.max))})(Number(e),t):!("text"in t)||null===t.text||String(e).length>0,Wa=e=>null==e||Array.isArray(e)?e:[e];let Za=class extends oe{constructor(){super(...arguments),this._search="",this._filter="",this.timer=0,this.showAll=!1}async showDialog(e){this._params=e,this.showAll=!1,await this.updateComplete}async closeDialog(){this._params&&this._params.cancel(),this._params=void 0,this._clearSearch(),this._height=void 0,this._width=void 0}async willUpdate(){this.hass.loadBackendTranslation("title")}render(){return this._params?q` + +
    + + +
    + ${Fi("ui.panel.options.conditions.add_condition",this.hass)} +
    + ${Be(this._params.domain)?"":q` + {e.target.firstElementChild.blur()}} + > + + + + + ${Fi("ui.dialog.action_picker.show_all",this.hass)} + + `} +
    + + +
    + ${this._search&&q` + + `} + +
    +
    +
    + + + ${this._renderOptions()} + +
    + `:q``}_opened(){var e;const t=null===(e=this.shadowRoot.querySelector("ha-list"))||void 0===e?void 0:e.getBoundingClientRect();this._width=null==t?void 0:t.width,this._height=null==t?void 0:t.height}_handleSearchChange(e){const t=e.currentTarget.value;this._search=t,clearTimeout(this.timer),this.timer=window.setTimeout(()=>{this._filter=this._search},100)}_clearSearch(){this._search="",this._filter=""}_renderOptions(){var e;let t=Object.assign({},null===(e=this._params)||void 0===e?void 0:e.cardConfig);this.showAll&&(t=Object.assign(Object.assign({},t),{include:void 0,exclude:void 0}));let i=Xs(this.hass,t);return i.sort((e,t)=>Yi(e.name,t.name)),this._filter&&(i=i.filter(e=>{const t=this._filter.toLowerCase().trim().split(" ");return t.every(t=>e.name.toLowerCase().includes(t))||t.every(t=>e.key.toLowerCase().includes(t))})),Object.keys(i).map(e=>q` + this._handleDomainClick(i[e].key)} + > + + ${i[e].name} + + `)}_handleDomainClick(e){this._params=Object.assign(Object.assign({},this._params),{domain:e}),this._params.confirm(e),this._params=void 0,this._clearSearch()}_toggleShowAll(){this.showAll?this.showAll=!1:this.showAll=!0}static get styles(){return r` + ha-dialog { + --dialog-content-padding: 0; + --ha-dialog-width-md: 480px; + } + ha-input { + display: block; + margin: 0 16px; + } + ha-list { + min-height: 300px; + } + ha-list-item:not([twoline]) { + height: 56px; + } + `}};t([le({attribute:!1})],Za.prototype,"hass",void 0),t([ce()],Za.prototype,"_params",void 0),t([ce()],Za.prototype,"_search",void 0),t([ce()],Za.prototype,"_filter",void 0),t([ce()],Za.prototype,"_width",void 0),t([ce()],Za.prototype,"_height",void 0),t([ce()],Za.prototype,"showAll",void 0),Za=t([re("dialog-select-condition")],Za);var Ka=Object.freeze({__proto__:null,get DialogSelectCondition(){return Za}});let Ga=class extends oe{constructor(){super(...arguments),this.conditionIdx=-1,this.conditionValid=!0,this.startDate="",this.endDate="",this.tags=[],this.customTagValue=""}async firstUpdated(){var e,t;(await window.loadCardHelpers()).importMoreInfoControl("input_datetime"),this.startDate=(null===(e=this.schedule)||void 0===e?void 0:e.start_date)||Ua(new Date),this.endDate=(null===(t=this.schedule)||void 0===t?void 0:t.end_date)||Ua(new Date);const i=(await Ds(this.hass)).map(e=>e.name),s=[this.config.tags||[]].flat();this.tags=[...new Set([...i,...s.filter(e=>!i.includes(e)&&!["none","disabled","enabled"].includes(e))])]}shouldUpdate(e){return e.get("schedule")&&this.dispatchEvent(new CustomEvent("change",{detail:{schedule:this.schedule}})),!0}render(){const e={select:{options:this.tags,multiple:!0,custom_value:!0}};return q` +
    + ${Fi("ui.panel.options.conditions.header",this.hass)}: + ${this.schedule.entries[0].slots[0].conditions.items.length?q` + {e.target.firstElementChild.blur()}} + placement="bottom-end" + > + + + + + ${Fi("ui.panel.options.conditions.options.logic_or",this.hass)} + + + + ${Fi("ui.panel.options.conditions.options.logic_and",this.hass)} + + + + ${Fi("ui.panel.options.conditions.options.track_changes",this.hass)} + + + `:""} +
    + + ${this.renderConditions()} + + +
    + + + ${Fi("ui.panel.options.conditions.add_condition",this.hass)} + +
    + + + ${Fi("ui.panel.options.period.header",this.hass)}: +
    +
    + + +
    +
    + ${Fi("ui.panel.options.period.start_date",this.hass)} +
    +
    + + +
    +
    + ${Fi("ui.panel.options.period.end_date",this.hass)} +
    +
    + + +
    +
    + + ${Xi("ui.common.name",this.hass)}: +
    + +
    + + ${Fi("ui.panel.options.tags",this.hass)}: +
    + + + + {e.stopPropagation(),e.target.querySelector("ha-button").blur()}} + @click=${e=>{e.preventDefault(),e.stopImmediatePropagation()}} + @wa-after-show=${e=>{e.target.querySelector("ha-input").focus()}} + placement="bottom-start" + > + + + ${Xi("ui.panel.config.tag.add_tag",this.hass)} + + +
    + {this.customTagValue=e.currentTarget.value}} + @keydown=${e=>{"Enter"===e.key&&this._customTagConfirmClick(e)}} + .placeholder="" + > + + ${Xi("ui.common.ok",this.hass)} + +
    +
    +
    + + ${Fi("ui.panel.options.repeat_type",this.hass)}: + + + ${Xi("ui.components.calendar.event.repeat.label",this.hass)} + + + + ${Xi("ui.dialogs.more_info_control.vacuum.stop",this.hass)} + + + + ${Xi("ui.common.delete",this.hass)} + + `}renderConditions(){let e=this.schedule.entries[0].slots[0].conditions.items;return this.conditionIdx==e.length&&(e=[...e,{}]),e.map((e,t)=>{var i;const s=this.conditionIdx==t?this.selectedEntity||e.entity_id||"":e.entity_id||"",a=this.conditionIdx==t&&this.selectedDomain||Wi(s),o=Qs(s||a,this.hass,this.config.customize),n=o&&o.hasOwnProperty("number")?[ge.Above,ge.Below]:[ge.Equal,ge.Unequal],r={[ge.Equal]:"mdi:equal",[ge.Unequal]:"mdi:not-equal-variant",[ge.Above]:"mdi:greater-than",[ge.Below]:"mdi:less-than"},d={[ge.Equal]:"ui.panel.options.conditions.types.equal_to",[ge.Unequal]:"ui.panel.options.conditions.types.unequal_to",[ge.Above]:"ui.panel.options.conditions.types.above",[ge.Below]:"ui.panel.options.conditions.types.below"};return this.conditionIdx!==t||this.selectedMatchType||(this.selectedMatchType=n[0]),q` + +
    + ${e.entity_id&&void 0!==e.value?q` + + ${Qi(Fi(d[e.match_type],this.hass,["{entity}","{value}"],[Os(e.entity_id,this.hass,this.config.customize)||"",null!==(i=ds(e.value,o,this.hass))&&void 0!==i?i:""]))} + `:Fi("ui.panel.options.conditions.add_condition",this.hass)} +
    + this._conditionItemOptionsClick(e,t)} + ?disabled=${!this.conditionValid&&this.conditionIdx!==t&&-1!=this.conditionIdx} + placement="bottom-end" + > + + + + + ${Xi("ui.panel.lovelace.editor.card.conditional.change_type",this.hass)} + + + + ${Xi("ui.common.delete",this.hass)} + + + +
    + + + + ${Xi("ui.components.selectors.selector.types.entity",this.hass)} + + + + + + + + ${Qi(Fi(d[this.conditionIdx==t?this.selectedMatchType:e.match_type],this.hass,["{entity}","{value}"],["",""]))} + {e.target.firstElementChild.blur()}} + > + + + ${n.map(i=>q` + + + ${Qi(Fi(d[i],this.hass,["{entity}","{value}"],["",""]))} + + `)} + + + + + +
    +
    + `})}_updateActiveCondition(e){const t=e.detail.item;if(t<0)return void(this.conditionIdx=-1);if(t===this.conditionIdx)return;this.conditionIdx=t;const i=this.schedule.entries[0].slots[0].conditions.items[t];this.selectedEntity=i?i.entity_id:void 0,this.selectedMatchType=i?i.match_type:void 0,this.conditionValue=i?i.value:void 0}_conditionItemOptionsClick(e,t){switch(e.detail.item.value){case"change_type":this._showConditionDialog(e).then(e=>{e&&(this.conditionIdx=t,this.selectedDomain=e,this.selectedEntity=void 0,this.selectedMatchType=void 0,this.conditionValue=void 0,this.conditionValid=!1)});break;case"delete":const i=this.schedule.entries[0].slots[0].conditions.items.filter((e,i)=>i!==t),s=e=>Object.assign(e,{conditions:Object.assign(Object.assign({},e.conditions),{items:i})}),a=e=>Object.assign(e,{slots:e.slots.map(s)});this.schedule=Object.assign(Object.assign({},this.schedule),{entries:this.schedule.entries.map(a)}),t===this.conditionIdx?this.conditionIdx=-1:void 0!==this.conditionIdx&&t{const i={cancel:()=>t(null),confirm:e=>t(e),domain:void 0,cardConfig:this.config};Ts(e.target,"show-dialog",{dialogTag:"dialog-select-condition",dialogImport:()=>Promise.resolve().then((function(){return Ka})),dialogParams:i})})}_selectEntity(e){const t=e.detail.value;if(this.selectedEntity=t?t.pop():void 0,this.selectedEntity){const e=Qs(this.selectedEntity,this.hass,this.config.customize),t=e&&e.hasOwnProperty("number")?[ge.Above,ge.Below]:[ge.Equal,ge.Unequal];this.selectedMatchType&&t.includes(this.selectedMatchType)||(this.selectedMatchType=t[0])}this._validateCondition()}_validateCondition(){if(this.conditionValid=!1,!this.selectedEntity||!Be(this.conditionValue)||!this.selectedMatchType||void 0===this.conditionIdx)return;const e=Qs(this.selectedEntity,this.hass,this.config.customize);if(!Ba(this.conditionValue,e))return;this.conditionValid=!0;const t={entity_id:this.selectedEntity,match_type:this.selectedMatchType,value:this.conditionValue,attribute:"state"},i=Object.assign(this.schedule.entries[0].slots[0].conditions.items,{[this.conditionIdx]:t}),s=e=>Object.assign(e,{conditions:Object.assign(Object.assign({},e.conditions),{items:i})});this.schedule=Object.assign(Object.assign({},this.schedule),{entries:this.schedule.entries.map(e=>Object.assign(e,{slots:e.slots.map(s)}))})}_conditionAddClick(e){this._showConditionDialog(e).then(e=>{e&&(this.conditionIdx=this.schedule.entries[0].slots[0].conditions.items.length,this.selectedDomain=e,this.selectedEntity=void 0,this.selectedMatchType=void 0,this.conditionValue=void 0,this.conditionValid=!1)})}_conditionConfigOptionsClick(e){let t=Object.assign({},this.schedule.entries[0].slots[0].conditions);switch(e.detail.item.value){case"or":if(t.type==_e.Or)return;t=Object.assign(Object.assign({},t),{type:_e.Or});break;case"and":if(t.type==_e.And)return;t=Object.assign(Object.assign({},t),{type:_e.And});break;case"track_changes":const e=!this.schedule.entries[0].slots[0].conditions.track_changes;t=Object.assign(Object.assign({},t),{track_changes:e})}const i=e=>Object.assign(e,{conditions:t});this.schedule=Object.assign(Object.assign({},this.schedule),{entries:this.schedule.entries.map(e=>Object.assign(e,{slots:e.slots.map(i)}))})}_setStartDate(e){const t=String(e.detail.value);if(!t)return;Fa(t)>Fa(this.endDate)&&(this.schedule=Object.assign(Object.assign({},this.schedule),{end_date:t}),this.endDate=t),this.schedule=Object.assign(Object.assign({},this.schedule),{start_date:t}),this.startDate=t}_setEndDate(e){const t=String(e.detail.value);if(!t)return;Fa(this.startDate)>Fa(t)&&(this.schedule=Object.assign(Object.assign({},this.schedule),{start_date:t}),this.startDate=t),this.schedule=Object.assign(Object.assign({},this.schedule),{end_date:t}),this.endDate=t}toggleEnableDateRange(e){const t=e.target.checked;this.schedule=Object.assign(Object.assign({},this.schedule),{start_date:t?this.startDate:void 0,end_date:t?this.endDate:void 0,repeat_type:t?this.schedule.repeat_type==fe.Repeat?fe.Pause:this.schedule.repeat_type:this.schedule.repeat_type==fe.Pause?fe.Repeat:this.schedule.repeat_type})}updateName(e){const t=e.target.value;this.schedule=Object.assign(Object.assign({},this.schedule),{name:t.trim()})}tagsUpdated(e){let t=e.detail.value;t=t.map(e=>e.trim()),t=t.filter(e=>!["none","disabled","enabled"].includes(e)),this.schedule=Object.assign(Object.assign({},this.schedule),{tags:t})}_customTagConfirmClick(e){let t=e.target;t=t.parentElement,t=t.parentElement;t.querySelector("ha-button").click(),e.preventDefault();let i=String(this.customTagValue).trim();if(i.length){let e=this.schedule.tags||[];e=[...new Set([...e,i])],e=e.filter(e=>!["none","disabled","enabled"].includes(e)),this.schedule=Object.assign(Object.assign({},this.schedule),{tags:e})}this.customTagValue=""}setRepeatType(e){const t=e.target.getAttribute("value");this.schedule=Object.assign(Object.assign({},this.schedule),{repeat_type:t})}static get styles(){return r` + ha-icon-button { + align-self: center; + } + ha-dropdown-item[disabled] ha-icon { + color: var(--disabled-text-color); + } + ha-dropdown-item[noninteractive] { + background-color: rgba(var(--rgb-primary-color), 0.12); + color: var(--sidebar-selected-text-color); + } + ha-dropdown-item[noninteractive] ha-icon { + color: var(--sidebar-selected-text-color); + } + div.period { + display: flex; + width: 100%; + flex-direction: row; + align-items: center; + gap: 5px; + } + div.period > div { + display: flex; + } + div.period > div.input { + position: relative; + overflow: hidden; + flex: 1; + } + ha-date-input, ha-input { + width: 100%; + } + .header { + display: flex; + margin-top: 5px; + width: 100%; + align-items: center; + } + .header.first { + margin-top: 0px; + padding-bottom: 4px; + align-items: flex-end; + justify-content: space-between; + } + .header > * { + display: flex; + } + .header ha-dropdown { + margin-bottom: -10px; + } + `}};t([le({attribute:!1})],Ga.prototype,"hass",void 0),t([le({attribute:!1})],Ga.prototype,"config",void 0),t([ce()],Ga.prototype,"schedule",void 0),t([ce()],Ga.prototype,"conditionIdx",void 0),t([ce()],Ga.prototype,"selectedDomain",void 0),t([ce()],Ga.prototype,"selectedEntity",void 0),t([ce()],Ga.prototype,"selectedMatchType",void 0),t([ce()],Ga.prototype,"conditionValue",void 0),t([ce()],Ga.prototype,"conditionValid",void 0),t([ce()],Ga.prototype,"startDate",void 0),t([ce()],Ga.prototype,"endDate",void 0),t([le()],Ga.prototype,"tags",void 0),t([ce()],Ga.prototype,"customTagValue",void 0),Ga=t([re("scheduler-options-panel")],Ga);let Ja=class extends oe{async showDialog(e){this._params=e,await this.updateComplete}async closeDialog(){this._params&&this._params.cancel(),this._params=void 0}render(){return this._params?q` + + + +
    + ${this._params.title} +
    +
    +
    + ${this._params.description} +
    + + + ${this._params.secondaryButtonLabel?q` + + ${this._params.secondaryButtonLabel} + + `:""} + + ${this._params.primaryButtonLabel} + + +
    + `:q``}confirmClick(){this._params.confirm()}cancelClick(){this._params.cancel()}};t([le({attribute:!1})],Ja.prototype,"hass",void 0),t([ce()],Ja.prototype,"_params",void 0),Ja=t([re("scheduler-generic-dialog")],Ja);var Ya=Object.freeze({__proto__:null,get GenericDialog(){return Ja}});let Qa=class extends oe{constructor(){super(...arguments),this.large=!1,this.selectedEntry=0,this.selectedSlot=null,this._panel="main",this._viewMode=me.Single}set viewMode(e){if(this._viewMode=e,e==me.Single){let e=this.schedule.entries[this.selectedEntry].slots.findIndex(e=>e.actions.length);this.selectedSlot=e>=0?e:1}}shouldUpdate(e){return 1!=e.size||!e.has("hass")||!Be(this.hass)}async showDialog(e){var t;this._params=e,this.schedule=e.schedule,this._panel="main",this.large=!1;const i=this.schedule.entries[this.selectedEntry].slots.filter(e=>e.actions.length&&Be(e.stop)).length>0||this.schedule.entries[this.selectedEntry].slots.filter(e=>e.actions.length).length>1||this.schedule.entries[this.selectedEntry].slots.length>3;let s=this.schedule.entries[this.selectedEntry].slots.findIndex(e=>e.actions.length);this.selectedSlot=s>=0?s:null,this.viewMode=i?me.Scheme:(null===(t=this._params)||void 0===t?void 0:t.cardConfig.default_editor)||me.Single,await this.updateComplete}async closeDialog(){this._params=void 0}willUpdate(){this.hass.loadBackendTranslation("config")}render(){var e;return this._params?q` + + + ${"main"==this._panel?q` + + {this._panel="options"}} + > + `:q` + {this._panel="main"}} + > + `} +
    this.large=!this.large}> + ${this._params.editItem?this.schedule.name?null===(e=this.schedule)||void 0===e?void 0:e.name:Fi("ui.panel.common.default_name",this.hass,"{id}",this._params.editItem):Fi("ui.panel.common.new_schedule",this.hass)} +
    +
    + +
    + + ${"main"==this._panel?q` + + + `:q` + + + `} +
    + +
    + + ${Xi("ui.common.delete",this.hass)} + + + ${Xi("ui.common.save",this.hass)} + +
    +
    + `:q``}_updateSchedule(e){let t=Object.keys(e.detail);if(t.includes("schedule")){let t=e.detail.schedule;this.schedule=t}t.includes("selectedSlot")&&(this.selectedSlot=e.detail.selectedSlot)}async _handleSaveClick(e){const t=va(this.schedule,this.hass,this._params.cardConfig.customize);if(t)await new Promise(i=>{const s={cancel:()=>i(!1),confirm:()=>i(!0),title:Xi("state_badge.default.error",this.hass),description:Fi("ui.panel.editor.validation_errors."+t,this.hass),primaryButtonLabel:Xi("ui.common.ok",this.hass)};Ts(e.target,"show-dialog",{dialogTag:"scheduler-generic-dialog",dialogImport:()=>Promise.resolve().then((function(){return Ya})),dialogParams:s})});else if(this.schedule.schedule_id){const t=Ie(await As(this.hass,this.schedule.schedule_id),this.hass);if(he(this.schedule,t))return void this.closeDialog();if(!t.enabled){await new Promise(t=>{const i={title:Fi("ui.dialog.enable_schedule.title",this.hass),description:Fi("ui.dialog.enable_schedule.description",this.hass),primaryButtonLabel:Xi("ui.common.yes",this.hass),secondaryButtonLabel:Xi("ui.common.no",this.hass),cancel:()=>{t(!1)},confirm:()=>{t(!0)}};Ts(e.target,"show-dialog",{dialogTag:"scheduler-generic-dialog",dialogImport:()=>Promise.resolve().then((function(){return Ya})),dialogParams:i})})&&this.hass.callService("switch","turn_on",{entity_id:t.entity_id})}((e,t)=>{const i=ya(t);return e.callApi("POST","scheduler/edit",i)})(this.hass,this.schedule).catch(e=>ka(e,this,this.hass)).then(()=>{this.closeDialog()})}else((e,t)=>{const i=ya(t);return e.callApi("POST","scheduler/add",i)})(this.hass,this.schedule).catch(e=>ka(e,this,this.hass)).then(()=>{this.closeDialog()})}async _handleDeleteClick(e){await new Promise(t=>{const i={cancel:()=>t(!1),confirm:()=>t(!0),title:Fi("ui.dialog.confirm_delete.title",this.hass),description:Fi("ui.dialog.confirm_delete.description",this.hass),primaryButtonLabel:Xi("ui.common.ok",this.hass),secondaryButtonLabel:Xi("ui.common.cancel",this.hass)};Ts(e.target,"show-dialog",{dialogTag:"scheduler-generic-dialog",dialogImport:()=>Promise.resolve().then((function(){return Ya})),dialogParams:i})}).then(e=>{var t,i;e&&(t=this.hass,i=this._params.editItem,t.callApi("POST","scheduler/remove",{schedule_id:i})).catch(e=>ka(e,this,this.hass)).then(()=>{this.closeDialog()})})}_setViewMode(e){let t=e.detail;const i=this.schedule.entries[this.selectedEntry].slots.filter(e=>e.actions.length).length>1;if(t!=me.Scheme)if(t!=me.Single||i)new Promise(t=>{const i={title:Fi("ui.dialog.confirm_migrate.title",this.hass),description:Fi("ui.dialog.confirm_migrate.description",this.hass),primaryButtonLabel:this.hass.localize("ui.common.yes"),secondaryButtonLabel:this.hass.localize("ui.common.no"),cancel:()=>{t(!1)},confirm:()=>{t(!0)}};Ts(e.target,"show-dialog",{dialogTag:"scheduler-generic-dialog",dialogImport:()=>Promise.resolve().then((function(){return Ya})),dialogParams:i})}).then(e=>{e&&(this.schedule=(e=>{const t=e=>{let t=e.findIndex(e=>e.actions.length);t<0&&(t=Math.floor(e.length/2));let i=Object.assign(Object.assign({},e[t]),{stop:void 0});const s=i.conditions,a=i.start;return e=[{start:"00:00:00",stop:a,actions:[],conditions:s},i,{start:Ne(Se(Ce(a),{minutes:1})),stop:"00:00:00",actions:[],conditions:s}]};return e=Object.assign(Object.assign({},e),{entries:e.entries.map(e=>Object(Object.assign(Object.assign({},e),{slots:t(e.slots)})))})})(this.schedule),this.viewMode=t)});else{if(he([...this.schedule.entries],[...Ue.entries]))this.schedule=Object.assign(Object.assign({},this.schedule),{entries:[...Fe.entries]});else{let e=Object.assign(Object.assign({},this.schedule),{entries:this.schedule.entries.map(e=>{let t=e.slots.findIndex(e=>e.actions.length);return t<0&&(t=Math.floor(e.slots.length/2)),Object.assign(Object.assign({},e),{slots:e.slots.map((e,i)=>i==t?Object.assign(Object.assign({},e),{stop:void 0}):null).filter(Be)})})});this.schedule=Ie(e,this.hass)}this.viewMode=t}else{this.viewMode=t;if(he([...this.schedule.entries],[...Fe.entries]))this.schedule=Object.assign(Object.assign({},this.schedule),{entries:[...Ue.entries]});else{this.schedule.entries.some(e=>e.slots.some(e=>void 0===e.stop))&&(this.schedule=Object.assign(Object.assign({},this.schedule),{entries:this.schedule.entries.map(e=>Object.assign(Object.assign({},e),{slots:e.slots.map((e,t,i)=>{if(void 0!==e.stop)return e;const s=i[t+1];return Object.assign(Object.assign({},e),{stop:s?s.start:"00:00:00"})})}))}))}}}};Qa.styles=pa,t([le({attribute:!1})],Qa.prototype,"hass",void 0),t([ce()],Qa.prototype,"_params",void 0),t([le({type:Boolean,reflect:!0})],Qa.prototype,"large",void 0),t([ce()],Qa.prototype,"schedule",void 0),t([ce()],Qa.prototype,"selectedEntry",void 0),t([ce()],Qa.prototype,"selectedSlot",void 0),t([ce()],Qa.prototype,"_panel",void 0),t([ce()],Qa.prototype,"_viewMode",void 0),Qa=t([re("dialog-scheduler-editor")],Qa);var Xa=Object.freeze({__proto__:null,get DialogSchedulerEditor(){return Qa}}); +/** + * @license + * Copyright 2017 Google LLC + * SPDX-License-Identifier: BSD-3-Clause + */class eo extends Ws{constructor(e){if(super(e),this.et=V,e.type!==Fs)throw Error(this.constructor.directiveName+"() can only be used in child bindings")}render(e){if(e===V||null==e)return this.ft=void 0,this.et=e;if(e===R)return e;if("string"!=typeof e)throw Error(this.constructor.directiveName+"() called with a non-string value");if(e===this.et)return this.ft;this.et=e;const t=[e];return t.raw=t,this.ft={_$litType$:this.constructor.resultType,strings:t,values:[]}}}eo.directiveName="unsafeHTML",eo.resultType=1;const to=Bs(eo);var io=window&&window.__assign||function(){return(io=Object.assign||function(e){for(var t,i=1,s=arguments.length;i{if(i===Ee.am_pm||!i&&t.time_format===Ee.am_pm){const t=Le(e.getHours()).am_pm;return`${Le(e.getHours()).hours}:${String(e.getMinutes()).padStart(2,"0")} ${t}`}return i===Ee.twenty_four||!i&&t.time_format===Ee.twenty_four?`${String(e.getHours()).padStart(2,"0")}:${String(e.getMinutes()).padStart(2,"0")}`:(()=>{try{(new Date).toLocaleTimeString("i")}catch(e){return"RangeError"===e.name}return!1})()?e.toLocaleTimeString(t.language,{hour:"numeric",minute:"2-digit",hour12:De(t)}):De(t)?ao(e,t,Ee.am_pm):ao(e,t,Ee.twenty_four)};let oo=class extends oe{constructor(){super(...arguments),this.updateInterval=60,this.timer=0}startRefreshTimer(e){clearInterval(this.timer),this.timer=window.setInterval(()=>{this.requestUpdate()},1e3*e),this.updateInterval=e}set hass(e){this._hass=e,this.startRefreshTimer(this.updateInterval)}relativeTime(e){if(!this._hass)return"";const t=new Date;let i=(t.getTime()-e.getTime())/1e3;const s=i>=0?"past":"future";i=Math.abs(i);const a=Math.round(i);if("future"==s&&a>0){if(i/3600>=6){const i=t.setHours(0,0,0,0),s=Math.floor((e.valueOf()-i.valueOf())/864e5);let a="";s>14?a=function(e,t){const i=()=>{try{(new Date).toLocaleDateString("i")}catch(e){return"RangeError"===e.name}return!1},s=(e,t)=>{switch(t){case"year":return e.getFullYear();case"month":return Ha[e.getMonth()];case"day":return e.getDate()}};return e.getFullYear()==(new Date).getFullYear()?i()?new Intl.DateTimeFormat(t.language,{month:"long",day:"numeric"}).format(e):`${s(e,"month")} ${s(e,"day")}`:i()?new Intl.DateTimeFormat(t.language,{year:"numeric",month:"long",day:"numeric"}).format(e):`${s(e,"month")} ${s(e,"day")}, ${s(e,"year")}`}(e,this._hass.locale):s>7?a=Fi("ui.components.date.next_week_day",this._hass,"{weekday}",ks(e,"long",this._hass)):1==s?a=Fi("ui.components.date.tomorrow",this._hass):s>0&&(a=ks(e,"long",this._hass));let o=Fi("ui.components.time.absolute",this._hass,"{time}",ao(e,this._hass.locale));return 12==e.getHours()&&0==e.getMinutes()?o=Fi("ui.components.time.at_noon",this._hass):0==e.getHours()&&0==e.getMinutes()&&(o=Fi("ui.components.time.at_midnight",this._hass)),String(a+" "+o).trim()}if(Math.round(i/60)>60&&Math.round(i/60)<120){const e=Math.round(i/60-60),t=Xi("ui.common.and",this._hass);return`${new Intl.RelativeTimeFormat(this._hass.language,{numeric:"auto"}).format(1,"hour")} ${t} ${Intl.NumberFormat(this._hass.locale.language,{style:"unit",unit:"minute",unitDisplay:"long"}).format(e)}`}if(Math.round(i)>60&&Math.round(i)<120){const e=Math.round(i-60),t=Xi("ui.common.and",this._hass);return`${new Intl.RelativeTimeFormat(this._hass.language,{numeric:"auto"}).format(1,"minute")} ${t} ${Intl.NumberFormat(this._hass.locale.language,{style:"unit",unit:"second",unitDisplay:"long"}).format(e)}`}}const o=function(e,t,i){void 0===t&&(t=Date.now()),void 0===i&&(i={});var s=io(io({},so),i||{}),a=(+e-+t)/1e3;if(Math.abs(a)0)return{value:Math.round(c),unit:"year"};var u=12*c+d.getMonth()-l.getMonth();if(Math.round(Math.abs(u))>0)return{value:Math.round(u),unit:"month"};var h=a/604800;return{value:Math.round(h),unit:"week"}}(e);return new Intl.RelativeTimeFormat(this._hass.language,{numeric:"auto"}).format(o.value,o.unit)}render(){if(!this._hass||!this.datetime)return q``;const e=new Date,t=Math.round((this.datetime.valueOf()-e.valueOf())/1e3);let i=60;return Math.abs(t)<=150&&(i=Math.max(Math.ceil(Math.abs(t))/10,2)),this.updateInterval!=i&&this.startRefreshTimer(i),q` + ${Qi(this.relativeTime(this.datetime))} + `}};t([le()],oo.prototype,"_hass",void 0),t([le()],oo.prototype,"datetime",void 0),oo=t([re("scheduler-relative-time")],oo);let no=class extends oe{render(){var e,t,i,s,a;try{const o=this.hass.states[this.schedule.entity_id];if(!o)return q``;const n=["off","completed"].includes(o.state),r=this.schedule.entries[0].slots[this.schedule.next_entries[0]||0].actions[0];let d=$a(r,this.config.customize);if("entity"==(null===(e=this.config.display_options)||void 0===e?void 0:e.icon)){let e=[(null===(t=r.target)||void 0===t?void 0:t.entity_id)||[]].flat().shift();["script","notify"].includes(Wi(r.service))&&(e=r.service),e&&(d=ea(e,this.config.customize,this.hass))}const l=![(null===(i=r.target)||void 0===i?void 0:i.entity_id)||[]].flat().every(e=>Object.keys(this.hass.states).includes(e));return l&&(d="mdi:help"),q` + + +
    + ${this.renderDisplayItem((null===(s=this.config.display_options)||void 0===s?void 0:s.primary_info)||"default")} +
    + ${this.renderDisplayItem((null===(a=this.config.display_options)||void 0===a?void 0:a.secondary_info)||Re)} +
    +
    +
    + ${!1!==this.config.show_toggle_switches?q``:""} +
    + + `}catch(e){return q` + + + Failed to display schedule ${this.schedule.entity_id}. + Reason: ${e} + + + `}}renderDisplayItem(e){const t=e=>{const t=e.split("");if(t.length>1){const e=this.schedule.timestamps[this.schedule.next_entries[0]||0];return q` + ${t[0]?to(t[0]):""} + + + ${t[1]?to(t[1]):""} + `}if(null!==e.match(/^([^<]*<\/tag>)+$/)){let t=e.split(/([^<]*)<\/tag>/).filter(e=>e);return q` +
    + ${null==t?void 0:t.map(e=>q`${e}`)} +
    `}return to(e)};return Ss(this.schedule,e,this.hass,this.config.customize).filter(e=>e.length).map(e=>q`${t(e)}
    `)}_handleItemClick(e){const t=new CustomEvent("editClick",{detail:{schedule_id:this.schedule_id}});this.dispatchEvent(t)}_handleIconClick(e){const t=new CustomEvent("editClick",{detail:{schedule_id:this.schedule_id}});this.dispatchEvent(t)}_toggleEnableDisable(e){const t=e.target.checked;this.hass.callService("switch",t?"turn_on":"turn_off",{entity_id:this.schedule.entity_id})}static get styles(){return r` + :host { + display: flex; + align-items: center; + flex-direction: row; + } + .info { + margin-left: 16px; + margin-right: 8px; + margin-inline-start: 16px; + margin-inline-end: 8px; + flex: 1 1 30%; + transition: color 0.2s ease-in-out; + cursor: pointer; + line-height: var(--ha-line-height-normal); + } + .info, + .info > * { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + .flex ::slotted(*) { + margin-left: 8px; + margin-inline-start: 8px; + margin-inline-end: initial; + min-width: 0; + } + .flex ::slotted([slot="secondary"]) { + margin-left: 0; + margin-inline-start: 0; + margin-inline-end: initial; + } + .secondary, + ha-relative-time { + color: var(--secondary-text-color); + transition: color 0.2s ease-in-out; + } + .state { + text-align: var(--float-end); + } + .value { + direction: ltr; + } + ha-icon { + display: flex; + flex: 0 0 40px; + color: var(--state-icon-color); + transition: color 0.2s ease-in-out; + cursor: pointer; + align-items: center; + justify-content: center; + } + ha-icon.disabled { + color: var(--disabled-text-color); + } + div.disabled { + --primary-text-color: var(--disabled-text-color); + --secondary-text-color: var(--disabled-text-color); + --state-icon-color: var(--disabled-text-color); + color: var(--disabled-text-color); + } + div.tags { + display: flex; + gap: 5px; + flex-wrap: wrap; + } + span.tag { + height: 28px; + border-radius: 14px; + background: rgba(var(--rgb-primary-color), 0.40); + color: var(--primary-text-color); + line-height: 1.25rem; + font-size: 0.875rem; + padding: 0px 12px; + display: flex; + align-items: center; + box-sizing: border-box; + } + .defective { + text-decoration: line-through; + } + `}};t([le()],no.prototype,"hass",void 0),t([le()],no.prototype,"schedule_id",void 0),t([le()],no.prototype,"schedule",void 0),t([le()],no.prototype,"config",void 0),no=t([re("scheduler-item-row")],no),e.SchedulerCard=class extends oe{constructor(){super(...arguments),this._config={},this.showDiscovered=!1,this.translationsLoaded=!1,this.connectionError=!1}async setConfig(e){e=(e=>{let t=[];if(We(e,"include")&&!Ye(e.include)&&t.push("'include' must be a list of strings"),We(e,"exclude")&&!Ye(e.exclude)&&t.push("'exclude' must be a list of strings"),We(e,"discover_existing")&&!Ze(e.discover_existing)&&t.push("'discover_existing' must be a boolean"),!We(e,"title")||Ze(e.title)||Ge(e.title)||t.push("'title' must be a boolean or string"),We(e,"time_step")&&(!Ke(e.time_step)||Number(e.time_step)<1||Number(e.time_step)>30)&&t.push("'time_step' must be a number between 1 and 30"),We(e,"show_header_toggle")&&!Ze(e.show_header_toggle)&&t.push("'show_header_toggle' must be a boolean"),We(e,"show_add_button")&&!Ze(e.show_add_button)&&t.push("'show_add_button' must be a boolean"),We(e,"show_toggle_switches")&&!Ze(e.show_toggle_switches)&&t.push("'show_toggle_switches' must be a boolean"),We(e,"display_options")&&(Je(e.display_options)?(!We(e.display_options,"primary_info")||Ge(e.display_options.primary_info)||Ye(e.display_options.primary_info)||t.push("in 'display_options': 'primary_info' must be a string or list of strings"),!We(e.display_options,"secondary_info")||Ge(e.display_options.secondary_info)||Ye(e.display_options.secondary_info)||t.push("in 'display_options': 'secondary_info' must be a string or list of strings"),!We(e.display_options,"icon")||Ge(e.display_options.icon)&&["action","entity"].includes(e.display_options.icon)||t.push("in 'display_options': 'icon' must be a set to either 'action' or 'entity' ")):t.push("'display_options' must be a struct")),!We(e,"sort_by")||Ge(e.sort_by)||Ye(e.sort_by)||t.push("'sort_by' must be a string or list of strings"),We(e,"customize")&&!Je(e.customize))t.push("'customize' must be a struct");else if(We(e,"customize")){let i=Object.entries(e.customize).map(([e,t])=>Qe(e,t)).filter(Be);i.length&&t.push(...i)}if(!We(e,"tags")||Ge(e.tags)||Ye(e.tags)||t.push("'tags' must be a string or list of strings"),!We(e,"exclude_tags")||Ge(e.tags)||Ye(e.tags)||t.push("'exclude_tags' must be a string or list of strings"),t.length)throw new Error(`Invalid configuration provided (${t.length} error${t.length>1?"s":""}): ${t.join(", ")}.`);return e})(e),this._config=Object.assign({},e)}async firstUpdated(){await(async()=>{if(customElements.get("ha-checkbox")&&customElements.get("ha-slider")&&customElements.get("ha-generic-picker"))return;await customElements.whenDefined("partial-panel-resolver");const e=document.createElement("partial-panel-resolver");e.hass={panels:[{url_path:"tmp",component_name:"config"}]},e._updateRoutes(),await e.routerOptions.routes.tmp.load(),await customElements.whenDefined("ha-panel-config");const t=document.createElement("ha-panel-config");await t.routerOptions.routes.automation.load()})();document.querySelector("home-assistant")._loadFragmentTranslations(this.hass.language,"config"),await Es(this.hass).then(e=>{e=Object.fromEntries(Object.entries(e).filter(([e])=>Gi(e,this._config))),this._config=Object.assign(Object.assign({},this._config),{customize:Object.assign(Object.assign({},e),this._config.customize||{})})})}willUpdate(){this.hass.loadBackendTranslation("services")}__checkSubscribed(){void 0===this.__unsubs&&this.isConnected&&void 0!==this.hass&&(this.__unsubs=this.hassSubscribe())}connectedCallback(){super.connectedCallback(),this.__checkSubscribed()}disconnectedCallback(){if(super.disconnectedCallback(),this.__unsubs){for(;this.__unsubs.length;){const e=this.__unsubs.pop();e instanceof Promise?e.then(e=>e()):e()}this.__unsubs=void 0}}updated(e){super.updated(e),e.has("hass")&&this.__checkSubscribed()}hassSubscribe(){return this.loadSchedules(),[this.hass.connection.subscribeMessage(e=>this.handleScheduleItemUpdated(e),{type:"scheduler_updated"})]}shouldUpdate(e){const t=e.get("hass"),i=e.get("_config");if(i&&this._config){Object.keys(i).filter(e=>i[e]!==this._config[e]).some(e=>["tags","discover_existing","sort_by","display_options"].includes(e))&&(async()=>{await this.loadSchedules()})()}return!this.translationsLoaded&&Xi("component.input_boolean.services.turn_on.name",this.hass,!1).length&&Xi("ui.panel.config.automation.editor.conditions.type.sun.sunrise",this.hass,!1).length?(this.translationsLoaded=!0,!0):!t||1!=e.size||!this.schedules||Object.values(this.schedules).some(e=>JSON.stringify(t.states[e.entity_id])!==JSON.stringify(this.hass.states[e.entity_id]))}render(){let e=[...this.schedules||[]],t=e.filter(e=>Ji(e,this._config)),i=e.filter(e=>!Ji(e,this._config));const s=this.showDiscovered?e.some(e=>{var t;return["on","triggered"].includes((null===(t=this.hass.states[e.entity_id])||void 0===t?void 0:t.state)||"")}):t.some(e=>{var t;return["on","triggered"].includes((null===(t=this.hass.states[e.entity_id])||void 0===t?void 0:t.state)||"")});return q` + +
    +
    + ${!Be(this._config.title)||"boolean"==typeof this._config.title&&this._config.title?Fi("ui.panel.common.title",this.hass):"boolean"==typeof this._config.title?"":this._config.title} +
    + + ${Object.keys(this.schedules||{}).length&&this._config.show_header_toggle?q` + + + `:""} +
    + +
    + + ${this.connectionError?q` +
    + + + ${Fi("ui.panel.overview.backend_error",this.hass)} + + +
    + `:Object.keys(e).length?t.map(e=>q` + {this._handleEditClick(t,e)}} + > + + `):q` +
    + ${Fi("ui.panel.overview.no_entries",this.hass)} +
    + `} + + ${Object.keys(e).length>t.length&&!1!==this._config.discover_existing?this.showDiscovered?q` + + ${i.map(e=>q` + {this._handleEditClick(t,e)}} + > + + `)} + +
    + {this.showDiscovered=!1}} + > + ${Fi("ui.panel.overview.hide_excluded",this.hass)} + +
    + `:q` +
    + {this.showDiscovered=!0}} + > + + + ${Fi("ui.panel.overview.excluded_items",this.hass,"{number}",Object.keys(e).length-t.length)} + +
    + `:""} +
    + ${!1!==this._config.show_add_button?q` +
    + ${this.connectionError?q` + ${Xi("ui.common.refresh",this.hass)} + + `:q` + ${Xi("ui.common.add",this.hass)} + + `} +
    `:""} +
    + `}async loadSchedules(){Oe(this.hass).then(e=>{this.schedules=Cs(e,this._config,this.hass)}).catch(e=>{this.schedules=[],this.connectionError=!0})}async getCardSize(){return new Promise(e=>{let t=0;const i=setInterval(()=>{var s;if(t++,!this._config||!this.schedules&&!this.connectionError&&t<50)return;let a=this._config.title||this._config.show_header_toggle?3:1;this._config.show_add_button&&(a+=1);const o=(([(null===(s=this._config.display_options)||void 0===s?void 0:s.secondary_info)||[]].flat().length||2)+1)/2;this.schedules&&(a+=this.showDiscovered?Object.keys(this.schedules).length*o:Object.values(this.schedules).filter(e=>Ji(e,this._config)).length*o),clearInterval(i),e(Math.round(a))},50)})}_retryConnection(){setTimeout(async()=>{await this.loadSchedules()},100),this.connectionError=!1,this.requestUpdate()}async handleScheduleItemUpdated(e){"scheduler_item_removed"!=e.event?As(this.hass,e.schedule_id).then(t=>{const i=this.schedules.findIndex(t=>t.schedule_id==e.schedule_id),s=i>=0?this.schedules[i]:null;let a=[...this.schedules||[]];!t||!1===this._config.discover_existing&&!Ji(t,this._config)?s&&(a=a.filter(t=>t.schedule_id!==e.schedule_id)):s?s.timestamps[s.next_entries[0]||0]==t.timestamps[t.next_entries[0]||0]?a=Object.assign(a,{[i]:t}):(a=Object.assign(a,{[i]:t}),a=Cs(a,this._config,this.hass)):a=Cs([...a,t],this._config,this.hass),this.schedules=[...a]}):this.schedules=(this.schedules||[]).filter(t=>t.schedule_id!==e.schedule_id)}_handleEditClick(e,t){if(!this.schedules)return;const i={schedule:Ie(t,this.hass),cardConfig:this._config,editItem:t.schedule_id};Ts(e.target,"show-dialog",{dialogTag:"dialog-scheduler-editor",dialogImport:()=>Promise.resolve().then((function(){return Xa})),dialogParams:i})}_addClick(e){const t=[this._config.tags||[]].flat().filter(e=>!["none","disabled","enabled"].includes(e));let i=this._config.default_editor==me.Scheme?JSON.parse(JSON.stringify(Ue)):JSON.parse(JSON.stringify(Fe));const s={schedule:Object.assign(Object.assign({},i),{tags:1==t.length?t:[]}),cardConfig:this._config};Ts(this,"show-dialog",{dialogTag:"dialog-scheduler-editor",dialogImport:()=>Promise.resolve().then((function(){return Xa})),dialogParams:s})}toggleDisableAll(e){if(!this.hass||!this.schedules)return;const t=e.target.checked;Object.values(this.schedules).filter(e=>this.showDiscovered||Ji(e,this._config)).forEach(e=>{this.hass.callService("switch",t?"turn_on":"turn_off",{entity_id:e.entity_id})})}static getConfigElement(){return document.createElement("scheduler-card-editor")}},e.SchedulerCard.styles=r` + .card-header { + display: flex; + justify-content: space-between; + } + .card-header .name { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + display: flex; + } + .card-header ha-switch { + display: flex; + align-self: center; + margin: 0px 6px; + line-height: 24px; + } + + #states > * { + margin: 8px 0; + } + #states > *:first-child { + margin-top: 0; + } + #states > *:last-child { + margin-bottom: 0; + } + + button.show-more { + color: var(--primary-color); + text-align: left; + cursor: pointer; + background: none; + border-width: initial; + border-style: none; + border-color: initial; + border-image: initial; + font: inherit; + } + button.show-more:focus { + outline: none; + text-decoration: underline; + } + .card-actions, .card-actions > * { + display: flex; + } + `,t([le({attribute:!1})],e.SchedulerCard.prototype,"hass",void 0),t([le()],e.SchedulerCard.prototype,"_config",void 0),t([ce()],e.SchedulerCard.prototype,"schedules",void 0),t([ce()],e.SchedulerCard.prototype,"showDiscovered",void 0),e.SchedulerCard=t([re("scheduler-card")],e.SchedulerCard),window.customCards=window.customCards||[],window.customCards.push({type:"scheduler-card",name:"Scheduler Card",description:"Card to manage schedule entities made with scheduler-component."}),console.info("%c SCHEDULER-CARD \n%c Version: "+"v4.0.18".padEnd(7," "),"color: orange; font-weight: bold; background: black","color: white; font-weight: bold; background: dimgray")}({}); diff --git a/www/community/scheduler-card/scheduler-card.js.gz b/www/community/scheduler-card/scheduler-card.js.gz new file mode 100644 index 0000000..cf0af04 Binary files /dev/null and b/www/community/scheduler-card/scheduler-card.js.gz differ diff --git a/www/community/sensor-bar-card-plus/sensor-bar-card-plus.js b/www/community/sensor-bar-card-plus/sensor-bar-card-plus.js new file mode 100644 index 0000000..748aa3b --- /dev/null +++ b/www/community/sensor-bar-card-plus/sensor-bar-card-plus.js @@ -0,0 +1,9551 @@ +(() => { + var __getOwnPropNames = Object.getOwnPropertyNames; + var __esm = (fn, res, err) => function __init() { + if (err) throw err[0]; + try { + return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res; + } catch (e) { + throw err = [e], e; + } + }; + var __commonJS = (cb, mod) => function __require() { + try { + return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports; + } catch (e) { + throw mod = 0, e; + } + }; + + // src/config/normalize.js + function normalizeResolvableValue(value, entityValue, percentValue = null) { + const normalized = { + fixed: value != null ? value : null, + entity: entityValue != null ? entityValue : null + }; + if (Number.isFinite(percentValue)) { + normalized.percent = percentValue; + } + return normalized; + } + function looksLikeEntityId(value) { + return typeof value === "string" && /^[a-z0-9_]+\.[a-z0-9_]+$/i.test(value.trim()); + } + function parsePercentLiteral(value) { + if (typeof value !== "string") return null; + const match = value.match(/^\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*%\s*$/); + if (!match) return null; + const percent = parseFloat(match[1]); + return Number.isFinite(percent) ? percent : null; + } + function getFiniteNumber(value) { + if (typeof value === "number") { + return Number.isFinite(value) ? value : null; + } + if (typeof value === "string") { + const trimmed = value.trim(); + if (!trimmed) return null; + const num = Number(trimmed); + return Number.isFinite(num) ? num : null; + } + return null; + } + function normalizeStructuredResolvableValue(input, inheritedResolvable = null, defaultValue = null, options = {}) { + var _a, _b, _c, _d, _e, _f; + const { allowPercent = false } = options; + const inherited = inheritedResolvable != null ? inheritedResolvable : normalizeResolvableValue(defaultValue, null); + if (input === void 0) { + return { ...inherited }; + } + if (input === null) { + return normalizeResolvableValue(null, null); + } + if (typeof input === "object" && !Array.isArray(input)) { + const value = (_b = (_a = input.fixed) != null ? _a : input.value) != null ? _b : null; + const entity = (_c = input.entity) != null ? _c : null; + const percent = allowPercent ? getFiniteNumber(input.percent) : null; + return normalizeResolvableValue(value, entity, percent); + } + if (looksLikeEntityId(input)) { + return normalizeResolvableValue( + (_e = (_d = inherited.fixed) != null ? _d : defaultValue) != null ? _e : null, + input, + (_f = inherited.percent) != null ? _f : null + ); + } + if (allowPercent) { + const percent = parsePercentLiteral(input); + if (Number.isFinite(percent)) { + return normalizeResolvableValue(null, null, percent); + } + } + return normalizeResolvableValue(input, null); + } + function normalizeBaselineDirectionConfig(input, inheritedDirection = null) { + var _a; + const inherited = inheritedDirection != null ? inheritedDirection : { color: null }; + if (input === void 0) { + return { ...inherited }; + } + if (input === null) { + return { color: null }; + } + if (typeof input === "object" && !Array.isArray(input)) { + return { + color: (_a = input.color) != null ? _a : null + }; + } + return { + color: input + }; + } + function normalizeOptionalEnabled(value) { + return value === true ? true : value === false ? false : null; + } + function normalizeBaselineConfig(entityConfig, cardConfig) { + var _a; + const cardBaseline = cardConfig == null ? void 0 : cardConfig.baseline; + const rawBaseline = entityConfig == null ? void 0 : entityConfig.baseline; + const inherited = { + enabled: normalizeOptionalEnabled(cardBaseline == null ? void 0 : cardBaseline.enabled), + at: (cardBaseline == null ? void 0 : cardBaseline.at) ? { ...cardBaseline.at } : normalizeResolvableValue(null, null), + above: normalizeBaselineDirectionConfig(void 0, cardBaseline == null ? void 0 : cardBaseline.above), + below: normalizeBaselineDirectionConfig(void 0, cardBaseline == null ? void 0 : cardBaseline.below) + }; + if (rawBaseline === void 0) { + return inherited; + } + if (rawBaseline === null) { + return { + enabled: null, + at: normalizeResolvableValue(null, null), + above: { color: null }, + below: { color: null } + }; + } + if (typeof rawBaseline !== "object" || Array.isArray(rawBaseline)) { + return { + enabled: inherited.enabled, + at: normalizeStructuredResolvableValue(rawBaseline, inherited.at, null), + above: inherited.above, + below: inherited.below + }; + } + return { + enabled: (_a = normalizeOptionalEnabled(rawBaseline.enabled)) != null ? _a : inherited.enabled, + at: normalizeStructuredResolvableValue(rawBaseline.at, inherited.at, null, { allowPercent: true }), + above: normalizeBaselineDirectionConfig(rawBaseline.above, inherited.above), + below: normalizeBaselineDirectionConfig(rawBaseline.below, inherited.below) + }; + } + function inferSegmentEndValues(segments, fallbackEnd = null) { + const sorted = [...segments].sort((a, b) => a.from - b.from); + return sorted.map((segment, index) => { + var _a; + let to = Number.isFinite(segment.to) ? segment.to : null; + if (!Number.isFinite(to) && index < sorted.length - 1) { + to = sorted[index + 1].from; + } + if (!Number.isFinite(to) && Number.isFinite(fallbackEnd)) { + to = fallbackEnd; + } + return { + from: segment.from, + to, + color: segment.color, + label: (_a = segment.label) != null ? _a : null + }; + }); + } + function normalizeSeverityToSegments(input) { + if (!Array.isArray(input)) return null; + const segments = input.filter((segment) => Number.isFinite(segment == null ? void 0 : segment.from) && (segment == null ? void 0 : segment.color)).map((segment) => { + var _a; + return { + from: segment.from, + to: Number.isFinite(segment == null ? void 0 : segment.to) ? segment.to : null, + color: segment.color, + label: (_a = segment.label) != null ? _a : null + }; + }); + return inferSegmentEndValues(segments, 100); + } + function hasResolvableMagnitude(resolvable) { + return !!resolvable && (Number.isFinite(getFiniteNumber(resolvable.fixed)) || Number.isFinite(resolvable.percent)); + } + function normalizeGaugeSegments(input) { + if (!Array.isArray(input)) return null; + const segments = input.map((segment) => { + var _a; + const from = normalizeStructuredResolvableValue(segment == null ? void 0 : segment.from, null, null, { allowPercent: true }); + const to = (segment == null ? void 0 : segment.to) === void 0 ? null : normalizeStructuredResolvableValue(segment.to, null, null, { allowPercent: true }); + if (!hasResolvableMagnitude(from) || !(segment == null ? void 0 : segment.color)) { + return null; + } + return { + from, + to, + color: segment.color, + label: (_a = segment.label) != null ? _a : null + }; + }).filter(Boolean); + return segments.map((segment, index) => { + var _a; + return { + from: { ...segment.from }, + to: segment.to ? { ...segment.to } : index < segments.length - 1 ? { ...segments[index + 1].from } : null, + color: segment.color, + label: (_a = segment.label) != null ? _a : null + }; + }); + } + function normalizeScaleBound(entityConfig, cardConfig, key, defaultValue) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l; + const cardScale = cardConfig == null ? void 0 : cardConfig.scale; + const entityScale = entityConfig == null ? void 0 : entityConfig.scale; + const entityKey = `${key}_entity`; + const inherited = normalizeResolvableValue( + (_e = (_d = (_c = (_a = cardScale == null ? void 0 : cardScale[key]) == null ? void 0 : _a.fixed) != null ? _c : (_b = cardScale == null ? void 0 : cardScale[key]) == null ? void 0 : _b.value) != null ? _d : cardConfig == null ? void 0 : cardConfig[key]) != null ? _e : defaultValue, + (_h = (_g = (_f = cardScale == null ? void 0 : cardScale[key]) == null ? void 0 : _f.entity) != null ? _g : cardConfig == null ? void 0 : cardConfig[entityKey]) != null ? _h : null + ); + if ((entityScale == null ? void 0 : entityScale[key]) !== void 0) { + return normalizeStructuredResolvableValue(entityScale[key], inherited, defaultValue); + } + const value = (_j = (_i = entityConfig[key]) != null ? _i : inherited.fixed) != null ? _j : defaultValue; + const entity = (_l = (_k = entityConfig[entityKey]) != null ? _k : inherited.entity) != null ? _l : null; + return normalizeResolvableValue(value, entity); + } + function normalizeScaleConfig(entityConfig, cardConfig) { + return { + min: normalizeScaleBound(entityConfig, cardConfig, "min", 0), + max: normalizeScaleBound(entityConfig, cardConfig, "max", 100) + }; + } + function fillStyleToColorMode(fillStyle) { + switch (fillStyle) { + case "solid": + return "single"; + case "gradient": + return "gradient"; + case "bands": + return "severity"; + case "soft_bands": + return "severity"; + case "band_gradient": + return "severity_gradient"; + default: + return null; + } + } + function colorModeToFillStyle(colorMode) { + switch (colorMode) { + case "single": + return "solid"; + case "gradient": + return "gradient"; + case "severity": + return "bands"; + case "severity_gradient": + return "band_gradient"; + default: + return null; + } + } + function normalizeBarModeConfig(barConfig = null, flatColorMode = null) { + var _a, _b, _c, _d, _e, _f; + const fillStyle = (_a = barConfig == null ? void 0 : barConfig.fill_style) != null ? _a : null; + const colorMode = (_c = (_b = barConfig == null ? void 0 : barConfig.color_mode) != null ? _b : flatColorMode) != null ? _c : null; + const normalizedColorMode = (_e = (_d = fillStyleToColorMode(fillStyle)) != null ? _d : colorMode) != null ? _e : "severity"; + return { + fill_style: (_f = fillStyle != null ? fillStyle : colorModeToFillStyle(normalizedColorMode)) != null ? _f : "bands", + color_mode: normalizedColorMode + }; + } + function resolveNormalizedBarMode(entityBar, entityConfig, cardBar, cardConfig) { + if ((entityBar == null ? void 0 : entityBar.fill_style) !== void 0 || (entityBar == null ? void 0 : entityBar.color_mode) !== void 0 || entityConfig.color_mode !== void 0) { + return normalizeBarModeConfig(entityBar, entityConfig.color_mode); + } + if ((cardBar == null ? void 0 : cardBar.fill_style) !== void 0 || (cardBar == null ? void 0 : cardBar.color_mode) !== void 0 || (cardConfig == null ? void 0 : cardConfig.color_mode) !== void 0) { + return normalizeBarModeConfig(cardBar, cardConfig == null ? void 0 : cardConfig.color_mode); + } + return normalizeBarModeConfig(null, null); + } + function normalizeGradientStops(input) { + if (!Array.isArray(input)) return input != null ? input : null; + return input.map((stop) => { + if (!stop || typeof stop !== "object" || Array.isArray(stop)) { + return stop; + } + const percentPos = parsePercentLiteral(stop.pos); + const numericPos = Number.isFinite(percentPos) ? percentPos : getFiniteNumber(stop.pos); + return { + ...stop, + pos: Number.isFinite(numericPos) ? numericPos : stop.pos + }; + }); + } + function normalizeNeedleConfig(input, inheritedNeedle = null) { + var _a, _b, _c, _d; + const base = inheritedNeedle ? { show: (_a = inheritedNeedle.show) != null ? _a : false, color: (_b = inheritedNeedle.color) != null ? _b : "#ffffff" } : { show: false, color: "#ffffff" }; + if (input === void 0) { + return { ...base }; + } + if (typeof input === "boolean") { + return { + show: input, + color: "#ffffff" + }; + } + if (input && typeof input === "object" && !Array.isArray(input)) { + return { + show: (_c = input.show) != null ? _c : base.show, + color: (_d = input.color) != null ? _d : base.color + }; + } + return { ...base }; + } + function normalizeBarConfig(entityConfig, cardConfig) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y; + const cardBar = cardConfig == null ? void 0 : cardConfig.bar; + const entityBar = entityConfig == null ? void 0 : entityConfig.bar; + const entityStructuredSegments = entityBar == null ? void 0 : entityBar.segments; + const entityTopLevelSegments = entityConfig.segments; + const entityLegacySeverity = entityConfig.severity; + const cardStructuredSegments = (_a = cardBar == null ? void 0 : cardBar.segments) != null ? _a : null; + const cardTopLevelSegments = (_b = cardConfig == null ? void 0 : cardConfig.segments) != null ? _b : null; + const cardLegacySeverity = (_c = cardConfig == null ? void 0 : cardConfig.severity) != null ? _c : null; + let segments = null; + let segment_space = (_d = cardBar == null ? void 0 : cardBar.segment_space) != null ? _d : "percent"; + if (entityStructuredSegments !== void 0 && entityStructuredSegments !== null) { + segments = normalizeGaugeSegments(entityStructuredSegments); + segment_space = "scale"; + } else if (entityTopLevelSegments !== void 0 && entityTopLevelSegments !== null) { + segments = normalizeGaugeSegments(entityTopLevelSegments); + segment_space = "scale"; + } else if (entityLegacySeverity !== void 0 && entityLegacySeverity !== null) { + segments = normalizeSeverityToSegments(entityLegacySeverity); + segment_space = "percent"; + } else if (cardStructuredSegments !== null && cardStructuredSegments !== void 0) { + segments = cardStructuredSegments.map((segment) => ({ ...segment })); + segment_space = (_e = cardBar == null ? void 0 : cardBar.segment_space) != null ? _e : "percent"; + } else if (cardTopLevelSegments !== null && cardTopLevelSegments !== void 0) { + segments = normalizeGaugeSegments(cardTopLevelSegments); + segment_space = "scale"; + } else if (cardLegacySeverity !== null && cardLegacySeverity !== void 0) { + segments = normalizeSeverityToSegments(cardLegacySeverity); + segment_space = "percent"; + } + const structuredAboveTargetColor = (entityConfig == null ? void 0 : entityConfig.target) && typeof entityConfig.target === "object" && !Array.isArray(entityConfig.target) ? (_f = entityConfig.target.when_exceeded) == null ? void 0 : _f.fill_color : void 0; + const inheritedStructuredAboveTargetColor = (cardConfig == null ? void 0 : cardConfig.target) && typeof cardConfig.target === "object" && !Array.isArray(cardConfig.target) ? (_g = cardConfig.target.when_exceeded) == null ? void 0 : _g.fill_color : void 0; + const normalizedMode = resolveNormalizedBarMode(entityBar, entityConfig, cardBar, cardConfig); + return { + fill_style: normalizedMode.fill_style, + color_mode: normalizedMode.color_mode, + needle: normalizeNeedleConfig(entityBar == null ? void 0 : entityBar.needle, cardBar == null ? void 0 : cardBar.needle), + solid_fill: (_i = (_h = entityBar == null ? void 0 : entityBar.solid_fill) != null ? _h : cardBar == null ? void 0 : cardBar.solid_fill) != null ? _i : false, + color: (_m = (_l = (_k = (_j = entityBar == null ? void 0 : entityBar.color) != null ? _j : entityConfig.color) != null ? _k : cardBar == null ? void 0 : cardBar.color) != null ? _l : cardConfig == null ? void 0 : cardConfig.color) != null ? _m : "#4a9eff", + gradient_stops: normalizeGradientStops( + (_q = (_p = (_o = (_n = entityBar == null ? void 0 : entityBar.gradient_stops) != null ? _n : entityConfig.gradient_stops) != null ? _o : cardBar == null ? void 0 : cardBar.gradient_stops) != null ? _p : cardConfig == null ? void 0 : cardConfig.gradient_stops) != null ? _q : null + ), + severity: segments, + segments, + segment_space, + animated: (_u = (_t = (_s = (_r = entityBar == null ? void 0 : entityBar.animated) != null ? _r : entityConfig.animated) != null ? _s : cardBar == null ? void 0 : cardBar.animated) != null ? _t : cardConfig == null ? void 0 : cardConfig.animated) != null ? _u : true, + above_target_color: (_y = (_x = (_w = (_v = structuredAboveTargetColor != null ? structuredAboveTargetColor : entityConfig.above_target_color) != null ? _v : cardBar == null ? void 0 : cardBar.above_target_color) != null ? _w : inheritedStructuredAboveTargetColor) != null ? _x : cardConfig == null ? void 0 : cardConfig.above_target_color) != null ? _y : null + }; + } + function clampSupportedRowHeight(height) { + return Math.max(24, height); + } + function normalizeLabelPosition(position, fallback = "left") { + const normalized = typeof position === "string" ? position.trim().toLowerCase() : ""; + return ["left", "above", "inside", "off", "hero"].includes(normalized) ? normalized : fallback; + } + function normalizeHeroSize(size, fallback = "medium") { + const normalized = typeof size === "string" ? size.trim().toLowerCase() : ""; + return ["small", "medium", "large"].includes(normalized) ? normalized : fallback; + } + function normalizeLayoutConfig(entityConfig, cardConfig) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v; + const cardLayout = cardConfig == null ? void 0 : cardConfig.layout; + const entityLayout = entityConfig == null ? void 0 : entityConfig.layout; + const entityLabel = entityLayout == null ? void 0 : entityLayout.label; + const cardLabel = cardLayout == null ? void 0 : cardLayout.label; + const isCardLevelNormalization = !cardConfig; + const rawHeight = (_d = (_c = (_b = (_a = entityLayout == null ? void 0 : entityLayout.height) != null ? _a : entityConfig.height) != null ? _b : cardLayout == null ? void 0 : cardLayout.height) != null ? _c : cardConfig == null ? void 0 : cardConfig.height) != null ? _d : 38; + const heightExplicit = (entityLayout == null ? void 0 : entityLayout.height) !== void 0 || entityConfig._height_explicit === true || !isCardLevelNormalization && entityConfig.height !== void 0 || (cardLayout == null ? void 0 : cardLayout.height_explicit) === true || (cardConfig == null ? void 0 : cardConfig._height_explicit) === true; + return { + label: { + position: normalizeLabelPosition( + (_h = (_g = (_f = (_e = entityLabel == null ? void 0 : entityLabel.position) != null ? _e : entityConfig.label_position) != null ? _f : cardLabel == null ? void 0 : cardLabel.position) != null ? _g : cardLayout == null ? void 0 : cardLayout.label_position) != null ? _h : cardConfig == null ? void 0 : cardConfig.label_position, + "left" + ), + width: (_m = (_l = (_k = (_j = (_i = entityLabel == null ? void 0 : entityLabel.width) != null ? _i : entityConfig.label_width) != null ? _j : cardLabel == null ? void 0 : cardLabel.width) != null ? _k : cardLayout == null ? void 0 : cardLayout.label_width) != null ? _l : cardConfig == null ? void 0 : cardConfig.label_width) != null ? _m : 100, + hero_size: normalizeHeroSize( + (_v = (_u = (_t = (_s = (_r = (_q = (_p = (_o = (_n = entityLabel == null ? void 0 : entityLabel.hero_size) != null ? _n : entityLabel == null ? void 0 : entityLabel.heroSize) != null ? _o : entityConfig.hero_size) != null ? _p : entityConfig.heroSize) != null ? _q : cardLabel == null ? void 0 : cardLabel.hero_size) != null ? _r : cardLabel == null ? void 0 : cardLabel.heroSize) != null ? _s : cardLayout == null ? void 0 : cardLayout.hero_size) != null ? _t : cardLayout == null ? void 0 : cardLayout.heroSize) != null ? _u : cardConfig == null ? void 0 : cardConfig.hero_size) != null ? _v : cardConfig == null ? void 0 : cardConfig.heroSize, + "medium" + ) + }, + height: clampSupportedRowHeight(rawHeight), + height_explicit: heightExplicit + }; + } + function normalizeFormattingConfig(entityConfig, cardConfig) { + var _a, _b, _c, _d, _e, _f, _g, _h; + const cardFormatting = cardConfig == null ? void 0 : cardConfig.formatting; + const entityFormatting = entityConfig == null ? void 0 : entityConfig.formatting; + return { + decimal: (_d = (_c = (_b = (_a = entityFormatting == null ? void 0 : entityFormatting.decimal) != null ? _a : entityConfig.decimal) != null ? _b : cardFormatting == null ? void 0 : cardFormatting.decimal) != null ? _c : cardConfig == null ? void 0 : cardConfig.decimal) != null ? _d : null, + unit: (_h = (_g = (_f = (_e = entityFormatting == null ? void 0 : entityFormatting.unit) != null ? _e : entityConfig.unit) != null ? _f : cardFormatting == null ? void 0 : cardFormatting.unit) != null ? _g : cardConfig == null ? void 0 : cardConfig.unit) != null ? _h : null + }; + } + function normalizeTargetMarkerConfig(entityConfig, cardConfig) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B; + const cardTarget = cardConfig == null ? void 0 : cardConfig.target_marker; + const rawTarget = entityConfig == null ? void 0 : entityConfig.target; + const legacyCardTarget = (cardConfig == null ? void 0 : cardConfig.target) && typeof cardConfig.target === "object" && !Array.isArray(cardConfig.target) ? null : (_a = cardConfig == null ? void 0 : cardConfig.target) != null ? _a : null; + const inheritedTarget = cardTarget != null ? cardTarget : { + enabled: null, + source: normalizeResolvableValue(null, null), + color: (_b = cardConfig == null ? void 0 : cardConfig.target_color) != null ? _b : "#888", + show_label: (_c = cardConfig == null ? void 0 : cardConfig.show_target_label) != null ? _c : false + }; + if (rawTarget && typeof rawTarget === "object" && !Array.isArray(rawTarget)) { + return { + enabled: (_e = (_d = normalizeOptionalEnabled(rawTarget.enabled)) != null ? _d : inheritedTarget.enabled) != null ? _e : null, + source: normalizeStructuredResolvableValue(rawTarget.at, inheritedTarget.source, null, { allowPercent: true }), + color: (_g = (_f = rawTarget.color) != null ? _f : entityConfig.target_color) != null ? _g : inheritedTarget.color, + show_label: (_j = (_i = (_h = rawTarget.label) == null ? void 0 : _h.show) != null ? _i : entityConfig.show_target_label) != null ? _j : inheritedTarget.show_label + }; + } + const value = (_o = (_n = (_l = entityConfig.target) != null ? _l : (_k = inheritedTarget.source) == null ? void 0 : _k.fixed) != null ? _n : (_m = inheritedTarget.source) == null ? void 0 : _m.value) != null ? _o : legacyCardTarget; + const entity = (_s = (_r = (_q = entityConfig.target_entity) != null ? _q : (_p = inheritedTarget.source) == null ? void 0 : _p.entity) != null ? _r : cardConfig == null ? void 0 : cardConfig.target_entity) != null ? _s : null; + const percent = entityConfig.target === void 0 && entityConfig.target_entity === void 0 ? (_u = (_t = inheritedTarget.source) == null ? void 0 : _t.percent) != null ? _u : null : null; + return { + enabled: (_v = inheritedTarget.enabled) != null ? _v : null, + source: normalizeResolvableValue(value, entity, percent), + color: (_y = (_x = (_w = entityConfig.target_color) != null ? _w : inheritedTarget.color) != null ? _x : cardConfig == null ? void 0 : cardConfig.target_color) != null ? _y : "#888", + show_label: (_B = (_A = (_z = entityConfig.show_target_label) != null ? _z : inheritedTarget.show_label) != null ? _A : cardConfig == null ? void 0 : cardConfig.show_target_label) != null ? _B : false + }; + } + function normalizePeakMarkerConfig(entityConfig, cardConfig) { + var _a, _b, _c, _d, _e, _f, _g, _h; + const cardPeak = cardConfig == null ? void 0 : cardConfig.peak_marker; + const entityPeak = entityConfig == null ? void 0 : entityConfig.peak; + return { + show: (_d = (_c = (_b = (_a = entityPeak == null ? void 0 : entityPeak.enabled) != null ? _a : entityConfig.show_peak) != null ? _b : cardPeak == null ? void 0 : cardPeak.show) != null ? _c : cardConfig == null ? void 0 : cardConfig.show_peak) != null ? _d : false, + color: (_h = (_g = (_f = (_e = entityPeak == null ? void 0 : entityPeak.color) != null ? _e : entityConfig.peak_color) != null ? _f : cardPeak == null ? void 0 : cardPeak.color) != null ? _g : cardConfig == null ? void 0 : cardConfig.peak_color) != null ? _h : "#888" + }; + } + function normalizeEntityConfig(entityConfig, cardConfig) { + var _a; + const normalizedEntity = { + ...entityConfig, + _normalized: true, + entity: entityConfig.entity, + name: (_a = entityConfig.name) != null ? _a : null, + icon: entityConfig.icon + }; + normalizedEntity.layout = normalizeLayoutConfig(entityConfig, cardConfig); + normalizedEntity.scale = normalizeScaleConfig(entityConfig, cardConfig); + normalizedEntity.bar = normalizeBarConfig(entityConfig, cardConfig); + normalizedEntity.baseline = normalizeBaselineConfig(entityConfig, cardConfig); + normalizedEntity.formatting = normalizeFormattingConfig(entityConfig, cardConfig); + normalizedEntity.target_marker = normalizeTargetMarkerConfig(entityConfig, cardConfig); + normalizedEntity.peak_marker = normalizePeakMarkerConfig(entityConfig, cardConfig); + normalizedEntity.min = normalizedEntity.scale.min.fixed; + normalizedEntity.min_entity = normalizedEntity.scale.min.entity; + normalizedEntity.max = normalizedEntity.scale.max.fixed; + normalizedEntity.max_entity = normalizedEntity.scale.max.entity; + normalizedEntity.height = normalizedEntity.layout.height; + normalizedEntity.label_position = normalizedEntity.layout.label.position; + normalizedEntity.label_width = normalizedEntity.layout.label.width; + normalizedEntity.color_mode = normalizedEntity.bar.color_mode; + normalizedEntity.fill_style = normalizedEntity.bar.fill_style; + normalizedEntity.solid_fill = normalizedEntity.bar.solid_fill; + normalizedEntity.color = normalizedEntity.bar.color; + normalizedEntity.gradient_stops = normalizedEntity.bar.gradient_stops; + normalizedEntity.severity = normalizedEntity.bar.severity; + normalizedEntity.animated = normalizedEntity.bar.animated; + normalizedEntity.above_target_color = normalizedEntity.bar.above_target_color; + normalizedEntity.decimal = normalizedEntity.formatting.decimal; + normalizedEntity.unit = normalizedEntity.formatting.unit; + normalizedEntity.target = normalizedEntity.target_marker.source.fixed; + normalizedEntity.target_entity = normalizedEntity.target_marker.source.entity; + normalizedEntity.target_color = normalizedEntity.target_marker.color; + normalizedEntity.show_target_label = normalizedEntity.target_marker.show_label; + normalizedEntity.show_peak = normalizedEntity.peak_marker.show; + normalizedEntity.peak_color = normalizedEntity.peak_marker.color; + return normalizedEntity; + } + function normalizeCardConfig(rawConfig) { + var _a; + const baseConfig = { + title: "", + label_position: "left", + color_mode: "severity", + color: "#4a9eff", + animated: true, + show_peak: false, + peak_color: "#888", + target: null, + target_entity: null, + target_color: "#888", + show_target_label: false, + above_target_color: null, + baseline: null, + decimal: null, + gradient_stops: null, + min: 0, + min_entity: null, + max: 100, + max_entity: null, + height: 38, + label_width: 100, + severity: [ + { from: 0, to: 33, color: "#4CAF50" }, + { from: 33, to: 75, color: "#FF9800" }, + { from: 75, to: 100, color: "#F44336" } + ], + ...rawConfig + }; + baseConfig._height_explicit = ((_a = rawConfig == null ? void 0 : rawConfig.layout) == null ? void 0 : _a.height) !== void 0 || (rawConfig == null ? void 0 : rawConfig.height) !== void 0; + if (baseConfig.entity && !baseConfig.entities) { + baseConfig.entities = [{ + entity: baseConfig.entity, + ...baseConfig.name !== void 0 ? { name: baseConfig.name } : {} + }]; + } + baseConfig.entities = baseConfig.entities.map( + (e) => typeof e === "string" ? { entity: e } : e + ); + const normalizedCard = { + ...baseConfig, + _normalized: true + }; + normalizedCard.layout = normalizeLayoutConfig(baseConfig, null); + normalizedCard.scale = normalizeScaleConfig(baseConfig, null); + normalizedCard.bar = normalizeBarConfig(baseConfig, null); + normalizedCard.baseline = normalizeBaselineConfig(baseConfig, null); + normalizedCard.formatting = normalizeFormattingConfig(baseConfig, null); + normalizedCard.target_marker = normalizeTargetMarkerConfig(baseConfig, null); + normalizedCard.peak_marker = normalizePeakMarkerConfig(baseConfig, null); + normalizedCard.entities = baseConfig.entities.map( + (entityCfg) => normalizeEntityConfig(entityCfg, normalizedCard) + ); + return normalizedCard; + } + var init_normalize = __esm({ + "src/config/normalize.js"() { + } + }); + + // src/config/resolve.js + function getEntityNumericValue(hass, entityId) { + var _a; + if (!entityId || !((_a = hass == null ? void 0 : hass.states) == null ? void 0 : _a[entityId])) return null; + const raw = hass.states[entityId].state; + const num = parseFloat(raw); + return Number.isFinite(num) ? num : null; + } + function getNumericValue(hass, value, entityId = null) { + const entityValue = getEntityNumericValue(hass, entityId); + if (entityValue !== null) return entityValue; + if (value === null || value === void 0 || value === "") return null; + const num = parseFloat(value); + return Number.isFinite(num) ? num : null; + } + function resolvePercentValue(percent, minValue, maxValue) { + if (!Number.isFinite(percent)) return null; + const safeMin = Number.isFinite(minValue) ? minValue : 0; + const safeMax = Number.isFinite(maxValue) ? maxValue : 100; + return safeMin + percent / 100 * (safeMax - safeMin); + } + function getNormalizedResolvableNumericValue(hass, resolvable, minValue = null, maxValue = null) { + var _a; + if (!resolvable) return null; + const entityValue = getEntityNumericValue(hass, resolvable.entity); + if (entityValue !== null) return entityValue; + const fixedValue = getNumericValue(hass, (_a = resolvable.fixed) != null ? _a : resolvable.value, null); + if (fixedValue !== null) return fixedValue; + if (Number.isFinite(resolvable.percent)) { + return resolvePercentValue(resolvable.percent, minValue, maxValue); + } + return null; + } + var init_resolve = __esm({ + "src/config/resolve.js"() { + init_normalize(); + } + }); + + // src/config/validate.js + function createDiagnostic(code, message, path, entity = null) { + return { code, message, path, entity }; + } + function getStaticFixedValue(resolvable) { + var _a; + if (!resolvable || resolvable.entity || Number.isFinite(resolvable.percent)) { + return null; + } + return getFiniteNumber((_a = resolvable.fixed) != null ? _a : resolvable.value); + } + function addWarning(diagnostics, code, message, path, entity = null) { + diagnostics.warnings.push(createDiagnostic(code, message, path, entity)); + } + function validateScaleBounds(diagnostics, scale, path, entity = null) { + const min = getStaticFixedValue(scale == null ? void 0 : scale.min); + const max = getStaticFixedValue(scale == null ? void 0 : scale.max); + if (Number.isFinite(min) && Number.isFinite(max) && min > max) { + addWarning(diagnostics, "scale.min_gt_max", "Scale minimum is greater than maximum.", path, entity); + } + return { min, max }; + } + function validateTargetRange(diagnostics, config, scaleBounds, path, entity = null) { + var _a; + const target = getStaticFixedValue((_a = config == null ? void 0 : config.target_marker) == null ? void 0 : _a.source); + if (!Number.isFinite(target)) return; + const { min, max } = scaleBounds; + if (Number.isFinite(min) && Number.isFinite(max) && (target < min || target > max)) { + addWarning(diagnostics, "target.outside_scale", "Fixed target value is outside the fixed scale range.", path, entity); + } + } + function validateBaselineRange(diagnostics, config, scaleBounds, path, entity = null) { + var _a; + const baseline = getStaticFixedValue((_a = config == null ? void 0 : config.baseline) == null ? void 0 : _a.at); + if (!Number.isFinite(baseline)) return; + const { min, max } = scaleBounds; + if (Number.isFinite(min) && Number.isFinite(max) && (baseline < min || baseline > max)) { + addWarning(diagnostics, "baseline.outside_scale", "Fixed baseline value is outside the fixed scale range.", path, entity); + } + } + function hasConfiguredResolvableValue(resolvable) { + var _a; + return !!resolvable && (Number.isFinite(getFiniteNumber((_a = resolvable.fixed) != null ? _a : resolvable.value)) || Number.isFinite(resolvable.percent) || !!resolvable.entity); + } + function validateBaselineSuppressesNeedle(diagnostics, config, path, entity = null) { + var _a, _b, _c, _d; + const needleEnabled = ((_b = (_a = config == null ? void 0 : config.bar) == null ? void 0 : _a.needle) == null ? void 0 : _b.show) === true; + const baselineConfigured = ((_c = config == null ? void 0 : config.baseline) == null ? void 0 : _c.enabled) !== false && hasConfiguredResolvableValue((_d = config == null ? void 0 : config.baseline) == null ? void 0 : _d.at); + if (needleEnabled && baselineConfigured) { + addWarning( + diagnostics, + "baseline-suppresses-needle", + "Baseline rendering suppresses the needle marker.", + path, + entity + ); + } + } + function getStaticSegmentBound(boundary) { + var _a; + if (!boundary || boundary.entity || Number.isFinite(boundary.percent)) return null; + return getFiniteNumber((_a = boundary.fixed) != null ? _a : boundary.value); + } + function validateSegments(diagnostics, segments, scaleBounds, path, entity = null) { + if (!Array.isArray(segments) || !segments.length) return; + const staticSegments = []; + const { min, max } = scaleBounds; + for (let index = 0; index < segments.length; index += 1) { + const segment = segments[index]; + const from = getStaticSegmentBound(segment == null ? void 0 : segment.from); + const to = getStaticSegmentBound(segment == null ? void 0 : segment.to); + const segmentPath = `${path}.segments[${index}]`; + if (Number.isFinite(from) && Number.isFinite(to) && from > to) { + addWarning(diagnostics, "segments.from_gt_to", "Segment start is greater than segment end.", segmentPath, entity); + } + if (Number.isFinite(min) && Number.isFinite(max)) { + if (Number.isFinite(from) && (from < min || from > max)) { + addWarning(diagnostics, "segments.outside_scale", "Segment boundary is outside the fixed scale range.", segmentPath, entity); + } + if (Number.isFinite(to) && (to < min || to > max)) { + addWarning(diagnostics, "segments.outside_scale", "Segment boundary is outside the fixed scale range.", segmentPath, entity); + } + } + if (Number.isFinite(from) && Number.isFinite(to)) { + staticSegments.push({ from, to, path: segmentPath }); + } + } + const sorted = [...staticSegments].sort((a, b) => a.from - b.from); + for (let index = 1; index < sorted.length; index += 1) { + const previous = sorted[index - 1]; + const current = sorted[index]; + if (current.from < previous.to) { + addWarning(diagnostics, "segments.overlap", "Fixed segments overlap.", current.path, entity); + } + } + } + function validateGradientStops(diagnostics, stops, path, entity = null) { + var _a; + if (!Array.isArray(stops)) return; + const seenPositions = /* @__PURE__ */ new Set(); + for (let index = 0; index < stops.length; index += 1) { + const pos = getFiniteNumber((_a = stops[index]) == null ? void 0 : _a.pos); + const stopPath = `${path}.gradient_stops[${index}]`; + if (Number.isFinite(pos) && (pos < 0 || pos > 100)) { + addWarning( + diagnostics, + "gradient_stops.outside_range", + "Gradient stop position is outside 0..100.", + stopPath, + entity + ); + } + if (Number.isFinite(pos)) { + if (seenPositions.has(pos)) { + addWarning( + diagnostics, + "duplicate-gradient-stop-position", + "Multiple gradient stops use the same position value.", + stopPath, + entity + ); + } else { + seenPositions.add(pos); + } + } + } + } + function validateConfigScope(diagnostics, config, path, entity = null) { + var _a, _b; + const scaleBounds = validateScaleBounds(diagnostics, config == null ? void 0 : config.scale, path, entity); + validateTargetRange(diagnostics, config, scaleBounds, path, entity); + validateBaselineRange(diagnostics, config, scaleBounds, path, entity); + validateBaselineSuppressesNeedle(diagnostics, config, path, entity); + validateSegments(diagnostics, (_a = config == null ? void 0 : config.bar) == null ? void 0 : _a.segments, scaleBounds, `${path}.bar`, entity); + validateGradientStops(diagnostics, (_b = config == null ? void 0 : config.bar) == null ? void 0 : _b.gradient_stops, `${path}.bar`, entity); + } + function validateNormalizedConfig(config) { + var _a; + const diagnostics = { + warnings: [], + errors: [] + }; + if (!config || typeof config !== "object") { + return diagnostics; + } + validateConfigScope(diagnostics, config, "card"); + const seenEntities = /* @__PURE__ */ new Set(); + const entities = Array.isArray(config.entities) ? config.entities : []; + for (let index = 0; index < entities.length; index += 1) { + const entityConfig = entities[index]; + const entityId = (_a = entityConfig == null ? void 0 : entityConfig.entity) != null ? _a : null; + const path = `entities[${index}]`; + if (!entityId) { + addWarning(diagnostics, "entities.missing_entity", "Entity row is missing an entity id.", path, null); + continue; + } + if (seenEntities.has(entityId)) { + addWarning(diagnostics, "entities.duplicate_entity", "Duplicate entity id in the same card config.", path, entityId); + } else { + seenEntities.add(entityId); + } + validateConfigScope(diagnostics, entityConfig, path, entityId); + } + return diagnostics; + } + var init_validate = __esm({ + "src/config/validate.js"() { + init_normalize(); + } + }); + + // src/view-model/row-view-model.js + function getDefaultEntityIcon(stateObj, entityId = "") { + var _a, _b, _c; + const deviceClass = String((_b = (_a = stateObj == null ? void 0 : stateObj.attributes) == null ? void 0 : _a.device_class) != null ? _b : "").trim(); + if (deviceClass) { + const deviceClassIcons = { + apparent_power: "mdi:flash", + battery: "mdi:battery", + carbon_dioxide: "mdi:molecule-co2", + current: "mdi:current-ac", + energy: "mdi:lightning-bolt", + gas: "mdi:meter-gas", + humidity: "mdi:water-percent", + monetary: "mdi:cash", + power: "mdi:flash", + pressure: "mdi:gauge", + temperature: "mdi:thermometer", + voltage: "mdi:sine-wave", + water: "mdi:water", + weight: "mdi:weight", + wind_speed: "mdi:weather-windy" + }; + if (deviceClassIcons[deviceClass]) { + return deviceClassIcons[deviceClass]; + } + } + const domain = String(entityId || "").split(".")[0]; + const domainIcons = { + sensor: "mdi:eye", + binary_sensor: "mdi:radiobox-marked", + switch: "mdi:toggle-switch-variant", + light: "mdi:lightbulb" + }; + return (_c = domainIcons[domain]) != null ? _c : null; + } + function toScalePct(value, minValue, maxValue) { + if (!Number.isFinite(value)) return null; + const safeMin = Number.isFinite(minValue) ? minValue : 0; + const safeMax = Number.isFinite(maxValue) ? maxValue : 100; + const range = safeMax - safeMin || 1; + return Math.min(100, Math.max(0, (value - safeMin) / range * 100)); + } + function formatNumericDisplay(rawVal, decimal = null) { + if (!Number.isFinite(rawVal)) return String(rawVal); + if (decimal !== null) { + return parseFloat(rawVal.toFixed(decimal)).toLocaleString(); + } + return rawVal.toLocaleString(); + } + function isTightUnit(unit) { + return ["h", "m", "s"].includes(String(unit || "").trim()); + } + function formatDisplayWithUnit(display, unit) { + if (!unit) return String(display); + const cleanUnit = String(unit); + return `${display}${isTightUnit(cleanUnit) ? "" : " "}${cleanUnit}`; + } + function parseColorToRgb(color) { + const value = String(color || "").trim(); + if (!value) return null; + const hexMatch = value.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (hexMatch) { + const hex = hexMatch[1]; + const full = hex.length === 3 ? hex.split("").map((char) => char + char).join("") : hex; + return { + r: parseInt(full.slice(0, 2), 16), + g: parseInt(full.slice(2, 4), 16), + b: parseInt(full.slice(4, 6), 16) + }; + } + const rgbMatch = value.match(/^rgba?\(([^)]+)\)$/i); + if (rgbMatch) { + const parts = rgbMatch[1].split(",").map((part) => part.trim()); + if (parts.length >= 3) { + return { + r: Math.max(0, Math.min(255, parseFloat(parts[0]))), + g: Math.max(0, Math.min(255, parseFloat(parts[1]))), + b: Math.max(0, Math.min(255, parseFloat(parts[2]))) + }; + } + } + return null; + } + function getNeedleBorderColor(color) { + const rgb = parseColorToRgb(color); + if (!rgb) return "#000000"; + const toLinear = (channel) => { + const srgb = channel / 255; + return srgb <= 0.04045 ? srgb / 12.92 : ((srgb + 0.055) / 1.055) ** 2.4; + }; + const luminance = 0.2126 * toLinear(rgb.r) + 0.7152 * toLinear(rgb.g) + 0.0722 * toLinear(rgb.b); + return luminance < 0.22 ? "#ffffff" : "#000000"; + } + function getNeedleState(entityConfig, numericValue, minValue, maxValue, baselinePercent) { + var _a, _b, _c; + const needle = (_a = entityConfig == null ? void 0 : entityConfig.bar) == null ? void 0 : _a.needle; + if (!(needle == null ? void 0 : needle.show) || Number.isFinite(baselinePercent) || !Number.isFinite(numericValue)) { + const color2 = (_b = needle == null ? void 0 : needle.color) != null ? _b : "#ffffff"; + return { + show: false, + percent: null, + pct: null, + color: color2, + borderColor: getNeedleBorderColor(color2), + edge: "middle" + }; + } + const color = (_c = needle.color) != null ? _c : "#ffffff"; + const percent = Math.min(100, Math.max(0, toScalePct(numericValue, minValue, maxValue))); + return { + show: true, + percent, + pct: percent, + color, + borderColor: getNeedleBorderColor(color), + edge: percent <= 0 ? "left" : percent >= 100 ? "right" : "middle" + }; + } + function getPeakState(entityId, numericValue, minValue, maxValue, peaks, peakEnabled) { + if (!peakEnabled || !Number.isFinite(numericValue)) { + return { + value: null, + percent: null, + display: null, + visible: false + }; + } + const existingPeak = getFiniteNumber(peaks == null ? void 0 : peaks[entityId]); + const peakValue = Number.isFinite(existingPeak) ? Math.max(existingPeak, numericValue) : numericValue; + return { + value: peakValue, + percent: toScalePct(peakValue, minValue, maxValue), + visible: true + }; + } + function buildRowViewModel(options) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z, _A, _B, _C, _D, _E, _F, _G, _H, _I, _J, _K, _L, _M, _N, _O, _P, _Q, _R, _S; + const { + hass, + cardConfig, + entityConfig, + entityState, + peaks + } = options; + void cardConfig; + const entityId = (_a = entityConfig == null ? void 0 : entityConfig.entity) != null ? _a : null; + const rawState = (_b = entityState == null ? void 0 : entityState.state) != null ? _b : ""; + const numericValue = getFiniteNumber(rawState); + const rawUnit = (_d = (_c = entityState == null ? void 0 : entityState.attributes) == null ? void 0 : _c.unit_of_measurement) != null ? _d : ""; + const displayUnit = numericValue !== null ? (_g = (_f = (_e = entityConfig == null ? void 0 : entityConfig.formatting) == null ? void 0 : _e.unit) != null ? _f : rawUnit) != null ? _g : "" : ""; + const min = getNormalizedResolvableNumericValue(hass, (_h = entityConfig == null ? void 0 : entityConfig.scale) == null ? void 0 : _h.min); + const max = getNormalizedResolvableNumericValue(hass, (_i = entityConfig == null ? void 0 : entityConfig.scale) == null ? void 0 : _i.max); + const safeMin = Number.isFinite(min) ? min : 0; + const safeMax = Number.isFinite(max) ? max : 100; + const percent = numericValue !== null ? toScalePct(numericValue, safeMin, safeMax) : 0; + const displayValue = numericValue === null ? rawState : formatNumericDisplay(numericValue, (_k = (_j = entityConfig == null ? void 0 : entityConfig.formatting) == null ? void 0 : _j.decimal) != null ? _k : null); + const targetValue = ((_l = entityConfig == null ? void 0 : entityConfig.target_marker) == null ? void 0 : _l.enabled) === false ? null : getNormalizedResolvableNumericValue(hass, (_m = entityConfig == null ? void 0 : entityConfig.target_marker) == null ? void 0 : _m.source, safeMin, safeMax); + const targetPercent = targetValue !== null ? toScalePct(targetValue, safeMin, safeMax) : null; + const targetVisible = targetValue !== null; + const targetDisplay = targetValue !== null ? formatDisplayWithUnit( + formatNumericDisplay(targetValue, (_o = (_n = entityConfig == null ? void 0 : entityConfig.formatting) == null ? void 0 : _n.decimal) != null ? _o : null), + (_r = (_q = (_p = entityConfig == null ? void 0 : entityConfig.formatting) == null ? void 0 : _p.unit) != null ? _q : rawUnit) != null ? _r : "" + ) : null; + const baselineValue = ((_s = entityConfig == null ? void 0 : entityConfig.baseline) == null ? void 0 : _s.enabled) === false ? null : getNormalizedResolvableNumericValue(hass, (_t = entityConfig == null ? void 0 : entityConfig.baseline) == null ? void 0 : _t.at, safeMin, safeMax); + const baselinePercent = Number.isFinite(baselineValue) ? toScalePct(baselineValue, safeMin, safeMax) : null; + const baselineVisible = Number.isFinite(baselineValue); + const peakState = getPeakState( + entityId, + numericValue, + safeMin, + safeMax, + peaks, + ((_u = entityConfig == null ? void 0 : entityConfig.peak_marker) == null ? void 0 : _u.show) === true + ); + const peakDisplay = peakState.visible ? formatNumericDisplay(peakState.value, (_w = (_v = entityConfig == null ? void 0 : entityConfig.formatting) == null ? void 0 : _v.decimal) != null ? _w : null) : null; + return { + entityId, + name: (_z = (_y = entityConfig == null ? void 0 : entityConfig.name) != null ? _y : (_x = entityState == null ? void 0 : entityState.attributes) == null ? void 0 : _x.friendly_name) != null ? _z : entityId, + icon: (entityConfig == null ? void 0 : entityConfig.icon) === false ? false : (_C = (_B = entityConfig == null ? void 0 : entityConfig.icon) != null ? _B : (_A = entityState == null ? void 0 : entityState.attributes) == null ? void 0 : _A.icon) != null ? _C : getDefaultEntityIcon(entityState, entityId), + state: rawState, + numericValue, + rawUnit, + displayUnit, + min: safeMin, + max: safeMax, + percent, + displayValue, + unit: displayUnit, + barColor: (_E = (_D = entityConfig == null ? void 0 : entityConfig.bar) == null ? void 0 : _D.color) != null ? _E : null, + fillStyle: (_G = (_F = entityConfig == null ? void 0 : entityConfig.bar) == null ? void 0 : _F.fill_style) != null ? _G : null, + target: targetValue, + targetPercent, + targetDisplay, + targetVisible, + baseline: baselineValue, + baselinePercent, + baselineVisible, + peak: peakState.value, + peakPercent: peakState.percent, + peakDisplay, + peakVisible: peakState.visible, + segments: (_I = (_H = entityConfig == null ? void 0 : entityConfig.bar) == null ? void 0 : _H.segments) != null ? _I : null, + gradientStops: (_K = (_J = entityConfig == null ? void 0 : entityConfig.bar) == null ? void 0 : _J.gradient_stops) != null ? _K : null, + needle: getNeedleState(entityConfig, numericValue, safeMin, safeMax, baselinePercent), + classes: { + labelPosition: (_N = (_M = (_L = entityConfig == null ? void 0 : entityConfig.layout) == null ? void 0 : _L.label) == null ? void 0 : _M.position) != null ? _N : "left", + animated: ((_O = entityConfig == null ? void 0 : entityConfig.bar) == null ? void 0 : _O.animated) !== false + }, + attributes: { + entity: entityId, + baseHeight: (_Q = (_P = entityConfig == null ? void 0 : entityConfig.layout) == null ? void 0 : _P.height) != null ? _Q : 38, + heightExplicit: ((_R = entityConfig == null ? void 0 : entityConfig.layout) == null ? void 0 : _R.height_explicit) === true, + barAnimated: ((_S = entityConfig == null ? void 0 : entityConfig.bar) == null ? void 0 : _S.animated) !== false + } + }; + } + var init_row_view_model = __esm({ + "src/view-model/row-view-model.js"() { + init_resolve(); + init_normalize(); + } + }); + + // src/utils/dom.js + function escapeHtml(value) { + if (value == null) return ""; + return String(value).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """).replace(/'/g, "'"); + } + var init_dom = __esm({ + "src/utils/dom.js"() { + } + }); + + // src/card/SensorBarCard.js + var SensorBarCard; + var init_SensorBarCard = __esm({ + "src/card/SensorBarCard.js"() { + init_normalize(); + init_resolve(); + init_validate(); + init_row_view_model(); + init_dom(); + SensorBarCard = class extends HTMLElement { + static getConfigElement() { + return document.createElement("sensor-bar-card-plus-editor"); + } + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this._baseDomReady = false; + this._config = {}; + this._diagnostics = { warnings: [], errors: [] }; + this._lastDiagnosticsSignature = null; + this._hass = null; + this._peaks = {}; + this._rendered = false; + this._resizeObserver = null; + this._densityPassScheduled = false; + this._densityPassDirty = false; + this._densityPassFrame = null; + this._densityPassRetries = 0; + this._boundWindowResize = () => this._schedulePostLayoutDensityPass(); + this._ensureBaseDom(); + } + connectedCallback() { + window.addEventListener("resize", this._boundWindowResize, { passive: true }); + this._schedulePostLayoutDensityPass(); + } + disconnectedCallback() { + window.removeEventListener("resize", this._boundWindowResize); + this._disconnectResizeObserver(); + if (this._densityPassFrame) { + cancelAnimationFrame(this._densityPassFrame); + this._densityPassFrame = null; + } + this._densityPassScheduled = false; + this._densityPassDirty = false; + } + setConfig(config) { + if (!config.entities && !config.entity) { + throw new Error("You must define entities or entity"); + } + this._rendered = false; + this._config = this.normalizeCardConfig(config); + const activeEntityIds = new Set( + (this._config.entities || []).map((entityConfig) => entityConfig.entity) + ); + for (const entityId of Object.keys(this._peaks)) { + if (!activeEntityIds.has(entityId)) { + delete this._peaks[entityId]; + } + } + this._diagnostics = validateNormalizedConfig(this._config); + this._logDiagnostics(); + this._render(); + } + _logDiagnostics() { + var _a; + const diagnostics = (_a = this._diagnostics) != null ? _a : { warnings: [], errors: [] }; + const signature = JSON.stringify(diagnostics); + if (signature === this._lastDiagnosticsSignature) return; + this._lastDiagnosticsSignature = signature; + diagnostics.warnings.forEach((diagnostic) => { + console.warn(`[sensor-bar-card-plus] ${diagnostic.message}`, diagnostic); + }); + diagnostics.errors.forEach((diagnostic) => { + console.warn(`[sensor-bar-card-plus] ${diagnostic.message}`, diagnostic); + }); + } + // The normalized model is internal only. It preserves today's flat YAML + // while giving future work one structured compatibility layer to build on. + normalizeCardConfig(rawConfig) { + return normalizeCardConfig(rawConfig); + } + normalizeEntityConfig(entityConfig, cardConfig) { + return normalizeEntityConfig(entityConfig, cardConfig); + } + // Internal resolvable shape preserves today's flat `value + *_entity` + // behavior while canonicalizing the normalized form to `fixed + entity`. + normalizeResolvableValue(value, entityValue, percentValue = null) { + return normalizeResolvableValue(value, entityValue, percentValue); + } + _looksLikeEntityId(value) { + return looksLikeEntityId(value); + } + _parsePercentLiteral(value) { + return parsePercentLiteral(value); + } + _getFiniteNumber(value) { + return getFiniteNumber(value); + } + normalizeStructuredResolvableValue(input, inheritedResolvable = null, defaultValue = null, options = {}) { + return normalizeStructuredResolvableValue(input, inheritedResolvable, defaultValue, options); + } + normalizeBaselineDirectionConfig(input, inheritedDirection = null) { + return normalizeBaselineDirectionConfig(input, inheritedDirection); + } + normalizeBaselineConfig(entityConfig, cardConfig) { + return normalizeBaselineConfig(entityConfig, cardConfig); + } + inferSegmentEndValues(segments, fallbackEnd = null) { + const sorted = [...segments].sort((a, b) => a.from - b.from); + return sorted.map((segment, index) => { + var _a; + let to = Number.isFinite(segment.to) ? segment.to : null; + if (!Number.isFinite(to) && index < sorted.length - 1) { + to = sorted[index + 1].from; + } + if (!Number.isFinite(to) && Number.isFinite(fallbackEnd)) { + to = fallbackEnd; + } + return { + from: segment.from, + to, + color: segment.color, + label: (_a = segment.label) != null ? _a : null + }; + }); + } + normalizeSeverityToSegments(input) { + return normalizeSeverityToSegments(input); + } + _hasResolvableMagnitude(resolvable) { + return !!resolvable && (Number.isFinite(this._getFiniteNumber(resolvable.fixed)) || Number.isFinite(resolvable.percent)); + } + normalizeGaugeSegments(input) { + return normalizeGaugeSegments(input); + } + normalizeScaleBound(entityConfig, cardConfig, key, defaultValue) { + return normalizeScaleBound(entityConfig, cardConfig, key, defaultValue); + } + normalizeScaleConfig(entityConfig, cardConfig) { + return normalizeScaleConfig(entityConfig, cardConfig); + } + _fillStyleToColorMode(fillStyle) { + return fillStyleToColorMode(fillStyle); + } + _colorModeToFillStyle(colorMode) { + return colorModeToFillStyle(colorMode); + } + _normalizeBarModeConfig(barConfig = null, flatColorMode = null) { + return normalizeBarModeConfig(barConfig, flatColorMode); + } + _resolveNormalizedBarMode(entityBar, entityConfig, cardBar, cardConfig) { + return resolveNormalizedBarMode(entityBar, entityConfig, cardBar, cardConfig); + } + _normalizeGradientStops(input) { + return normalizeGradientStops(input); + } + normalizeNeedleConfig(input, inheritedNeedle = null) { + return normalizeNeedleConfig(input, inheritedNeedle); + } + normalizeBarConfig(entityConfig, cardConfig) { + return normalizeBarConfig(entityConfig, cardConfig); + } + normalizeLayoutConfig(entityConfig, cardConfig) { + return normalizeLayoutConfig(entityConfig, cardConfig); + } + _clampSupportedRowHeight(height) { + return clampSupportedRowHeight(height); + } + normalizeFormattingConfig(entityConfig, cardConfig) { + return normalizeFormattingConfig(entityConfig, cardConfig); + } + normalizeTargetMarkerConfig(entityConfig, cardConfig) { + return normalizeTargetMarkerConfig(entityConfig, cardConfig); + } + normalizePeakMarkerConfig(entityConfig, cardConfig) { + return normalizePeakMarkerConfig(entityConfig, cardConfig); + } + _normalizeOptionalEnabled(value) { + return normalizeOptionalEnabled(value); + } + set hass(hass) { + const oldHass = this._hass; + this._hass = hass; + if (!oldHass) { + this._update(); + return; + } + if (this._shouldUpdate(oldHass, hass)) { + this._update(); + } + } + // Merge global config with per-entity overrides + _resolve(entityCfg) { + var _a, _b, _c, _d, _e, _f, _g; + const ecfg = (entityCfg == null ? void 0 : entityCfg._normalized) ? entityCfg : this.normalizeEntityConfig(entityCfg, this._config); + const stateObj = (_c = (_b = (_a = this._hass) == null ? void 0 : _a.states) == null ? void 0 : _b[ecfg.entity]) != null ? _c : null; + return { + ...ecfg, + icon: ecfg.icon === false ? false : (_f = (_e = ecfg.icon) != null ? _e : (_d = stateObj == null ? void 0 : stateObj.attributes) == null ? void 0 : _d.icon) != null ? _f : this._getDefaultEntityIcon(stateObj, ecfg.entity), + name: (_g = ecfg.name) != null ? _g : null + }; + } + _getDefaultEntityIcon(stateObj, entityId = "") { + var _a, _b, _c; + const deviceClass = String((_b = (_a = stateObj == null ? void 0 : stateObj.attributes) == null ? void 0 : _a.device_class) != null ? _b : "").trim(); + if (deviceClass) { + const deviceClassIcons = { + apparent_power: "mdi:flash", + battery: "mdi:battery", + carbon_dioxide: "mdi:molecule-co2", + current: "mdi:current-ac", + energy: "mdi:lightning-bolt", + gas: "mdi:meter-gas", + humidity: "mdi:water-percent", + monetary: "mdi:cash", + power: "mdi:flash", + pressure: "mdi:gauge", + temperature: "mdi:thermometer", + voltage: "mdi:sine-wave", + water: "mdi:water", + weight: "mdi:weight", + wind_speed: "mdi:weather-windy" + }; + if (deviceClassIcons[deviceClass]) { + return deviceClassIcons[deviceClass]; + } + } + const domain = String(entityId || "").split(".")[0]; + const domainIcons = { + sensor: "mdi:eye", + binary_sensor: "mdi:radiobox-marked", + switch: "mdi:toggle-switch-variant", + light: "mdi:lightbulb" + }; + return (_c = domainIcons[domain]) != null ? _c : null; + } + _shouldUpdate(oldHass, newHass) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j; + if (!this._config || !this._config.entities) return true; + for (const entityCfg of this._config.entities) { + const ecfg = this._resolve(entityCfg); + const entitiesToWatch = [ + entityCfg.entity, + (_b = (_a = ecfg.scale) == null ? void 0 : _a.min) == null ? void 0 : _b.entity, + (_d = (_c = ecfg.scale) == null ? void 0 : _c.max) == null ? void 0 : _d.entity, + (_f = (_e = ecfg.baseline) == null ? void 0 : _e.at) == null ? void 0 : _f.entity, + (_h = (_g = ecfg.target_marker) == null ? void 0 : _g.source) == null ? void 0 : _h.entity + ].filter(Boolean); + for (const ent of entitiesToWatch) { + const oldState = (_i = oldHass.states[ent]) != null ? _i : null; + const newState = (_j = newHass.states[ent]) != null ? _j : null; + if (oldState !== newState) { + return true; + } + } + } + return false; + } + _setStyleIfChanged(el, prop, value) { + var _a, _b; + if (!(el == null ? void 0 : el.style)) return false; + const nextValue = value == null ? "" : String(value); + if (prop.startsWith("--")) { + const currentValue2 = typeof el.style.getPropertyValue === "function" ? el.style.getPropertyValue(prop) : (_a = el.style[prop]) != null ? _a : ""; + if (currentValue2 === nextValue) return false; + if (typeof el.style.setProperty === "function") { + el.style.setProperty(prop, nextValue); + } else { + el.style[prop] = nextValue; + } + return true; + } + const currentValue = (_b = el.style[prop]) != null ? _b : ""; + if (currentValue === nextValue) return false; + el.style[prop] = nextValue; + return true; + } + _setStyleTextIfChanged(el, value) { + var _a; + if (!(el == null ? void 0 : el.style)) return false; + const nextValue = value == null ? "" : String(value); + const currentValue = (_a = el.style.cssText) != null ? _a : ""; + if (currentValue === nextValue) return false; + el.style.cssText = nextValue; + return true; + } + _setTextIfChanged(el, value) { + var _a; + if (!el) return false; + const nextValue = value == null ? "" : String(value); + if (((_a = el.textContent) != null ? _a : "") === nextValue) return false; + el.textContent = nextValue; + return true; + } + _setDatasetIfChanged(el, key, value) { + var _a; + if (!(el == null ? void 0 : el.dataset)) return false; + const nextValue = value == null ? "" : String(value); + const currentValue = (_a = el.dataset[key]) != null ? _a : ""; + if (currentValue === nextValue) return false; + el.dataset[key] = nextValue; + return true; + } + _setClassNameIfChanged(el, value) { + var _a; + if (!el) return false; + const nextValue = value == null ? "" : String(value); + if (((_a = el.className) != null ? _a : "") === nextValue) return false; + el.className = nextValue; + return true; + } + _repositionAllTargetLabels() { + if (!this.shadowRoot) return; + this.shadowRoot.querySelectorAll(".row[data-entity]").forEach((row) => { + this._positionTargetLabel(row); + }); + } + _positionTargetLabel(row) { + const track = row.querySelector(".bar-track"); + const label = row.querySelector(".target-value-label"); + const marker = row.querySelector(".target-marker"); + if (!track || !label || !marker) return; + if (marker.style.display === "none" || !label.textContent.trim()) { + this._setStyleIfChanged(label, "visibility", "hidden"); + return; + } + const trackRect = track.getBoundingClientRect(); + const maxLabelWidth = Math.max(0, Math.floor(trackRect.width - 4)); + this._setStyleIfChanged(label, "maxWidth", `${maxLabelWidth}px`); + const labelRect = label.getBoundingClientRect(); + const markerPercent = parseFloat(marker.style.left); + if (!Number.isFinite(markerPercent) || trackRect.width <= 0 || labelRect.width <= 0 || maxLabelWidth <= 10) { + this._setStyleIfChanged(label, "visibility", "hidden"); + return; + } + const markerX = markerPercent / 100 * trackRect.width; + const halfLabel = labelRect.width / 2; + const clampedX = Math.max(halfLabel, Math.min(trackRect.width - halfLabel, markerX)); + this._setStyleIfChanged(label, "left", `${clampedX}px`); + this._setStyleIfChanged(label, "transform", "translateX(-50%)"); + this._setStyleIfChanged(label, "visibility", "visible"); + } + _getEntityNumericValue(entityId) { + return getEntityNumericValue(this._hass, entityId); + } + _getNumericValue(value, entityId = null) { + return getNumericValue(this._hass, value, entityId); + } + _resolvePercentValue(percent, minValue, maxValue) { + return resolvePercentValue(percent, minValue, maxValue); + } + _getNormalizedResolvableNumericValue(resolvable, minValue = null, maxValue = null) { + return getNormalizedResolvableNumericValue(this._hass, resolvable, minValue, maxValue); + } + _hexToRgb(color) { + if (!color || typeof color !== "string") return null; + const hex = color.replace("#", "").trim(); + const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex; + if (!/^[0-9a-fA-F]{6}$/.test(full)) return null; + return { + r: parseInt(full.slice(0, 2), 16), + g: parseInt(full.slice(2, 4), 16), + b: parseInt(full.slice(4, 6), 16) + }; + } + _getSeverityInterpolationStops(ecfg, minValue = 0, maxValue = 100) { + const bands = this._getSegmentsForRendering(ecfg, minValue, maxValue); + const sorted = bands.filter((s) => Number.isFinite(s == null ? void 0 : s.from) && Number.isFinite(s == null ? void 0 : s.to) && (s == null ? void 0 : s.color)).sort((a, b) => a.from - b.from); + if (!sorted.length) return []; + const stops = []; + for (let i = 0; i < sorted.length; i++) { + const band = sorted[i]; + const rgb = this._hexToRgb(band.color); + if (!rgb) continue; + let anchor; + if (i === 0) { + anchor = band.from; + } else if (i === sorted.length - 1) { + anchor = band.to; + } else { + anchor = band.from + (band.to - band.from) / 2; + } + if (!stops.length || stops[stops.length - 1].p !== anchor) { + stops.push({ p: anchor, ...rgb }); + } + } + return stops; + } + _getSeverityBandGradientCss(ecfg, minValue = 0, maxValue = 100) { + const bands = this._getSegmentsForRendering(ecfg, minValue, maxValue); + const sorted = bands.filter((s) => Number.isFinite(s == null ? void 0 : s.from) && Number.isFinite(s == null ? void 0 : s.to) && (s == null ? void 0 : s.color)).sort((a, b) => a.from - b.from); + if (!sorted.length) return null; + const stops = []; + for (const band of sorted) { + stops.push(`${band.color} ${band.from}%`, `${band.color} ${band.to}%`); + } + return `linear-gradient(to right, ${stops.join(", ")})`; + } + _getSoftBandBlendWidthPct() { + return 1.5; + } + _pushGradientColorStop(stops, pos, color) { + if (!Array.isArray(stops) || !color) return; + const clampedPos = Math.min(100, Math.max(0, pos)); + const last = stops[stops.length - 1]; + if (last && last.color === color && Math.abs(last.p - clampedPos) < 1e-4) return; + stops.push({ p: clampedPos, color }); + } + _getSoftBandGradientStops(ecfg, minValue = 0, maxValue = 100) { + const bands = this._getSegmentsForRendering(ecfg, minValue, maxValue); + const sorted = bands.filter((s) => Number.isFinite(s == null ? void 0 : s.from) && Number.isFinite(s == null ? void 0 : s.to) && (s == null ? void 0 : s.color)).sort((a, b) => a.from - b.from); + if (!sorted.length) return []; + const blendWidth = this._getSoftBandBlendWidthPct(); + const blendHalf = blendWidth / 2; + const stops = []; + this._pushGradientColorStop(stops, sorted[0].from, sorted[0].color); + for (let i = 0; i < sorted.length - 1; i++) { + const current = sorted[i]; + const next = sorted[i + 1]; + const boundary = current.to; + const currentWidth = current.to - current.from; + const nextWidth = next.to - next.from; + const soften = currentWidth >= blendWidth && nextWidth >= blendWidth; + if (soften) { + this._pushGradientColorStop(stops, Math.max(current.from, boundary - blendHalf), current.color); + this._pushGradientColorStop(stops, Math.min(next.to, boundary + blendHalf), next.color); + } else { + this._pushGradientColorStop(stops, boundary, current.color); + this._pushGradientColorStop(stops, boundary, next.color); + } + } + this._pushGradientColorStop(stops, sorted[sorted.length - 1].to, sorted[sorted.length - 1].color); + return stops; + } + _getSoftBandGradientCss(ecfg, minValue = 0, maxValue = 100) { + const stops = this._getSoftBandGradientStops(ecfg, minValue, maxValue); + if (!stops.length) return null; + return `linear-gradient(to right, ${stops.map((stop) => `${stop.color} ${stop.p}%`).join(", ")})`; + } + _resolveSegmentBoundaryPct(boundary, minValue, maxValue) { + if (boundary === null || boundary === void 0) return null; + if (typeof boundary === "object" && !Array.isArray(boundary)) { + const fixed2 = this._getFiniteNumber(boundary.fixed); + if (Number.isFinite(fixed2)) { + return this._toScalePct(fixed2, minValue, maxValue); + } + if (Number.isFinite(boundary.percent)) { + return boundary.percent; + } + return null; + } + const percent = this._parsePercentLiteral(boundary); + if (Number.isFinite(percent)) { + return percent; + } + const fixed = this._getFiniteNumber(boundary); + return Number.isFinite(fixed) ? this._toScalePct(fixed, minValue, maxValue) : null; + } + _getEffectiveFillStyle(ecfg) { + var _a, _b, _c, _d; + return (_d = (_c = (_a = ecfg == null ? void 0 : ecfg.bar) == null ? void 0 : _a.fill_style) != null ? _c : this._colorModeToFillStyle((_b = ecfg == null ? void 0 : ecfg.bar) == null ? void 0 : _b.color_mode)) != null ? _d : "bands"; + } + _segmentsNeedBoundaryResolution(segments) { + return Array.isArray(segments) && segments.some((segment) => (segment == null ? void 0 : segment.from) && typeof segment.from === "object" && !Array.isArray(segment.from) || (segment == null ? void 0 : segment.to) && typeof segment.to === "object" && !Array.isArray(segment.to)); + } + _getSegmentsForRendering(ecfg, minValue = 0, maxValue = 100) { + var _a, _b; + const safeMin = Number.isFinite(minValue) ? minValue : 0; + const safeMax = Number.isFinite(maxValue) ? maxValue : 100; + const rawSegments = Array.isArray((_a = ecfg.bar) == null ? void 0 : _a.segments) ? ecfg.bar.segments : []; + if (((_b = ecfg.bar) == null ? void 0 : _b.segment_space) === "scale" || this._segmentsNeedBoundaryResolution(rawSegments)) { + const resolvedSegments = rawSegments.map((segment) => { + var _a2; + return { + from: this._resolveSegmentBoundaryPct(segment.from, safeMin, safeMax), + to: this._resolveSegmentBoundaryPct(segment.to, safeMin, safeMax), + color: segment.color, + label: (_a2 = segment.label) != null ? _a2 : null + }; + }).filter((segment) => Number.isFinite(segment.from) && segment.color); + return this.inferSegmentEndValues(resolvedSegments, 100).filter((segment) => Number.isFinite(segment.from) && Number.isFinite(segment.to) && segment.color); + } + return this.inferSegmentEndValues(rawSegments, 100).filter((segment) => Number.isFinite(segment.from) && Number.isFinite(segment.to) && segment.color); + } + _getColor(pct, ecfg, minValue = 0, maxValue = 100) { + const fillStyle = this._getEffectiveFillStyle(ecfg); + if (fillStyle === "solid") return ecfg.bar.color; + if (fillStyle === "gradient" || fillStyle === "band_gradient" || fillStyle === "soft_bands") { + let stops; + if (fillStyle === "band_gradient") { + stops = this._getSeverityInterpolationStops(ecfg, minValue, maxValue); + } else if (fillStyle === "soft_bands") { + stops = this._getSoftBandGradientStops(ecfg, minValue, maxValue).map((stop) => { + const rgb = this._hexToRgb(stop.color); + return rgb ? { p: stop.p, ...rgb } : null; + }).filter(Boolean); + } else if (ecfg.bar.gradient_stops && ecfg.bar.gradient_stops.length >= 2) { + stops = ecfg.bar.gradient_stops.map((s) => { + const hex = s.color.replace("#", ""); + const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex; + return { p: s.pos, r: parseInt(full.slice(0, 2), 16), g: parseInt(full.slice(2, 4), 16), b: parseInt(full.slice(4, 6), 16) }; + }); + stops.sort((a, b) => a.p - b.p); + } else { + stops = [ + { p: 0, r: 76, g: 175, b: 80 }, + { p: 50, r: 255, g: 152, b: 0 }, + { p: 100, r: 244, g: 67, b: 54 } + ]; + } + if (!stops || !stops.length) return ecfg.bar.color; + let lo = stops[0], hi = stops[stops.length - 1]; + for (let i = 0; i < stops.length - 1; i++) { + if (pct >= stops[i].p && pct <= stops[i + 1].p) { + lo = stops[i]; + hi = stops[i + 1]; + break; + } + } + const t = lo.p === hi.p ? 0 : (pct - lo.p) / (hi.p - lo.p); + return `rgb(${Math.round(lo.r + t * (hi.r - lo.r))},${Math.round(lo.g + t * (hi.g - lo.g))},${Math.round(lo.b + t * (hi.b - lo.b))})`; + } + for (const s of this._getSegmentsForRendering(ecfg, minValue, maxValue)) { + if (pct >= s.from && pct <= s.to) return s.color; + } + return ecfg.bar.color; + } + _buildFullScaleGradientStyle(stops) { + if (!Array.isArray(stops) || !stops.length) return null; + const cssStops = stops.map((stop) => { + var _a; + const cssColor = (_a = stop.color) != null ? _a : this._rgbToCss(stop); + return cssColor ? `${cssColor} ${stop.p}%` : null; + }).filter(Boolean); + if (!cssStops.length) return null; + return `background:linear-gradient(to right,${cssStops.join(",")});background-repeat:no-repeat;`; + } + _getGradientInterpolationStops(ecfg, minValue = 0, maxValue = 100) { + const fillStyle = this._getEffectiveFillStyle(ecfg); + if (fillStyle === "band_gradient") { + return this._getSeverityInterpolationStops(ecfg, minValue, maxValue); + } + if (fillStyle === "soft_bands") { + return this._getSoftBandGradientStops(ecfg, minValue, maxValue).map((stop) => { + const rgb = this._hexToRgb(stop.color); + return rgb ? { p: stop.p, ...rgb } : null; + }).filter(Boolean).sort((a, b) => a.p - b.p); + } + if (ecfg.bar.gradient_stops && ecfg.bar.gradient_stops.length >= 2) { + return ecfg.bar.gradient_stops.map((s) => { + const rgb = this._hexToRgb(s.color); + return rgb ? { p: s.pos, ...rgb } : null; + }).filter(Boolean).sort((a, b) => a.p - b.p); + } + return [ + { p: 0, r: 76, g: 175, b: 80 }, + { p: 50, r: 255, g: 152, b: 0 }, + { p: 100, r: 244, g: 67, b: 54 } + ]; + } + _rgbToCss(rgb) { + if (!rgb) return null; + return `rgb(${rgb.r},${rgb.g},${rgb.b})`; + } + _buildSolidGradientStyle(color) { + return `linear-gradient(to right,${color} 0%,${color} 100%)`; + } + _getBasePaintGradient(color, ecfg, minValue = 0, maxValue = 100) { + var _a; + const fillStyle = this._getEffectiveFillStyle(ecfg); + if (ecfg.bar.solid_fill) { + return this._buildSolidGradientStyle(color); + } + if (fillStyle === "bands") { + return this._getSeverityBandGradientCss(ecfg, minValue, maxValue); + } + if (fillStyle === "soft_bands") { + return this._getSoftBandGradientCss(ecfg, minValue, maxValue); + } + if (fillStyle === "gradient" || fillStyle === "band_gradient") { + const stops = this._getGradientInterpolationStops(ecfg, minValue, maxValue); + return (_a = this._buildFullScaleGradientStyle(stops)) == null ? void 0 : _a.replace(/^background:/, "").replace(/;background-repeat:no-repeat;$/, ""); + } + return this._buildSolidGradientStyle(color); + } + _getOverlayGradient(startPct, endPct, color) { + if (!color) return null; + const start = Math.min(100, Math.max(0, startPct)); + const end = Math.min(100, Math.max(0, endPct)); + if (end <= start) return null; + return `linear-gradient(to right,transparent 0%,transparent ${start}%,${color} ${start}%,${color} ${end}%,transparent ${end}%,transparent 100%)`; + } + _toScalePct(value, minValue, maxValue) { + if (!Number.isFinite(value)) return null; + const safeMin = Number.isFinite(minValue) ? minValue : 0; + const safeMax = Number.isFinite(maxValue) ? maxValue : 100; + const range = safeMax - safeMin || 1; + return Math.min(100, Math.max(0, (value - safeMin) / range * 100)); + } + _resolveBaselinePct(ecfg, safeMin, safeMax) { + var _a, _b; + if (((_a = ecfg.baseline) == null ? void 0 : _a.enabled) === false) return null; + const baselineValue = this._getNormalizedResolvableNumericValue((_b = ecfg.baseline) == null ? void 0 : _b.at, safeMin, safeMax); + if (!Number.isFinite(baselineValue)) return null; + return this._toScalePct(baselineValue, safeMin, safeMax); + } + _formatNumericDisplay(rawVal, decimal = null) { + if (!Number.isFinite(rawVal)) return String(rawVal); + if (decimal !== null) { + return parseFloat(rawVal.toFixed(decimal)).toLocaleString(); + } + return rawVal.toLocaleString(); + } + _getNormalizedPercent(valuePct, baselinePct = null) { + const clampedValue = Math.min(100, Math.max(0, valuePct)); + if (!Number.isFinite(baselinePct)) { + return { + usesBaseline: false, + start: 0, + end: clampedValue, + positive: true, + baseline: null, + hidden: clampedValue <= 0 + }; + } + const clampedBaseline = Math.min(100, Math.max(0, baselinePct)); + return { + usesBaseline: true, + start: Math.min(clampedValue, clampedBaseline), + end: Math.max(clampedValue, clampedBaseline), + positive: clampedValue >= clampedBaseline, + baseline: clampedBaseline, + hidden: clampedValue === clampedBaseline + }; + } + _getEndpointSemantics(geometry) { + if (geometry == null ? void 0 : geometry.endpointSemantics) { + return geometry.endpointSemantics; + } + if (!(geometry == null ? void 0 : geometry.usesBaseline)) { + return { + left: "scale", + right: "value" + }; + } + return geometry.positive ? { left: "baseline", right: "value" } : { left: "value", right: "baseline" }; + } + _getRevealCornerRadii(geometry) { + const endpoints = this._getEndpointSemantics(geometry); + const isRounded = (endpointType) => endpointType === "value" || endpointType === "range" || endpointType === "scale"; + const leftRadius = isRounded(endpoints.left) ? "6px" : "0"; + const rightRadius = isRounded(endpoints.right) ? "6px" : "0"; + return `${leftRadius} ${rightRadius} ${rightRadius} ${leftRadius}`; + } + _getAboveTargetOverlayInterval(targetPct = null) { + if (!Number.isFinite(targetPct)) return null; + const start = Math.min(100, Math.max(0, targetPct)); + if (start >= 100) return null; + return { + start, + end: 100 + }; + } + _getAboveTargetLayerGeometry(targetPct = null) { + const interval = this._getAboveTargetOverlayInterval(targetPct); + if (!interval) return null; + return { + start: interval.start, + end: interval.end, + hidden: false + }; + } + _getFullScalePaintStyle(ecfg, color, targetPct = null, baselinePct = null, minValue = 0, maxValue = 100) { + var _a, _b, _c, _d, _e, _f; + const layers = []; + const basePaint = this._getBasePaintGradient(color, ecfg, minValue, maxValue); + const clampedBaseline = Number.isFinite(baselinePct) ? Math.min(100, Math.max(0, baselinePct)) : null; + if (Number.isFinite(clampedBaseline)) { + const belowColor = (_c = (_b = (_a = ecfg.baseline) == null ? void 0 : _a.below) == null ? void 0 : _b.color) != null ? _c : null; + const aboveColor = (_f = (_e = (_d = ecfg.baseline) == null ? void 0 : _d.above) == null ? void 0 : _e.color) != null ? _f : null; + const belowOverlay = this._getOverlayGradient(0, clampedBaseline, belowColor); + const aboveOverlay = this._getOverlayGradient(clampedBaseline, 100, aboveColor); + if (belowOverlay) layers.push(belowOverlay); + if (aboveOverlay) layers.push(aboveOverlay); + } + if (basePaint) layers.push(basePaint); + if (!layers.length) return "display:none;"; + return `display:block;inset:0;background-image:${layers.join(",")};background-repeat:no-repeat;background-size:100% 100%;`; + } + _getRevealShapeStyle(geometry, h) { + var _a, _b; + const heightValue = typeof h === "number" ? `${h}px` : h; + const start = Math.min(100, Math.max(0, (_a = geometry == null ? void 0 : geometry.start) != null ? _a : 0)); + const end = Math.min(100, Math.max(0, (_b = geometry == null ? void 0 : geometry.end) != null ? _b : 0)); + if (geometry == null ? void 0 : geometry.hidden) { + return `display:none;height:${heightValue};clip-path:inset(0 100% 0 0 round 0);`; + } + const topInset = "0"; + const rightInset = `${Math.max(0, 100 - end)}%`; + const bottomInset = "0"; + const leftInset = `${start}%`; + const radii = this._getRevealCornerRadii(geometry); + return `display:block;height:${heightValue};clip-path:inset(${topInset} ${rightInset} ${bottomInset} ${leftInset} round ${radii});`; + } + _getStaticLayerRevealStyle(geometry) { + if (!(geometry == null ? void 0 : geometry.hidden) && Number.isFinite(geometry == null ? void 0 : geometry.start) && Number.isFinite(geometry == null ? void 0 : geometry.end) && geometry.end > geometry.start) { + const start = Math.min(100, Math.max(0, geometry.start)); + const end = Math.min(100, Math.max(0, geometry.end)); + return `display:block;clip-path:inset(0 ${Math.max(0, 100 - end)}% 0 ${start}% round 0);`; + } + return "display:none;clip-path:inset(0 100% 0 0 round 0);"; + } + _getFillPaintLayers(geometry, h, ecfg, color, targetPct = null, baselinePct = null, minValue = 0, maxValue = 100) { + var _a, _b; + const basePaintStyle = this._getFullScalePaintStyle(ecfg, color, targetPct, baselinePct, minValue, maxValue); + const baseLayer = { + id: "base", + zIndex: 1, + visible: true, + paintStyle: basePaintStyle, + revealStyle: "display:block;" + }; + const aboveTargetGeometry = this._getAboveTargetLayerGeometry(targetPct); + const aboveTargetLayer = { + id: "above-target", + zIndex: 2, + visible: !!(((_a = ecfg == null ? void 0 : ecfg.bar) == null ? void 0 : _a.above_target_color) && aboveTargetGeometry), + paintStyle: ((_b = ecfg == null ? void 0 : ecfg.bar) == null ? void 0 : _b.above_target_color) ? `display:block;inset:0;background:${ecfg.bar.above_target_color};` : "display:none;", + revealStyle: aboveTargetGeometry ? this._getStaticLayerRevealStyle(aboveTargetGeometry) : this._getStaticLayerRevealStyle({ start: 0, end: 0, hidden: true }) + }; + return [baseLayer, aboveTargetLayer]; + } + _getFillRenderState(pct, h, ecfg, color, targetPct = null, baselinePct = null, minValue = 0, maxValue = 100, needleActive = false) { + var _a, _b; + const geometry = needleActive ? this._getNormalizedPercent(100, null) : this._getNormalizedPercent(pct, baselinePct); + const paintLayers = this._getFillPaintLayers(geometry, h, ecfg, color, targetPct, baselinePct, minValue, maxValue); + return { + geometry, + paintLayers, + paintStyle: (_b = (_a = paintLayers[0]) == null ? void 0 : _a.paintStyle) != null ? _b : "display:none;", + revealStyle: this._getRevealShapeStyle(geometry, h) + }; + } + _getNeedleRenderState(rawValue, ecfg, minValue = 0, maxValue = 100, baselinePct = null) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i; + const needle = (_a = ecfg == null ? void 0 : ecfg.bar) == null ? void 0 : _a.needle; + if (!(needle == null ? void 0 : needle.show)) { + return { + show: false, + pct: null, + color: (_b = needle == null ? void 0 : needle.color) != null ? _b : "#ffffff", + borderColor: this._getNeedleBorderColor((_c = needle == null ? void 0 : needle.color) != null ? _c : "#ffffff"), + edge: "middle" + }; + } + if (Number.isFinite(baselinePct)) { + return { + show: false, + pct: null, + color: (_d = needle.color) != null ? _d : "#ffffff", + borderColor: this._getNeedleBorderColor((_e = needle.color) != null ? _e : "#ffffff"), + edge: "middle" + }; + } + if (!Number.isFinite(rawValue)) { + return { + show: false, + pct: null, + color: (_f = needle.color) != null ? _f : "#ffffff", + borderColor: this._getNeedleBorderColor((_g = needle.color) != null ? _g : "#ffffff"), + edge: "middle" + }; + } + const pct = Math.min(100, Math.max(0, this._toScalePct(rawValue, minValue, maxValue))); + return { + show: true, + pct, + color: (_h = needle.color) != null ? _h : "#ffffff", + borderColor: this._getNeedleBorderColor((_i = needle.color) != null ? _i : "#ffffff"), + edge: pct <= 0 ? "left" : pct >= 100 ? "right" : "middle" + }; + } + _ensureBaseDom() { + if (this._baseDomReady) return; + if (this.shadowRoot.querySelector("ha-card")) { + this._baseDomReady = true; + return; + } + this.shadowRoot.innerHTML = ` + + + +
    + +
    +
    +
    +
    + `; + this._baseDomReady = true; + } + _render() { + const cfg = this._config; + this._ensureBaseDom(); + const titleEl = this.shadowRoot.querySelector(".card-title"); + if (titleEl) { + if (cfg.title) { + titleEl.textContent = cfg.title; + titleEl.style.display = ""; + } else { + titleEl.textContent = ""; + titleEl.style.display = "none"; + } + } + this._disconnectResizeObserver(); + this._resizeObserver = new ResizeObserver(() => { + this._applyCompactTier(); + this._schedulePostLayoutDensityPass(); + }); + const surface = this.shadowRoot.querySelector("ha-card"); + const card = this.shadowRoot.querySelector(".card"); + if (surface && card) { + this._applyCompactTier(); + this._resizeObserver.observe(surface); + this._resizeObserver.observe(this); + } + this._update(); + this._schedulePostLayoutDensityPass(); + } + _disconnectResizeObserver() { + if (!this._resizeObserver) return; + this._resizeObserver.disconnect(); + this._resizeObserver = null; + } + _isReliableWidth(width, minWidth = 16) { + return Number.isFinite(width) && width >= minWidth; + } + _getHeroLabelReservedWidth(headerEl, labelEl, labelHidden) { + var _a, _b, _c; + if (labelHidden || !headerEl || !labelEl) return 0; + const headerWidth = Math.floor((_b = (_a = headerEl.getBoundingClientRect) == null ? void 0 : _a.call(headerEl).width) != null ? _b : 0); + const naturalLabelWidth = Math.ceil(labelEl.scrollWidth || ((_c = labelEl.getBoundingClientRect) == null ? void 0 : _c.call(labelEl).width) || 0); + if (!this._isReliableWidth(headerWidth, 8) || !Number.isFinite(naturalLabelWidth) || naturalLabelWidth <= 0) { + return 0; + } + return Math.min(naturalLabelWidth, headerWidth * 0.45); + } + _classifyCompactTier(width, currentTier = "normal") { + if (!this._isReliableWidth(width)) return currentTier || "normal"; + if (width < 180) return "compressed"; + if (width < 220) return "dense"; + if (width < 280) return "tight"; + if (width < 360) return "compact"; + return "normal"; + } + _classifyLeftDensity(width, currentDensity = "normal") { + if (!this._isReliableWidth(width)) return currentDensity || "normal"; + if (width < 170) return "compressed"; + if (width < 210) return "dense"; + if (width < 255) return "tight"; + if (width < 320) return "compact"; + return "normal"; + } + _classifyRowDensity(width, currentDensity = "normal") { + if (!this._isReliableWidth(width)) return currentDensity || "normal"; + if (width < 150) return "compressed"; + if (width < 190) return "dense"; + if (width < 245) return "tight"; + if (width < 300) return "compact"; + return "normal"; + } + _schedulePostLayoutDensityPass() { + if (!this.isConnected) return; + if (this._densityPassScheduled) { + this._densityPassDirty = true; + return; + } + this._densityPassScheduled = true; + this._densityPassFrame = requestAnimationFrame(() => { + var _a, _b; + this._densityPassScheduled = false; + this._densityPassFrame = null; + const runAgain = this._densityPassDirty; + this._densityPassDirty = false; + if (!this.isConnected) return; + const surface = (_a = this.shadowRoot) == null ? void 0 : _a.querySelector("ha-card"); + const width = (_b = surface == null ? void 0 : surface.getBoundingClientRect().width) != null ? _b : 0; + if (!this._isReliableWidth(width)) { + if (this._densityPassRetries < 4) { + this._densityPassRetries += 1; + this._schedulePostLayoutDensityPass(); + } + return; + } + this._densityPassRetries = 0; + this._applyCompactTier(); + this._runPostLayoutPasses(); + if (runAgain) { + this._schedulePostLayoutDensityPass(); + } + }); + } + _applyCompactTier() { + if (!this.shadowRoot) return; + const surface = this.shadowRoot.querySelector("ha-card"); + const card = this.shadowRoot.querySelector(".card"); + if (!surface || !card) return; + const width = surface.getBoundingClientRect().width; + if (!this._isReliableWidth(width)) { + this._schedulePostLayoutDensityPass(); + if (!card.dataset.compact) card.dataset.compact = "normal"; + return; + } + card.dataset.compact = this._classifyCompactTier(width, card.dataset.compact); + } + _applyLeftModeDensity() { + if (!this.shadowRoot) return; + const densities = ["normal", "compact", "tight", "dense", "compressed"]; + this.shadowRoot.querySelectorAll(".main-line.left-mode").forEach((mainLine) => { + const width = mainLine.getBoundingClientRect().width; + if (!this._isReliableWidth(width)) { + this._schedulePostLayoutDensityPass(); + if (!mainLine.dataset.leftDensity) mainLine.dataset.leftDensity = "normal"; + return; + } + let density = this._classifyLeftDensity(width, mainLine.dataset.leftDensity); + const labelText = mainLine.querySelector(".label-left-text"); + const fullLabelWidth = labelText ? labelText.scrollWidth : Number.POSITIVE_INFINITY; + const visibleLabelWidth = labelText ? labelText.clientWidth : Number.POSITIVE_INFINITY; + const labelIsTruncated = labelText ? fullLabelWidth > visibleLabelWidth + 1 : false; + const effectiveLabelWidth = labelIsTruncated ? visibleLabelWidth : fullLabelWidth; + let relaxBy = 0; + if (Number.isFinite(effectiveLabelWidth)) { + if (effectiveLabelWidth <= 72 && width >= 185) relaxBy = 1; + if (effectiveLabelWidth <= 44 && width >= 205) relaxBy = 2; + } + const currentIndex = densities.indexOf(density); + if (currentIndex !== -1 && relaxBy > 0) { + density = densities[Math.max(0, currentIndex - relaxBy)]; + } + mainLine.dataset.leftDensity = density; + }); + } + _applyInsideLabelDensity() { + if (!this.shadowRoot) return; + this.shadowRoot.querySelectorAll(".bar-inner-label").forEach((innerLabel) => { + var _a, _b, _c, _d, _e, _f; + const track = innerLabel.closest(".bar-track"); + const mainLine = innerLabel.closest(".main-line"); + const nameEl = innerLabel.querySelector(".inside-name"); + const valueEl = innerLabel.querySelector(".inside-value"); + if (!track || !nameEl || !valueEl) return; + const trackWidth = track.getBoundingClientRect().width; + const valueDisplay = this._decodeDataAttr(valueEl.dataset.display || valueEl.textContent || ""); + const valueUnit = this._decodeDataAttr(valueEl.dataset.unit || ((_a = valueEl.querySelector(".inside-unit")) == null ? void 0 : _a.textContent) || ""); + const valueWidth = this._measureInsideValueMarkupWidth(valueEl, valueDisplay, valueUnit, false); + const valueOnlyWidth = this._measureInsideValueMarkupWidth(valueEl, valueDisplay, valueUnit, true); + let density = this._classifyInsideDensity(trackWidth, valueWidth); + const rowWidth = typeof (mainLine == null ? void 0 : mainLine.getBoundingClientRect) === "function" ? mainLine.getBoundingClientRect().width : 0; + const rowDensity = this._isReliableWidth(rowWidth) ? this._classifyRowDensity(rowWidth, ((_b = mainLine == null ? void 0 : mainLine.dataset) == null ? void 0 : _b.rowDensity) || "normal") : ((_c = mainLine == null ? void 0 : mainLine.dataset) == null ? void 0 : _c.rowDensity) || "normal"; + const iconWrap = (_e = (_d = mainLine == null ? void 0 : mainLine.querySelector) == null ? void 0 : _d.call(mainLine, ".icon-wrap")) != null ? _e : null; + let hideIcon = rowDensity === "dense" || rowDensity === "compressed"; + const reclaimedWidth = iconWrap ? this._getLeftModeIconWidth(iconWrap, mainLine) + this._getLeftModeGap(mainLine) : 0; + if (!hideIcon && valueWidth > this._getInsideValueVisibleCap(trackWidth, density)) { + hideIcon = true; + } + if (iconWrap && hideIcon) { + density = this._classifyInsideDensity(trackWidth + reclaimedWidth, valueWidth); + } + const effectiveTrackWidth = trackWidth + (hideIcon ? reclaimedWidth : 0); + let hideName = false; + if (rowDensity === "compressed") { + density = "compressed"; + hideIcon = true; + } + const rawValueCap = this._getInsideValueVisibleCap(trackWidth, density); + const innerPadding = this._getNumericStyleValue(innerLabel, "padding-left", 0) + this._getNumericStyleValue(innerLabel, "padding-right", 0); + const valueCap = Math.max(0, rawValueCap - innerPadding); + const hideUnit = !!valueUnit && valueWidth > valueCap; + const hideValue = valueOnlyWidth > valueCap; + const reservedValueWidth = hideValue ? 0 : Math.ceil(hideUnit ? valueOnlyWidth : valueWidth); + const labelGap = !hideValue ? this._getNumericStyleValue(innerLabel, "gap", 0) : 0; + if (!hideValue) { + const nameText = (nameEl.textContent || "").trim(); + const nameFullWidth = nameEl.scrollWidth || ((_f = nameEl.getBoundingClientRect) == null ? void 0 : _f.call(nameEl).width) || this._measureTextWidthWithStyles(nameEl, nameText); + if (!nameText || !Number.isFinite(nameFullWidth) || nameFullWidth <= 0) { + hideName = density === "dense" || density === "compressed"; + } else { + const usefulNameWidth = this._getInsideUsefulNameWidth(nameEl, nameText, nameFullWidth); + const availableNameWidth = Math.max( + 0, + trackWidth - innerPadding - reservedValueWidth - labelGap + ); + const visibleChars = this._measureVisibleLabelCharacters(nameEl, nameText, availableNameWidth); + const minUsefulChars = Math.min(4, nameText.length); + const nameNeedsTruncation = nameFullWidth > availableNameWidth + 1; + const hasUsefulRoom = availableNameWidth >= usefulNameWidth && (!nameNeedsTruncation || visibleChars >= minUsefulChars); + hideName = !hasUsefulRoom; + } + } + if (hideName && density === "normal") density = "dense"; + innerLabel.dataset.insideDensity = density; + innerLabel.dataset.hideName = hideName ? "true" : "false"; + valueEl.dataset.hideUnit = hideUnit ? "true" : "false"; + valueEl.dataset.hideValue = hideValue ? "true" : "false"; + if (mainLine) { + mainLine.dataset.hideInsideIcon = hideIcon ? "true" : "false"; + } + }); + } + _getInsideValueVisibleCap(trackWidth, density) { + if (density === "dense" || density === "compressed") { + return trackWidth; + } + return trackWidth * 0.56; + } + _classifyInsideDensity(trackWidth, valueWidth) { + if (trackWidth < Math.max(72, valueWidth + 12)) return "compressed"; + if (trackWidth < valueWidth + 56) return "dense"; + if (trackWidth < valueWidth + 92) return "tight"; + if (trackWidth < valueWidth + 128) return "compact"; + return "normal"; + } + _applyRowDensity() { + if (!this.shadowRoot) return; + this.shadowRoot.querySelectorAll(".main-line").forEach((mainLine) => { + const width = mainLine.getBoundingClientRect().width; + if (!this._isReliableWidth(width)) { + this._schedulePostLayoutDensityPass(); + if (!mainLine.dataset.rowDensity) mainLine.dataset.rowDensity = "normal"; + return; + } + mainLine.dataset.rowDensity = this._classifyRowDensity(width, mainLine.dataset.rowDensity); + }); + } + _applyAboveLabelDensity() { + if (!this.shadowRoot) return; + this.shadowRoot.querySelectorAll(".above-line, .hero-line").forEach((aboveLine) => { + var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t; + const label = aboveLine.querySelector(".above-bar-label, .hero-header"); + if (!label) return; + const isHeroLine = aboveLine.classList.contains("hero-line"); + const width = isHeroLine ? this._getHeroDensityWidth(aboveLine) : label.getBoundingClientRect().width; + let density = "normal"; + if (width < 90) density = "xs"; + else if (width < 110) density = "compressed"; + else if (width < 150) density = "dense"; + else if (width < 210) density = "tight"; + else if (width < 280) density = "compact"; + if (isHeroLine) { + aboveLine.dataset.heroDensity = density; + label.dataset.hideName = density === "dense" || density === "compressed" || density === "xs" ? "true" : "false"; + aboveLine.dataset.hideHeroIcon = density === "compressed" || density === "xs" ? "true" : "false"; + return; + } + aboveLine.dataset.aboveDensity = density; + const labelText = typeof label.querySelector === "function" ? label.querySelector(".above-bar-label-name") : null; + const valueEl = typeof label.querySelector === "function" ? label.querySelector(".above-bar-label-value") : null; + const display = this._decodeDataAttr(((_a = valueEl == null ? void 0 : valueEl.dataset) == null ? void 0 : _a.display) || ""); + const unit = this._decodeDataAttr(((_b = valueEl == null ? void 0 : valueEl.dataset) == null ? void 0 : _b.unit) || ""); + const row = (_c = aboveLine.closest) == null ? void 0 : _c.call(aboveLine, ".row"); + const mainLine = (_d = row == null ? void 0 : row.querySelector) == null ? void 0 : _d.call(row, ".main-line.above-mode"); + const rowDensity = ((_e = mainLine == null ? void 0 : mainLine.dataset) == null ? void 0 : _e.rowDensity) || "normal"; + const iconWrap = (_g = (_f = mainLine == null ? void 0 : mainLine.querySelector) == null ? void 0 : _f.call(mainLine, ".icon-wrap")) != null ? _g : null; + const iconRect = (_h = iconWrap == null ? void 0 : iconWrap.getBoundingClientRect) == null ? void 0 : _h.call(iconWrap); + const iconStyle = iconWrap && typeof getComputedStyle === "function" ? getComputedStyle(iconWrap) : null; + const mainIconVisible = !!iconWrap && rowDensity !== "compressed" && (iconStyle == null ? void 0 : iconStyle.display) !== "none" && ((_i = iconRect == null ? void 0 : iconRect.width) != null ? _i : 0) > 0 && ((_j = iconRect == null ? void 0 : iconRect.height) != null ? _j : 0) > 0; + let hideName = density === "dense" || density === "compressed"; + let hideSpacer = !mainIconVisible; + if (labelText && valueEl && display) { + const rowStack = (_k = aboveLine.closest) == null ? void 0 : _k.call(aboveLine, ".row-stack"); + const lineWidth = (_o = (_n = (_l = rowStack == null ? void 0 : rowStack.getBoundingClientRect) == null ? void 0 : _l.call(rowStack).width) != null ? _n : (_m = aboveLine.getBoundingClientRect) == null ? void 0 : _m.call(aboveLine).width) != null ? _o : width; + const spacerEl = typeof aboveLine.querySelector === "function" ? aboveLine.querySelector(".above-icon-spacer") : null; + const spacerWidth = (_q = (_p = spacerEl == null ? void 0 : spacerEl.getBoundingClientRect) == null ? void 0 : _p.call(spacerEl).width) != null ? _q : 0; + const lineGap = spacerEl ? this._getNumericStyleValue(aboveLine, "gap", 0) : 0; + const nameWidth = (_t = (_s = (_r = labelText.getBoundingClientRect) == null ? void 0 : _r.call(labelText).width) != null ? _s : labelText.clientWidth) != null ? _t : 0; + const labelGap = this._getNumericStyleValue(label, "gap", this._getLeftModeGap(aboveLine)); + const fullValueWidth = Math.ceil(this._measureValueMarkupWidth(valueEl, display, unit, false) + 2); + const valueOnlyWidth = Math.ceil(this._measureValueMarkupWidth(valueEl, display, unit, true) + 2); + const text = (labelText.textContent || "").trim(); + const visibleWidth = labelText.clientWidth; + const fullWidth = labelText.scrollWidth; + const visibleChars = this._measureVisibleLabelCharacters(labelText, text, visibleWidth); + const labelIsUnhelpful = this._shouldHideLeftLabel(text, fullWidth, visibleWidth, visibleChars); + const spacerReserve = mainIconVisible && spacerWidth > 0 ? spacerWidth + lineGap : 0; + const availableWithName = Math.max(0, lineWidth - spacerReserve - nameWidth - labelGap); + const availableValueOnly = Math.max(0, lineWidth); + hideName = labelIsUnhelpful; + if (!hideName && fullValueWidth <= availableWithName) { + hideSpacer = !mainIconVisible; + } else if (!hideName && valueOnlyWidth <= availableWithName) { + hideSpacer = !mainIconVisible; + } else { + hideName = true; + hideSpacer = true; + } + const availableWidth = hideName ? availableValueOnly : availableWithName; + const hideUnit = !!unit && fullValueWidth > availableWidth; + valueEl.dataset.hideUnit = hideUnit ? "true" : "false"; + } else if (valueEl) { + valueEl.dataset.hideUnit = "false"; + } + label.dataset.hideName = hideName ? "true" : "false"; + aboveLine.dataset.hideAboveIcon = hideSpacer ? "true" : "false"; + }); + } + _getHeroDensityWidth(heroLine) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i; + const row = (_a = heroLine == null ? void 0 : heroLine.closest) == null ? void 0 : _a.call(heroLine, ".row"); + const rowStack = (_b = heroLine == null ? void 0 : heroLine.closest) == null ? void 0 : _b.call(heroLine, ".row-stack"); + const mainLine = (_c = rowStack == null ? void 0 : rowStack.querySelector) == null ? void 0 : _c.call(rowStack, ".main-line.hero-mode"); + const barWrap = (_d = mainLine == null ? void 0 : mainLine.querySelector) == null ? void 0 : _d.call(mainLine, ".bar-wrap"); + const candidates = [ + (_e = row == null ? void 0 : row.getBoundingClientRect) == null ? void 0 : _e.call(row).width, + (_f = rowStack == null ? void 0 : rowStack.getBoundingClientRect) == null ? void 0 : _f.call(rowStack).width, + (_g = mainLine == null ? void 0 : mainLine.getBoundingClientRect) == null ? void 0 : _g.call(mainLine).width, + (_h = barWrap == null ? void 0 : barWrap.getBoundingClientRect) == null ? void 0 : _h.call(barWrap).width, + (_i = heroLine == null ? void 0 : heroLine.getBoundingClientRect) == null ? void 0 : _i.call(heroLine).width + ]; + for (const width of candidates) { + if (this._isReliableWidth(width, 8)) return width; + } + return 0; + } + _measureHeroValueWidth(heroLine, valueEl, valueFit = "normal", hideUnit = false) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l; + const layer = (_a = this.shadowRoot) == null ? void 0 : _a.querySelector(".measure-layer"); + if (!layer || !heroLine || !valueEl) return 0; + const display = this._decodeDataAttr(valueEl.dataset.display || valueEl.textContent || ""); + const unit = this._decodeDataAttr(valueEl.dataset.unit || ""); + if (!display) return 0; + const wrapper = document.createElement("div"); + wrapper.className = "hero-line"; + wrapper.dataset.heroSize = heroLine.dataset.heroSize || "medium"; + wrapper.dataset.heroDensity = heroLine.dataset.heroDensity || "normal"; + wrapper.dataset.heroValueFit = valueFit; + wrapper.dataset.hideHeroUnit = hideUnit ? "true" : "false"; + wrapper.style.display = "inline-block"; + const measureValue = document.createElement("span"); + measureValue.className = "hero-value"; + measureValue.dataset.display = valueEl.dataset.display || ""; + measureValue.dataset.unit = valueEl.dataset.unit || ""; + measureValue.innerHTML = this._formatRightValueMarkup(display, unit, hideUnit); + measureValue.style.display = "inline-flex"; + measureValue.style.flex = "0 0 auto"; + measureValue.style.width = "auto"; + measureValue.style.minWidth = "0"; + measureValue.style.maxWidth = "none"; + measureValue.style.justifyContent = "flex-start"; + measureValue.style.justifySelf = "start"; + measureValue.style.overflow = "visible"; + const text = measureValue.querySelector(".value-right-text"); + if (text) { + text.style.display = "inline-flex"; + text.style.flex = "0 0 auto"; + text.style.width = "auto"; + text.style.minWidth = "0"; + text.style.maxWidth = "none"; + text.style.justifyContent = "flex-start"; + text.style.overflow = "visible"; + } + const number = measureValue.querySelector(".value-right-number"); + if (number) { + number.style.flex = "0 0 auto"; + number.style.minWidth = "0"; + number.style.overflow = "visible"; + number.style.textOverflow = "clip"; + } + const unitGroup = measureValue.querySelector(".unit-group"); + if (unitGroup) { + unitGroup.style.flex = "0 0 auto"; + unitGroup.style.minWidth = "0"; + unitGroup.style.overflow = "visible"; + } + wrapper.appendChild(measureValue); + layer.replaceChildren(wrapper); + return Math.max( + Math.ceil((_c = (_b = measureValue.getBoundingClientRect) == null ? void 0 : _b.call(measureValue).width) != null ? _c : 0), + Math.ceil(measureValue.scrollWidth || 0), + Math.ceil((_f = (_e = (_d = text == null ? void 0 : text.getBoundingClientRect) == null ? void 0 : _d.call(text).width) != null ? _e : text == null ? void 0 : text.scrollWidth) != null ? _f : 0), + Math.ceil((_i = (_h = (_g = number == null ? void 0 : number.getBoundingClientRect) == null ? void 0 : _g.call(number).width) != null ? _h : number == null ? void 0 : number.scrollWidth) != null ? _i : 0), + Math.ceil((_l = (_k = (_j = unitGroup == null ? void 0 : unitGroup.getBoundingClientRect) == null ? void 0 : _j.call(unitGroup).width) != null ? _k : unitGroup == null ? void 0 : unitGroup.scrollWidth) != null ? _l : 0) + ); + } + _applyHeroValueFit() { + if (!this.shadowRoot) return; + this.shadowRoot.querySelectorAll(".hero-line").forEach((heroLine) => { + var _a, _b; + const headerEl = heroLine.querySelector(".hero-header"); + const labelEl = heroLine.querySelector(".hero-label"); + const valueEl = heroLine.querySelector(".hero-value"); + const unitGroup = heroLine.querySelector(".unit-group"); + if (!headerEl || !valueEl) return; + heroLine.dataset.hideHeroUnit = "false"; + heroLine.dataset.heroValueFit = "normal"; + delete headerEl.dataset.priorityHideName; + const headerWidth = Math.floor((_b = (_a = headerEl.getBoundingClientRect) == null ? void 0 : _a.call(headerEl).width) != null ? _b : 0); + if (!this._isReliableWidth(headerWidth, 8)) { + this._schedulePostLayoutDensityPass(); + return; + } + const hasUnit = !!unitGroup && unitGroup.textContent.trim().length > 0; + const labelHiddenByDensity = headerEl.dataset.hideName === "true"; + const visibleLabelGap = !labelHiddenByDensity && labelEl ? this._getNumericStyleValue(headerEl, "column-gap", 0) : 0; + const valueWidth = (valueFit = "normal", hideUnit = false) => this._measureHeroValueWidth(heroLine, valueEl, valueFit, hideUnit); + const availableWithLabel = Math.max(0, headerWidth - visibleLabelGap - 4); + const availableWithoutLabel = Math.max(0, headerWidth - 4); + if (!this._isReliableWidth(availableWithoutLabel, 8)) { + heroLine.dataset.hideHeroUnit = hasUnit ? "true" : "false"; + heroLine.dataset.heroValueFit = "minimum"; + this._schedulePostLayoutDensityPass(); + return; + } + if (valueWidth("normal", false) <= availableWithLabel) return; + if (!labelHiddenByDensity && labelEl) { + headerEl.dataset.priorityHideName = "true"; + if (valueWidth("normal", false) <= availableWithoutLabel) return; + } + if (hasUnit) { + heroLine.dataset.hideHeroUnit = "true"; + if (valueWidth("normal", true) <= availableWithoutLabel) return; + } + heroLine.dataset.heroValueFit = "tight"; + if (valueWidth("tight", hasUnit) <= availableWithoutLabel) return; + heroLine.dataset.heroValueFit = "minimum"; + if (valueWidth("minimum", hasUnit) <= availableWithoutLabel) return; + heroLine.dataset.heroValueFit = "hidden"; + }); + } + _measureValueMarkupWidth(valueEl, display, unit, hideUnit) { + var _a; + const layer = (_a = this.shadowRoot) == null ? void 0 : _a.querySelector(".measure-layer"); + if (!layer || !valueEl) return 0; + const clone = valueEl.cloneNode(false); + clone.removeAttribute("data-hide-unit"); + clone.style.removeProperty("--sbcp-value-extra-width"); + clone.style.width = "auto"; + clone.style.minWidth = "0"; + clone.style.maxWidth = "none"; + clone.style.flex = "0 0 auto"; + clone.innerHTML = this._formatRightValueMarkup(display, unit, hideUnit); + layer.replaceChildren(clone); + return clone.scrollWidth; + } + _measureInsideValueMarkupWidth(valueEl, display, unit, hideUnit = false) { + var _a; + const layer = (_a = this.shadowRoot) == null ? void 0 : _a.querySelector(".measure-layer"); + if (!layer || !valueEl) return (valueEl == null ? void 0 : valueEl.scrollWidth) || 0; + const clone = valueEl.cloneNode(false); + clone.style.width = "auto"; + clone.style.minWidth = "0"; + clone.style.maxWidth = "none"; + clone.style.flex = "0 0 auto"; + clone.style.overflow = "visible"; + clone.style.textOverflow = "clip"; + clone.style.whiteSpace = "nowrap"; + clone.innerHTML = this._formatInsideValueMarkup(display, unit, hideUnit); + layer.replaceChildren(clone); + let extraWidth = 0; + try { + const style = getComputedStyle(valueEl); + extraWidth += parseFloat(style.paddingLeft || "0") || 0; + extraWidth += parseFloat(style.paddingRight || "0") || 0; + extraWidth += parseFloat(style.borderLeftWidth || "0") || 0; + extraWidth += parseFloat(style.borderRightWidth || "0") || 0; + } catch (_err) { + } + return clone.getBoundingClientRect().width + extraWidth; + } + _measureTextWidthWithStyles(sourceEl, text) { + var _a; + const layer = (_a = this.shadowRoot) == null ? void 0 : _a.querySelector(".measure-layer"); + if (!layer || !sourceEl) return 0; + const clone = sourceEl.cloneNode(false); + clone.textContent = text; + clone.style.width = "auto"; + clone.style.minWidth = "0"; + clone.style.maxWidth = "none"; + clone.style.flex = "0 0 auto"; + clone.style.overflow = "visible"; + clone.style.textOverflow = "clip"; + clone.style.whiteSpace = "nowrap"; + layer.replaceChildren(clone); + return clone.scrollWidth; + } + _measureVisibleLabelCharacters(labelTextEl, text, visibleWidth) { + if (!labelTextEl || !text || !Number.isFinite(visibleWidth) || visibleWidth <= 0) return 0; + const ellipsisWidth = this._measureTextWidthWithStyles(labelTextEl, "..."); + const availableTextWidth = Math.max(0, visibleWidth - ellipsisWidth); + if (availableTextWidth <= 0) return 0; + let low = 0; + let high = text.length; + while (low < high) { + const mid = Math.ceil((low + high) / 2); + const width = this._measureTextWidthWithStyles(labelTextEl, text.slice(0, mid)); + if (width <= availableTextWidth) { + low = mid; + } else { + high = mid - 1; + } + } + return low; + } + _shouldHideLeftLabel(text, fullWidth, visibleWidth, visibleChars) { + if (!text) return false; + if (!Number.isFinite(fullWidth) || !Number.isFinite(visibleWidth)) return false; + const truncated = fullWidth > visibleWidth + 1; + return truncated && visibleChars < 5; + } + _getInsideUsefulNameWidth(labelTextEl, text, fullWidth = NaN) { + if (!text) return 0; + const naturalWidth = Number.isFinite(fullWidth) && fullWidth > 0 ? fullWidth : this._measureTextWidthWithStyles(labelTextEl, text); + const usefulChars = text.slice(0, Math.min(5, text.length)); + const usefulWidth = this._measureTextWidthWithStyles(labelTextEl, usefulChars) + this._measureTextWidthWithStyles(labelTextEl, "..."); + const minUsefulWidth = Math.max(36, Math.min(44, usefulWidth || 0)); + return Math.min(naturalWidth || minUsefulWidth, minUsefulWidth); + } + _applyValueWidthReservation() { + if (!this.shadowRoot) return; + this.shadowRoot.querySelectorAll(".value-right").forEach((valueEl) => { + var _a, _b, _c; + const display = this._decodeDataAttr(valueEl.dataset.display || ""); + const unit = this._decodeDataAttr(valueEl.dataset.unit || ""); + if (!display) { + valueEl.style.setProperty("--sbcp-value-extra-width", "0px"); + return; + } + const getStyle = typeof globalThis.getComputedStyle === "function" && globalThis.getComputedStyle.bind(globalThis) || typeof window !== "undefined" && typeof window.getComputedStyle === "function" && window.getComputedStyle.bind(window) || ((_c = (_b = (_a = valueEl == null ? void 0 : valueEl.ownerDocument) == null ? void 0 : _a.defaultView) == null ? void 0 : _b.getComputedStyle) == null ? void 0 : _c.bind(valueEl.ownerDocument.defaultView)); + if (!getStyle) return; + const style = getStyle(valueEl); + const baseWidth = parseFloat(style.getPropertyValue("--sbcp-value-width")) || valueEl.clientWidth || 0; + const desiredWidth = Math.ceil(this._measureValueMarkupWidth(valueEl, display, unit, false) + 2); + const extraWidth = Math.max(0, desiredWidth - baseWidth); + valueEl.style.setProperty("--sbcp-value-extra-width", `${extraWidth}px`); + }); + } + _applyValueVisibility() { + if (!this.shadowRoot) return; + this.shadowRoot.querySelectorAll(".value-right, .top-right-value, .above-bar-label-value").forEach((valueEl) => { + var _a, _b, _c, _d; + const display = this._decodeDataAttr(valueEl.dataset.display || ""); + const unit = this._decodeDataAttr(valueEl.dataset.unit || ""); + let hideUnit = valueEl.dataset.hideUnit === "true"; + const isAboveValue = (_a = valueEl.classList) == null ? void 0 : _a.contains("above-bar-label-value"); + if (!isAboveValue && display) { + const availableWidth = (_d = (_c = (_b = valueEl.getBoundingClientRect) == null ? void 0 : _b.call(valueEl).width) != null ? _c : valueEl.clientWidth) != null ? _d : 0; + const fullValueWidth = Math.ceil(this._measureValueMarkupWidth(valueEl, display, unit, false) + 2); + hideUnit = !!unit && availableWidth > 0 ? fullValueWidth > availableWidth : false; + valueEl.dataset.hideUnit = hideUnit ? "true" : "false"; + } + if (valueEl.innerHTML !== this._formatRightValueMarkup(display, unit, hideUnit)) { + valueEl.innerHTML = this._formatRightValueMarkup(display, unit, hideUnit); + } + }); + this.shadowRoot.querySelectorAll(".inside-value").forEach((valueEl) => { + const display = this._decodeDataAttr(valueEl.dataset.display || ""); + const unit = this._decodeDataAttr(valueEl.dataset.unit || ""); + const hideUnit = valueEl.dataset.hideUnit === "true"; + const hideValue = valueEl.dataset.hideValue === "true"; + if (!hideValue && !display) return; + if (!valueEl.dataset.hideUnit) valueEl.dataset.hideUnit = "false"; + if (!valueEl.dataset.hideValue) valueEl.dataset.hideValue = "false"; + const nextMarkup = this._formatInsideValueMarkup(display, unit, hideUnit); + if (valueEl.innerHTML !== nextMarkup) { + valueEl.innerHTML = nextMarkup; + } + }); + } + _getMinimumBarShare() { + return 0.5; + } + _getMinimumBarShareHysteresis() { + return 0.02; + } + _getTopValueEnableShare() { + return this._getMinimumBarShare() - this._getMinimumBarShareHysteresis(); + } + _getTopValueDisableShare() { + return this._getMinimumBarShare() + this._getMinimumBarShareHysteresis(); + } + _getNumericStyleValue(el, propertyName, fallback = 0) { + if (!el) return fallback; + try { + const value = parseFloat(getComputedStyle(el).getPropertyValue(propertyName)); + return Number.isFinite(value) ? value : fallback; + } catch (_err) { + return fallback; + } + } + _getLeftModeGap(mainLine) { + var _a; + const computedGap = this._getNumericStyleValue(mainLine, "gap", NaN); + if (Number.isFinite(computedGap)) return computedGap; + const density = ((_a = mainLine == null ? void 0 : mainLine.dataset) == null ? void 0 : _a.rowDensity) || "normal"; + if (density === "tight") return 7; + if (density === "dense" || density === "compressed") return 6; + return 8; + } + _getLeftModeBarMinWidth(mainLine) { + var _a; + const computedMin = this._getNumericStyleValue(mainLine, "--sbcp-bar-min-width", NaN); + if (Number.isFinite(computedMin)) return computedMin; + const density = ((_a = mainLine == null ? void 0 : mainLine.dataset) == null ? void 0 : _a.leftDensity) || "normal"; + if (density === "compact") return 52; + if (density === "tight") return 48; + if (density === "dense") return 44; + if (density === "compressed") return 40; + return 56; + } + _getLeftModeIconWidth(iconWrap, mainLine) { + var _a, _b; + const measured = (_a = iconWrap == null ? void 0 : iconWrap.getBoundingClientRect) == null ? void 0 : _a.call(iconWrap).width; + if (this._isReliableWidth(measured, 1)) return measured; + const computed = this._getNumericStyleValue(iconWrap || mainLine, "--sbcp-icon-width", NaN); + if (Number.isFinite(computed)) return computed; + const density = ((_b = mainLine == null ? void 0 : mainLine.dataset) == null ? void 0 : _b.leftDensity) || "normal"; + if (density === "compact") return 26; + if (density === "tight") return 24; + if (density === "dense") return 23; + if (density === "compressed") return 22; + return 28; + } + _getReservedInlineValueWidth(valueEl) { + var _a, _b, _c; + if (!valueEl) return 0; + const display = this._decodeDataAttr(valueEl.dataset.display || ""); + const unit = this._decodeDataAttr(valueEl.dataset.unit || ""); + const baseWidth = this._getNumericStyleValue(valueEl, "--sbcp-value-width", valueEl.clientWidth || 0); + const inlineExtra = parseFloat(((_b = (_a = valueEl.style) == null ? void 0 : _a.getPropertyValue) == null ? void 0 : _b.call(_a, "--sbcp-value-extra-width")) || ((_c = valueEl.style) == null ? void 0 : _c["--sbcp-value-extra-width"]) || "0"); + const extraWidth = Number.isFinite(inlineExtra) ? inlineExtra : this._getNumericStyleValue(valueEl, "--sbcp-value-extra-width", 0); + const reservedWidth = Math.max(0, baseWidth + extraWidth); + if (!display) return reservedWidth; + const fullMarkupWidth = Math.ceil(this._measureValueMarkupWidth(valueEl, display, unit, false) + 2); + return Math.max(reservedWidth, fullMarkupWidth); + } + _estimateLeftModeWidthBudget(row) { + var _a, _b, _c, _d, _e, _f; + const mainLine = row == null ? void 0 : row.querySelector(".main-line"); + if (!mainLine) return null; + const rowWidth = (_b = (_a = mainLine.getBoundingClientRect) == null ? void 0 : _a.call(mainLine).width) != null ? _b : 0; + if (!this._isReliableWidth(rowWidth)) return null; + const labelWrap = row.querySelector(".label-left"); + const iconWrap = row.querySelector(".icon-wrap"); + const valueEl = row.querySelector(".value-right"); + const labelMetrics = this._getLabelSacrificeMetrics(row, "left", { rowWidth }); + const labelWidth = ((_c = labelWrap == null ? void 0 : labelWrap.dataset) == null ? void 0 : _c.hidden) === "true" ? 0 : (_f = (_e = (_d = labelWrap == null ? void 0 : labelWrap.getBoundingClientRect) == null ? void 0 : _d.call(labelWrap).width) != null ? _e : labelMetrics == null ? void 0 : labelMetrics.labelWidth) != null ? _f : 0; + const iconWidth = iconWrap ? this._getLeftModeIconWidth(iconWrap, mainLine) : 0; + const valueWidth = this._getReservedInlineValueWidth(valueEl); + const gap = this._getLeftModeGap(mainLine); + const barMinWidth = this._getLeftModeBarMinWidth(mainLine); + const baseLabelVisible = !!labelWrap && labelWrap.dataset.hidden !== "true"; + const labelSacrificial = baseLabelVisible ? this._isLabelWorthSacrificing(row, "left", { rowWidth }) : true; + const hasIcon = !!iconWrap; + return { + rowWidth, + gap, + barMinWidth, + labelWidth: this._isReliableWidth(labelWidth, 0) ? labelWidth : 0, + iconWidth: this._isReliableWidth(iconWidth, 0) ? iconWidth : 0, + valueWidth: this._isReliableWidth(valueWidth, 0) ? valueWidth : 0, + baseLabelVisible, + labelSacrificial, + hasIcon, + mainLine, + labelWrap, + iconWrap, + valueEl, + rowStack: row.querySelector(".row-stack") + }; + } + _predictLeftModeBarShareForState(row, state, budget = null) { + const effectiveBudget = budget || this._estimateLeftModeWidthBudget(row); + if (!effectiveBudget) return null; + const showLabel = effectiveBudget.baseLabelVisible && !state.hideLabel; + const showIcon = effectiveBudget.hasIcon && !state.hideIcon; + const showInlineValue = !state.topValue; + const visibleItems = 1 + (showIcon ? 1 : 0) + (showLabel ? 1 : 0) + (showInlineValue ? 1 : 0); + const gapCount = Math.max(0, visibleItems - 1); + const reservedWidth = (showIcon ? effectiveBudget.iconWidth : 0) + (showLabel ? effectiveBudget.labelWidth : 0) + (showInlineValue ? effectiveBudget.valueWidth : 0) + gapCount * effectiveBudget.gap; + const predictedBarWidth = Math.max(0, effectiveBudget.rowWidth - reservedWidth); + return { + rowWidth: effectiveBudget.rowWidth, + barWidth: predictedBarWidth, + share: predictedBarWidth / effectiveBudget.rowWidth, + showLabel, + showIcon, + showInlineValue, + reservedWidth, + gapCount + }; + } + _getLeftModeCandidateStates(budget) { + const states = [ + { hideLabel: false, topValue: false, hideIcon: false } + ]; + if (budget == null ? void 0 : budget.labelSacrificial) { + states.push({ hideLabel: true, topValue: false, hideIcon: false }); + } + states.push( + { hideLabel: false, topValue: true, hideIcon: false }, + { hideLabel: true, topValue: true, hideIcon: false }, + { hideLabel: true, topValue: true, hideIcon: true } + ); + return states; + } + _chooseFirstPredictedLeftModeState(row, states, threshold, budget) { + for (const state of states) { + const predicted = this._predictLeftModeBarShareForState(row, state, budget); + if (!predicted) continue; + if (predicted.share >= threshold) return { ...state, predicted }; + } + return null; + } + _chooseFallbackPredictedLeftModeState(row, states, budget) { + let fallback = null; + for (const state of states) { + const predicted = this._predictLeftModeBarShareForState(row, state, budget); + if (!predicted) continue; + fallback = { ...state, predicted }; + } + return fallback; + } + _chooseLeftModeResponsiveState(row) { + var _a, _b; + const budget = this._estimateLeftModeWidthBudget(row); + if (!budget) return null; + const minimumBarShare = this._getMinimumBarShare(); + const states = this._getLeftModeCandidateStates(budget); + const previousTopValue = ((_b = (_a = budget.rowStack) == null ? void 0 : _a.dataset) == null ? void 0 : _b.forceTopValue) === "true"; + const inlineStates = states.filter((state) => !state.topValue); + const topStates = states.filter((state) => state.topValue); + const enableShare = this._getTopValueEnableShare(); + const disableShare = this._getTopValueDisableShare(); + const inlineChoice = previousTopValue ? this._chooseFirstPredictedLeftModeState(row, inlineStates, disableShare, budget) : this._chooseFirstPredictedLeftModeState(row, inlineStates, enableShare, budget); + if (inlineChoice) return inlineChoice; + const topChoice = this._chooseFirstPredictedLeftModeState(row, topStates, minimumBarShare, budget) || this._chooseFallbackPredictedLeftModeState(row, topStates, budget); + return topChoice; + } + _applyLeftModeResponsiveState(row, state) { + const mainLine = row == null ? void 0 : row.querySelector(".main-line"); + const rowStack = row == null ? void 0 : row.querySelector(".row-stack"); + const leftLabel = row == null ? void 0 : row.querySelector(".label-left"); + if (!mainLine || !rowStack) return; + delete rowStack.dataset.forceTopValue; + delete mainLine.dataset.hideLeftIcon; + if (leftLabel) delete leftLabel.dataset.priorityHidden; + if ((state == null ? void 0 : state.hideLabel) && leftLabel) { + leftLabel.dataset.priorityHidden = "true"; + } + if (state == null ? void 0 : state.topValue) { + rowStack.dataset.forceTopValue = "true"; + } + if (state == null ? void 0 : state.hideIcon) { + mainLine.dataset.hideLeftIcon = "true"; + } + } + _getMeasuredBarShare(row) { + const mainLine = row == null ? void 0 : row.querySelector(".main-line"); + const track = row == null ? void 0 : row.querySelector(".bar-track"); + if (!mainLine || !track) return null; + const rowWidth = mainLine.getBoundingClientRect().width; + const barWidth = track.getBoundingClientRect().width; + if (!this._isReliableWidth(rowWidth) || !this._isReliableWidth(barWidth, 1)) return null; + return { rowWidth, barWidth, share: barWidth / rowWidth, mainLine, track }; + } + _clearMinimumBarShareOverrides(row) { + const mainLine = row == null ? void 0 : row.querySelector(".main-line"); + const rowStack = row == null ? void 0 : row.querySelector(".row-stack"); + const leftLabel = row == null ? void 0 : row.querySelector(".label-left"); + const aboveLabel = row == null ? void 0 : row.querySelector(".above-bar-label"); + const innerLabel = row == null ? void 0 : row.querySelector(".bar-inner-label"); + const aboveLine = row == null ? void 0 : row.querySelector(".above-line"); + if (rowStack) delete rowStack.dataset.forceTopValue; + if (leftLabel) delete leftLabel.dataset.priorityHidden; + if (aboveLabel) delete aboveLabel.dataset.priorityHideName; + if (innerLabel) delete innerLabel.dataset.priorityHideName; + if (!mainLine) return; + delete mainLine.dataset.hideLeftIcon; + delete mainLine.dataset.hideAboveIcon; + delete mainLine.dataset.priorityHideInsideIcon; + } + _hideMinimumBarShareLabel(row, mode) { + if (mode === "left") { + const leftLabel = row.querySelector(".label-left"); + if (leftLabel) leftLabel.dataset.priorityHidden = "true"; + return; + } + if (mode === "above") { + const aboveLabel = row.querySelector(".above-bar-label"); + if (aboveLabel) aboveLabel.dataset.priorityHideName = "true"; + return; + } + if (mode === "inside") { + const innerLabel = row.querySelector(".bar-inner-label"); + if (innerLabel) innerLabel.dataset.priorityHideName = "true"; + } + } + _forceMinimumBarShareTopValue(row, mode) { + if (mode !== "left") return; + const rowStack = row.querySelector(".row-stack"); + if (rowStack) rowStack.dataset.forceTopValue = "true"; + } + _hideMinimumBarShareIcon(row, mode) { + const mainLine = row.querySelector(".main-line"); + if (!mainLine) return; + if (mode === "left") { + mainLine.dataset.hideLeftIcon = "true"; + return; + } + if (mode === "above") { + mainLine.dataset.hideAboveIcon = "true"; + const aboveLine = row.querySelector(".above-line"); + if (aboveLine) aboveLine.dataset.hideAboveIcon = "true"; + return; + } + if (mode === "inside") { + mainLine.dataset.priorityHideInsideIcon = "true"; + } + } + _getLabelSacrificeMetrics(row, mode, measurement) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j; + const rowWidth = (_d = (_c = measurement == null ? void 0 : measurement.rowWidth) != null ? _c : (_b = (_a = row == null ? void 0 : row.querySelector(".main-line")) == null ? void 0 : _a.getBoundingClientRect) == null ? void 0 : _b.call(_a).width) != null ? _d : 0; + if (!this._isReliableWidth(rowWidth)) return null; + if (mode === "left") { + const labelWrap = row.querySelector(".label-left"); + const labelText = row.querySelector(".label-left-text"); + if (!labelWrap || !labelText) return null; + const text = (labelText.textContent || "").trim(); + const visibleWidth = labelText.clientWidth; + const fullWidth = labelText.scrollWidth; + const visibleChars = this._measureVisibleLabelCharacters(labelText, text, visibleWidth); + const labelWidth = (_f = (_e = labelWrap.getBoundingClientRect) == null ? void 0 : _e.call(labelWrap).width) != null ? _f : visibleWidth; + return { text, visibleWidth, fullWidth, visibleChars, labelWidth, rowWidth }; + } + if (mode === "above") { + const labelText = row.querySelector(".above-bar-label-name"); + if (!labelText) return null; + const text = (labelText.textContent || "").trim(); + const visibleWidth = labelText.clientWidth; + const fullWidth = labelText.scrollWidth; + const visibleChars = this._measureVisibleLabelCharacters(labelText, text, visibleWidth); + const labelWidth = (_h = (_g = labelText.getBoundingClientRect) == null ? void 0 : _g.call(labelText).width) != null ? _h : visibleWidth; + return { text, visibleWidth, fullWidth, visibleChars, labelWidth, rowWidth }; + } + if (mode === "inside") { + const labelText = row.querySelector(".inside-name"); + if (!labelText) return null; + const text = (labelText.textContent || "").trim(); + const visibleWidth = labelText.clientWidth; + const fullWidth = labelText.scrollWidth; + const visibleChars = this._measureVisibleLabelCharacters(labelText, text, visibleWidth); + const labelWidth = (_j = (_i = labelText.getBoundingClientRect) == null ? void 0 : _i.call(labelText).width) != null ? _j : visibleWidth; + return { text, visibleWidth, fullWidth, visibleChars, labelWidth, rowWidth }; + } + return null; + } + _isLabelWorthSacrificing(row, mode, measurement) { + const metrics = this._getLabelSacrificeMetrics(row, mode, measurement); + if (!metrics || !metrics.text) return false; + return this._shouldHideLeftLabel(metrics.text, metrics.fullWidth, metrics.visibleWidth, metrics.visibleChars); + } + _ensureMinimumBarShare(rows = null) { + if (!this.shadowRoot) return; + const targetRows = rows || this.shadowRoot.querySelectorAll(".row[data-entity]"); + const minimumBarShare = this._getMinimumBarShare(); + targetRows.forEach((row) => { + const mainLine = row.querySelector(".main-line"); + if (!mainLine) return; + const mode = mainLine.classList.contains("left-mode") ? "left" : mainLine.classList.contains("above-mode") ? "above" : mainLine.classList.contains("inside-mode") ? "inside" : "other"; + if (mode === "other") return; + if (mode === "left") { + const state = this._chooseLeftModeResponsiveState(row); + if (state) this._applyLeftModeResponsiveState(row, state); + return; + } + this._clearMinimumBarShareOverrides(row); + let measurement = this._getMeasuredBarShare(row); + if (!measurement || measurement.share >= minimumBarShare) return; + if (this._isLabelWorthSacrificing(row, mode, measurement)) { + this._hideMinimumBarShareLabel(row, mode); + measurement = this._getMeasuredBarShare(row); + if (!measurement || measurement.share >= minimumBarShare) return; + } + this._forceMinimumBarShareTopValue(row, mode); + this._applyTopRightValueLayout(); + measurement = this._getMeasuredBarShare(row); + if (!measurement || measurement.share >= minimumBarShare) return; + this._hideMinimumBarShareIcon(row, mode); + }); + } + _shouldUseTopValueRow(mainLine) { + var _a, _b, _c; + if (!((_a = mainLine == null ? void 0 : mainLine.classList) == null ? void 0 : _a.contains("left-mode"))) return false; + return ((_c = (_b = mainLine.closest) == null ? void 0 : _b.call(mainLine, ".row-stack")) == null ? void 0 : _c.dataset.forceTopValue) === "true"; + } + _getAdaptiveDensityForMainLine(mainLine) { + var _a; + if (!mainLine) return "normal"; + if ((_a = mainLine.classList) == null ? void 0 : _a.contains("left-mode")) { + return mainLine.dataset.leftDensity || "normal"; + } + return mainLine.dataset.rowDensity || "normal"; + } + _getAdaptiveDefaultHeightForDensity(density) { + if (density === "compressed") return 24; + if (density === "dense") return 28; + return 38; + } + _getEffectiveRowHeight(baseHeight, heightExplicit, mainLine) { + if (heightExplicit) return this._clampSupportedRowHeight(baseHeight); + return this._clampSupportedRowHeight(this._getAdaptiveDefaultHeightForDensity(this._getAdaptiveDensityForMainLine(mainLine))); + } + _applyAdaptiveRowHeight() { + if (!this.shadowRoot) return; + this.shadowRoot.querySelectorAll(".row[data-entity]").forEach((row) => { + const mainLine = row.querySelector(".main-line"); + const rowStack = row.querySelector(".row-stack"); + if (!mainLine) return; + const baseHeight = parseFloat(row.dataset.baseHeight || "38") || 38; + const explicit = row.dataset.heightExplicit === "true"; + const effectiveHeight = this._getEffectiveRowHeight(baseHeight, explicit, mainLine); + row.style.setProperty("--sbcp-row-height", `${effectiveHeight}px`); + if (rowStack) rowStack.style.setProperty("--sbcp-row-height", `${effectiveHeight}px`); + mainLine.style.height = `${effectiveHeight}px`; + const labelLeft = mainLine.querySelector(".label-left"); + if (labelLeft) labelLeft.style.height = `${effectiveHeight}px`; + const iconWrap = mainLine.querySelector(".icon-wrap"); + if (iconWrap) { + iconWrap.style.height = `${effectiveHeight}px`; + iconWrap.style.minHeight = `${effectiveHeight}px`; + } + const track = mainLine.querySelector(".bar-track"); + if (track) track.style.height = `${effectiveHeight}px`; + const inlineValue = mainLine.querySelector(".value-right"); + if (inlineValue) inlineValue.style.height = `${effectiveHeight}px`; + }); + } + _applyTopRightValueLayout() { + if (!this.shadowRoot) return; + this.shadowRoot.querySelectorAll(".main-line.left-mode").forEach((mainLine) => { + var _a, _b, _c, _d, _e; + const rowStack = mainLine.closest(".row-stack"); + const inlineValue = mainLine.querySelector(".value-right"); + const topValue = rowStack == null ? void 0 : rowStack.querySelector(".top-right-value"); + if (!rowStack || !inlineValue || !topValue) return; + const active = this._shouldUseTopValueRow(mainLine); + rowStack.dataset.topValue = active ? "true" : "false"; + topValue.dataset.active = active ? "true" : "false"; + const display = this._decodeDataAttr(inlineValue.dataset.display || ""); + const unit = this._decodeDataAttr(inlineValue.dataset.unit || ""); + topValue.dataset.display = inlineValue.dataset.display || ""; + topValue.dataset.unit = inlineValue.dataset.unit || ""; + topValue.dataset.hideUnit = "false"; + const availableWidth = (_e = (_d = (_c = (_a = rowStack.getBoundingClientRect) == null ? void 0 : _a.call(rowStack).width) != null ? _c : (_b = topValue.getBoundingClientRect) == null ? void 0 : _b.call(topValue).width) != null ? _d : topValue.clientWidth) != null ? _e : 0; + const fullValueWidth = display ? Math.ceil(this._measureValueMarkupWidth(topValue, display, unit, false) + 2) : 0; + const hideUnit = !!unit && availableWidth > 0 && fullValueWidth > availableWidth; + topValue.dataset.hideUnit = hideUnit ? "true" : "false"; + topValue.innerHTML = this._formatRightValueMarkup(display, unit, hideUnit); + }); + } + _applyLeftLabelUsefulness() { + if (!this.shadowRoot) return; + this.shadowRoot.querySelectorAll(".main-line.left-mode").forEach((mainLine) => { + const labelWrap = mainLine.querySelector(".label-left"); + const labelText = mainLine.querySelector(".label-left-text"); + if (!labelWrap || !labelText) return; + labelWrap.dataset.hidden = "false"; + const text = (labelText.textContent || "").trim(); + const fullWidth = labelText.scrollWidth; + const visibleWidth = labelText.clientWidth; + const visibleChars = this._measureVisibleLabelCharacters(labelText, text, visibleWidth); + const shouldHide = this._shouldHideLeftLabel(text, fullWidth, visibleWidth, visibleChars); + labelWrap.dataset.hidden = shouldHide ? "true" : "false"; + }); + } + _runPostLayoutPasses(rows = null) { + requestAnimationFrame(() => { + this._applyRowDensity(); + this._applyLeftModeDensity(); + this._applyAboveLabelDensity(); + this._applyHeroValueFit(); + this._applyInsideLabelDensity(); + this._applyValueWidthReservation(); + requestAnimationFrame(() => { + var _a; + this._applyAdaptiveRowHeight(); + this._applyValueVisibility(); + this._applyLeftLabelUsefulness(); + this._applyTopRightValueLayout(); + this._ensureMinimumBarShare(rows); + this._applyTopRightValueLayout(); + this._applyLeftLabelUsefulness(); + const targetRows = rows || ((_a = this.shadowRoot) == null ? void 0 : _a.querySelectorAll(".row[data-entity]")) || []; + targetRows.forEach((row) => { + this._positionTargetLabel(row); + }); + }); + }); + } + _isTightUnit(unit) { + return ["h", "m", "s"].includes(String(unit || "").trim()); + } + _encodeDataAttr(value) { + return encodeURIComponent(String(value != null ? value : "")); + } + _decodeDataAttr(value) { + const raw = String(value != null ? value : ""); + try { + return decodeURIComponent(raw); + } catch (_err) { + return raw; + } + } + _parseColorToRgb(color) { + const value = String(color || "").trim(); + if (!value) return null; + const hexMatch = value.match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); + if (hexMatch) { + const hex = hexMatch[1]; + const full = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex; + return { + r: parseInt(full.slice(0, 2), 16), + g: parseInt(full.slice(2, 4), 16), + b: parseInt(full.slice(4, 6), 16) + }; + } + const rgbMatch = value.match(/^rgba?\(([^)]+)\)$/i); + if (rgbMatch) { + const parts = rgbMatch[1].split(",").map((p) => p.trim()); + if (parts.length >= 3) { + return { + r: Math.max(0, Math.min(255, parseFloat(parts[0]))), + g: Math.max(0, Math.min(255, parseFloat(parts[1]))), + b: Math.max(0, Math.min(255, parseFloat(parts[2]))) + }; + } + } + return null; + } + _rgbToHsl({ r, g, b }) { + const rn = r / 255; + const gn = g / 255; + const bn = b / 255; + const max = Math.max(rn, gn, bn); + const min = Math.min(rn, gn, bn); + const l = (max + min) / 2; + if (max === min) { + return { h: 0, s: 0, l: l * 100 }; + } + const d = max - min; + const s = l > 0.5 ? d / (2 - max - min) : d / (max + min); + let h; + switch (max) { + case rn: + h = ((gn - bn) / d + (gn < bn ? 6 : 0)) / 6; + break; + case gn: + h = ((bn - rn) / d + 2) / 6; + break; + default: + h = ((rn - gn) / d + 4) / 6; + break; + } + return { h: h * 360, s: s * 100, l: l * 100 }; + } + _getMarkerContrastColor(color) { + const rgb = this._parseColorToRgb(color); + if (!rgb) return "#f3f4f6"; + const { h, s, l } = this._rgbToHsl(rgb); + const contrastL = Math.abs(l - 90) >= Math.abs(l - 10) ? 90 : 10; + const contrastS = Math.max(40, Math.min(100, s)); + return `hsl(${Math.round(h)} ${Math.round(contrastS)}% ${Math.round(contrastL)}%)`; + } + _getNeedleBorderColor(color) { + const rgb = this._parseColorToRgb(color); + if (!rgb) return "#000000"; + const toLinear = (channel) => { + const srgb = channel / 255; + return srgb <= 0.04045 ? srgb / 12.92 : ((srgb + 0.055) / 1.055) ** 2.4; + }; + const luminance = 0.2126 * toLinear(rgb.r) + 0.7152 * toLinear(rgb.g) + 0.0722 * toLinear(rgb.b); + return luminance < 0.22 ? "#ffffff" : "#000000"; + } + _formatDisplayWithUnit(display, unit) { + if (!unit) return String(display); + const cleanUnit = String(unit); + return `${display}${this._isTightUnit(cleanUnit) ? "" : " "}${cleanUnit}`; + } + _formatRightValueMarkup(display, unit, hideUnit = false) { + const escapedDisplay = escapeHtml(display); + if (!unit || hideUnit) { + return `${escapedDisplay}`; + } + const cleanUnit = String(unit); + const escapedUnit = escapeHtml(cleanUnit); + const tightUnit = this._isTightUnit(cleanUnit); + const textClass = tightUnit ? "value-right-text tight-unit" : "value-right-text has-unit"; + return `${escapedDisplay}${escapedUnit}`; + } + _formatAboveValueMarkup(display, unit, hideUnit = false) { + return `${this._formatRightValueMarkup(display, unit, hideUnit)}`; + } + _formatInsideValueMarkup(display, unit, hideUnit = false) { + const escapedDisplay = escapeHtml(display); + if (!unit || hideUnit) return `${escapedDisplay}`; + const cleanUnit = String(unit); + const escapedUnit = escapeHtml(cleanUnit); + const unitModeClass = this._isTightUnit(cleanUnit) ? "tight-unit" : "has-unit"; + return `${escapedDisplay}${escapedUnit}`; + } + _buildRow(entityCfg, stateDisplay, unit, pct, color, peakPct, peakDisplay, targetPct, targetDisplay, peakColor, targetColor, minValue, maxValue) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p, _q, _r, _s, _t, _u; + const ecfg = this._resolve(entityCfg); + const stateObj = (_c = (_b = (_a = this._hass) == null ? void 0 : _a.states) == null ? void 0 : _b[entityCfg.entity]) != null ? _c : null; + const rowViewModel = stateObj ? buildRowViewModel({ + hass: this._hass, + cardConfig: this._config, + entityConfig: ecfg, + entityState: stateObj, + peaks: this._peaks + }) : null; + const layout = ecfg.layout; + const bar = ecfg.bar; + const targetMarkerCfg = ecfg.target_marker; + const peakMarkerCfg = ecfg.peak_marker; + const safeMin = Number.isFinite(minValue) ? minValue : 0; + const safeMax = Number.isFinite(maxValue) ? maxValue : 100; + const baselinePct = (_d = rowViewModel == null ? void 0 : rowViewModel.baselinePercent) != null ? _d : this._resolveBaselinePct(ecfg, safeMin, safeMax); + const lp = layout.label.position; + const h = (_f = (_e = rowViewModel == null ? void 0 : rowViewModel.attributes) == null ? void 0 : _e.baseHeight) != null ? _f : layout.height; + const name = (_j = (_i = (_g = rowViewModel == null ? void 0 : rowViewModel.name) != null ? _g : ecfg.name) != null ? _i : (_h = stateObj == null ? void 0 : stateObj.attributes) == null ? void 0 : _h.friendly_name) != null ? _j : entityCfg.entity; + const escapedEntityId = escapeHtml((_k = rowViewModel == null ? void 0 : rowViewModel.entityId) != null ? _k : entityCfg.entity); + const escapedName = escapeHtml(name); + const targetEnabled = (targetMarkerCfg == null ? void 0 : targetMarkerCfg.enabled) !== false; + const peakMarkerColor = peakColor || "#888"; + const targetMarkerColor = targetColor || "#888"; + const peakContrastColor = this._getMarkerContrastColor(peakMarkerColor); + const targetContrastColor = this._getMarkerContrastColor(targetMarkerColor); + const rawValue = (_l = rowViewModel == null ? void 0 : rowViewModel.numericValue) != null ? _l : this._getFiniteNumber(stateDisplay); + const needleState = (_m = rowViewModel == null ? void 0 : rowViewModel.needle) != null ? _m : this._getNeedleRenderState(rawValue, ecfg, safeMin, safeMax, baselinePct); + const fillState = this._getFillRenderState(pct, "var(--sbcp-row-height)", ecfg, color, targetPct, baselinePct, safeMin, safeMax, needleState.show); + const peakMarker = peakMarkerCfg.show && peakPct !== null ? ` +
    +
    +
    +
    ` : ""; + const targetMarker = ` +
    +
    +
    +
    `; + const targetValueLabel = targetEnabled && targetMarkerCfg.show_label ? ` +
    + ${targetDisplay !== null ? escapeHtml(targetDisplay) : ""} +
    ` : ""; + const needleMarker = ((_o = (_n = ecfg.bar) == null ? void 0 : _n.needle) == null ? void 0 : _o.show) && !Number.isFinite(baselinePct) ? ` +
    +
    +
    ` : ""; + const paintLayers = fillState.paintLayers.map((layer) => ` +
    `).join(""); + const aboveLabel = lp === "above" ? ` +
    + ${ecfg.icon && ecfg.icon !== false ? `
    ` : ""} +
    + ${escapedName} + ${this._formatAboveValueMarkup(stateDisplay, unit, false)} +
    +
    ` : ""; + const heroSize = (_q = layout.label.hero_size) != null ? _q : "small"; + const heroHeader = lp === "hero" ? ` +
    +
    + ${escapedName} + ${this._formatRightValueMarkup(stateDisplay, unit, false)} +
    +
    ` : ""; + const innerLabel = lp === "inside" ? ` +
    + ${escapedName} + ${this._formatInsideValueMarkup(stateDisplay, unit, false)} +
    ` : ""; + const leftLabel = lp === "left" ? `
    ${escapedName}
    ` : ""; + const rightValue = lp !== "inside" && lp !== "above" && lp !== "hero" ? `
    ${this._formatRightValueMarkup(stateDisplay, unit, false)}
    ` : ""; + const topRightValue = lp === "left" ? `
    ${this._formatRightValueMarkup(stateDisplay, unit, false)}
    ` : ""; + const escapedIcon = ecfg.icon && ecfg.icon !== false ? escapeHtml(ecfg.icon) : ""; + const mainIcon = escapedIcon && lp !== "hero" ? `
    ` : ""; + return ` +
    +
    + ${aboveLabel} + ${heroHeader} + ${topRightValue} +
    + ${mainIcon} + ${leftLabel} +
    +
    +
    +${paintLayers} +
    + ${innerLabel} + ${peakMarker} + ${targetMarker} + ${needleMarker} +
    + ${targetValueLabel} +
    + ${rightValue} +
    +
    +
    `; + } + _patchRow(row, entityCfg, stateObj) { + var _a; + if (!row || !stateObj) return; + const ecfg = this._resolve(entityCfg); + const rowViewModel = buildRowViewModel({ + hass: this._hass, + cardConfig: this._config, + entityConfig: ecfg, + entityState: stateObj, + peaks: this._peaks + }); + const rawVal = rowViewModel.numericValue; + const safeMin = rowViewModel.min; + const safeMax = rowViewModel.max; + const targetVal = rowViewModel.target; + const pct = rowViewModel.percent; + const color = this._getColor(pct, ecfg, safeMin, safeMax); + const display = rowViewModel.displayValue; + const displayUnit = rowViewModel.displayUnit; + const fillReveal = row.querySelector(".bar-fill-reveal"); + const paintLayer = row.querySelector('.bar-paint-layer[data-layer="base"]'); + const liveTargetPct = rowViewModel.targetPercent; + const liveBaselinePct = rowViewModel.baselinePercent; + const needleState = rowViewModel.needle; + const fillState = this._getFillRenderState(pct, "var(--sbcp-row-height)", ecfg, color, liveTargetPct, liveBaselinePct, safeMin, safeMax, needleState.show); + if (fillReveal) { + this._setStyleTextIfChanged(fillReveal, fillState.revealStyle); + this._setClassNameIfChanged(fillReveal, `bar-fill-reveal${ecfg.bar.animated ? "" : " no-anim"}`); + } + if (paintLayer) { + const baseLayerState = fillState.paintLayers.find((layer) => layer.id === "base"); + if (baseLayerState) { + this._setStyleTextIfChanged(paintLayer, `z-index:${baseLayerState.zIndex};${baseLayerState.paintStyle}${baseLayerState.revealStyle}`); + } + } + const aboveTargetLayer = row.querySelector('.bar-paint-layer[data-layer="above-target"]'); + if (aboveTargetLayer) { + const aboveTargetState = fillState.paintLayers.find((layer) => layer.id === "above-target"); + if (aboveTargetState) { + this._setStyleTextIfChanged(aboveTargetLayer, `z-index:${aboveTargetState.zIndex};${aboveTargetState.paintStyle}${aboveTargetState.revealStyle}`); + } + } + const needleEl = row.querySelector(".needle-marker"); + if (needleEl) { + this._setStyleIfChanged(needleEl, "display", needleState.show ? "block" : "none"); + this._setStyleIfChanged(needleEl, "left", `${(_a = needleState.pct) != null ? _a : 0}%`); + this._setStyleIfChanged(needleEl, "--needle-color", needleState.color); + this._setStyleIfChanged(needleEl, "--needle-border-color", needleState.borderColor); + this._setDatasetIfChanged(needleEl, "edge", needleState.edge); + } + this._setDatasetIfChanged(row, "baseHeight", rowViewModel.attributes.baseHeight); + this._setDatasetIfChanged(row, "heightExplicit", rowViewModel.attributes.heightExplicit ? "true" : "false"); + this._setDatasetIfChanged(row, "barAnimated", rowViewModel.attributes.barAnimated ? "true" : "false"); + const valueEl = row.querySelector(".value-right"); + if (valueEl) { + valueEl.dataset.display = this._encodeDataAttr(display); + valueEl.dataset.unit = this._encodeDataAttr(displayUnit); + valueEl.dataset.hideUnit = "false"; + valueEl.innerHTML = this._formatRightValueMarkup(display, displayUnit, false); + } + const topValueEl = row.querySelector(".top-right-value"); + if (topValueEl) { + topValueEl.dataset.display = this._encodeDataAttr(display); + topValueEl.dataset.unit = this._encodeDataAttr(displayUnit); + topValueEl.dataset.hideUnit = "false"; + topValueEl.innerHTML = this._formatRightValueMarkup(display, displayUnit, false); + } + const innerLabel = row.querySelector(".bar-inner-label"); + if (innerLabel) { + const valueSpan = innerLabel.querySelector(".inside-value"); + if (valueSpan) { + valueSpan.dataset.display = this._encodeDataAttr(display); + valueSpan.dataset.unit = this._encodeDataAttr(displayUnit); + valueSpan.dataset.hideUnit = "false"; + valueSpan.dataset.hideValue = "false"; + valueSpan.innerHTML = this._formatInsideValueMarkup(display, displayUnit, false); + } + } + const heroHeader = row.querySelector(".hero-header"); + if (heroHeader) { + heroHeader.innerHTML = `${escapeHtml(rowViewModel.name)}${this._formatRightValueMarkup(display, displayUnit, false)}`; + } + const aboveLabel = heroHeader ? null : row.querySelector(".above-bar-label"); + if (aboveLabel) { + aboveLabel.innerHTML = `${escapeHtml(rowViewModel.name)}${this._formatAboveValueMarkup(display, displayUnit, false)}`; + } + if (ecfg.peak_marker.show && Number.isFinite(rawVal)) { + const key = entityCfg.entity; + if (this._peaks[key] === void 0 || rawVal > this._peaks[key]) { + this._peaks[key] = rawVal; + } + const peakVal = this._peaks[key]; + const peakPct = this._toScalePct(peakVal, safeMin, safeMax); + const peakEl = row.querySelector(".peak-marker"); + if (peakEl) { + if (Number.isFinite(peakPct)) { + this._setStyleIfChanged(peakEl, "left", `${peakPct}%`); + } + this._setStyleIfChanged(peakEl, "--marker-color", ecfg.peak_marker.color); + this._setStyleIfChanged(peakEl, "--marker-contrast-color", this._getMarkerContrastColor(ecfg.peak_marker.color)); + } + } + const targetEl = row.querySelector(".target-marker"); + const targetLabelEl = row.querySelector(".target-value-label"); + if (targetVal !== null) { + const targetPct = rowViewModel.targetPercent; + if (targetEl) { + this._setStyleIfChanged(targetEl, "display", ""); + this._setStyleIfChanged(targetEl, "left", `${targetPct}%`); + this._setStyleIfChanged(targetEl, "--marker-color", ecfg.target_marker.color); + this._setStyleIfChanged(targetEl, "--marker-contrast-color", this._getMarkerContrastColor(ecfg.target_marker.color)); + } + if (targetLabelEl) { + this._setTextIfChanged(targetLabelEl, rowViewModel.targetDisplay); + } + } else { + if (targetEl) this._setStyleIfChanged(targetEl, "display", "none"); + if (targetLabelEl) this._setStyleIfChanged(targetLabelEl, "visibility", "hidden"); + } + } + _update() { + if (!this._hass || !this._config) return; + const rowsEl = this.shadowRoot.querySelector(".rows"); + if (!rowsEl) return; + const entities = this._config.entities; + if (!this._rendered) { + let html = ""; + for (let entityIndex = 0; entityIndex < entities.length; entityIndex++) { + const entityCfg = entities[entityIndex]; + const stateObj = this._hass.states[entityCfg.entity]; + if (!stateObj) { + html += `
    Entity not found: ${escapeHtml(entityCfg.entity)}
    `; + continue; + } + const ecfg = this._resolve(entityCfg); + const rowViewModel = buildRowViewModel({ + hass: this._hass, + cardConfig: this._config, + entityConfig: ecfg, + entityState: stateObj, + peaks: this._peaks + }); + const rawVal = rowViewModel.numericValue; + const safeMin = rowViewModel.min; + const safeMax = rowViewModel.max; + const targetVal = rowViewModel.target; + const pct = rowViewModel.percent; + const color = this._getColor(pct, ecfg, safeMin, safeMax); + const display = rowViewModel.displayValue; + const displayUnit = rowViewModel.displayUnit; + const targetPct = rowViewModel.targetPercent; + const targetDisplay = rowViewModel.targetDisplay; + let peakPct = null, peakDisplay = null; + if (ecfg.peak_marker.show && Number.isFinite(rawVal)) { + if (this._peaks[entityCfg.entity] === void 0 || rawVal > this._peaks[entityCfg.entity]) { + this._peaks[entityCfg.entity] = rawVal; + } + const peakVal = this._peaks[entityCfg.entity]; + peakPct = this._toScalePct(peakVal, safeMin, safeMax); + peakDisplay = this._formatNumericDisplay(peakVal, ecfg.formatting.decimal); + } + html += this._buildRow(entityCfg, display, displayUnit, pct, color, peakPct, peakDisplay, targetPct, targetDisplay, ecfg.peak_marker.color, ecfg.target_marker.color, safeMin, safeMax); + } + rowsEl.innerHTML = html; + this._rendered = true; + const builtRows = rowsEl.querySelectorAll(".row[data-entity]"); + builtRows.forEach((row, idx) => { + const entityCfg = entities[idx]; + const stateObj = entityCfg ? this._hass.states[entityCfg.entity] : null; + if (entityCfg && stateObj) { + this._patchRow(row, entityCfg, stateObj); + } + }); + this._runPostLayoutPasses(builtRows); + builtRows.forEach((row) => { + row.addEventListener("click", () => { + const entityId = row.dataset.entity; + const event = new CustomEvent("hass-more-info", { composed: true, detail: { entityId } }); + this.dispatchEvent(event); + }); + }); + return; + } + const rows = rowsEl.querySelectorAll(".row[data-entity]"); + let rowIdx = 0; + for (const entityCfg of entities) { + const stateObj = this._hass.states[entityCfg.entity]; + if (!stateObj) { + rowIdx++; + continue; + } + const row = rows[rowIdx]; + if (!row) { + rowIdx++; + continue; + } + this._patchRow(row, entityCfg, stateObj); + rowIdx++; + } + this._runPostLayoutPasses(rows); + } + }; + } + }); + + // src/editor/SensorBarCardPlusEditor.js + var SensorBarCardPlusEditor; + var init_SensorBarCardPlusEditor = __esm({ + "src/editor/SensorBarCardPlusEditor.js"() { + SensorBarCardPlusEditor = class extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this._config = {}; + this._draftConfig = {}; + this._hass = null; + this._isRendering = false; + this._renderScheduled = false; + this._lastRenderedConfigJson = null; + this._lastEmittedConfigJson = null; + this._shadowListenersAttached = false; + this._expandedEntityOverrides = /* @__PURE__ */ new Set(); + this._expandedOverrideGroups = /* @__PURE__ */ new Set(); + this._expandedCardGroups = /* @__PURE__ */ new Set(); + this._gradientStopsDrafts = /* @__PURE__ */ new Map(); + this._gradientStopsUiRows = /* @__PURE__ */ new Map(); + this._gradientStopPosTexts = /* @__PURE__ */ new Map(); + this._gradientStopValidationMessages = /* @__PURE__ */ new Map(); + this._segmentDrafts = /* @__PURE__ */ new Map(); + this._segmentUiRows = /* @__PURE__ */ new Map(); + this._segmentBoundaryTexts = /* @__PURE__ */ new Map(); + this._targetAboveFillDrafts = /* @__PURE__ */ new Map(); + this._baselineColorDrafts = /* @__PURE__ */ new Map(); + this._pendingFocusSelector = null; + this._boundHandleClick = (event) => this._handleClick(event); + this._boundHandleChange = (event) => this._handleChange(event); + this._boundHandleInput = (event) => this._handleInput(event); + this._boundHandleValueChanged = (event) => this._handleValueChanged(event); + this._boundHandleKeydown = (event) => this._handleKeydown(event); + } + setConfig(config) { + var _a; + const nextConfig = this._cloneDeep(config != null ? config : {}); + const nextConfigJson = this._serializeConfig(nextConfig); + const currentConfigJson = this._serializeConfig(this._config); + const currentDraftJson = this._serializeConfig(this._draftConfig); + if (nextConfigJson === currentConfigJson) { + return; + } + if (nextConfigJson === currentDraftJson) { + this._config = nextConfig; + return; + } + const shouldRender = !((_a = this.shadowRoot) == null ? void 0 : _a.innerHTML) || nextConfigJson !== this._lastRenderedConfigJson; + this._config = nextConfig; + this._draftConfig = this._cloneDeep(nextConfig); + this._gradientStopsDrafts = /* @__PURE__ */ new Map(); + this._gradientStopsUiRows = /* @__PURE__ */ new Map(); + this._gradientStopPosTexts = /* @__PURE__ */ new Map(); + this._gradientStopValidationMessages = /* @__PURE__ */ new Map(); + this._segmentDrafts = /* @__PURE__ */ new Map(); + this._segmentUiRows = /* @__PURE__ */ new Map(); + this._segmentBoundaryTexts = /* @__PURE__ */ new Map(); + this._targetAboveFillDrafts = /* @__PURE__ */ new Map(); + this._baselineColorDrafts = /* @__PURE__ */ new Map(); + if (shouldRender) { + this._render(); + } + } + set hass(hass) { + var _a; + this._hass = hass; + if (!((_a = this.shadowRoot) == null ? void 0 : _a.innerHTML)) { + this._render(); + return; + } + this._syncEntityPickers(); + } + _cloneContainer(value) { + return Array.isArray(value) ? [...value] : { ...value != null ? value : {} }; + } + _cloneDeep(value) { + if (Array.isArray(value)) { + return value.map((entry) => this._cloneDeep(entry)); + } + if (this._isObject(value)) { + const clone = {}; + for (const [key, entry] of Object.entries(value)) { + clone[key] = this._cloneDeep(entry); + } + return clone; + } + return value; + } + _serializeConfig(value) { + const normalize = (input) => { + if (Array.isArray(input)) { + return input.map((entry) => normalize(entry)); + } + if (this._isObject(input)) { + return Object.keys(input).sort().reduce((acc, key) => { + acc[key] = normalize(input[key]); + return acc; + }, {}); + } + return input; + }; + return JSON.stringify(normalize(value != null ? value : null)); + } + _isObject(value) { + return !!value && typeof value === "object" && !Array.isArray(value); + } + _setPathValue(target, path, value) { + if (!path.length) { + return value; + } + const root = this._cloneContainer(target != null ? target : {}); + let cursor = root; + let sourceCursor = target; + for (let index = 0; index < path.length - 1; index++) { + const key = path[index]; + const nextSource = this._isObject(sourceCursor == null ? void 0 : sourceCursor[key]) || Array.isArray(sourceCursor == null ? void 0 : sourceCursor[key]) ? sourceCursor[key] : {}; + cursor[key] = this._cloneContainer(nextSource); + cursor = cursor[key]; + sourceCursor = nextSource; + } + cursor[path[path.length - 1]] = value; + return root; + } + _deletePathValue(target, path) { + if (!path.length || !this._isObject(target)) { + return target; + } + const [key, ...rest] = path; + if (!(key in target)) { + return target; + } + const cloned = this._cloneContainer(target); + if (!rest.length) { + delete cloned[key]; + return cloned; + } + const nextValue = this._deletePathValue(cloned[key], rest); + if (nextValue === cloned[key]) { + return target; + } + if (this._isObject(nextValue) && !Object.keys(nextValue).length) { + delete cloned[key]; + return cloned; + } + cloned[key] = nextValue; + return cloned; + } + _getPathValue(target, path) { + let cursor = target; + for (const key of path) { + if (cursor == null) return void 0; + cursor = cursor[key]; + } + return cursor; + } + _hasPath(target, path) { + let cursor = target; + for (const key of path) { + if (!this._isObject(cursor) && !Array.isArray(cursor)) return false; + if (!(key in cursor)) return false; + cursor = cursor[key]; + } + return true; + } + _normalizeTextValue(value) { + return typeof value === "string" ? value : value == null ? "" : String(value); + } + _normalizeOptionalEnabled(value) { + return value === true ? true : value === false ? false : null; + } + _normalizeNumberValue(value) { + if (value === "" || value === null || value === void 0) { + return null; + } + const parsed = Number(value); + return Number.isFinite(parsed) ? parsed : null; + } + _normalizeDecimalValue(value) { + if (value === "" || value === null || value === void 0) { + return null; + } + const parsed = Number(value); + if (!Number.isFinite(parsed) || parsed < 0 || !Number.isInteger(parsed)) { + return null; + } + return parsed; + } + _preferStructuredPath(structuredPath, legacyPath = null) { + if (this._hasPath(this._draftConfig, structuredPath.slice(0, -1))) { + return structuredPath; + } + if (this._hasPath(this._draftConfig, structuredPath)) { + return structuredPath; + } + if (legacyPath && this._hasPath(this._draftConfig, legacyPath)) { + return legacyPath; + } + return structuredPath; + } + _getEntitiesValue() { + var _a, _b; + if (Array.isArray(this._draftConfig.entities)) { + return this._draftConfig.entities.map((entry) => { + var _a2, _b2, _c; + return typeof entry === "string" ? { entity: entry } : { + entity: (_a2 = entry == null ? void 0 : entry.entity) != null ? _a2 : "", + name: (_b2 = entry == null ? void 0 : entry.name) != null ? _b2 : "", + icon: (_c = entry == null ? void 0 : entry.icon) != null ? _c : "" + }; + }); + } + if (this._draftConfig.entity) { + return [{ + entity: this._draftConfig.entity, + name: (_a = this._draftConfig.name) != null ? _a : "", + icon: (_b = this._draftConfig.icon) != null ? _b : "" + }]; + } + return []; + } + _getRawEntityRows() { + if (Array.isArray(this._draftConfig.entities)) { + return this._cloneDeep(this._draftConfig.entities); + } + if (this._draftConfig.entity !== void 0) { + const hasTopLevelIdentity = this._draftConfig.name !== void 0 || this._draftConfig.icon !== void 0; + if (!hasTopLevelIdentity) { + return [this._draftConfig.entity]; + } + return [{ + entity: this._draftConfig.entity, + ...this._draftConfig.name !== void 0 ? { name: this._draftConfig.name } : {}, + ...this._draftConfig.icon !== void 0 ? { icon: this._draftConfig.icon } : {} + }]; + } + return []; + } + _buildEntityConfigEntries(entities) { + const usesShorthand = !Array.isArray(this._draftConfig.entities) && this._draftConfig.entity !== void 0; + const source = Array.isArray(this._draftConfig.entities) ? this._draftConfig.entities : this._draftConfig.entity !== void 0 ? [{ + entity: this._draftConfig.entity, + ...this._draftConfig.name !== void 0 ? { name: this._draftConfig.name } : {}, + ...this._draftConfig.icon !== void 0 ? { icon: this._draftConfig.icon } : {} + }] : []; + const entries = entities.map((entry, index) => { + const rawEntry = source[index]; + if (this._isObject(rawEntry)) { + const mergedEntry = { + ...rawEntry, + entity: entry.entity + }; + const normalizedName2 = this._normalizeTextValue(entry.name).trim(); + if (normalizedName2) { + mergedEntry.name = normalizedName2; + } else { + delete mergedEntry.name; + } + const rawIconValue = entry == null ? void 0 : entry.icon; + const normalizedIcon2 = typeof rawIconValue === "string" ? rawIconValue.trim() : rawIconValue; + if (normalizedIcon2 === false) { + mergedEntry.icon = false; + } else if (typeof normalizedIcon2 === "string" && normalizedIcon2) { + mergedEntry.icon = normalizedIcon2; + } else if (rawEntry.icon === false) { + mergedEntry.icon = false; + } else { + delete mergedEntry.icon; + } + return mergedEntry; + } + const nextEntry = { + entity: entry.entity + }; + const normalizedName = this._normalizeTextValue(entry.name).trim(); + if (normalizedName) { + nextEntry.name = normalizedName; + } + const normalizedIcon = typeof (entry == null ? void 0 : entry.icon) === "string" ? entry.icon.trim() : entry == null ? void 0 : entry.icon; + if (normalizedIcon === false) { + nextEntry.icon = false; + } else if (typeof normalizedIcon === "string" && normalizedIcon) { + nextEntry.icon = normalizedIcon; + } + return nextEntry; + }); + if (!usesShorthand) { + return entries.map((entry) => { + if (this._isObject(entry) && Object.keys(entry).length === 1 && entry.entity !== void 0) { + return entry.entity; + } + return entry; + }); + } + return entries; + } + _updateConfig(nextConfig) { + this._draftConfig = this._cloneDeep(nextConfig); + } + _emitConfigChanged() { + const emittedConfig = this._cleanupEditorEmittedConfig(this._cloneDeep(this._draftConfig)); + const nextConfigJson = this._serializeConfig(emittedConfig); + if (nextConfigJson === this._lastEmittedConfigJson) { + return false; + } + this._lastEmittedConfigJson = nextConfigJson; + this.dispatchEvent(new CustomEvent("config-changed", { + detail: { config: emittedConfig }, + bubbles: true, + composed: true + })); + return true; + } + _scheduleRender() { + if (this._renderScheduled || this._isRendering) return; + this._renderScheduled = true; + setTimeout(() => { + this._renderScheduled = false; + this._render(); + }, 0); + } + _applyUserConfig(nextConfig, options = {}) { + const { rerender = false } = options; + const nextConfigJson = this._serializeConfig(nextConfig); + const currentDraftJson = this._serializeConfig(this._draftConfig); + if (nextConfigJson === currentDraftJson) { + return false; + } + this._updateConfig(nextConfig); + this._emitConfigChanged(); + this._refreshDerivedEditorUi(); + if (rerender) { + this._scheduleRender(); + } + return true; + } + _getShadowElementById(id) { + var _a, _b, _c, _d, _e, _f; + if (!this.shadowRoot) { + return null; + } + return (_f = (_e = (_b = (_a = this.shadowRoot).getElementById) == null ? void 0 : _b.call(_a, id)) != null ? _e : (_d = (_c = this.shadowRoot).querySelector) == null ? void 0 : _d.call(_c, `#${id}`)) != null ? _f : null; + } + _setElementChecked(id, checked) { + const element = this._getShadowElementById(id); + if (element) { + element.checked = !!checked; + } + } + _setElementText(id, value) { + const element = this._getShadowElementById(id); + if (element) { + element.textContent = value; + } + } + _refreshDerivedEditorUi() { + if (!this.shadowRoot) { + return; + } + this._refreshCardDerivedUi(); + this._refreshEntityDerivedUi(); + } + _refreshCardDerivedUi() { + this._setElementText("card-group-segments-summary", this._getSegmentsSummary({ type: "card" })); + this._setElementText("card-group-gradient-stops-summary", this._getGradientStopsSummary({ type: "card" })); + this._setElementChecked("target-above-fill-enabled", this._isTargetAboveFillEnabled({ type: "card" })); + this._setElementChecked("baseline-above-color-enabled", this._isBaselineDirectionalColorEnabled({ type: "card" }, "above")); + this._setElementChecked("baseline-below-color-enabled", this._isBaselineDirectionalColorEnabled({ type: "card" }, "below")); + this._refreshSegmentUi({ type: "card" }); + } + _refreshEntityDerivedUi() { + const count = this._getEntitiesValue().length; + for (let index = 0; index < count; index += 1) { + const scope = { type: "entity", index }; + this._setElementChecked(`entity-${index}-scale-inherit`, !this._hasResolvableOverride(this._getResolvableScopedValue(scope, "min")) && !this._hasResolvableOverride(this._getResolvableScopedValue(scope, "max"))); + this._setElementChecked(`entity-${index}-target-inherit`, !this._hasTargetOverride(scope)); + this._setElementChecked(`entity-${index}-baseline-inherit`, !this._hasBaselineOverride(scope)); + this._setElementChecked(`entity-${index}-needle-inherit`, !this._hasNeedleOverride(scope)); + this._setElementChecked(`entity-${index}-peak-inherit`, !this._hasPeakOverride(scope)); + this._setElementChecked(`entity-${index}-bar-inherit`, !this._hasEntityBarAppearanceOverride(scope)); + this._setElementChecked(`entity-${index}-segments-inherit`, !this._hasSegmentsOverride(scope)); + this._setElementChecked(`entity-${index}-gradient-stops-inherit`, !this._hasGradientStopsOverride(scope)); + this._setElementChecked(`entity-${index}-layout-inherit`, !this._hasLayoutOverride(scope)); + this._setElementChecked(`entity-${index}-formatting-inherit`, !this._hasFormattingOverride(scope)); + this._setElementChecked(`entity-${index}-target-above-fill-enabled`, this._isTargetAboveFillEnabled(scope)); + this._setElementChecked(`entity-${index}-baseline-above-color-enabled`, this._isBaselineDirectionalColorEnabled(scope, "above")); + this._setElementChecked(`entity-${index}-baseline-below-color-enabled`, this._isBaselineDirectionalColorEnabled(scope, "below")); + this._setElementText(`entity-${index}-group-scale-summary`, this._getScaleOverrideSummary(scope)); + this._setElementText(`entity-${index}-group-target-summary`, this._getTargetOverrideSummary(scope)); + this._setElementText(`entity-${index}-group-baseline-summary`, this._getBaselineOverrideSummary(scope)); + this._setElementText(`entity-${index}-group-needle-summary`, this._getNeedleSummary(scope)); + this._setElementText(`entity-${index}-group-peak-summary`, this._getPeakSummary(scope)); + this._setElementText(`entity-${index}-group-bar-summary`, this._getBarAppearanceSummary(scope)); + this._setElementText(`entity-${index}-group-segments-summary`, this._getSegmentsSummary(scope)); + this._setElementText(`entity-${index}-group-gradient-stops-summary`, this._getGradientStopsSummary(scope)); + this._setElementText(`entity-${index}-group-layout-summary`, this._getLayoutSummary(scope)); + this._setElementText(`entity-${index}-group-formatting-summary`, this._getFormattingSummary(scope)); + this._refreshSegmentUi(scope); + } + } + _queuePostRenderFocus(selector) { + this._pendingFocusSelector = selector || null; + } + _applyPendingFocus() { + if (!this._pendingFocusSelector || !this.shadowRoot) { + return; + } + const selector = this._pendingFocusSelector; + this._pendingFocusSelector = null; + setTimeout(() => { + var _a, _b; + const element = (_b = (_a = this.shadowRoot) == null ? void 0 : _a.querySelector) == null ? void 0 : _b.call(_a, selector); + if (!element || typeof element.focus !== "function") { + return; + } + try { + element.focus({ preventScroll: true }); + } catch (_error) { + element.focus(); + } + }, 0); + } + _setValueAtPath(path, value, options = {}) { + const nextConfig = value === void 0 ? this._deletePathValue(this._draftConfig, path) : this._setPathValue(this._draftConfig, path, value); + return this._applyUserConfig(nextConfig, options); + } + _setTitle(value) { + this._setValueAtPath(["title"], value); + } + _setEntityField(index, key, value) { + const normalizedValue = this._normalizeTextValue(value); + if (!Array.isArray(this._draftConfig.entities) && this._draftConfig.entity !== void 0 && index === 0) { + if (!normalizedValue.trim()) { + return this._setValueAtPath([key], void 0); + } + return this._setValueAtPath([key], normalizedValue.trim()); + } + const nextConfig = this._withEntityScopeConfig((entries) => { + var _a; + const rawEntry = entries[index]; + const nextEntry = this._isObject(rawEntry) ? this._cloneDeep(rawEntry) : { entity: (_a = rawEntry == null ? void 0 : rawEntry.entity) != null ? _a : "" }; + if (!normalizedValue.trim()) { + delete nextEntry[key]; + } else { + nextEntry[key] = normalizedValue.trim(); + } + entries[index] = nextEntry; + return entries; + }); + return this._applyUserConfig(nextConfig); + } + _cleanupEntityIdentityForEmit(target) { + if (!this._isObject(target)) { + return target; + } + const nextTarget = this._cloneDeep(target); + const normalizedName = this._normalizeTextValue(nextTarget.name).trim(); + if (!normalizedName) { + delete nextTarget.name; + } else { + nextTarget.name = normalizedName; + } + if (nextTarget.icon === false) { + return nextTarget; + } + const normalizedIcon = this._normalizeTextValue(nextTarget.icon).trim(); + if (!normalizedIcon) { + delete nextTarget.icon; + } else { + nextTarget.icon = normalizedIcon; + } + return nextTarget; + } + _cleanupNeedleForEmit(target, scope = { type: "card" }) { + var _a; + if (!this._isObject(target) || !this._isObject(target.bar)) { + return target; + } + const nextTarget = this._cloneDeep(target); + const rawNeedle = (_a = nextTarget.bar) == null ? void 0 : _a.needle; + if (rawNeedle === void 0) { + return nextTarget; + } + const defaultColor = this._normalizeColorComparisonValue("#ffffff"); + let nextNeedle = null; + if (rawNeedle === true) { + nextNeedle = { show: true }; + } else if (rawNeedle === false) { + nextNeedle = (scope == null ? void 0 : scope.type) === "entity" ? { show: false } : null; + } else if (this._isObject(rawNeedle)) { + const color = this._normalizeTextValue(rawNeedle.color).trim(); + if (rawNeedle.show === true) { + nextNeedle = { show: true }; + } else if (rawNeedle.show === false) { + nextNeedle = (scope == null ? void 0 : scope.type) === "entity" ? { show: false } : null; + } else if ((scope == null ? void 0 : scope.type) === "entity" && color && this._normalizeColorComparisonValue(color) !== defaultColor) { + nextNeedle = {}; + } + if (nextNeedle && color && this._normalizeColorComparisonValue(color) !== defaultColor) { + nextNeedle.color = color; + } + } else { + nextNeedle = null; + } + if (nextNeedle) { + nextTarget.bar.needle = nextNeedle; + } else { + delete nextTarget.bar.needle; + if (!Object.keys(nextTarget.bar).length) { + delete nextTarget.bar; + } + } + return nextTarget; + } + _cleanupResolvableValueForEmit(value) { + const nextValue = {}; + if (this._isObject(value)) { + const fixed = this._normalizeNumberValue(value.fixed); + const entity = this._normalizeTextValue(value.entity).trim(); + if (fixed !== null) { + nextValue.fixed = fixed; + } + if (entity) { + nextValue.entity = entity; + } + } else { + const fixed = this._normalizeNumberValue(value); + if (fixed !== null) { + nextValue.fixed = fixed; + } + } + return Object.keys(nextValue).length ? nextValue : null; + } + _cleanupScaleForEmit(target) { + if (!this._isObject(target) || !this._isObject(target.scale)) { + return target; + } + const nextTarget = this._cloneDeep(target); + const nextScale = this._cloneDeep(nextTarget.scale); + ["min", "max"].forEach((key) => { + if (!Object.prototype.hasOwnProperty.call(nextScale, key)) { + return; + } + const cleanedValue = this._cleanupResolvableValueForEmit(nextScale[key]); + if (cleanedValue) { + nextScale[key] = cleanedValue; + delete nextTarget[key]; + delete nextTarget[`${key}_entity`]; + } else { + delete nextScale[key]; + } + }); + if (Object.keys(nextScale).length) { + nextTarget.scale = nextScale; + } else { + delete nextTarget.scale; + } + return nextTarget; + } + _cleanupFormattingForEmit(target) { + if (!this._isObject(target) || !this._isObject(target.formatting)) { + return target; + } + const nextTarget = this._cloneDeep(target); + const nextFormatting = this._cloneDeep(nextTarget.formatting); + const unit = this._normalizeTextValue(nextFormatting.unit).trim(); + const decimal = this._normalizeDecimalValue(nextFormatting.decimal); + if (unit) { + nextFormatting.unit = unit; + delete nextTarget.unit; + } else { + delete nextFormatting.unit; + } + if (decimal !== null) { + nextFormatting.decimal = decimal; + delete nextTarget.decimal; + } else { + delete nextFormatting.decimal; + } + if (Object.keys(nextFormatting).length) { + nextTarget.formatting = nextFormatting; + } else { + delete nextTarget.formatting; + } + return nextTarget; + } + _cleanupLayoutForEmit(target) { + if (!this._isObject(target) || !this._isObject(target.layout)) { + return target; + } + const nextTarget = this._cloneDeep(target); + const nextLayout = this._cloneDeep(nextTarget.layout); + const nextLabel = this._isObject(nextLayout.label) ? this._cloneDeep(nextLayout.label) : null; + const height = this._normalizeNumberValue(nextLayout.height); + const width = this._normalizeNumberValue(nextLabel == null ? void 0 : nextLabel.width); + const position = this._normalizeTextValue(nextLabel == null ? void 0 : nextLabel.position).trim(); + if (height !== null && height >= 24) { + nextLayout.height = height; + delete nextTarget.height; + } else { + delete nextLayout.height; + } + if (nextLabel) { + if (position) { + nextLabel.position = position; + delete nextTarget.label_position; + } else { + delete nextLabel.position; + } + if (width !== null) { + nextLabel.width = width; + delete nextTarget.label_width; + } else { + delete nextLabel.width; + } + if (Object.keys(nextLabel).length) { + nextLayout.label = nextLabel; + } else { + delete nextLayout.label; + } + } + if (Object.keys(nextLayout).length) { + nextTarget.layout = nextLayout; + } else { + delete nextTarget.layout; + } + return nextTarget; + } + _cleanupTargetForEmit(target) { + var _a, _b; + if (!this._isObject(target) || !this._isObject(target.target)) { + return target; + } + const nextTarget = this._cloneDeep(target); + const nextMarker = this._cloneDeep(nextTarget.target); + const cleanedAt = this._cleanupResolvableValueForEmit(nextMarker.at); + const color = this._normalizeTextValue(nextMarker.color).trim(); + const labelShow = ((_a = nextMarker.label) == null ? void 0 : _a.show) === true; + const fillColor = this._normalizeTextValue((_b = nextMarker.when_exceeded) == null ? void 0 : _b.fill_color).trim(); + if (typeof nextMarker.enabled !== "boolean") { + delete nextMarker.enabled; + } + if (cleanedAt) { + nextMarker.at = cleanedAt; + delete nextTarget.target_entity; + } else { + delete nextMarker.at; + } + if (color && this._normalizeColorComparisonValue(color) !== this._normalizeColorComparisonValue("#888")) { + nextMarker.color = color; + delete nextTarget.target_color; + } else { + delete nextMarker.color; + } + if (labelShow) { + nextMarker.label = { ...this._isObject(nextMarker.label) ? nextMarker.label : {}, show: true }; + delete nextTarget.show_target_label; + } else { + delete nextMarker.label; + } + if (fillColor) { + nextMarker.when_exceeded = { + ...this._isObject(nextMarker.when_exceeded) ? nextMarker.when_exceeded : {}, + fill_color: fillColor + }; + delete nextTarget.above_target_color; + } else { + delete nextMarker.when_exceeded; + } + if (Object.keys(nextMarker).length) { + nextTarget.target = nextMarker; + } else { + delete nextTarget.target; + } + return nextTarget; + } + _cleanupBaselineForEmit(target) { + if (!this._isObject(target) || !this._isObject(target.baseline)) { + return target; + } + const nextTarget = this._cloneDeep(target); + const nextBaseline = this._cloneDeep(nextTarget.baseline); + const cleanedAt = this._cleanupResolvableValueForEmit(nextBaseline.at); + if (typeof nextBaseline.enabled !== "boolean") { + delete nextBaseline.enabled; + } + if (cleanedAt) { + nextBaseline.at = cleanedAt; + } else { + delete nextBaseline.at; + } + ["above", "below"].forEach((direction) => { + if (!this._isObject(nextBaseline[direction])) { + delete nextBaseline[direction]; + return; + } + const color = this._normalizeTextValue(nextBaseline[direction].color).trim(); + if (color) { + nextBaseline[direction] = { ...nextBaseline[direction], color }; + } else { + delete nextBaseline[direction]; + } + }); + if (Object.keys(nextBaseline).length) { + nextTarget.baseline = nextBaseline; + } else { + delete nextTarget.baseline; + } + return nextTarget; + } + _cleanupPeakForEmit(target) { + if (!this._isObject(target) || !this._isObject(target.peak)) { + return target; + } + const nextTarget = this._cloneDeep(target); + const nextPeak = this._cloneDeep(nextTarget.peak); + const color = this._normalizeTextValue(nextPeak.color).trim(); + if (typeof nextPeak.enabled !== "boolean") { + delete nextPeak.enabled; + } + if (color && this._normalizeColorComparisonValue(color) !== this._normalizeColorComparisonValue("#888")) { + nextPeak.color = color; + delete nextTarget.peak_color; + } else { + delete nextPeak.color; + } + if (Object.keys(nextPeak).length) { + nextTarget.peak = nextPeak; + } else { + delete nextTarget.peak; + } + return nextTarget; + } + _cleanupBarForEmit(target) { + if (!this._isObject(target) || !this._isObject(target.bar)) { + return target; + } + const nextTarget = this._cloneDeep(target); + const nextBar = this._cloneDeep(nextTarget.bar); + const fillStyle = this._normalizeTextValue(nextBar.fill_style).trim(); + const color = this._normalizeTextValue(nextBar.color).trim(); + const segments = Array.isArray(nextBar.segments) ? nextBar.segments : null; + const gradientStops = Array.isArray(nextBar.gradient_stops) ? nextBar.gradient_stops : null; + if (fillStyle && fillStyle !== "bands") { + nextBar.fill_style = fillStyle; + delete nextTarget.color_mode; + } else { + delete nextBar.fill_style; + } + if (color && this._normalizeColorComparisonValue(color) !== this._normalizeColorComparisonValue("#4a9eff")) { + nextBar.color = color; + delete nextTarget.color; + } else { + delete nextBar.color; + } + if (nextBar.solid_fill === true) { + nextBar.solid_fill = true; + } else { + delete nextBar.solid_fill; + } + if (segments && segments.length && !this._segmentsEqualForEditor(segments, this._getDefaultSegments())) { + nextBar.segments = segments; + delete nextTarget.segments; + delete nextTarget.severity; + } else { + delete nextBar.segments; + } + if (gradientStops && gradientStops.length >= 2 && !this._isDefaultGradientStops(gradientStops)) { + nextBar.gradient_stops = this._sanitizeGradientStopsForEmit(gradientStops); + delete nextTarget.gradient_stops; + } else { + delete nextBar.gradient_stops; + } + if (Object.keys(nextBar).length) { + nextTarget.bar = nextBar; + } else { + delete nextTarget.bar; + } + return nextTarget; + } + _getEditorKnownKeyOrder(path = []) { + const pathKey = path.join("."); + switch (pathKey) { + case "": + return ["type", "title", "entities", "scale", "target", "baseline", "peak", "layout", "formatting", "bar"]; + case "entities.*": + return ["entity", "name", "icon", "scale", "target", "baseline", "peak", "layout", "formatting", "bar"]; + case "scale": + return ["min", "max"]; + case "scale.min": + case "scale.max": + case "target.at": + case "baseline.at": + return ["fixed", "entity"]; + case "target": + return ["enabled", "at", "color", "label", "when_exceeded"]; + case "target.label": + return ["show"]; + case "target.when_exceeded": + return ["fill_color"]; + case "baseline": + return ["enabled", "at", "above", "below"]; + case "baseline.above": + case "baseline.below": + return ["color"]; + case "peak": + return ["enabled", "color"]; + case "layout": + return ["height", "label"]; + case "layout.label": + return ["position", "hero_size", "width"]; + case "formatting": + return ["unit", "decimal"]; + case "bar": + return ["fill_style", "color", "solid_fill", "needle", "segments", "gradient_stops"]; + case "bar.needle": + return ["show", "color"]; + default: + return null; + } + } + _orderEditorConfigKeys(value, path = []) { + var _a; + if (Array.isArray(value)) { + const nextPath = path[0] === "entities" ? ["entities", "*"] : path; + return value.map((entry) => this._orderEditorConfigKeys(entry, nextPath)); + } + if (!this._isObject(value)) { + return value; + } + const orderedValue = {}; + const knownOrder = (_a = this._getEditorKnownKeyOrder(path)) != null ? _a : []; + const seenKeys = /* @__PURE__ */ new Set(); + knownOrder.forEach((key) => { + if (!Object.prototype.hasOwnProperty.call(value, key)) { + return; + } + orderedValue[key] = this._orderEditorConfigKeys(value[key], [...path, key]); + seenKeys.add(key); + }); + Object.keys(value).forEach((key) => { + if (seenKeys.has(key)) { + return; + } + orderedValue[key] = this._orderEditorConfigKeys(value[key], [...path, key]); + }); + return orderedValue; + } + _cleanupEditorEmittedConfig(config) { + if (!this._isObject(config)) { + return config; + } + let nextConfig = this._cleanupEntityIdentityForEmit(config); + nextConfig = this._cleanupScaleForEmit(nextConfig); + nextConfig = this._cleanupTargetForEmit(nextConfig); + nextConfig = this._cleanupBaselineForEmit(nextConfig); + nextConfig = this._cleanupPeakForEmit(nextConfig); + nextConfig = this._cleanupLayoutForEmit(nextConfig); + nextConfig = this._cleanupFormattingForEmit(nextConfig); + nextConfig = this._cleanupNeedleForEmit(nextConfig, { type: "card" }); + nextConfig = this._cleanupBarForEmit(nextConfig); + if (Array.isArray(nextConfig.entities)) { + nextConfig.entities = nextConfig.entities.map((entry) => { + if (!this._isObject(entry)) { + return entry; + } + let cleanedEntry = this._cleanupEntityIdentityForEmit(entry); + cleanedEntry = this._cleanupScaleForEmit(cleanedEntry); + cleanedEntry = this._cleanupTargetForEmit(cleanedEntry); + cleanedEntry = this._cleanupBaselineForEmit(cleanedEntry); + cleanedEntry = this._cleanupPeakForEmit(cleanedEntry); + cleanedEntry = this._cleanupLayoutForEmit(cleanedEntry); + cleanedEntry = this._cleanupFormattingForEmit(cleanedEntry); + cleanedEntry = this._cleanupNeedleForEmit(cleanedEntry, { type: "entity" }); + cleanedEntry = this._cleanupBarForEmit(cleanedEntry); + return cleanedEntry; + }); + } + return this._orderEditorConfigKeys(nextConfig); + } + _getScopedPath(scope, keyPath) { + const normalizedPath = Array.isArray(keyPath) ? keyPath : [keyPath]; + if (!scope || scope.type === "card") { + return normalizedPath; + } + if (scope.type === "entity") { + return ["entities", scope.index, ...normalizedPath]; + } + return normalizedPath; + } + _normalizePath(keyPath) { + return Array.isArray(keyPath) ? keyPath : [keyPath]; + } + _getEntityRawEntries() { + if (Array.isArray(this._draftConfig.entities)) { + return this._draftConfig.entities.map((entry) => this._isObject(entry) ? this._cloneDeep(entry) : { entity: entry }); + } + if (this._draftConfig.entity !== void 0) { + return [{ + entity: this._draftConfig.entity, + ...this._draftConfig.name !== void 0 ? { name: this._draftConfig.name } : {}, + ...this._draftConfig.icon !== void 0 ? { icon: this._draftConfig.icon } : {} + }]; + } + return []; + } + _withEntityScopeConfig(mutator) { + const rawEntries = this._getEntityRawEntries(); + const nextEntries = mutator(rawEntries.map((entry) => this._cloneDeep(entry))); + let nextConfig = this._setPathValue(this._draftConfig, ["entities"], nextEntries); + if (!Array.isArray(this._draftConfig.entities) && this._draftConfig.entity !== void 0) { + nextConfig = this._deletePathValue(nextConfig, ["entity"]); + if (this._draftConfig.name !== void 0) { + nextConfig = this._deletePathValue(nextConfig, ["name"]); + } + if (this._draftConfig.icon !== void 0) { + nextConfig = this._deletePathValue(nextConfig, ["icon"]); + } + } + return nextConfig; + } + _setEntityRowsRaw(nextRows, options = {}) { + var _a; + const normalizedRows = this._cloneDeep(nextRows); + if (Array.isArray(this._draftConfig.entities) || this._draftConfig.entity === void 0 || normalizedRows.length !== 1) { + let nextConfig2 = this._setPathValue(this._draftConfig, ["entities"], normalizedRows); + if (!Array.isArray(this._draftConfig.entities) && this._draftConfig.entity !== void 0) { + nextConfig2 = this._deletePathValue(nextConfig2, ["entity"]); + nextConfig2 = this._deletePathValue(nextConfig2, ["name"]); + nextConfig2 = this._deletePathValue(nextConfig2, ["icon"]); + } + return this._applyUserConfig(nextConfig2, options); + } + const [row] = normalizedRows; + if (typeof row === "string") { + let nextConfig2 = this._setPathValue(this._draftConfig, ["entity"], row); + nextConfig2 = this._deletePathValue(nextConfig2, ["name"]); + nextConfig2 = this._deletePathValue(nextConfig2, ["icon"]); + return this._applyUserConfig(nextConfig2, options); + } + let nextConfig = this._setPathValue(this._draftConfig, ["entity"], (_a = row == null ? void 0 : row.entity) != null ? _a : ""); + if (row && Object.prototype.hasOwnProperty.call(row, "name")) { + nextConfig = this._setPathValue(nextConfig, ["name"], row.name); + } else { + nextConfig = this._deletePathValue(nextConfig, ["name"]); + } + if (row && Object.prototype.hasOwnProperty.call(row, "icon")) { + nextConfig = this._setPathValue(nextConfig, ["icon"], row.icon); + } else { + nextConfig = this._deletePathValue(nextConfig, ["icon"]); + } + return this._applyUserConfig(nextConfig, options); + } + _moveEntityRow(index, direction) { + const rows = this._getRawEntityRows(); + const nextIndex = index + direction; + if (index < 0 || index >= rows.length || nextIndex < 0 || nextIndex >= rows.length) { + return false; + } + const nextRows = this._cloneDeep(rows); + [nextRows[index], nextRows[nextIndex]] = [nextRows[nextIndex], nextRows[index]]; + return this._setEntityRowsRaw(nextRows, { rerender: true }); + } + _duplicateEntityRow(index) { + const rows = this._getRawEntityRows(); + if (index < 0 || index >= rows.length) { + return false; + } + const sourceRow = this._cloneDeep(rows[index]); + const duplicateRow = typeof sourceRow === "string" ? { entity: sourceRow } : this._cloneDeep(sourceRow); + if (this._isObject(duplicateRow) && typeof duplicateRow.name === "string" && duplicateRow.name.trim()) { + duplicateRow.name = `${duplicateRow.name.trim()} copy`; + } + const nextRows = this._cloneDeep(rows); + nextRows.splice(index + 1, 0, duplicateRow); + return this._setEntityRowsRaw(nextRows, { rerender: true }); + } + _removeEntityRow(index) { + const rows = this._getRawEntityRows(); + if (rows.length <= 1 || index < 0 || index >= rows.length) { + return false; + } + const nextRows = rows.filter((_, rowIndex) => rowIndex !== index); + return this._setEntityRowsRaw(nextRows, { rerender: true }); + } + _getScopedValue(scope, keyPath) { + if ((scope == null ? void 0 : scope.type) === "entity") { + const entry = this._getEntityRawEntries()[scope.index]; + return this._getPathValue(entry, this._normalizePath(keyPath)); + } + return this._getPathValue(this._draftConfig, this._getScopedPath(scope, keyPath)); + } + _removeScopedValue(scope, keyPath, options = {}) { + if ((scope == null ? void 0 : scope.type) === "entity") { + const nextConfig = this._withEntityScopeConfig((entries) => { + var _a, _b; + const entry = this._isObject(entries[scope.index]) ? { ...entries[scope.index] } : { entity: (_b = (_a = entries[scope.index]) == null ? void 0 : _a.entity) != null ? _b : "" }; + entries[scope.index] = this._deletePathValue(entry, this._normalizePath(keyPath)); + return entries; + }); + return this._applyUserConfig(nextConfig, options); + } + return this._setValueAtPath(this._getScopedPath(scope, keyPath), void 0, options); + } + _applyScopedMutation(scope, mutator, options = {}) { + if ((scope == null ? void 0 : scope.type) === "entity") { + const nextConfig2 = this._withEntityScopeConfig((entries) => { + var _a; + const rawEntry = entries[scope.index]; + const entry = this._isObject(rawEntry) ? this._cloneDeep(rawEntry) : { entity: (_a = rawEntry == null ? void 0 : rawEntry.entity) != null ? _a : "" }; + entries[scope.index] = mutator(entry); + return entries; + }); + return this._applyUserConfig(nextConfig2, options); + } + const nextConfig = mutator(this._cloneDeep(this._draftConfig)); + return this._applyUserConfig(nextConfig, options); + } + _setScopedValue(scope, keyPath, value, options = {}) { + return this._applyScopedMutation(scope, (target) => this._setPathValue(target, this._normalizePath(keyPath), value), options); + } + _removePathsFromTarget(target, keyPaths = []) { + return keyPaths.reduce((nextTarget, keyPath) => this._deletePathValue(nextTarget, this._normalizePath(keyPath)), target); + } + _pruneEmptyObjectsInTarget(target, keyPath) { + let nextTarget = target; + const normalizedPath = this._normalizePath(keyPath); + for (let index = normalizedPath.length; index > 0; index--) { + const currentPath = normalizedPath.slice(0, index); + const currentValue = this._getPathValue(nextTarget, currentPath); + if (!this._isObject(currentValue) || Object.keys(currentValue).length) { + break; + } + nextTarget = this._deletePathValue(nextTarget, currentPath); + } + return nextTarget; + } + _setCanonicalScopedValue(scope, canonicalPath, value, options = {}) { + const { deprecatedKeys = [], prunePaths = [] } = options; + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._setPathValue(target, this._normalizePath(canonicalPath), value); + nextTarget = this._removePathsFromTarget(nextTarget, deprecatedKeys); + const pathsToPrune = [this._normalizePath(canonicalPath).slice(0, -1), ...prunePaths.map((path) => this._normalizePath(path))]; + pathsToPrune.forEach((path) => { + if (path.length) { + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, path); + } + }); + return nextTarget; + }, options); + } + _removeCanonicalScopedValue(scope, canonicalPath, options = {}) { + const { deprecatedKeys = [], prunePaths = [] } = options; + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._deletePathValue(target, this._normalizePath(canonicalPath)); + nextTarget = this._removePathsFromTarget(nextTarget, deprecatedKeys); + const pathsToPrune = [this._normalizePath(canonicalPath).slice(0, -1), ...prunePaths.map((path) => this._normalizePath(path))]; + pathsToPrune.forEach((path) => { + if (path.length) { + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, path); + } + }); + return nextTarget; + }, options); + } + _setScopedNumericOverride(scope, keyPath, rawValue, options = {}) { + const normalizedValue = this._normalizeNumberValue(rawValue); + if (rawValue === "" || rawValue === null || rawValue === void 0) { + return this._removeScopedValue(scope, keyPath, options); + } + if (normalizedValue === null) { + return false; + } + return this._setScopedValue(scope, keyPath, normalizedValue, options); + } + _setScopedTextOverride(scope, keyPath, rawValue, options = {}) { + const normalizedValue = this._normalizeTextValue(rawValue).trim(); + if (!normalizedValue) { + return this._removeScopedValue(scope, keyPath, options); + } + return this._setScopedValue(scope, keyPath, normalizedValue, options); + } + _setCanonicalScopedNumericOverride(scope, canonicalPath, rawValue, options = {}) { + const normalizedValue = this._normalizeNumberValue(rawValue); + if (rawValue === "" || rawValue === null || rawValue === void 0 || normalizedValue === null) { + return this._removeCanonicalScopedValue(scope, canonicalPath, options); + } + return this._setCanonicalScopedValue(scope, canonicalPath, normalizedValue, options); + } + _setCanonicalScopedTextOverride(scope, canonicalPath, rawValue, options = {}) { + const normalizedValue = this._normalizeTextValue(rawValue).trim(); + if (!normalizedValue) { + return this._removeCanonicalScopedValue(scope, canonicalPath, options); + } + return this._setCanonicalScopedValue(scope, canonicalPath, normalizedValue, options); + } + _getResolvablePartsFromTarget(target, field, options = {}) { + var _a, _b, _c, _d; + const canonicalBasePath = (_a = options.canonicalBasePath) != null ? _a : ["scale", field]; + const legacyFixedPath = (_b = options.legacyFixedPath) != null ? _b : [field]; + const legacyEntityPath = (_c = options.legacyEntityPath) != null ? _c : [`${field}_entity`]; + const structuredValue = this._getPathValue(target, canonicalBasePath); + const legacyFixedValue = this._getPathValue(target, legacyFixedPath); + const legacyEntityValue = this._getPathValue(target, legacyEntityPath); + const structuredFixedValue = this._isObject(structuredValue) ? structuredValue == null ? void 0 : structuredValue.fixed : structuredValue; + const structuredEntityValue = this._isObject(structuredValue) ? structuredValue == null ? void 0 : structuredValue.entity : void 0; + return { + fixed: structuredFixedValue != null ? structuredFixedValue : !this._isObject(legacyFixedValue) && legacyFixedValue !== void 0 ? legacyFixedValue : "", + entity: (_d = structuredEntityValue != null ? structuredEntityValue : legacyEntityValue) != null ? _d : "" + }; + } + _getResolvableScopedValue(scope, field, options = {}) { + const target = (scope == null ? void 0 : scope.type) === "entity" ? this._getEntityRawEntries()[scope.index] : this._draftConfig; + return this._getResolvablePartsFromTarget(target != null ? target : {}, field, options); + } + _getEffectiveResolvableScopedValue(scope, field, options = {}) { + const localValue = this._getResolvableScopedValue(scope, field, options); + if ((scope == null ? void 0 : scope.type) !== "entity") { + return localValue; + } + const inheritedValue = this._getResolvableScopedValue({ type: "card" }, field, options); + return { + fixed: this._hasExplicitOverrideValue(localValue.fixed) ? localValue.fixed : inheritedValue.fixed, + entity: this._hasExplicitOverrideValue(localValue.entity) ? localValue.entity : inheritedValue.entity + }; + } + _setCanonicalResolvablePart(scope, field, part, rawValue, options = {}) { + var _a, _b, _c, _d; + const canonicalBasePath = (_a = options.canonicalBasePath) != null ? _a : ["scale", field]; + const legacyFixedPath = (_b = options.legacyFixedPath) != null ? _b : [field]; + const legacyEntityPath = (_c = options.legacyEntityPath) != null ? _c : [`${field}_entity`]; + const prunePaths = (_d = options.prunePaths) != null ? _d : [canonicalBasePath, canonicalBasePath.slice(0, -1)]; + const normalizedValue = part === "fixed" ? this._normalizeNumberValue(rawValue) : this._normalizeTextValue(rawValue).trim(); + return this._applyScopedMutation(scope, (target) => { + const currentParts = this._getResolvablePartsFromTarget(target != null ? target : {}, field, { + canonicalBasePath, + legacyFixedPath, + legacyEntityPath + }); + const nextParts = { ...currentParts }; + if (part === "fixed") { + if (rawValue === "" || rawValue === null || rawValue === void 0 || normalizedValue === null) { + delete nextParts.fixed; + } else { + nextParts.fixed = normalizedValue; + } + } else if (!normalizedValue) { + delete nextParts.entity; + } else { + nextParts.entity = normalizedValue; + } + let nextTarget = this._cloneDeep(target); + nextTarget = this._deletePathValue(nextTarget, legacyEntityPath); + const legacyFixedValue = this._getPathValue(nextTarget, legacyFixedPath); + if (!this._isObject(legacyFixedValue)) { + nextTarget = this._deletePathValue(nextTarget, legacyFixedPath); + } + nextTarget = this._deletePathValue(nextTarget, canonicalBasePath); + const hasFixed = nextParts.fixed !== void 0 && nextParts.fixed !== null && nextParts.fixed !== ""; + const hasEntity = nextParts.entity !== void 0 && nextParts.entity !== null && nextParts.entity !== ""; + if (hasFixed || hasEntity) { + const nextValue = {}; + if (hasFixed) nextValue.fixed = nextParts.fixed; + if (hasEntity) nextValue.entity = nextParts.entity; + nextTarget = this._setPathValue(nextTarget, canonicalBasePath, nextValue); + } + prunePaths.forEach((path) => { + if (path.length) { + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, path); + } + }); + return nextTarget; + }, options); + } + _clearCanonicalResolvableValue(scope, field, options = {}) { + var _a, _b, _c, _d; + const canonicalBasePath = (_a = options.canonicalBasePath) != null ? _a : ["scale", field]; + const legacyFixedPath = (_b = options.legacyFixedPath) != null ? _b : [field]; + const legacyEntityPath = (_c = options.legacyEntityPath) != null ? _c : [`${field}_entity`]; + const prunePaths = (_d = options.prunePaths) != null ? _d : [canonicalBasePath, canonicalBasePath.slice(0, -1)]; + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._deletePathValue(target, canonicalBasePath); + nextTarget = this._deletePathValue(nextTarget, legacyEntityPath); + const legacyFixedValue = this._getPathValue(nextTarget, legacyFixedPath); + if (!this._isObject(legacyFixedValue)) { + nextTarget = this._deletePathValue(nextTarget, legacyFixedPath); + } + prunePaths.forEach((path) => { + if (path.length) { + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, path); + } + }); + return nextTarget; + }, options); + } + _getScopedDisplayValue(scope, canonicalPath, fallbackPaths = []) { + const valuesToTry = [canonicalPath, ...fallbackPaths]; + for (const path of valuesToTry) { + const value = this._getScopedValue(scope, path); + if (value !== void 0 && value !== null && value !== "") { + return value; + } + } + return ""; + } + _getEffectiveScopedDisplayValue(scope, canonicalPath, fallbackPaths = []) { + const valuesToTry = [canonicalPath, ...fallbackPaths]; + for (const path of valuesToTry) { + const value = this._getScopedValue(scope, path); + if (value !== void 0 && value !== null && value !== "") { + return value; + } + } + if ((scope == null ? void 0 : scope.type) === "entity") { + for (const path of valuesToTry) { + const value = this._getScopedValue({ type: "card" }, path); + if (value !== void 0 && value !== null && value !== "") { + return value; + } + } + } + return ""; + } + _getScopedFormattingValue(scope, key) { + var _a, _b; + return (_b = (_a = this._getScopedValue(scope, ["formatting", key])) != null ? _a : this._getScopedValue(scope, [key])) != null ? _b : ""; + } + _getEffectiveScopedFormattingValue(scope, key) { + return this._getEffectiveScopedDisplayValue(scope, ["formatting", key], [[key]]); + } + _setScopedFormattingUnit(scope, rawValue) { + return this._setCanonicalScopedTextOverride(scope, ["formatting", "unit"], rawValue, { + deprecatedKeys: [["unit"]], + prunePaths: [["formatting"]] + }); + } + _setScopedFormattingDecimal(scope, rawValue) { + const normalizedValue = this._normalizeDecimalValue(rawValue); + if (rawValue === "" || rawValue === null || rawValue === void 0) { + return this._removeCanonicalScopedValue(scope, ["formatting", "decimal"], { + deprecatedKeys: [["decimal"]], + prunePaths: [["formatting"]] + }); + } + if (normalizedValue === null) { + return false; + } + return this._setCanonicalScopedValue(scope, ["formatting", "decimal"], normalizedValue, { + deprecatedKeys: [["decimal"]], + prunePaths: [["formatting"]] + }); + } + _clearFormattingOverride(scope) { + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._deletePathValue(target, ["formatting", "unit"]); + nextTarget = this._deletePathValue(nextTarget, ["formatting", "decimal"]); + nextTarget = this._deletePathValue(nextTarget, ["unit"]); + nextTarget = this._deletePathValue(nextTarget, ["decimal"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["formatting"]); + return nextTarget; + }, { rerender: true }); + } + _hasFormattingOverride(scope) { + var _a; + const formattingValue = (_a = this._getScopedValue(scope, ["formatting"])) != null ? _a : {}; + if (this._isObject(formattingValue) && (Object.prototype.hasOwnProperty.call(formattingValue, "unit") || Object.prototype.hasOwnProperty.call(formattingValue, "decimal"))) { + return true; + } + return this._getScopedValue(scope, ["unit"]) !== void 0 || this._getScopedValue(scope, ["decimal"]) !== void 0; + } + _getScopedLayoutValue(scope, key) { + var _a, _b, _c, _d, _e, _f, _g; + if (key === "height") { + return (_b = (_a = this._getScopedValue(scope, ["layout", "height"])) != null ? _a : this._getScopedValue(scope, ["height"])) != null ? _b : ""; + } + if (key === "position") { + return (_d = (_c = this._getScopedValue(scope, ["layout", "label", "position"])) != null ? _c : this._getScopedValue(scope, ["label_position"])) != null ? _d : ""; + } + if (key === "width") { + return (_f = (_e = this._getScopedValue(scope, ["layout", "label", "width"])) != null ? _e : this._getScopedValue(scope, ["label_width"])) != null ? _f : ""; + } + if (key === "hero_size") { + return (_g = this._getScopedValue(scope, ["layout", "label", "hero_size"])) != null ? _g : ""; + } + return ""; + } + _getEffectiveScopedLayoutValue(scope, key) { + if (key === "height") { + return this._getEffectiveScopedDisplayValue(scope, ["layout", "height"], [["height"]]); + } + if (key === "position") { + return this._getEffectiveScopedDisplayValue(scope, ["layout", "label", "position"], [["label_position"]]); + } + if (key === "width") { + return this._getEffectiveScopedDisplayValue(scope, ["layout", "label", "width"], [["label_width"]]); + } + if (key === "hero_size") { + return this._getEffectiveScopedDisplayValue(scope, ["layout", "label", "hero_size"]); + } + return ""; + } + _setScopedLayoutLabelPosition(scope, value) { + if (!value) { + return this._removeCanonicalScopedValue(scope, ["layout", "label", "position"], { + deprecatedKeys: [["label_position"]], + prunePaths: [["layout", "label"], ["layout"]], + rerender: true + }); + } + const didSet = this._setCanonicalScopedValue(scope, ["layout", "label", "position"], value, { + deprecatedKeys: [["label_position"]], + prunePaths: [["layout", "label"], ["layout"]], + rerender: true + }); + if (!didSet || value === "hero") return didSet; + this._removeCanonicalScopedValue(scope, ["layout", "label", "hero_size"], { + prunePaths: [["layout", "label"], ["layout"]], + rerender: true + }); + return true; + } + _setLayoutLabelPosition(value) { + return this._setScopedLayoutLabelPosition({ type: "card" }, value); + } + _setScopedLayoutHeroSize(scope, value) { + if (!value || value === "medium") { + return this._removeCanonicalScopedValue(scope, ["layout", "label", "hero_size"], { + prunePaths: [["layout", "label"], ["layout"]] + }); + } + return this._setCanonicalScopedValue(scope, ["layout", "label", "hero_size"], value, { + prunePaths: [["layout", "label"], ["layout"]] + }); + } + _setLayoutHeroSize(value) { + return this._setScopedLayoutHeroSize({ type: "card" }, value); + } + _setScopedLayoutHeight(scope, value) { + const numericValue = this._normalizeNumberValue(value); + if (value === "" || value === null || value === void 0) { + return this._removeCanonicalScopedValue(scope, ["layout", "height"], { + deprecatedKeys: [["height"]], + prunePaths: [["layout"]] + }); + } + if (numericValue === null || numericValue < 24) { + return false; + } + return this._setCanonicalScopedValue(scope, ["layout", "height"], numericValue, { + deprecatedKeys: [["height"]], + prunePaths: [["layout"]] + }); + } + _setLayoutHeight(value) { + return this._setScopedLayoutHeight({ type: "card" }, value); + } + _setScopedLayoutLabelWidth(scope, value) { + const numericValue = this._normalizeNumberValue(value); + if (value === "" || value === null || value === void 0) { + return this._removeCanonicalScopedValue(scope, ["layout", "label", "width"], { + deprecatedKeys: [["label_width"]], + prunePaths: [["layout", "label"], ["layout"]] + }); + } + if (numericValue === null) { + return false; + } + return this._setCanonicalScopedValue(scope, ["layout", "label", "width"], numericValue, { + deprecatedKeys: [["label_width"]], + prunePaths: [["layout", "label"], ["layout"]] + }); + } + _clearLayoutOverride(scope) { + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._deletePathValue(target, ["layout", "height"]); + nextTarget = this._deletePathValue(nextTarget, ["layout", "label", "position"]); + nextTarget = this._deletePathValue(nextTarget, ["layout", "label", "hero_size"]); + nextTarget = this._deletePathValue(nextTarget, ["layout", "label", "width"]); + nextTarget = this._deletePathValue(nextTarget, ["height"]); + nextTarget = this._deletePathValue(nextTarget, ["label_position"]); + nextTarget = this._deletePathValue(nextTarget, ["label_width"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["layout", "label"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["layout"]); + return nextTarget; + }, { rerender: true }); + } + _hasLayoutOverride(scope) { + var _a, _b; + const layoutValue = (_a = this._getScopedValue(scope, ["layout"])) != null ? _a : {}; + const labelValue = this._isObject(layoutValue) ? (_b = layoutValue.label) != null ? _b : {} : {}; + if (this._isObject(layoutValue) && (Object.prototype.hasOwnProperty.call(layoutValue, "height") || this._isObject(labelValue) && (Object.prototype.hasOwnProperty.call(labelValue, "position") || Object.prototype.hasOwnProperty.call(labelValue, "hero_size") || Object.prototype.hasOwnProperty.call(labelValue, "width")))) { + return true; + } + return this._getScopedValue(scope, ["height"]) !== void 0 || this._getScopedValue(scope, ["label_position"]) !== void 0 || this._getScopedValue(scope, ["label_width"]) !== void 0; + } + _setScaleBound(key, value) { + return this._setCanonicalResolvablePart({ type: "card" }, key, "fixed", value); + } + _clearScaleOverride(scope) { + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._deletePathValue(target, ["scale", "min"]); + nextTarget = this._deletePathValue(nextTarget, ["scale", "max"]); + nextTarget = this._deletePathValue(nextTarget, ["min"]); + nextTarget = this._deletePathValue(nextTarget, ["max"]); + nextTarget = this._deletePathValue(nextTarget, ["min_entity"]); + nextTarget = this._deletePathValue(nextTarget, ["max_entity"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["scale"]); + return nextTarget; + }, { rerender: true }); + } + _setBarFillStyle(value) { + return this._setScopedBarFillStyle({ type: "card" }, value); + } + _setBarColor(value) { + return this._setScopedBarColor({ type: "card" }, value); + } + _setGradientStops(stops, options = {}) { + return this._setScopedGradientStops({ type: "card" }, stops, options); + } + _setSegments(segments, options = {}) { + return this._setScopedSegments({ type: "card" }, segments, options); + } + _getDefaultGradientStops() { + return [ + { pos: 0, color: "#4CAF50" }, + { pos: 50, color: "#FF9800" }, + { pos: 100, color: "#F44336" } + ]; + } + _normalizeGradientStopPosValue(rawValue) { + const numericValue = this._normalizeNumberValue(rawValue); + if (numericValue === null || !Number.isFinite(numericValue)) { + return null; + } + if (numericValue < 0 || numericValue > 100) { + return null; + } + return numericValue; + } + _sanitizeGradientStopsForEmit(stops) { + if (!Array.isArray(stops)) { + return []; + } + return stops.map((stop) => { + if (!this._isObject(stop)) { + return null; + } + const pos = this._normalizeGradientStopPosValue(stop.pos); + const color = this._normalizeTextValue(stop.color).trim(); + if (pos === null || !color) { + return null; + } + return { + ...stop, + pos, + color + }; + }).filter(Boolean).sort((left, right) => left.pos - right.pos); + } + _getGradientStopDraftColorDefault(scope = { type: "card" }) { + var _a; + const committedStops = this._sanitizeGradientStopsForEmit(this._getScopedGradientStopsValue(scope)); + if (committedStops.length) { + return (_a = committedStops[committedStops.length - 1].color) != null ? _a : "#4CAF50"; + } + return this._getDefaultGradientStops()[0].color; + } + _getNextSuggestedGradientStopPos(scope = { type: "card" }) { + const committedStops = this._sanitizeGradientStopsForEmit(this._getScopedGradientStopsValue(scope)); + if (!committedStops.length) { + return 0; + } + const highest = committedStops[committedStops.length - 1]; + if (highest.pos >= 100) { + return ""; + } + let suggestedPos; + if (committedStops.length === 1) { + suggestedPos = highest.pos + 25; + } else { + const previous = committedStops[committedStops.length - 2]; + suggestedPos = highest.pos + (highest.pos - previous.pos); + } + const clampedPos = Math.min(100, Math.max(0, suggestedPos)); + if (committedStops.some((stop) => stop.pos === clampedPos)) { + return ""; + } + return clampedPos; + } + _getGradientStopsDraftKey(scope) { + return (scope == null ? void 0 : scope.type) === "entity" ? `entity:${scope.index}` : "card"; + } + _getGradientStopPosTextKey(scope, stopIndex) { + return `${this._getGradientStopsDraftKey(scope)}:pos:${stopIndex}`; + } + _getGradientStopPosText(scope = { type: "card" }, stopIndex, fallbackValue = "") { + const key = this._getGradientStopPosTextKey(scope, stopIndex); + if (this._gradientStopPosTexts.has(key)) { + return this._gradientStopPosTexts.get(key); + } + if (fallbackValue === "" || fallbackValue === null || fallbackValue === void 0) { + return ""; + } + return String(fallbackValue); + } + _setGradientStopPosText(scope, stopIndex, rawValue) { + this._gradientStopPosTexts.set( + this._getGradientStopPosTextKey(scope, stopIndex), + this._normalizeTextValue(rawValue) + ); + } + _clearGradientStopPosText(scope, stopIndex) { + this._gradientStopPosTexts.delete(this._getGradientStopPosTextKey(scope, stopIndex)); + } + _clearGradientStopScopeTextState(scope) { + const prefix = `${this._getGradientStopsDraftKey(scope)}:pos:`; + for (const key of this._gradientStopPosTexts.keys()) { + if (key.startsWith(prefix)) { + this._gradientStopPosTexts.delete(key); + } + } + for (const key of this._gradientStopValidationMessages.keys()) { + if (key.startsWith(prefix)) { + this._gradientStopValidationMessages.delete(key); + } + } + } + _getGradientStopsUiRows(scope = { type: "card" }) { + const key = this._getGradientStopsDraftKey(scope); + if (this._gradientStopsUiRows.has(key)) { + return this._cloneDeep(this._gradientStopsUiRows.get(key)); + } + return null; + } + _setGradientStopsUiRows(scope, stops) { + this._gradientStopsUiRows.set(this._getGradientStopsDraftKey(scope), this._cloneDeep(stops)); + } + _getStoredScopedGradientStops(scope = { type: "card" }) { + const structuredValue = this._getScopedValue(scope, ["bar", "gradient_stops"]); + if (structuredValue !== void 0) { + return structuredValue; + } + const legacyValue = this._getScopedValue(scope, ["gradient_stops"]); + if (legacyValue !== void 0) { + return legacyValue; + } + return null; + } + _getFallbackGradientStops(scope = { type: "card" }) { + if ((scope == null ? void 0 : scope.type) === "entity") { + const inheritedStops = this._sanitizeGradientStopsForEmit(this._getScopedGradientStopsValue({ type: "card" })); + return inheritedStops.length ? inheritedStops : this._getDefaultGradientStops(); + } + return this._getDefaultGradientStops(); + } + _createGradientStopDraftState(scope = { type: "card" }) { + const suggestedPos = this._getNextSuggestedGradientStopPos(scope); + return { + pos: suggestedPos === "" ? "" : String(suggestedPos), + color: this._getGradientStopDraftColorDefault(scope) + }; + } + _getGradientStopsDraftState(scope = { type: "card" }) { + const key = this._getGradientStopsDraftKey(scope); + if (!this._gradientStopsDrafts.has(key)) { + this._gradientStopsDrafts.set(key, this._createGradientStopDraftState(scope)); + } + return this._cloneDeep(this._gradientStopsDrafts.get(key)); + } + _setGradientStopsDraftState(scope, nextDraft, options = {}) { + var _a, _b; + this._gradientStopsDrafts.set(this._getGradientStopsDraftKey(scope), { + pos: (_a = nextDraft == null ? void 0 : nextDraft.pos) != null ? _a : "", + color: (_b = nextDraft == null ? void 0 : nextDraft.color) != null ? _b : this._getGradientStopDraftColorDefault(scope) + }); + if (options == null ? void 0 : options.refreshOnly) { + this._refreshGradientDraftUi(scope); + return; + } + this._render(); + } + _setGradientStopsDraftField(scope, field, rawValue) { + const currentDraft = this._getGradientStopsDraftState(scope); + const nextValue = field === "color" ? this._normalizeTextValue(rawValue).trim() : this._normalizeTextValue(rawValue); + this._setGradientStopsDraftState(scope, { + ...currentDraft, + [field]: nextValue + }, { refreshOnly: field === "pos" }); + } + _getValidGradientDraftStop(scope = { type: "card" }) { + const draft = this._getGradientStopsDraftState(scope); + const pos = this._normalizeGradientStopPosValue(draft.pos); + const color = this._normalizeTextValue(draft.color).trim(); + if (pos === null || !color) { + return null; + } + return { pos, color }; + } + _hasGradientStopDuplicate(scope = { type: "card" }, candidatePos, excludeIndex = null) { + return this._sanitizeGradientStopsForEmit(this._getScopedGradientStopsValue(scope)).some((stop, index) => index !== excludeIndex && stop.pos === candidatePos); + } + _canAddGradientStop(scope = { type: "card" }) { + const draftStop = this._getValidGradientDraftStop(scope); + if (!draftStop) { + return false; + } + return !this._hasGradientStopDuplicate(scope, draftStop.pos); + } + _getGradientDraftValidationMessage(scope = { type: "card" }) { + const draft = this._getGradientStopsDraftState(scope); + const normalizedPosText = this._normalizeTextValue(draft.pos).trim(); + const normalizedColor = this._normalizeTextValue(draft.color).trim(); + if (!normalizedPosText) { + return "Enter a position to add a stop."; + } + if (this._normalizeGradientStopPosValue(draft.pos) === null) { + return "Enter a value from 0 to 100."; + } + if (!normalizedColor) { + return "Choose a color to add a stop."; + } + if (this._hasGradientStopDuplicate(scope, this._normalizeGradientStopPosValue(draft.pos))) { + return "Position already exists."; + } + return ""; + } + _isDefaultGradientStops(stops) { + const sanitizedStops = this._sanitizeGradientStopsForEmit(stops); + const defaultStops = this._getDefaultGradientStops(); + if (sanitizedStops.length !== defaultStops.length) { + return false; + } + return sanitizedStops.every((stop, index) => stop.pos === defaultStops[index].pos && this._normalizeColorComparisonValue(stop.color) === this._normalizeColorComparisonValue(defaultStops[index].color)); + } + _setScopedGradientStops(scope, stops, options = {}) { + var _a; + const sanitizedStops = this._sanitizeGradientStopsForEmit(stops); + const shouldRemove = sanitizedStops.length < 2 || this._isDefaultGradientStops(sanitizedStops); + const previousUiRowsJson = this._serializeConfig((_a = this._getGradientStopsUiRows(scope)) != null ? _a : []); + this._clearGradientStopScopeTextState(scope); + this._setGradientStopsUiRows(scope, sanitizedStops); + const applied = this._applyScopedMutation(scope, (target) => { + let nextTarget = this._deletePathValue(target, ["gradient_stops"]); + nextTarget = this._deletePathValue(nextTarget, ["bar", "gradient_stops"]); + if (!shouldRemove) { + nextTarget = this._setPathValue(nextTarget, ["bar", "gradient_stops"], sanitizedStops); + } + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["bar"]); + return nextTarget; + }, options); + if (applied === false && (options == null ? void 0 : options.rerender) && this._serializeConfig(sanitizedStops) !== previousUiRowsJson) { + this._render(); + } + if (applied !== false && !(options == null ? void 0 : options.rerender)) { + this._refreshGradientDraftUi(scope); + } + return applied; + } + _clearGradientStopsOverride(scope) { + var _a; + const previousUiRowsJson = this._serializeConfig((_a = this._getGradientStopsUiRows(scope)) != null ? _a : []); + this._gradientStopsDrafts.delete(this._getGradientStopsDraftKey(scope)); + this._gradientStopsUiRows.delete(this._getGradientStopsDraftKey(scope)); + this._clearGradientStopScopeTextState(scope); + const applied = this._applyScopedMutation(scope, (target) => { + let nextTarget = this._deletePathValue(target, ["bar", "gradient_stops"]); + nextTarget = this._deletePathValue(nextTarget, ["gradient_stops"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["bar"]); + return nextTarget; + }, { rerender: true }); + if (applied === false && previousUiRowsJson !== this._serializeConfig([])) { + this._render(); + } + return applied; + } + _setScopedSegments(scope, segments, options = {}) { + const nextSegments = (options == null ? void 0 : options.sort) === false ? this._cloneDeep(segments) : this._sortSegmentsForEditor(segments); + const fallbackSegments = this._getFallbackSegments(scope); + const shouldRemove = !Array.isArray(nextSegments) || !nextSegments.length || fallbackSegments.length > 0 && this._segmentsEqualForEditor(nextSegments, fallbackSegments); + this._setSegmentsUiRows(scope, nextSegments); + const applied = this._applyScopedMutation(scope, (target) => { + let nextTarget = this._deletePathValue(target, ["bar", "segments"]); + nextTarget = this._deletePathValue(nextTarget, ["segments"]); + nextTarget = this._deletePathValue(nextTarget, ["severity"]); + if (!shouldRemove) { + nextTarget = this._setPathValue(nextTarget, ["bar", "segments"], nextSegments); + } + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["bar"]); + return nextTarget; + }, options); + if (applied !== false) { + this._refreshSegmentUi(scope); + } + return applied; + } + _clearSegmentsOverride(scope) { + this._segmentDrafts.delete(this._getSegmentsScopeKey(scope)); + this._segmentUiRows.delete(this._getSegmentsScopeKey(scope)); + this._clearSegmentScopeTextState(scope); + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._deletePathValue(target, ["bar", "segments"]); + nextTarget = this._deletePathValue(nextTarget, ["segments"]); + nextTarget = this._deletePathValue(nextTarget, ["severity"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["bar"]); + return nextTarget; + }, { rerender: true }); + } + _setNeedle(value) { + return this._setScopedNeedleMode({ type: "card" }, value ? "enabled" : "disabled"); + } + _getScopedPeakConfig(scope) { + var _a, _b; + const rawPeak = this._getScopedValue(scope, ["peak"]); + const rawPeakMarker = this._getScopedValue(scope, ["peak_marker"]); + const rawLegacyShow = this._getScopedValue(scope, ["show_peak"]); + const rawLegacyColor = this._getScopedValue(scope, ["peak_color"]); + const defaultColor = "#888"; + let mode = (scope == null ? void 0 : scope.type) === "entity" ? "inherit" : "disabled"; + let color = ""; + if (this._isObject(rawPeak)) { + if (rawPeak.enabled === true) { + mode = "enabled"; + } else if (rawPeak.enabled === false) { + mode = "disabled"; + } + color = (_a = rawPeak.color) != null ? _a : color; + } + if (this._isObject(rawPeakMarker)) { + if (rawPeakMarker.show === true) { + mode = "enabled"; + } else if (rawPeakMarker.show === false) { + mode = "disabled"; + } else if ((scope == null ? void 0 : scope.type) !== "entity") { + mode = "disabled"; + } + color = (_b = rawPeakMarker.color) != null ? _b : color; + } + if (rawLegacyShow === true) { + mode = "enabled"; + } else if (rawLegacyShow === false) { + mode = "disabled"; + } + color = color || rawLegacyColor || ""; + if (color && this._normalizeColorComparisonValue(color) === this._normalizeColorComparisonValue(defaultColor)) { + color = ""; + } + return { mode, color }; + } + _getEffectiveScopedPeakConfig(scope) { + const localPeak = this._getScopedPeakConfig(scope); + if ((scope == null ? void 0 : scope.type) !== "entity") { + return localPeak; + } + if (!this._hasPeakOverride(scope)) { + return this._getScopedPeakConfig({ type: "card" }); + } + const inheritedPeak = this._getScopedPeakConfig({ type: "card" }); + return { + mode: localPeak.mode === "inherit" ? inheritedPeak.mode : localPeak.mode, + color: localPeak.color || inheritedPeak.color + }; + } + _hasPeakOverride(scope) { + var _a, _b; + const peakValue = (_a = this._getScopedValue(scope, ["peak"])) != null ? _a : {}; + if (this._isObject(peakValue) && (Object.prototype.hasOwnProperty.call(peakValue, "enabled") || Object.prototype.hasOwnProperty.call(peakValue, "color"))) { + return true; + } + const peakMarkerValue = (_b = this._getScopedValue(scope, ["peak_marker"])) != null ? _b : {}; + if (this._isObject(peakMarkerValue) && (Object.prototype.hasOwnProperty.call(peakMarkerValue, "show") || Object.prototype.hasOwnProperty.call(peakMarkerValue, "color"))) { + return true; + } + return this._getScopedValue(scope, ["show_peak"]) !== void 0 || this._getScopedValue(scope, ["peak_color"]) !== void 0; + } + _getPeakSummary(scope) { + if ((scope == null ? void 0 : scope.type) === "entity" && !this._hasPeakOverride(scope)) return "Inherited"; + const peak = this._getScopedPeakConfig(scope); + if (peak.mode === "disabled") return peak.color ? "Disabled \u2022 Custom color" : "Disabled"; + if (peak.mode === "enabled") return peak.color ? "Enabled \u2022 Custom color" : "Enabled"; + if (peak.color) return "Custom color"; + return "Inherited"; + } + _clearPeakOverride(scope) { + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._deletePathValue(target, ["peak", "enabled"]); + nextTarget = this._deletePathValue(nextTarget, ["peak", "color"]); + nextTarget = this._deletePathValue(nextTarget, ["show_peak"]); + nextTarget = this._deletePathValue(nextTarget, ["peak_color"]); + nextTarget = this._deletePathValue(nextTarget, ["peak_marker"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["peak"]); + return nextTarget; + }, { rerender: true }); + } + _setScopedPeakEnabled(scope, value) { + const boolValue = !!value; + const defaultColor = "#888"; + return this._applyScopedMutation(scope, (target) => { + var _a, _b, _c; + let nextTarget = this._cloneDeep(target); + const currentPeak = this._isObject(this._getPathValue(nextTarget, ["peak"])) ? this._cloneDeep(this._getPathValue(nextTarget, ["peak"])) : {}; + const currentColor = (_c = (_b = (_a = currentPeak.color) != null ? _a : this._isObject(this._getPathValue(nextTarget, ["peak_marker"])) ? this._getPathValue(nextTarget, ["peak_marker", "color"]) : void 0) != null ? _b : this._getPathValue(nextTarget, ["peak_color"])) != null ? _c : defaultColor; + if ((scope == null ? void 0 : scope.type) === "entity" || boolValue) { + currentPeak.enabled = boolValue; + } else { + delete currentPeak.enabled; + } + if (currentColor && this._normalizeColorComparisonValue(currentColor) !== this._normalizeColorComparisonValue(defaultColor)) { + currentPeak.color = currentColor; + } else { + delete currentPeak.color; + } + if (Object.keys(currentPeak).length) { + nextTarget = this._setPathValue(nextTarget, ["peak"], currentPeak); + } else { + nextTarget = this._deletePathValue(nextTarget, ["peak"]); + } + nextTarget = this._deletePathValue(nextTarget, ["show_peak"]); + nextTarget = this._deletePathValue(nextTarget, ["peak_color"]); + nextTarget = this._deletePathValue(nextTarget, ["peak_marker"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["peak"]); + return nextTarget; + }); + } + _setScopedPeakColor(scope, rawValue) { + const normalizedValue = this._normalizeTextValue(rawValue).trim(); + const defaultColor = "#888"; + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._cloneDeep(target); + const currentPeak = this._isObject(this._getPathValue(nextTarget, ["peak"])) ? this._cloneDeep(this._getPathValue(nextTarget, ["peak"])) : {}; + const currentConfig = this._getScopedPeakConfig(scope); + delete currentPeak.color; + if (normalizedValue && this._normalizeColorComparisonValue(normalizedValue) !== this._normalizeColorComparisonValue(defaultColor)) { + currentPeak.color = normalizedValue; + } + if ((scope == null ? void 0 : scope.type) === "entity") { + if (currentConfig.mode === "enabled") currentPeak.enabled = true; + if (currentConfig.mode === "disabled") currentPeak.enabled = false; + } else if (currentConfig.mode === "enabled") { + currentPeak.enabled = true; + } + if (Object.keys(currentPeak).length) { + nextTarget = this._setPathValue(nextTarget, ["peak"], currentPeak); + } else { + nextTarget = this._deletePathValue(nextTarget, ["peak"]); + } + nextTarget = this._deletePathValue(nextTarget, ["show_peak"]); + nextTarget = this._deletePathValue(nextTarget, ["peak_color"]); + nextTarget = this._deletePathValue(nextTarget, ["peak_marker"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["peak"]); + return nextTarget; + }); + } + _setFixedMarkerValue(rootKey, enabled, value) { + const numericValue = this._normalizeNumberValue(value); + if (!enabled || numericValue === null) { + return this._removeCanonicalScopedValue({ type: "card" }, [rootKey, "at", "fixed"], { + deprecatedKeys: rootKey === "target" ? [["target_entity"]] : [], + prunePaths: [[rootKey, "at"], [rootKey]] + }); + } + return this._setCanonicalScopedValue({ type: "card" }, [rootKey, "at", "fixed"], numericValue, { + deprecatedKeys: rootKey === "target" ? [["target_entity"]] : [], + prunePaths: [[rootKey, "at"], [rootKey]] + }); + } + _setPeakShow(value) { + return this._setScopedPeakEnabled({ type: "card" }, value); + } + _readFixedMarker(rootKey) { + const rawValue = this._draftConfig[rootKey]; + if (this._isObject(rawValue)) { + const fixed = this._isObject(rawValue == null ? void 0 : rawValue.at) ? rawValue.at.fixed : rawValue == null ? void 0 : rawValue.at; + return { + enabled: fixed !== void 0 && fixed !== null && fixed !== "", + value: fixed != null ? fixed : "" + }; + } + return { + enabled: rawValue !== void 0 && rawValue !== null && rawValue !== "", + value: rawValue != null ? rawValue : "" + }; + } + _getGradientStopsValue() { + return this._getScopedGradientStopsValue({ type: "card" }); + } + _getSegmentsValue() { + return this._getScopedSegmentsValue({ type: "card" }); + } + _getSegmentsScopeKey(scope = { type: "card" }) { + return (scope == null ? void 0 : scope.type) === "entity" ? `entity:${scope.index}` : "card"; + } + _getSegmentBoundaryTextKey(scope, segmentIndex, field) { + return `${this._getSegmentsScopeKey(scope)}:${segmentIndex}:${field}`; + } + _getSegmentBoundaryText(scope = { type: "card" }, segmentIndex, field, fallbackValue = "") { + const key = this._getSegmentBoundaryTextKey(scope, segmentIndex, field); + if (this._segmentBoundaryTexts.has(key)) { + return this._segmentBoundaryTexts.get(key); + } + return this._formatSegmentBoundaryValue(fallbackValue); + } + _setSegmentBoundaryText(scope, segmentIndex, field, rawValue) { + this._segmentBoundaryTexts.set( + this._getSegmentBoundaryTextKey(scope, segmentIndex, field), + this._normalizeTextValue(rawValue) + ); + } + _clearSegmentBoundaryText(scope, segmentIndex, field) { + this._segmentBoundaryTexts.delete(this._getSegmentBoundaryTextKey(scope, segmentIndex, field)); + } + _clearSegmentScopeTextState(scope) { + const prefix = `${this._getSegmentsScopeKey(scope)}:`; + for (const key of this._segmentBoundaryTexts.keys()) { + if (key.startsWith(prefix)) { + this._segmentBoundaryTexts.delete(key); + } + } + } + _getSegmentsUiRows(scope = { type: "card" }) { + const key = this._getSegmentsScopeKey(scope); + if (this._segmentUiRows.has(key)) { + return this._cloneDeep(this._segmentUiRows.get(key)); + } + return null; + } + _setSegmentsUiRows(scope, rows) { + this._segmentUiRows.set(this._getSegmentsScopeKey(scope), this._cloneDeep(rows)); + } + _getSegmentDraftState(scope = { type: "card" }) { + const key = this._getSegmentsScopeKey(scope); + if (!this._segmentDrafts.has(key)) { + this._segmentDrafts.set(key, this._createSegmentDraftState(scope)); + } + return this._cloneDeep(this._segmentDrafts.get(key)); + } + _setSegmentDraftState(scope, nextDraft, options = {}) { + var _a, _b, _c; + this._segmentDrafts.set(this._getSegmentsScopeKey(scope), { + from: (_a = nextDraft == null ? void 0 : nextDraft.from) != null ? _a : "", + to: (_b = nextDraft == null ? void 0 : nextDraft.to) != null ? _b : "", + color: (_c = nextDraft == null ? void 0 : nextDraft.color) != null ? _c : this._getSegmentDraftColorDefault(scope) + }); + if (options == null ? void 0 : options.refreshOnly) { + this._refreshSegmentUi(scope); + return; + } + this._render(); + } + _setSegmentDraftField(scope, field, rawValue) { + const currentDraft = this._getSegmentDraftState(scope); + const nextValue = field === "color" ? this._normalizeTextValue(rawValue).trim() : this._normalizeTextValue(rawValue); + this._setSegmentDraftState(scope, { + ...currentDraft, + [field]: nextValue + }, { refreshOnly: true }); + } + _isSegmentFillStyle(fillStyle) { + return ["bands", "soft_bands", "band_gradient"].includes(fillStyle); + } + _getDefaultSegments() { + return [ + { from: "0%", to: "33%", color: "#4CAF50" }, + { from: "33%", to: "75%", color: "#FF9800" }, + { from: "75%", to: "100%", color: "#F44336" } + ]; + } + _getStoredScopedSegments(scope = { type: "card" }) { + const structuredValue = this._getScopedValue(scope, ["bar", "segments"]); + if (structuredValue !== void 0) { + return structuredValue; + } + const legacySegments = this._getScopedValue(scope, ["segments"]); + if (legacySegments !== void 0) { + return legacySegments; + } + const legacySeverity = this._getScopedValue(scope, ["severity"]); + if (legacySeverity !== void 0) { + return legacySeverity; + } + return null; + } + _parseSegmentBoundaryInput(rawValue) { + const normalizedValue = this._normalizeTextValue(rawValue).trim(); + if (!normalizedValue) { + return null; + } + const percentMatch = normalizedValue.match(/^\s*([+-]?(?:\d+(?:\.\d+)?|\.\d+))\s*%\s*$/); + const percent = percentMatch ? parseFloat(percentMatch[1]) : null; + if (Number.isFinite(percent)) { + return `${percent}%`; + } + const numericValue = this._normalizeNumberValue(normalizedValue); + return numericValue === null ? null : numericValue; + } + _formatSegmentBoundaryValue(value) { + if (typeof value === "string") { + return value; + } + if (typeof value === "number" && Number.isFinite(value)) { + return String(value); + } + if (this._isObject(value)) { + if (Number.isFinite(value.percent)) { + return `${value.percent}%`; + } + if (Number.isFinite(this._getFiniteNumber(value.fixed))) { + return String(this._getFiniteNumber(value.fixed)); + } + } + return ""; + } + _getSegmentDraftColorDefault(scope = { type: "card" }) { + var _a; + const segments = this._getScopedSegmentsValue(scope); + if (segments.length) { + return this._normalizeTextValue((_a = segments[segments.length - 1]) == null ? void 0 : _a.color).trim() || "#4a9eff"; + } + return "#4CAF50"; + } + _getNewSegmentDefaults(scope = { type: "card" }) { + var _a; + const segments = this._getScopedSegmentsValue(scope); + const previous = segments[segments.length - 1]; + const previousTo = (_a = previous == null ? void 0 : previous.to) != null ? _a : null; + return { + from: previousTo != null ? previousTo : "0%", + to: "100%", + color: "#4a9eff" + }; + } + _createSegmentDraftState(scope = { type: "card" }) { + var _a; + const defaults = this._getNewSegmentDefaults(scope); + const formattedFrom = this._formatSegmentBoundaryValue(defaults.from); + const formattedTo = this._formatSegmentBoundaryValue(defaults.to); + const draftFrom = formattedFrom === "100%" || formattedFrom === "100" ? "" : formattedFrom; + const draftTo = draftFrom ? formattedTo : ""; + return { + from: draftFrom, + to: draftTo, + color: (_a = defaults.color) != null ? _a : this._getSegmentDraftColorDefault(scope) + }; + } + _normalizeSegmentForEditorComparison(segment) { + if (!this._isObject(segment)) { + return null; + } + const from = this._formatSegmentBoundaryValue(segment.from).trim(); + const to = this._formatSegmentBoundaryValue(segment.to).trim(); + const color = this._normalizeColorComparisonValue(segment.color); + if (!from || !to || !color) { + return null; + } + return { from, to, color }; + } + _segmentsEqualForEditor(leftSegments, rightSegments) { + const left = Array.isArray(leftSegments) ? leftSegments.map((segment) => this._normalizeSegmentForEditorComparison(segment)).filter(Boolean) : []; + const right = Array.isArray(rightSegments) ? rightSegments.map((segment) => this._normalizeSegmentForEditorComparison(segment)).filter(Boolean) : []; + if (left.length !== right.length) { + return false; + } + return left.every((segment, index) => segment.from === right[index].from && segment.to === right[index].to && segment.color === right[index].color); + } + _getFallbackSegments(scope = { type: "card" }) { + if ((scope == null ? void 0 : scope.type) === "entity") { + const cardStoredSegments = this._getStoredScopedSegments({ type: "card" }); + if (cardStoredSegments !== null) { + return this._cloneDeep(this._getScopedSegmentsValue({ type: "card" })); + } + } + if (!this._isSegmentFillStyle(this._getEffectiveFillStyleValue(scope))) { + return []; + } + return this._cloneDeep(this._getDefaultSegments()); + } + _parseSegmentBoundaryText(rawValue) { + const normalizedValue = this._normalizeTextValue(rawValue).trim(); + if (!normalizedValue) { + return { state: "empty", value: null }; + } + const parsed = this._parseSegmentBoundaryInput(normalizedValue); + if (parsed === null) { + return { state: "invalid", value: null }; + } + return { state: "valid", value: parsed }; + } + _compareSegmentBoundaries(left, right) { + const leftValue = this._getSegmentPreviewBoundaryValue(left); + const rightValue = this._getSegmentPreviewBoundaryValue(right); + if (leftValue === null || rightValue === null) { + return null; + } + if (leftValue < rightValue) return -1; + if (leftValue > rightValue) return 1; + return 0; + } + _buildSegmentValidationRows(scope = { type: "card" }) { + var _a; + const rows = (_a = this._getSegmentsUiRows(scope)) != null ? _a : this._getScopedSegmentsValue(scope); + return rows.map((segment, index) => { + const rawFrom = this._getSegmentBoundaryText(scope, index, "from", segment == null ? void 0 : segment.from); + const rawTo = this._getSegmentBoundaryText(scope, index, "to", segment == null ? void 0 : segment.to); + const parsedFrom = this._parseSegmentBoundaryText(rawFrom); + const parsedTo = this._parseSegmentBoundaryText(rawTo); + return { + index, + rawFrom, + rawTo, + parsedFrom, + parsedTo + }; + }); + } + _getSegmentRowValidationMessage(scope = { type: "card" }, segmentIndex) { + const rows = this._buildSegmentValidationRows(scope); + const row = rows[segmentIndex]; + if (!row) { + return ""; + } + if (row.parsedFrom.state === "invalid" || row.parsedTo.state === "invalid" || row.parsedFrom.state === "empty" || row.parsedTo.state === "empty") { + return "Enter valid from/to values."; + } + if (this._compareSegmentBoundaries(row.parsedFrom.value, row.parsedTo.value) !== -1) { + return "From must be below To."; + } + const candidateFrom = this._getSegmentPreviewBoundaryValue(row.parsedFrom.value); + const candidateTo = this._getSegmentPreviewBoundaryValue(row.parsedTo.value); + for (const other of rows) { + if (other.index === segmentIndex) continue; + if (other.parsedFrom.state !== "valid" || other.parsedTo.state !== "valid") continue; + const otherFrom = this._getSegmentPreviewBoundaryValue(other.parsedFrom.value); + const otherTo = this._getSegmentPreviewBoundaryValue(other.parsedTo.value); + if (candidateFrom === otherFrom) { + return "Duplicate segment start."; + } + if (candidateFrom < otherTo && candidateTo > otherFrom) { + return "Segments overlap."; + } + } + return ""; + } + _getValidSegmentDraft(scope = { type: "card" }) { + const draft = this._getSegmentDraftState(scope); + const parsedFrom = this._parseSegmentBoundaryText(draft.from); + const parsedTo = this._parseSegmentBoundaryText(draft.to); + const color = this._normalizeTextValue(draft.color).trim(); + if (parsedFrom.state !== "valid" || parsedTo.state !== "valid" || !color) { + return null; + } + if (this._compareSegmentBoundaries(parsedFrom.value, parsedTo.value) !== -1) { + return null; + } + const candidateFrom = this._getSegmentPreviewBoundaryValue(parsedFrom.value); + const candidateTo = this._getSegmentPreviewBoundaryValue(parsedTo.value); + const rows = this._buildSegmentValidationRows(scope); + for (const row of rows) { + if (row.parsedFrom.state !== "valid" || row.parsedTo.state !== "valid") continue; + const otherFrom = this._getSegmentPreviewBoundaryValue(row.parsedFrom.value); + const otherTo = this._getSegmentPreviewBoundaryValue(row.parsedTo.value); + if (candidateFrom === otherFrom || candidateFrom < otherTo && candidateTo > otherFrom) { + return null; + } + } + return { + from: parsedFrom.value, + to: parsedTo.value, + color + }; + } + _canAddSegment(scope = { type: "card" }) { + return !!this._getValidSegmentDraft(scope); + } + _getSegmentDraftValidationMessage(scope = { type: "card" }) { + const draft = this._getSegmentDraftState(scope); + const parsedFrom = this._parseSegmentBoundaryText(draft.from); + const parsedTo = this._parseSegmentBoundaryText(draft.to); + const color = this._normalizeTextValue(draft.color).trim(); + if (!this._normalizeTextValue(draft.from).trim() && !this._normalizeTextValue(draft.to).trim()) { + return ""; + } + if (parsedFrom.state !== "valid" || parsedTo.state !== "valid") { + return "Enter valid from/to values."; + } + if (this._compareSegmentBoundaries(parsedFrom.value, parsedTo.value) !== -1) { + return "From must be below To."; + } + if (!color) { + return "Choose a color to add a segment."; + } + const candidateFrom = this._getSegmentPreviewBoundaryValue(parsedFrom.value); + const candidateTo = this._getSegmentPreviewBoundaryValue(parsedTo.value); + const rows = this._buildSegmentValidationRows(scope); + for (const row of rows) { + if (row.parsedFrom.state !== "valid" || row.parsedTo.state !== "valid") continue; + const otherFrom = this._getSegmentPreviewBoundaryValue(row.parsedFrom.value); + const otherTo = this._getSegmentPreviewBoundaryValue(row.parsedTo.value); + if (candidateFrom === otherFrom) { + return "Duplicate segment start."; + } + if (candidateFrom < otherTo && candidateTo > otherFrom) { + return "Segments overlap."; + } + } + return ""; + } + _getSegmentPreviewBoundaryValue(value) { + if (typeof value === "string") { + const match = value.trim().match(/^([+-]?(?:\d+(?:\.\d+)?|\.\d+))%$/); + if (match) { + const parsed = parseFloat(match[1]); + return Number.isFinite(parsed) ? parsed : null; + } + } + if (typeof value === "number" && Number.isFinite(value)) { + return value; + } + if (this._isObject(value)) { + if (Number.isFinite(value.percent)) { + return value.percent; + } + const fixedValue = this._getFiniteNumber(value.fixed); + if (Number.isFinite(fixedValue)) { + return fixedValue; + } + } + return null; + } + _sortSegmentsForEditor(segments) { + if (!Array.isArray(segments)) { + return []; + } + return this._cloneDeep(segments).sort((left, right) => { + const leftFrom = this._getSegmentPreviewBoundaryValue(left == null ? void 0 : left.from); + const rightFrom = this._getSegmentPreviewBoundaryValue(right == null ? void 0 : right.from); + if (leftFrom === null && rightFrom === null) return 0; + if (leftFrom === null) return 1; + if (rightFrom === null) return -1; + return leftFrom - rightFrom; + }); + } + _getSegmentPreviewRows(scope = { type: "card" }) { + var _a; + const baseSegments = (_a = this._getSegmentsUiRows(scope)) != null ? _a : this._getScopedSegmentsValue(scope); + const previewSegments = this._sortSegmentsForEditor(baseSegments); + const validDraft = this._getValidSegmentDraft(scope); + if (validDraft) { + previewSegments.push(validDraft); + } + return this._sortSegmentsForEditor(previewSegments).filter((segment) => { + const from = this._getSegmentPreviewBoundaryValue(segment == null ? void 0 : segment.from); + const to = this._getSegmentPreviewBoundaryValue(segment == null ? void 0 : segment.to); + return from !== null && to !== null && typeof (segment == null ? void 0 : segment.color) === "string" && segment.color.trim(); + }); + } + _buildEditorSegmentPreviewStyle(scope = { type: "card" }) { + const segments = this._getSegmentPreviewRows(scope); + if (!segments.length) { + return ""; + } + const fillStyle = this._getEffectiveFillStyleValue(scope); + const stops = []; + segments.forEach((segment) => { + const from = Math.max(0, Math.min(100, this._getSegmentPreviewBoundaryValue(segment.from))); + const to = Math.max(0, Math.min(100, this._getSegmentPreviewBoundaryValue(segment.to))); + stops.push(`${segment.color} ${from}%`, `${segment.color} ${to}%`); + }); + if (fillStyle === "bands") { + return `background:linear-gradient(to right,${stops.join(",")});background-repeat:no-repeat;`; + } + return `background:linear-gradient(to right,${stops.join(",")});background-repeat:no-repeat;`; + } + _getSegmentPreviewDomIds(scope = { type: "card" }) { + if ((scope == null ? void 0 : scope.type) === "entity") { + return { + previewId: `entity-${scope.index}-segment-preview`, + trackId: `entity-${scope.index}-segment-preview-track` + }; + } + return { + previewId: "card-segment-preview", + trackId: "card-segment-preview-track" + }; + } + _renderSegmentPreview(scope = { type: "card" }) { + var _a; + const { previewId, trackId } = this._getSegmentPreviewDomIds(scope); + const segments = this._getSegmentPreviewRows(scope); + const markers = []; + segments.forEach((segment) => { + const from = this._getSegmentPreviewBoundaryValue(segment.from); + const to = this._getSegmentPreviewBoundaryValue(segment.to); + if (from !== null) markers.push(from); + if (to !== null) markers.push(to); + }); + const uniqueMarkers = [...new Set(markers)].sort((left, right) => left - right); + return ` +
    +
    + ${uniqueMarkers.map((marker, index) => ` + + `).join("")} +
    +
    + `; + } + _refreshSegmentPreview(scope = { type: "card" }) { + var _a; + const { previewId, trackId } = this._getSegmentPreviewDomIds(scope); + const track = this._getShadowElementById(trackId); + if (!track) { + return; + } + track.setAttribute("style", (_a = this._buildEditorSegmentPreviewStyle(scope)) != null ? _a : ""); + const segments = this._getSegmentPreviewRows(scope); + const markers = []; + segments.forEach((segment) => { + const from = this._getSegmentPreviewBoundaryValue(segment.from); + const to = this._getSegmentPreviewBoundaryValue(segment.to); + if (from !== null) markers.push(from); + if (to !== null) markers.push(to); + }); + const uniqueMarkers = [...new Set(markers)].sort((left, right) => left - right); + track.innerHTML = uniqueMarkers.map((marker, index) => ` + + `).join(""); + } + _getSegmentDomIds(scope = { type: "card" }) { + if ((scope == null ? void 0 : scope.type) === "entity") { + return { + hintPrefix: `entity-${scope.index}-segment-row-hint-`, + draftHintId: `entity-${scope.index}-segment-draft-hint`, + addSelector: `button[data-action="add-entity-segment"][data-index="${scope.index}"]` + }; + } + return { + hintPrefix: "segment-row-hint-", + draftHintId: "segment-draft-hint", + addSelector: 'button[data-action="add-segment"]' + }; + } + _refreshSegmentUi(scope = { type: "card" }) { + var _a, _b; + this._refreshSegmentPreview(scope); + if (!this.shadowRoot) { + return; + } + const { hintPrefix, draftHintId, addSelector } = this._getSegmentDomIds(scope); + const addButton = this.shadowRoot.querySelector(addSelector); + if (addButton) { + addButton.disabled = !this._canAddSegment(scope); + } + const rows = (_a = this._getSegmentsUiRows(scope)) != null ? _a : this._getScopedSegmentsValue(scope); + rows.forEach((_, index) => { + var _a2; + const hint = this._getShadowElementById(`${hintPrefix}${index}`); + const message = this._getSegmentRowValidationMessage(scope, index); + if (hint) { + hint.textContent = message; + (_a2 = hint.setAttribute) == null ? void 0 : _a2.call(hint, "style", message ? "" : "display:none"); + } + }); + const draftHint = this._getShadowElementById(draftHintId); + if (draftHint) { + const message = this._getSegmentDraftValidationMessage(scope); + draftHint.textContent = message; + (_b = draftHint.setAttribute) == null ? void 0 : _b.call(draftHint, "style", message ? "" : "display:none"); + } + } + _commitSegmentDraft(scope = { type: "card" }) { + var _a; + const draftSegment = this._getValidSegmentDraft(scope); + if (!draftSegment) { + this._refreshSegmentUi(scope); + return false; + } + const committedSegments = (_a = this._getSegmentsUiRows(scope)) != null ? _a : this._getScopedSegmentsValue(scope); + const nextSegments = this._sortSegmentsForEditor([...committedSegments, draftSegment]); + const applied = this._setScopedSegments(scope, nextSegments, { rerender: true }); + if (applied !== false) { + this._segmentDrafts.set(this._getSegmentsScopeKey(scope), this._createSegmentDraftState(scope)); + } + return applied; + } + _commitSegmentBoundaryEdit(scope = { type: "card" }, segmentIndex, field, rawValue, inputEl = null) { + var _a, _b; + this._setSegmentBoundaryText(scope, segmentIndex, field, rawValue); + const normalizedText = this._normalizeTextValue(rawValue).trim(); + const parsedValue = this._parseSegmentBoundaryInput(rawValue); + const nextValue = parsedValue === null ? normalizedText : parsedValue; + const currentSegments = (_a = this._getSegmentsUiRows(scope)) != null ? _a : this._getScopedSegmentsValue(scope); + const nextSegments = currentSegments.map((segment, currentIndex) => currentIndex === segmentIndex ? { ...segment, [field]: nextValue } : segment); + this._clearSegmentBoundaryText(scope, segmentIndex, field); + const applied = this._setScopedSegments(scope, nextSegments, { rerender: true }); + const message = this._getSegmentRowValidationMessage(scope, segmentIndex); + if (inputEl == null ? void 0 : inputEl.setCustomValidity) { + inputEl.setCustomValidity(message || ""); + if (message) { + (_b = inputEl.reportValidity) == null ? void 0 : _b.call(inputEl); + } + } + return applied; + } + _commitGradientStopDraft(scope = { type: "card" }) { + const draftStop = this._getValidGradientDraftStop(scope); + if (!draftStop || this._hasGradientStopDuplicate(scope, draftStop.pos)) { + this._refreshGradientDraftUi(scope); + return false; + } + const committedStops = this._sanitizeGradientStopsForEmit(this._getScopedGradientStopsValue(scope)); + const nextStops = [...committedStops, draftStop].sort((left, right) => left.pos - right.pos); + const applied = this._setScopedGradientStops(scope, nextStops, { rerender: true }); + if (applied !== false) { + this._gradientStopsDrafts.set(this._getGradientStopsDraftKey(scope), { + pos: (() => { + const suggestion = this._getNextSuggestedGradientStopPos(scope); + return suggestion === "" ? "" : String(suggestion); + })(), + color: draftStop.color + }); + } + return applied; + } + _getGradientPreviewDomIds(scope = { type: "card" }) { + if ((scope == null ? void 0 : scope.type) === "entity") { + return { + previewId: `entity-${scope.index}-gradient-preview`, + trackId: `entity-${scope.index}-gradient-preview-track`, + hintId: `entity-${scope.index}-gradient-draft-hint`, + addSelector: `button[data-action="add-entity-gradient-stop"][data-index="${scope.index}"]`, + draftInputId: `entity-${scope.index}-gradient-draft-pos` + }; + } + return { + previewId: "card-gradient-preview", + trackId: "card-gradient-preview-track", + hintId: "gradient-draft-hint", + addSelector: 'button[data-action="add-gradient-stop"]', + draftInputId: "gradient-draft-pos" + }; + } + _refreshGradientDraftUi(scope = { type: "card" }) { + var _a, _b, _c; + if (!this.shadowRoot) { + return; + } + const getById = (_b = (_a = this.shadowRoot.getElementById) == null ? void 0 : _a.bind(this.shadowRoot)) != null ? _b : ((id) => { + var _a2, _b2, _c2; + return (_c2 = (_b2 = (_a2 = this.shadowRoot).querySelector) == null ? void 0 : _b2.call(_a2, `#${id}`)) != null ? _c2 : null; + }); + const { previewId, trackId, hintId, addSelector, draftInputId } = this._getGradientPreviewDomIds(scope); + const addButton = this.shadowRoot.querySelector(addSelector); + if (addButton) { + addButton.disabled = !this._canAddGradientStop(scope); + } + const draftInput = getById(draftInputId); + if (draftInput && typeof draftInput.closest !== "function") { + this._render(); + return; + } + const draftContainer = draftInput == null ? void 0 : draftInput.closest(".gradient-stop-draft"); + const nextMessage = this._getGradientDraftValidationMessage(scope); + const existingHint = getById(hintId); + if (nextMessage) { + if (existingHint) { + existingHint.textContent = nextMessage; + } else if (draftContainer) { + const hint = document.createElement("div"); + hint.id = hintId; + hint.className = "section-note"; + hint.textContent = nextMessage; + draftContainer.appendChild(hint); + } + } else if (existingHint) { + existingHint.remove(); + } + const preview = getById(previewId); + const track = getById(trackId); + if (!preview || !track) { + return; + } + track.setAttribute("style", (_c = this._getGradientPreviewStyle(scope)) != null ? _c : ""); + const markerStops = this._buildGradientPreviewEffectiveStops(scope); + const renderedStops = markerStops.length ? markerStops : this._getDefaultGradientStops(); + track.innerHTML = renderedStops.map((stop, index) => ` + + `).join(""); + } + _commitGradientStopPosEdit(scope = { type: "card" }, stopIndex, rawValue, inputEl = null) { + const nextPos = this._normalizeGradientStopPosValue(rawValue); + if (nextPos === null || this._hasGradientStopDuplicate(scope, nextPos, stopIndex)) { + if (inputEl == null ? void 0 : inputEl.setCustomValidity) { + inputEl.setCustomValidity(nextPos === null ? "Enter a value from 0 to 100." : "Position already exists."); + if (inputEl.reportValidity) { + inputEl.reportValidity(); + } + } + return false; + } + if (inputEl == null ? void 0 : inputEl.setCustomValidity) { + inputEl.setCustomValidity(""); + } + const currentStops = this._getScopedGradientStopsValue(scope); + const nextStops = currentStops.map((stop, currentStopIndex) => currentStopIndex === stopIndex ? { ...stop, pos: nextPos } : stop); + this._clearGradientStopPosText(scope, stopIndex); + return this._setScopedGradientStops(scope, nextStops, { rerender: true }); + } + _getFillStyleValue() { + return this._getScopedFillStyleValue({ type: "card" }); + } + _getFillStyleFromColorMode(colorMode) { + switch (colorMode) { + case "single": + return "solid"; + case "gradient": + return "gradient"; + case "severity": + return "bands"; + case "severity_gradient": + return "band_gradient"; + default: + return "bands"; + } + } + _getScopedFillStyleValue(scope) { + var _a; + const fillStyle = this._getScopedValue(scope, ["bar", "fill_style"]); + if (fillStyle) return fillStyle; + const colorMode = (_a = this._getScopedValue(scope, ["bar", "color_mode"])) != null ? _a : this._getScopedValue(scope, ["color_mode"]); + return this._getFillStyleFromColorMode(colorMode); + } + _getEffectiveScopedFillStyleValue(scope) { + if ((scope == null ? void 0 : scope.type) !== "entity") { + return this._getScopedFillStyleValue(scope); + } + return this._getEffectiveFillStyleValue(scope); + } + _setScopedBarFillStyle(scope, rawValue) { + const normalizedValue = this._normalizeTextValue(rawValue).trim(); + if (!normalizedValue || normalizedValue === "bands") { + return this._removeCanonicalScopedValue(scope, ["bar", "fill_style"], { + deprecatedKeys: [["color_mode"]], + prunePaths: [["bar"]] + }); + } + return this._setCanonicalScopedTextOverride(scope, ["bar", "fill_style"], normalizedValue, { + deprecatedKeys: [["color_mode"]], + prunePaths: [["bar"]] + }); + } + _getScopedBarColorValue(scope) { + var _a, _b; + return (_b = (_a = this._getScopedValue(scope, ["bar", "color"])) != null ? _a : this._getScopedValue(scope, ["color"])) != null ? _b : "#4a9eff"; + } + _getEffectiveScopedBarColorValue(scope) { + const value = this._getEffectiveScopedDisplayValue(scope, ["bar", "color"], [["color"]]); + return value || "#4a9eff"; + } + _setScopedBarColor(scope, rawValue) { + const normalizedValue = this._normalizeTextValue(rawValue).trim(); + if (!normalizedValue || this._normalizeColorComparisonValue(normalizedValue) === this._normalizeColorComparisonValue("#4a9eff")) { + return this._removeCanonicalScopedValue(scope, ["bar", "color"], { + deprecatedKeys: [["color"]], + prunePaths: [["bar"]] + }); + } + return this._setCanonicalScopedTextOverride(scope, ["bar", "color"], normalizedValue, { + deprecatedKeys: [["color"]], + prunePaths: [["bar"]] + }); + } + _getScopedBarSolidFillValue(scope) { + return !!this._getScopedValue(scope, ["bar", "solid_fill"]); + } + _getEffectiveScopedBarSolidFillValue(scope) { + if ((scope == null ? void 0 : scope.type) !== "entity") { + return this._getScopedBarSolidFillValue(scope); + } + const localValue = this._getScopedValue(scope, ["bar", "solid_fill"]); + if (localValue !== void 0) { + return !!localValue; + } + return this._getScopedBarSolidFillValue({ type: "card" }); + } + _setScopedBarSolidFill(scope, value) { + if (!value) { + return this._removeCanonicalScopedValue(scope, ["bar", "solid_fill"], { + prunePaths: [["bar"]] + }); + } + return this._setCanonicalScopedValue(scope, ["bar", "solid_fill"], true, { + prunePaths: [["bar"]] + }); + } + _clearEntityBarAppearance(scope) { + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._deletePathValue(target, ["bar", "fill_style"]); + nextTarget = this._deletePathValue(nextTarget, ["bar", "color"]); + nextTarget = this._deletePathValue(nextTarget, ["bar", "solid_fill"]); + nextTarget = this._deletePathValue(nextTarget, ["color_mode"]); + nextTarget = this._deletePathValue(nextTarget, ["color"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["bar"]); + return nextTarget; + }, { rerender: true }); + } + _hasEntityBarAppearanceOverride(scope) { + var _a; + const barValue = (_a = this._getScopedValue(scope, ["bar"])) != null ? _a : {}; + if (this._isObject(barValue) && (Object.prototype.hasOwnProperty.call(barValue, "fill_style") || Object.prototype.hasOwnProperty.call(barValue, "color") || Object.prototype.hasOwnProperty.call(barValue, "solid_fill"))) { + return true; + } + return this._getScopedValue(scope, ["color_mode"]) !== void 0 || this._getScopedValue(scope, ["color"]) !== void 0; + } + _getScopedNeedleConfig(scope) { + var _a; + const rawNeedle = this._getScopedValue(scope, ["bar", "needle"]); + const defaultColor = "#ffffff"; + let mode = (scope == null ? void 0 : scope.type) === "entity" ? "inherit" : "disabled"; + let color = ""; + if (typeof rawNeedle === "boolean") { + mode = rawNeedle ? "enabled" : "disabled"; + } else if (this._isObject(rawNeedle)) { + if (rawNeedle.show === true) { + mode = "enabled"; + } else if (rawNeedle.show === false) { + mode = "disabled"; + } else if ((scope == null ? void 0 : scope.type) !== "entity") { + mode = "disabled"; + } + color = (_a = rawNeedle.color) != null ? _a : ""; + } + if (color === defaultColor) { + color = ""; + } + return { mode, color }; + } + _hasNeedleOverride(scope) { + return this._getScopedValue(scope, ["bar", "needle"]) !== void 0; + } + _getEffectiveScopedNeedleConfig(scope) { + const localNeedle = this._getScopedNeedleConfig(scope); + if ((scope == null ? void 0 : scope.type) !== "entity") { + return localNeedle; + } + if (!this._hasNeedleOverride(scope)) { + return this._getScopedNeedleConfig({ type: "card" }); + } + const inheritedNeedle = this._getScopedNeedleConfig({ type: "card" }); + return { + mode: localNeedle.mode === "inherit" ? inheritedNeedle.mode : localNeedle.mode, + color: localNeedle.color || inheritedNeedle.color + }; + } + _setScopedNeedleMode(scope, mode) { + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._cloneDeep(target); + const existingNeedle = this._getPathValue(nextTarget, ["bar", "needle"]); + const existingColor = this._isObject(existingNeedle) ? existingNeedle.color : void 0; + nextTarget = this._deletePathValue(nextTarget, ["bar", "needle"]); + if ((scope == null ? void 0 : scope.type) === "entity" && mode === "inherit") { + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["bar"]); + return nextTarget; + } + if (mode === "disabled") { + if ((scope == null ? void 0 : scope.type) === "entity") { + nextTarget = this._setPathValue(nextTarget, ["bar", "needle"], { show: false }); + } + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["bar"]); + return nextTarget; + } + const nextNeedle = { show: true }; + if (existingColor && existingColor !== "#ffffff") { + nextNeedle.color = existingColor; + } + nextTarget = this._setPathValue(nextTarget, ["bar", "needle"], nextNeedle); + const baselineEnabled = this._getPathValue(nextTarget, ["baseline", "enabled"]); + const baselineParts = this._getResolvablePartsFromTarget(nextTarget != null ? nextTarget : {}, "baseline", { + canonicalBasePath: ["baseline", "at"], + legacyFixedPath: ["baseline"], + legacyEntityPath: ["baseline", "at", "entity"] + }); + const baselineActive = baselineEnabled === true || baselineEnabled !== false && this._hasResolvableOverride(baselineParts); + if (baselineActive) { + nextTarget = this._deletePathValue(nextTarget, ["baseline"]); + } + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["bar"]); + return nextTarget; + }); + } + _setScopedNeedleColor(scope, rawValue) { + const normalizedValue = this._normalizeTextValue(rawValue).trim(); + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._cloneDeep(target); + const current = this._getScopedNeedleConfig(scope); + const hasCustomColor = normalizedValue && this._normalizeColorComparisonValue(normalizedValue) !== this._normalizeColorComparisonValue("#ffffff"); + nextTarget = this._deletePathValue(nextTarget, ["bar", "needle", "color"]); + if ((scope == null ? void 0 : scope.type) !== "entity" && current.mode === "disabled") { + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["bar", "needle"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["bar"]); + return nextTarget; + } + const showValue = current.mode === "enabled" ? true : current.mode === "disabled" ? false : void 0; + const nextNeedle = {}; + if (showValue !== void 0) { + nextNeedle.show = showValue; + } + if (hasCustomColor) { + nextNeedle.color = normalizedValue; + } + if (Object.keys(nextNeedle).length) { + nextTarget = this._setPathValue(nextTarget, ["bar", "needle"], nextNeedle); + } else { + nextTarget = this._deletePathValue(nextTarget, ["bar", "needle"]); + } + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["bar", "needle"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["bar"]); + return nextTarget; + }); + } + _getNeedleValue() { + return this._getScopedNeedleConfig({ type: "card" }).mode === "enabled"; + } + _getPeakShowValue() { + return this._getScopedPeakConfig({ type: "card" }).mode === "enabled"; + } + _getScaleFixedValue(key, fallbackKey) { + return this._getResolvableScopedValue({ type: "card" }, key).fixed; + } + _getScaleEntityValue(key) { + return this._getResolvableScopedValue({ type: "card" }, key).entity; + } + _getTargetResolvableValue(scope) { + return this._getResolvableScopedValue(scope, "target", { + canonicalBasePath: ["target", "at"], + legacyFixedPath: ["target"], + legacyEntityPath: ["target_entity"] + }); + } + _getEffectiveTargetResolvableValue(scope) { + return this._getEffectiveResolvableScopedValue(scope, "target", { + canonicalBasePath: ["target", "at"], + legacyFixedPath: ["target"], + legacyEntityPath: ["target_entity"] + }); + } + _getTargetMode(scope) { + const enabled = this._getScopedValue(scope, ["target", "enabled"]); + if ((scope == null ? void 0 : scope.type) === "entity") { + if (enabled === false) return "disabled"; + if (enabled === true || this._hasTargetOverride(scope)) return "enabled"; + return "inherit"; + } + if (enabled === true) return "enabled"; + if (enabled === false) return "disabled"; + return "auto"; + } + _getEffectiveTargetMode(scope) { + const mode = this._getTargetMode(scope); + if ((scope == null ? void 0 : scope.type) !== "entity" || mode !== "inherit") { + return mode; + } + const cardMode = this._getTargetMode({ type: "card" }); + if (cardMode === "disabled") return "disabled"; + if (cardMode === "enabled") return "enabled"; + const cardTarget = this._getTargetResolvableValue({ type: "card" }); + return this._hasResolvableOverride(cardTarget) || this._hasCustomTargetColor({ type: "card" }) || this._getTargetLabelShowValue({ type: "card" }) || !!this._getTargetAboveFillColorValue({ type: "card" }) ? "enabled" : "disabled"; + } + _setTargetMode(scope, mode) { + if ((scope == null ? void 0 : scope.type) === "entity" && mode === "inherit") { + return this._clearTargetOverride(scope); + } + if (mode === "auto") { + return this._removeCanonicalScopedValue(scope, ["target", "enabled"], { + prunePaths: [["target"]] + }); + } + return this._setCanonicalScopedValue(scope, ["target", "enabled"], mode === "enabled", { + prunePaths: [["target"]] + }); + } + _setTargetResolvablePart(scope, part, rawValue) { + return this._setCanonicalResolvablePart(scope, "target", part, rawValue, { + canonicalBasePath: ["target", "at"], + legacyFixedPath: ["target"], + legacyEntityPath: ["target_entity"], + prunePaths: [["target", "at"], ["target"]] + }); + } + _clearTargetOverride(scope) { + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._cloneDeep(target); + const rawTarget = this._getPathValue(nextTarget, ["target"]); + if (this._isObject(rawTarget)) { + nextTarget = this._deletePathValue(nextTarget, ["target", "enabled"]); + nextTarget = this._deletePathValue(nextTarget, ["target", "at"]); + nextTarget = this._deletePathValue(nextTarget, ["target", "color"]); + nextTarget = this._deletePathValue(nextTarget, ["target", "label", "show"]); + nextTarget = this._deletePathValue(nextTarget, ["target", "when_exceeded", "fill_color"]); + } else { + nextTarget = this._deletePathValue(nextTarget, ["target"]); + } + nextTarget = this._deletePathValue(nextTarget, ["target_entity"]); + nextTarget = this._deletePathValue(nextTarget, ["target_color"]); + nextTarget = this._deletePathValue(nextTarget, ["show_target_label"]); + nextTarget = this._deletePathValue(nextTarget, ["above_target_color"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["target", "label"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["target", "when_exceeded"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["target"]); + return nextTarget; + }, { rerender: true }); + } + _getTargetColorValue(scope) { + var _a, _b; + return (_b = (_a = this._getScopedValue(scope, ["target", "color"])) != null ? _a : this._getScopedValue(scope, ["target_color"])) != null ? _b : ""; + } + _getEffectiveTargetColorValue(scope) { + return this._getEffectiveScopedDisplayValue(scope, ["target", "color"], [["target_color"]]); + } + _hasCustomTargetColor(scope) { + const color = this._getTargetColorValue(scope); + return !!color && this._normalizeColorComparisonValue(color) !== this._normalizeColorComparisonValue("#888"); + } + _setTargetColor(scope, rawValue) { + const normalizedValue = this._normalizeTextValue(rawValue).trim(); + if (!normalizedValue || this._normalizeColorComparisonValue(normalizedValue) === this._normalizeColorComparisonValue("#888")) { + return this._removeCanonicalScopedValue(scope, ["target", "color"], { + deprecatedKeys: [["target_color"]], + prunePaths: [["target"]] + }); + } + return this._setCanonicalScopedTextOverride(scope, ["target", "color"], normalizedValue, { + deprecatedKeys: [["target_color"]], + prunePaths: [["target"]] + }); + } + _getTargetLabelShowValue(scope) { + const structuredValue = this._getScopedValue(scope, ["target", "label", "show"]); + if (structuredValue !== void 0) { + return !!structuredValue; + } + return !!this._getScopedValue(scope, ["show_target_label"]); + } + _getEffectiveTargetLabelShowValue(scope) { + const structuredValue = this._getScopedValue(scope, ["target", "label", "show"]); + if (structuredValue !== void 0) { + return !!structuredValue; + } + const legacyValue = this._getScopedValue(scope, ["show_target_label"]); + if (legacyValue !== void 0) { + return !!legacyValue; + } + if ((scope == null ? void 0 : scope.type) === "entity") { + return this._getTargetLabelShowValue({ type: "card" }); + } + return false; + } + _setTargetLabelShow(scope, value) { + if (!value) { + return this._removeCanonicalScopedValue(scope, ["target", "label", "show"], { + deprecatedKeys: [["show_target_label"]], + prunePaths: [["target", "label"], ["target"]] + }); + } + return this._setCanonicalScopedValue(scope, ["target", "label", "show"], true, { + deprecatedKeys: [["show_target_label"]], + prunePaths: [["target", "label"], ["target"]] + }); + } + _getTargetAboveFillColorValue(scope) { + var _a, _b; + return (_b = (_a = this._getScopedValue(scope, ["target", "when_exceeded", "fill_color"])) != null ? _a : this._getScopedValue(scope, ["above_target_color"])) != null ? _b : ""; + } + _getTargetAboveFillDraftKey(scope = { type: "card" }) { + return (scope == null ? void 0 : scope.type) === "entity" ? `entity:${scope.index}` : "card"; + } + _setTargetAboveFillDraft(scope, rawValue) { + const normalizedValue = this._normalizeTextValue(rawValue).trim(); + const key = this._getTargetAboveFillDraftKey(scope); + if (normalizedValue) { + this._targetAboveFillDrafts.set(key, normalizedValue); + } else { + this._targetAboveFillDrafts.delete(key); + } + } + _getTargetAboveFillDraft(scope) { + var _a; + return (_a = this._targetAboveFillDrafts.get(this._getTargetAboveFillDraftKey(scope))) != null ? _a : ""; + } + _getBaselineColorDraftKey(scope = { type: "card" }, direction = "above") { + const scopeKey = (scope == null ? void 0 : scope.type) === "entity" ? `entity:${scope.index}` : "card"; + return `${scopeKey}:${direction}`; + } + _setBaselineColorDraft(scope, direction, rawValue) { + const normalizedValue = this._normalizeTextValue(rawValue).trim(); + const key = this._getBaselineColorDraftKey(scope, direction); + if (normalizedValue) { + this._baselineColorDrafts.set(key, normalizedValue); + } else { + this._baselineColorDrafts.delete(key); + } + } + _getBaselineColorDraft(scope, direction) { + var _a; + return (_a = this._baselineColorDrafts.get(this._getBaselineColorDraftKey(scope, direction))) != null ? _a : ""; + } + _getEffectiveTargetAboveFillColorValue(scope) { + return this._getEffectiveScopedDisplayValue(scope, ["target", "when_exceeded", "fill_color"], [["above_target_color"]]); + } + _setTargetAboveFillColor(scope, rawValue) { + const normalizedValue = this._normalizeTextValue(rawValue).trim(); + this._setTargetAboveFillDraft(scope, normalizedValue); + if (!this._isTargetAboveFillEnabled(scope)) { + return false; + } + if (!normalizedValue) { + return this._removeCanonicalScopedValue(scope, ["target", "when_exceeded", "fill_color"], { + deprecatedKeys: [["above_target_color"]], + prunePaths: [["target", "when_exceeded"], ["target"]] + }); + } + return this._setCanonicalScopedTextOverride(scope, ["target", "when_exceeded", "fill_color"], normalizedValue, { + deprecatedKeys: [["above_target_color"]], + prunePaths: [["target", "when_exceeded"], ["target"]] + }); + } + _isTargetAboveFillEnabled(scope) { + return !!this._normalizeTextValue(this._getTargetAboveFillColorValue(scope)).trim(); + } + _setTargetAboveFillEnabled(scope, value) { + const currentValue = this._normalizeTextValue(this._getTargetAboveFillColorValue(scope)).trim(); + if (!value) { + if (currentValue) { + this._setTargetAboveFillDraft(scope, currentValue); + } + return this._removeCanonicalScopedValue(scope, ["target", "when_exceeded", "fill_color"], { + deprecatedKeys: [["above_target_color"]], + prunePaths: [["target", "when_exceeded"], ["target"]] + }); + } + const nextValue = this._getTargetAboveFillDraft(scope) || this._normalizeTextValue(this._getEffectiveTargetAboveFillColorValue(scope)).trim() || currentValue || "#000000"; + return this._setCanonicalScopedTextOverride(scope, ["target", "when_exceeded", "fill_color"], nextValue, { + deprecatedKeys: [["above_target_color"]], + prunePaths: [["target", "when_exceeded"], ["target"]] + }); + } + _isBaselineDirectionalColorEnabled(scope, direction) { + return !!this._normalizeTextValue(this._getBaselineDirectionalColorValue(scope, direction)).trim(); + } + _setBaselineDirectionalColorEnabled(scope, direction, value) { + const currentValue = this._normalizeTextValue(this._getBaselineDirectionalColorValue(scope, direction)).trim(); + if (!value) { + if (currentValue) { + this._setBaselineColorDraft(scope, direction, currentValue); + } + return this._removeCanonicalScopedValue(scope, ["baseline", direction, "color"], { + prunePaths: [["baseline", direction], ["baseline"]] + }); + } + const nextValue = this._getBaselineColorDraft(scope, direction) || this._normalizeTextValue(this._getEffectiveBaselineDirectionalColorValue(scope, direction)).trim() || currentValue || "#000000"; + return this._setCanonicalScopedTextOverride(scope, ["baseline", direction, "color"], nextValue, { + prunePaths: [["baseline", direction], ["baseline"]] + }); + } + _hasTargetOverride(scope) { + const targetValue = this._getScopedValue(scope, ["target"]); + if (this._isObject(targetValue) && Object.keys(targetValue).length) { + return true; + } + if (!this._isObject(targetValue) && targetValue !== void 0 && targetValue !== null && targetValue !== "") { + return true; + } + return ["target_entity", "target_color", "show_target_label", "above_target_color"].some((key) => { + const value = this._getScopedValue(scope, [key]); + return value !== void 0 && value !== null && value !== "" && value !== false; + }); + } + _getBaselineResolvableValue(scope) { + return this._getResolvableScopedValue(scope, "baseline", { + canonicalBasePath: ["baseline", "at"], + legacyFixedPath: ["baseline"], + legacyEntityPath: ["baseline", "at", "entity"] + }); + } + _getEffectiveBaselineResolvableValue(scope) { + return this._getEffectiveResolvableScopedValue(scope, "baseline", { + canonicalBasePath: ["baseline", "at"], + legacyFixedPath: ["baseline"], + legacyEntityPath: ["baseline", "at", "entity"] + }); + } + _getBaselineMode(scope) { + const enabled = this._getScopedValue(scope, ["baseline", "enabled"]); + if ((scope == null ? void 0 : scope.type) === "entity") { + if (enabled === false) return "disabled"; + if (enabled === true || this._hasBaselineOverride(scope)) return "enabled"; + return "inherit"; + } + if (enabled === true) return "enabled"; + if (enabled === false) return "disabled"; + return "auto"; + } + _getEffectiveBaselineMode(scope) { + const mode = this._getBaselineMode(scope); + if ((scope == null ? void 0 : scope.type) !== "entity" || mode !== "inherit") { + return mode; + } + const cardMode = this._getBaselineMode({ type: "card" }); + if (cardMode === "disabled") return "disabled"; + if (cardMode === "enabled") return "enabled"; + const cardBaseline = this._getBaselineResolvableValue({ type: "card" }); + return this._hasResolvableOverride(cardBaseline) || !!this._getBaselineDirectionalColorValue({ type: "card" }, "above") || !!this._getBaselineDirectionalColorValue({ type: "card" }, "below") ? "enabled" : "disabled"; + } + _setBaselineMode(scope, mode) { + if ((scope == null ? void 0 : scope.type) === "entity" && mode === "inherit") { + return this._clearBaselineOverride(scope); + } + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._cloneDeep(target); + if (mode === "auto") { + nextTarget = this._deletePathValue(nextTarget, ["baseline", "enabled"]); + } else { + nextTarget = this._setPathValue(nextTarget, ["baseline", "enabled"], mode === "enabled"); + } + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["baseline"]); + return nextTarget; + }); + } + _setBaselineResolvablePart(scope, part, rawValue) { + const normalizedValue = part === "fixed" ? this._normalizeNumberValue(rawValue) : this._normalizeTextValue(rawValue).trim(); + return this._applyScopedMutation(scope, (target) => { + const currentParts = this._getResolvablePartsFromTarget(target != null ? target : {}, "baseline", { + canonicalBasePath: ["baseline", "at"], + legacyFixedPath: ["baseline"], + legacyEntityPath: ["baseline", "at", "entity"] + }); + const nextParts = { ...currentParts }; + if (part === "fixed") { + if (rawValue === "" || rawValue === null || rawValue === void 0 || normalizedValue === null) { + delete nextParts.fixed; + } else { + nextParts.fixed = normalizedValue; + } + } else if (!normalizedValue) { + delete nextParts.entity; + } else { + nextParts.entity = normalizedValue; + } + let nextTarget = this._cloneDeep(target); + nextTarget = this._deletePathValue(nextTarget, ["baseline", "at"]); + const hasFixed = nextParts.fixed !== void 0 && nextParts.fixed !== null && nextParts.fixed !== ""; + const hasEntity = nextParts.entity !== void 0 && nextParts.entity !== null && nextParts.entity !== ""; + if (hasFixed || hasEntity) { + const nextValue = {}; + if (hasFixed) nextValue.fixed = nextParts.fixed; + if (hasEntity) nextValue.entity = nextParts.entity; + nextTarget = this._setPathValue(nextTarget, ["baseline", "at"], nextValue); + } + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["baseline", "at"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["baseline"]); + return nextTarget; + }); + } + _removeScopedNeedle(scope) { + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._deletePathValue(target, ["bar", "needle"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["bar"]); + return nextTarget; + }); + } + _setBaselineDirectionalColor(scope, direction, rawValue) { + const normalizedValue = this._normalizeTextValue(rawValue).trim(); + this._setBaselineColorDraft(scope, direction, normalizedValue); + const path = ["baseline", direction, "color"]; + if (!normalizedValue) { + return this._removeCanonicalScopedValue(scope, path, { + prunePaths: [["baseline", direction], ["baseline"]] + }); + } + if (!this._isBaselineDirectionalColorEnabled(scope, direction)) { + return this._setBaselineDirectionalColorEnabled(scope, direction, true); + } + return this._applyScopedMutation(scope, (target) => { + return this._setPathValue(target, path, normalizedValue); + }); + } + _getBaselineDirectionalColorValue(scope, direction) { + var _a; + return (_a = this._getScopedValue(scope, ["baseline", direction, "color"])) != null ? _a : ""; + } + _getEffectiveBaselineDirectionalColorValue(scope, direction) { + return this._getEffectiveScopedDisplayValue(scope, ["baseline", direction, "color"]); + } + _clearBaselineOverride(scope) { + return this._applyScopedMutation(scope, (target) => { + let nextTarget = this._cloneDeep(target); + const rawBaseline = this._getPathValue(nextTarget, ["baseline"]); + if (this._isObject(rawBaseline)) { + nextTarget = this._deletePathValue(nextTarget, ["baseline", "enabled"]); + nextTarget = this._deletePathValue(nextTarget, ["baseline", "at"]); + nextTarget = this._deletePathValue(nextTarget, ["baseline", "above", "color"]); + nextTarget = this._deletePathValue(nextTarget, ["baseline", "below", "color"]); + } else { + nextTarget = this._deletePathValue(nextTarget, ["baseline"]); + } + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["baseline", "above"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["baseline", "below"]); + nextTarget = this._pruneEmptyObjectsInTarget(nextTarget, ["baseline"]); + return nextTarget; + }, { rerender: true }); + } + _hasBaselineOverride(scope) { + const baselineValue = this._getScopedValue(scope, ["baseline"]); + if (this._isObject(baselineValue) && Object.keys(baselineValue).length) { + return true; + } + return !this._isObject(baselineValue) && baselineValue !== void 0 && baselineValue !== null && baselineValue !== ""; + } + _isEntityOverrideExpanded(index) { + return this._expandedEntityOverrides.has(index); + } + _toggleEntityOverrideExpanded(index) { + if (this._expandedEntityOverrides.has(index)) { + this._expandedEntityOverrides.delete(index); + } else { + this._expandedEntityOverrides.add(index); + } + this._render(); + } + _syncExpandedEntityOverrides(entityCount) { + const nextExpanded = /* @__PURE__ */ new Set(); + this._expandedEntityOverrides.forEach((index) => { + if (index < entityCount) nextExpanded.add(index); + }); + this._expandedEntityOverrides = nextExpanded; + const nextGroups = /* @__PURE__ */ new Set(); + this._expandedOverrideGroups.forEach((key) => { + const [indexText, group] = String(key).split(":"); + const index = Number(indexText); + if (Number.isInteger(index) && index < entityCount && group) { + nextGroups.add(`${index}:${group}`); + } + }); + this._expandedOverrideGroups = nextGroups; + } + _isCardGroupExpanded(group) { + return this._expandedCardGroups.has(group); + } + _toggleCardGroupExpanded(group) { + if (this._expandedCardGroups.has(group)) { + this._expandedCardGroups.delete(group); + } else { + this._expandedCardGroups.add(group); + } + this._render(); + } + _getOverrideGroupKey(index, group) { + return `${index}:${group}`; + } + _isOverrideGroupExpanded(index, group) { + return this._expandedOverrideGroups.has(this._getOverrideGroupKey(index, group)); + } + _toggleOverrideGroupExpanded(index, group) { + const key = this._getOverrideGroupKey(index, group); + if (this._expandedOverrideGroups.has(key)) { + this._expandedOverrideGroups.delete(key); + } else { + this._expandedOverrideGroups.add(key); + } + this._render(); + } + _hasExplicitOverrideValue(value) { + return value !== "" && value !== void 0 && value !== null; + } + _hasResolvableOverride(parts) { + return this._hasExplicitOverrideValue(parts == null ? void 0 : parts.fixed) || this._hasExplicitOverrideValue(parts == null ? void 0 : parts.entity); + } + _getScaleOverrideSummary(scope) { + const parts = []; + const min = this._getResolvableScopedValue(scope, "min"); + const max = this._getResolvableScopedValue(scope, "max"); + if (min.entity) parts.push("Min entity"); + else if (min.fixed !== "" && min.fixed !== void 0) parts.push(`Min ${min.fixed}`); + if (max.entity) parts.push("Max entity"); + else if (max.fixed !== "" && max.fixed !== void 0) parts.push(`Max ${max.fixed}`); + return parts.length ? parts.join(" \u2022 ") : "Inherited"; + } + _getLayoutSummary(scope) { + const parts = []; + const height = this._getScopedLayoutValue(scope, "height"); + const position = this._getScopedLayoutValue(scope, "position"); + const width = this._getScopedLayoutValue(scope, "width"); + if (height !== "") parts.push(`Height ${height}`); + if (position !== "") parts.push(`${position}`); + if (width !== "") parts.push(`Width ${width}`); + return parts.length ? parts.join(" \u2022 ") : "Inherited"; + } + _getTargetOverrideSummary(scope) { + const mode = this._getTargetMode(scope); + if (mode === "disabled") return "Disabled"; + const parts = []; + const target = this._getTargetResolvableValue(scope); + if (target.fixed !== "" && target.fixed !== void 0) parts.push(`Target ${target.fixed}`); + if (target.entity) parts.push("Entity"); + if (this._hasCustomTargetColor(scope)) parts.push("Custom color"); + if (this._getTargetLabelShowValue(scope)) parts.push("Label"); + if (this._getTargetAboveFillColorValue(scope)) parts.push("Above"); + return parts.length ? parts.join(" \u2022 ") : "Inherited"; + } + _getBaselineOverrideSummary(scope) { + const mode = this._getBaselineMode(scope); + if (mode === "disabled") return "Disabled"; + const parts = []; + const baseline = this._getBaselineResolvableValue(scope); + if (baseline.fixed !== "" && baseline.fixed !== void 0) parts.push(`Baseline ${baseline.fixed}`); + if (baseline.entity) parts.push("Entity"); + if (this._getBaselineDirectionalColorValue(scope, "above")) parts.push("Above"); + if (this._getBaselineDirectionalColorValue(scope, "below")) parts.push("Below"); + return parts.length ? parts.join(" \u2022 ") : "Inherited"; + } + _getBarAppearanceSummary(scope) { + var _a; + const parts = []; + const fillStyle = this._getScopedFillStyleValue(scope); + const color = (_a = this._getScopedValue(scope, ["bar", "color"])) != null ? _a : this._getScopedValue(scope, ["color"]); + if (fillStyle && fillStyle !== "bands") parts.push(fillStyle.replace(/_/g, " ")); + if (color && this._normalizeColorComparisonValue(color) !== this._normalizeColorComparisonValue("#4a9eff")) { + parts.push("Custom color"); + } + return parts.length ? parts.join(" \u2022 ") : "Inherited"; + } + _getScopedSegmentsValue(scope) { + const uiRows = this._getSegmentsUiRows(scope); + if (uiRows !== null) { + return this._cloneDeep(uiRows); + } + const storedSegments = this._getStoredScopedSegments(scope); + if (storedSegments !== null) { + return this._sortSegmentsForEditor(storedSegments); + } + return this._sortSegmentsForEditor(this._getFallbackSegments(scope)); + } + _hasSegmentsOverride(scope) { + return this._getStoredScopedSegments(scope) !== null; + } + _getSegmentsSummary(scope) { + const segments = this._getScopedSegmentsValue(scope); + if (!Array.isArray(segments) || segments.length === 0) { + return "Inherited"; + } + if ((scope == null ? void 0 : scope.type) !== "entity" && !this._hasSegmentsOverride(scope) && this._isSegmentFillStyle(this._getEffectiveFillStyleValue(scope))) { + return "Default bands"; + } + return `${segments.length} segments`; + } + _getEffectiveFillStyleValue(scope) { + if ((scope == null ? void 0 : scope.type) === "entity") { + const hasEntityFillStyle = this._getScopedValue(scope, ["bar", "fill_style"]) !== void 0 || this._getScopedValue(scope, ["bar", "color_mode"]) !== void 0 || this._getScopedValue(scope, ["color_mode"]) !== void 0; + if (hasEntityFillStyle) { + return this._getScopedFillStyleValue(scope); + } + return this._getFillStyleValue(); + } + return this._getScopedFillStyleValue(scope); + } + _getScopedGradientStopsValue(scope) { + const localRows = this._getGradientStopsUiRows(scope); + if (Array.isArray(localRows)) { + return localRows; + } + const storedStops = this._getStoredScopedGradientStops(scope); + if (storedStops !== null) { + return this._sanitizeGradientStopsForEmit(storedStops); + } + return this._cloneDeep(this._getFallbackGradientStops(scope)); + } + _hasGradientStopsOverride(scope) { + if (this._getStoredScopedGradientStops(scope) !== null) { + return true; + } + const localRows = this._getGradientStopsUiRows(scope); + if (!Array.isArray(localRows)) { + return false; + } + return this._serializeConfig(this._sanitizeGradientStopsForEmit(localRows)) !== this._serializeConfig(this._sanitizeGradientStopsForEmit(this._getFallbackGradientStops(scope))); + } + _getGradientStopsSummary(scope) { + if ((scope == null ? void 0 : scope.type) === "entity" && !this._hasGradientStopsOverride(scope)) { + return "Inherited"; + } + const gradientStops = this._sanitizeGradientStopsForEmit(this._getScopedGradientStopsValue(scope)); + if (this._getEffectiveFillStyleValue(scope) !== "gradient") { + return "Inactive fill style"; + } + if (!gradientStops.length) { + return (scope == null ? void 0 : scope.type) === "entity" ? "Inherited" : "Default gradient"; + } + if (this._isDefaultGradientStops(gradientStops)) { + return "Default gradient"; + } + return `${gradientStops.length} stops`; + } + _buildGradientPreviewEffectiveStops(scope = { type: "card" }) { + const committedStops = this._sanitizeGradientStopsForEmit(this._getScopedGradientStopsValue(scope)); + const draftStop = this._getValidGradientDraftStop(scope); + const previewStops = [...committedStops]; + if (draftStop && !this._hasGradientStopDuplicate(scope, draftStop.pos)) { + previewStops.push(draftStop); + } + return previewStops.sort((left, right) => left.pos - right.pos); + } + _buildEditorGradientPreviewStyle(stops) { + if (!Array.isArray(stops) || !stops.length) { + return ""; + } + const cssStops = stops.map((stop) => { + var _a; + const color = this._normalizeTextValue(stop.color).trim(); + const pos = this._normalizeGradientStopPosValue((_a = stop.p) != null ? _a : stop.pos); + if (!color || pos === null) { + return null; + } + return `${color} ${pos}%`; + }).filter(Boolean); + if (!cssStops.length) { + return ""; + } + return `background:linear-gradient(to right,${cssStops.join(",")});background-repeat:no-repeat;`; + } + _getGradientPreviewStyle(scope = { type: "card" }) { + const previewStops = this._buildGradientPreviewEffectiveStops(scope); + const resolvedStops = previewStops.length >= 2 ? previewStops.map((stop) => ({ p: stop.pos, color: stop.color })) : this._getDefaultGradientStops().map((stop) => ({ p: stop.pos, color: stop.color })); + return this._buildEditorGradientPreviewStyle(resolvedStops); + } + _renderGradientPreview(scope = { type: "card" }, options = {}) { + var _a, _b, _c; + const previewId = (_a = options.previewId) != null ? _a : "gradient-preview"; + const trackId = (_b = options.trackId) != null ? _b : `${previewId}-track`; + const effectiveStops = this._buildGradientPreviewEffectiveStops(scope); + const markerStops = effectiveStops.length ? effectiveStops : this._getDefaultGradientStops(); + return ` +
    +
    + ${markerStops.map((stop, index) => ` + + `).join("")} +
    +
    + `; + } + _getNeedleSummary(scope) { + if ((scope == null ? void 0 : scope.type) === "entity" && !this._hasNeedleOverride(scope)) return "Inherited"; + const needle = this._getScopedNeedleConfig(scope); + if (needle.mode === "disabled") return needle.color ? "Disabled \u2022 Custom color" : "Disabled"; + if (needle.mode === "enabled") return needle.color ? "Enabled \u2022 Custom color" : "Enabled"; + if (needle.color) return "Custom color"; + return "Inherited"; + } + _getFormattingSummary(scope) { + const parts = []; + const unit = this._getScopedFormattingValue(scope, "unit"); + const decimal = this._getScopedFormattingValue(scope, "decimal"); + if (unit !== "") parts.push(`Unit ${unit}`); + if (decimal !== "") parts.push(`${decimal} ${Number(decimal) === 1 ? "decimal" : "decimals"}`); + return parts.length ? parts.join(" \u2022 ") : "Inherited"; + } + _renderOverrideGroup({ index, group, title, summary, content }) { + const expanded = this._isOverrideGroupExpanded(index, group); + return ` +
    + +
    + ${content} +
    +
    + `; + } + _renderCardGroup({ group, title, summary, content, inactive = false }) { + const expanded = this._isCardGroupExpanded(group); + return ` +
    + +
    + ${content} +
    +
    + `; + } + _renderEntityInput(entry, index) { + if (customElements.get("ha-entity-picker")) { + return ``; + } + return ``; + } + _renderEntitySourceInput(kind, index, value, placeholder = "sensor.example") { + if (customElements.get("ha-entity-picker")) { + return ``; + } + return ``; + } + _escapeAttribute(value) { + return this._normalizeTextValue(value).replace(/&/g, "&").replace(/"/g, """).replace(//g, ">"); + } + _isHexColorValue(value) { + return typeof value === "string" && /^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/.test(value.trim()); + } + _expandHexColor(value) { + if (!this._isHexColorValue(value)) { + return null; + } + const normalized = value.trim().toLowerCase(); + if (normalized.length === 7) { + return normalized; + } + return `#${normalized.slice(1).split("").map((char) => char + char).join("")}`; + } + _normalizeColorComparisonValue(value) { + var _a; + const normalizedText = this._normalizeTextValue(value).trim().toLowerCase(); + if (!normalizedText) { + return ""; + } + return (_a = this._expandHexColor(normalizedText)) != null ? _a : normalizedText; + } + _getColorPickerValue(value, fallbackHex = "#000000") { + var _a, _b; + return (_b = (_a = this._expandHexColor(value)) != null ? _a : this._expandHexColor(fallbackHex)) != null ? _b : "#000000"; + } + _renderColorInput({ id, field = null, kind = null, index = null, value = "", fallbackHex = "#000000", placeholder = "", extraDataset = {} }) { + const controlValue = this._normalizeTextValue(value).trim(); + const pickerValue = this._getColorPickerValue(controlValue, fallbackHex); + const extraAttrs = Object.entries(extraDataset).map(([key, entry]) => `data-${key}="${this._escapeAttribute(entry)}"`).join(" "); + const baseAttrs = field ? `data-field="${field}"${extraAttrs ? ` ${extraAttrs}` : ""}` : `data-kind="${kind}" data-index="${index}"${extraAttrs ? ` ${extraAttrs}` : ""}`; + const fallbackAttrs = field ? `data-field="${field}-text-fallback"${extraAttrs ? ` ${extraAttrs}` : ""}` : `data-kind="${kind}-text-fallback" data-index="${index}"${extraAttrs ? ` ${extraAttrs}` : ""}`; + return ` +
    + + ${controlValue && !this._isHexColorValue(controlValue) ? `` : ""} +
    + `; + } + _renderListRows(items, renderItem) { + return items.map((item, index) => renderItem(item, index)).join(""); + } + _render() { + var _a; + if (!this.shadowRoot || this._isRendering) return; + this._isRendering = true; + try { + const entities = this._getEntitiesValue(); + const fillStyle = this._getFillStyleValue(); + const layoutLabelPosition = this._getScopedLayoutValue({ type: "card" }, "position") || "left"; + const layoutHeroSize = this._getScopedLayoutValue({ type: "card" }, "hero_size") || "medium"; + const layoutHeight = this._getScopedLayoutValue({ type: "card" }, "height"); + const layoutLabelWidth = this._getScopedLayoutValue({ type: "card" }, "width"); + const barColor = this._getScopedBarColorValue({ type: "card" }); + const barSolidFill = this._getScopedBarSolidFillValue({ type: "card" }); + const cardNeedle = this._getScopedNeedleConfig({ type: "card" }); + const gradientStops = this._getGradientStopsValue(); + const gradientDraft = this._getGradientStopsDraftState({ type: "card" }); + const gradientDraftMessage = this._getGradientDraftValidationMessage({ type: "card" }); + const segments = this._getSegmentsValue(); + const baseline = this._getBaselineResolvableValue({ type: "card" }); + const baselineMode = this._getBaselineMode({ type: "card" }); + const baselineAboveColor = this._getBaselineDirectionalColorValue({ type: "card" }, "above"); + const baselineBelowColor = this._getBaselineDirectionalColorValue({ type: "card" }, "below"); + const target = this._getTargetResolvableValue({ type: "card" }); + const targetMode = this._getTargetMode({ type: "card" }); + const targetColor = this._getTargetColorValue({ type: "card" }); + const targetLabelShow = this._getTargetLabelShowValue({ type: "card" }); + const targetAboveFillColor = this._getTargetAboveFillColorValue({ type: "card" }); + const formattingUnit = this._getScopedFormattingValue({ type: "card" }, "unit"); + const formattingDecimal = this._getScopedFormattingValue({ type: "card" }, "decimal"); + const cardPeak = this._getScopedPeakConfig({ type: "card" }); + const scaleMin = this._getScaleFixedValue("min", "min"); + const scaleMax = this._getScaleFixedValue("max", "max"); + const scaleMinEntity = this._getScaleEntityValue("min"); + const scaleMaxEntity = this._getScaleEntityValue("max"); + const gradientStopsSummary = this._getGradientStopsSummary({ type: "card" }); + const gradientStopsInactive = this._getEffectiveFillStyleValue({ type: "card" }) !== "gradient"; + const defaultSegmentsVisible = !this._hasSegmentsOverride({ type: "card" }) && this._isSegmentFillStyle(this._getEffectiveFillStyleValue({ type: "card" })); + this._syncExpandedEntityOverrides(entities.length); + this.shadowRoot.innerHTML = ` + +
    +
    +
    +

    Basics

    +
    +
    +
    + + +
    +
    +
    + +
    +
    +

    Entities

    +
    Overrides replace card defaults only for this entity.
    +
    +
    +
    + ${this._renderListRows(entities, (entry, index) => { + var _a2, _b; + return ` +
    +
    +
    +
    +
    Entity ${index + 1}
    +
    ${this._escapeAttribute(entry.entity || "Configure entity")}
    +
    +
    + + + + +
    +
    +
    + ${this._renderEntityInput(entry, index)} + + +
    +
    + +
    +
    Overrides replace card defaults only for this entity.
    + ${(() => { + const scope = { type: "entity", index }; + const minParts = this._getEffectiveResolvableScopedValue(scope, "min"); + const maxParts = this._getEffectiveResolvableScopedValue(scope, "max"); + const barAppearanceInherited = !this._hasEntityBarAppearanceOverride(scope); + const needleInherited = !this._hasNeedleOverride(scope); + const entityNeedle = this._getEffectiveScopedNeedleConfig(scope); + const baselineInherited = !this._hasBaselineOverride(scope); + const baselineParts = this._getEffectiveBaselineResolvableValue(scope); + const baselineMode2 = this._getEffectiveBaselineMode(scope); + const targetInherited = !this._hasTargetOverride(scope); + const targetParts = this._getEffectiveTargetResolvableValue(scope); + const targetMode2 = this._getEffectiveTargetMode(scope); + const formattingInherited = !this._hasFormattingOverride(scope); + const layoutInherited = !this._hasLayoutOverride(scope); + const peakInherited = !this._hasPeakOverride(scope); + const gradientStopsInherited = !this._hasGradientStopsOverride(scope); + const segmentsInherited = !this._hasSegmentsOverride(scope); + const scaleInherited = !this._hasResolvableOverride(this._getResolvableScopedValue(scope, "min")) && !this._hasResolvableOverride(this._getResolvableScopedValue(scope, "max")); + const entityPeak = this._getEffectiveScopedPeakConfig(scope); + const entityGradientStops = this._getScopedGradientStopsValue(scope); + const entityGradientDraft = this._getGradientStopsDraftState(scope); + const entityGradientDraftMessage = this._getGradientDraftValidationMessage(scope); + const entitySegments = this._getScopedSegmentsValue(scope); + const scaleGroup = this._renderOverrideGroup({ + index, + group: "scale", + title: "Scale", + summary: this._getScaleOverrideSummary(scope), + content: ` +
    +
    + + +
    +
    +
    Fixed values are used as fallback when entity values are unavailable.
    +
    + + +
    +
    + + ${this._renderEntitySourceInput("entity-override-min-entity-source", index, minParts.entity, "inherit card default")} +
    +
    + + +
    +
    + + ${this._renderEntitySourceInput("entity-override-max-entity-source", index, maxParts.entity, "inherit card default")} +
    + ` + }); + const layoutGroup = this._renderOverrideGroup({ + index, + group: "layout", + title: "Layout", + summary: this._getLayoutSummary(scope), + content: ` +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    + ${(this._getEffectiveScopedLayoutValue(scope, "position") || "") === "hero" ? ` +
    + + +
    + ` : ""} +
    + + +
    + ` + }); + const barGroup = this._renderOverrideGroup({ + index, + group: "bar", + title: "Bar Appearance", + summary: this._getBarAppearanceSummary(scope), + content: ` +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + ${this._renderColorInput({ + id: `entity-${index}-bar-color`, + kind: "entity-bar-color", + index, + value: this._getEffectiveScopedBarColorValue(scope), + fallbackHex: "#4a9eff", + placeholder: "inherit card default" + })} +
    + ` + }); + const needleGroup = this._renderOverrideGroup({ + index, + group: "needle", + title: "Needle", + summary: this._getNeedleSummary(scope), + content: ` +
    +
    + + +
    +
    +
    + + +
    +
    + + ${this._renderColorInput({ + id: `entity-${index}-needle-color`, + kind: "entity-needle-color", + index, + value: entityNeedle.color, + fallbackHex: "#ffffff", + placeholder: "#ffffff" + })} +
    + ` + }); + const formattingGroup = this._renderOverrideGroup({ + index, + group: "formatting", + title: "Formatting", + summary: this._getFormattingSummary(scope), + content: ` +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    + ` + }); + const peakGroup = this._renderOverrideGroup({ + index, + group: "peak", + title: "Peak", + summary: this._getPeakSummary(scope), + content: ` +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    + + ${this._renderColorInput({ + id: `entity-${index}-peak-color`, + kind: "entity-peak-color", + index, + value: entityPeak.color, + fallbackHex: "#888", + placeholder: "inherit card default" + })} +
    + ` + }); + const segmentsGroup = this._renderOverrideGroup({ + index, + group: "segments", + title: "Segments", + summary: this._getSegmentsSummary(scope), + content: ` +
    +
    + + +
    +
    + ${this._isSegmentFillStyle(this._getEffectiveFillStyleValue(scope)) ? "" : '
    Only used with segment-based fill styles.
    '} + ${this._renderSegmentPreview(scope)} +
    + +
    + ${this._renderListRows(entitySegments, (segment, segmentIndex) => { + var _a3; + return ` +
    +
    + + + + +
    + +
    + `; + })} +
    +
    + + + + +
    + +
    +
    +
    + ` + }); + const gradientStopsGroup = this._renderOverrideGroup({ + index, + group: "gradient-stops", + title: "Gradient Stops", + summary: this._getGradientStopsSummary(scope), + content: ` +
    +
    + + +
    +
    + ${this._getEffectiveFillStyleValue(scope) !== "gradient" ? '
    Only used with Gradient fill style
    ' : ""} + ${this._renderGradientPreview(scope, { + previewId: `entity-${index}-gradient-preview`, + trackId: `entity-${index}-gradient-preview-track` + })} +
    + +
    + ${this._renderListRows(entityGradientStops, (stop, stopIndex) => { + var _a3, _b2; + return ` +
    + + ${this._renderColorInput({ + id: `entity-${index}-gradient-color-${stopIndex}`, + kind: "entity-gradient-color", + index, + value: (_b2 = stop == null ? void 0 : stop.color) != null ? _b2 : "#4a9eff", + fallbackHex: "#4CAF50", + placeholder: "CSS color value", + extraDataset: { "stop-index": stopIndex } + })} + +
    + `; + })} +
    +
    + + ${this._renderColorInput({ + id: `entity-${index}-gradient-draft-color`, + kind: "entity-gradient-draft-color", + index, + value: entityGradientDraft.color, + fallbackHex: "#4CAF50", + placeholder: "CSS color value" + })} + +
    + ${entityGradientDraftMessage ? `
    ${this._escapeAttribute(entityGradientDraftMessage)}
    ` : ""} +
    +
    +
    + ` + }); + const baselineGroup = this._renderOverrideGroup({ + index, + group: "baseline", + title: "Baseline", + summary: this._getBaselineOverrideSummary(scope), + content: ` +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + ${this._renderEntitySourceInput("entity-baseline-entity-source", index, baselineParts.entity, "inherit card default")} +
    +
    +
    + + +
    +
    +
    + + ${this._renderColorInput({ + id: `entity-${index}-baseline-above-color`, + kind: "entity-baseline-above-color", + index, + value: this._getEffectiveBaselineDirectionalColorValue(scope, "above"), + fallbackHex: "#000000", + placeholder: "inherit card default" + })} +
    +
    +
    + + +
    +
    +
    + + ${this._renderColorInput({ + id: `entity-${index}-baseline-below-color`, + kind: "entity-baseline-below-color", + index, + value: this._getEffectiveBaselineDirectionalColorValue(scope, "below"), + fallbackHex: "#000000", + placeholder: "inherit card default" + })} +
    + ` + }); + const targetGroup = this._renderOverrideGroup({ + index, + group: "target", + title: "Target", + summary: this._getTargetOverrideSummary(scope), + content: ` +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    +
    + + ${this._renderEntitySourceInput("entity-target-entity-source", index, targetParts.entity, "inherit card default")} +
    +
    + + ${this._renderColorInput({ + id: `entity-${index}-target-color`, + kind: "entity-target-color", + index, + value: this._getEffectiveTargetColorValue(scope), + fallbackHex: "#888", + placeholder: "inherit card default" + })} +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    + + ${this._renderColorInput({ + id: `entity-${index}-target-above-fill`, + kind: "entity-target-above-fill-color", + index, + value: this._getEffectiveTargetAboveFillColorValue(scope), + fallbackHex: "#000000", + placeholder: "inherit card default" + })} +
    + ` + }); + return ` + ${scaleGroup} + ${targetGroup} + ${baselineGroup} + ${needleGroup} + ${peakGroup} + ${barGroup} + ${segmentsGroup} + ${gradientStopsGroup} + ${layoutGroup} + ${formattingGroup} + `; + })()} +
    +
    + `; + })} + +
    +
    +
    + +
    +
    +

    Scale

    +
    Entity values take precedence. Fixed values are used as fallback.
    +
    +
    +
    + + +
    +
    + + ${this._renderEntitySourceInput("scale-min-entity-source", "card", scaleMinEntity)} +
    +
    + + +
    +
    + + ${this._renderEntitySourceInput("scale-max-entity-source", "card", scaleMaxEntity)} +
    +
    +
    + +
    +
    +

    Markers

    +
    Target, baseline, and peak markers help compare the current value against reference points.
    +
    +
    +
    + + +
    +
    + + +
    +
    + + ${this._renderEntitySourceInput("target-entity-source", "card", target.entity)} +
    +
    + + ${this._renderColorInput({ + id: "target-color", + field: "target-color", + value: targetColor, + fallbackHex: "#888", + placeholder: "#888" + })} +
    +
    +
    + + +
    +
    +
    +
    + + +
    +
    +
    + + ${this._renderColorInput({ + id: "target-above-fill-color", + field: "target-above-fill-color", + value: targetAboveFillColor, + fallbackHex: "#000000" + })} +
    +
    + + +
    +
    +
    Auto shows the baseline when a baseline value is configured.
    +
    +
    + + +
    +
    + + ${this._renderEntitySourceInput("baseline-entity-source", "card", baseline.entity)} +
    +
    +
    + + +
    +
    +
    + + ${this._renderColorInput({ + id: "baseline-above-color", + field: "baseline-above-color", + value: baselineAboveColor, + fallbackHex: "#000000" + })} +
    +
    +
    + + +
    +
    +
    + + ${this._renderColorInput({ + id: "baseline-below-color", + field: "baseline-below-color", + value: baselineBelowColor, + fallbackHex: "#000000" + })} +
    +
    +
    + + +
    +
    +
    + + ${this._renderColorInput({ + id: "peak-color", + field: "peak-color", + value: cardPeak.color, + fallbackHex: "#888", + placeholder: "#888" + })} +
    +
    +
    + +
    +
    +

    Bar Appearance

    +
    Choose the bar rendering mode and base bar colors.
    +
    +
    +
    + + +
    +
    +
    + + +
    +
    +
    + + ${this._renderColorInput({ + id: "bar-color", + field: "bar-color", + value: barColor, + fallbackHex: "#4a9eff", + placeholder: "#4a9eff" + })} +
    +
    + + +
    +
    + + ${this._renderColorInput({ + id: "bar-needle-color", + field: "bar-needle-color", + value: cardNeedle.color, + fallbackHex: "#ffffff", + placeholder: "#ffffff" + })} +
    +
    +
    + +
    +
    +

    Segments

    +
    Segments define colored value ranges.
    +
    + ${this._renderCardGroup({ + group: "segments", + title: "Segments", + summary: this._getSegmentsSummary({ type: "card" }), + inactive: !this._isSegmentFillStyle(fillStyle), + content: ` + ${this._isSegmentFillStyle(fillStyle) ? "" : '
    Only used with segment-based fill styles.
    '} + ${this._renderSegmentPreview({ type: "card" })} +
    + +
    + ${defaultSegmentsVisible ? '
    Default bands
    ' : ""} + ${this._renderListRows(segments, (segment, index) => { + var _a2; + return ` +
    +
    + + + + +
    + +
    + `; + })} +
    +
    + + + + +
    + +
    +
    +
    + ` + })} +
    + +
    +
    +

    Gradient Stops

    +
    Gradient stops define a smooth color transition from 0 to 100%.
    +
    + ${this._renderCardGroup({ + group: "gradient-stops", + title: "Gradient Stops", + summary: gradientStopsSummary, + inactive: gradientStopsInactive, + content: ` + ${gradientStopsInactive ? '
    Only used with Gradient fill style
    ' : ""} + ${this._renderGradientPreview({ type: "card" }, { + previewId: "card-gradient-preview", + trackId: "card-gradient-preview-track" + })} +
    + +
    + ${this._renderListRows(gradientStops, (stop, index) => { + var _a2, _b; + return ` +
    + + ${this._renderColorInput({ + id: `gradient-color-${index}`, + kind: "gradient-color", + index, + value: (_b = stop == null ? void 0 : stop.color) != null ? _b : "#4a9eff", + fallbackHex: "#4CAF50", + placeholder: "CSS color value" + })} + +
    + `; + })} +
    +
    + + ${this._renderColorInput({ + id: "gradient-draft-color", + kind: "gradient-draft-color", + index: "card", + value: gradientDraft.color, + fallbackHex: "#4CAF50", + placeholder: "CSS color value" + })} + +
    + ${gradientDraftMessage ? `
    ${this._escapeAttribute(gradientDraftMessage)}
    ` : ""} +
    +
    +
    + ` + })} +
    + +
    +
    +

    Layout

    +
    +
    +
    + + +
    +
    + + +
    + ${layoutLabelPosition === "hero" ? ` +
    + + +
    + ` : ""} +
    + + +
    +
    +
    + +
    +
    +

    Formatting

    +
    +
    +
    + + +
    +
    + + +
    +
    +
    +
    + `; + this._bindShadowListeners(); + this._syncEntityPickers(); + this._lastRenderedConfigJson = this._serializeConfig(this._draftConfig); + this._applyPendingFocus(); + } finally { + this._isRendering = false; + } + } + _bindShadowListeners() { + if (!this.shadowRoot || this._shadowListenersAttached) return; + this.shadowRoot.addEventListener("click", this._boundHandleClick); + this.shadowRoot.addEventListener("change", this._boundHandleChange); + this.shadowRoot.addEventListener("input", this._boundHandleInput); + this.shadowRoot.addEventListener("value-changed", this._boundHandleValueChanged); + this.shadowRoot.addEventListener("keydown", this._boundHandleKeydown); + this._shadowListenersAttached = true; + } + _syncEntityPickers() { + if (!this.shadowRoot) return; + const entities = this._getEntitiesValue(); + const syncPicker = (picker) => { + var _a; + const kind = picker.dataset.kind; + const indexValue = picker.dataset.index; + const index = Number(indexValue); + picker.hass = this._hass; + picker.allowCustomEntity = true; + if (kind === "entity-picker") { + const entry = entities[index]; + picker.value = (_a = entry == null ? void 0 : entry.entity) != null ? _a : ""; + picker.label = `Entity ${index + 1}`; + return; + } + if (kind === "scale-min-entity-source") { + picker.value = this._getScaleEntityValue("min"); + picker.label = "Min entity"; + return; + } + if (kind === "scale-max-entity-source") { + picker.value = this._getScaleEntityValue("max"); + picker.label = "Max entity"; + return; + } + if (kind === "baseline-entity-source") { + picker.value = this._getBaselineResolvableValue({ type: "card" }).entity; + picker.label = "Baseline entity"; + return; + } + if (kind === "target-entity-source") { + picker.value = this._getTargetResolvableValue({ type: "card" }).entity; + picker.label = "Target entity"; + return; + } + if (kind === "entity-override-min-entity-source") { + picker.value = this._getEffectiveResolvableScopedValue({ type: "entity", index }, "min").entity; + picker.label = `Entity ${index + 1} min entity`; + return; + } + if (kind === "entity-override-max-entity-source") { + picker.value = this._getEffectiveResolvableScopedValue({ type: "entity", index }, "max").entity; + picker.label = `Entity ${index + 1} max entity`; + return; + } + if (kind === "entity-baseline-entity-source") { + picker.value = this._getEffectiveBaselineResolvableValue({ type: "entity", index }).entity; + picker.label = `Entity ${index + 1} baseline entity`; + return; + } + if (kind === "entity-target-entity-source") { + picker.value = this._getEffectiveTargetResolvableValue({ type: "entity", index }).entity; + picker.label = `Entity ${index + 1} target entity`; + } + }; + [ + 'ha-entity-picker[data-kind="entity-picker"]', + 'ha-entity-picker[data-kind="scale-min-entity-source"]', + 'ha-entity-picker[data-kind="scale-max-entity-source"]', + 'ha-entity-picker[data-kind="baseline-entity-source"]', + 'ha-entity-picker[data-kind="target-entity-source"]', + 'ha-entity-picker[data-kind="entity-override-min-entity-source"]', + 'ha-entity-picker[data-kind="entity-override-max-entity-source"]', + 'ha-entity-picker[data-kind="entity-baseline-entity-source"]', + 'ha-entity-picker[data-kind="entity-target-entity-source"]' + ].forEach((selector) => { + this.shadowRoot.querySelectorAll(selector).forEach(syncPicker); + }); + if (customElements.whenDefined) { + customElements.whenDefined("ha-entity-picker").then(() => { + [ + 'ha-entity-picker[data-kind="entity-picker"]', + 'ha-entity-picker[data-kind="scale-min-entity-source"]', + 'ha-entity-picker[data-kind="scale-max-entity-source"]', + 'ha-entity-picker[data-kind="baseline-entity-source"]', + 'ha-entity-picker[data-kind="target-entity-source"]', + 'ha-entity-picker[data-kind="entity-override-min-entity-source"]', + 'ha-entity-picker[data-kind="entity-override-max-entity-source"]', + 'ha-entity-picker[data-kind="entity-baseline-entity-source"]', + 'ha-entity-picker[data-kind="entity-target-entity-source"]' + ].forEach((selector) => { + var _a; + (_a = this.shadowRoot) == null ? void 0 : _a.querySelectorAll(selector).forEach(syncPicker); + }); + }).catch(() => { + }); + } + } + _handleClick(event) { + var _a, _b, _c, _d, _e, _f; + const target = (_c = (_b = (_a = event.target) == null ? void 0 : _a.closest) == null ? void 0 : _b.call(_a, "[data-action]")) != null ? _c : event.target; + const action = (_d = target == null ? void 0 : target.dataset) == null ? void 0 : _d.action; + if (!action) return; + if (target == null ? void 0 : target.disabled) return; + if (action === "add-entity") { + const nextEntities = [...this._getEntitiesValue(), { entity: "" }]; + const nextEntries = this._buildEntityConfigEntries(nextEntities); + this._queuePostRenderFocus(`[data-kind="entity-picker"][data-index="${nextEntities.length - 1}"], [data-kind="entity-input"][data-index="${nextEntities.length - 1}"]`); + if (Array.isArray(this._draftConfig.entities) || nextEntries.length > 1 || !this._draftConfig.entity) { + let nextConfig = this._setPathValue(this._draftConfig, ["entities"], nextEntries); + if (!Array.isArray(this._draftConfig.entities) && this._draftConfig.entity !== void 0) { + nextConfig = this._deletePathValue(nextConfig, ["entity"]); + if (this._draftConfig.name !== void 0) { + nextConfig = this._deletePathValue(nextConfig, ["name"]); + } + if (this._draftConfig.icon !== void 0) { + nextConfig = this._deletePathValue(nextConfig, ["icon"]); + } + } + this._applyUserConfig(nextConfig, { rerender: true }); + } else { + this._setValueAtPath(["entity"], (_f = (_e = nextEntities[0]) == null ? void 0 : _e.entity) != null ? _f : "", { rerender: true }); + } + return; + } + if (action === "move-entity-up") { + this._moveEntityRow(Number(target.dataset.index), -1); + return; + } + if (action === "move-entity-down") { + this._moveEntityRow(Number(target.dataset.index), 1); + return; + } + if (action === "duplicate-entity") { + const sourceIndex = Number(target.dataset.index); + this._queuePostRenderFocus(`[data-kind="entity-name"][data-index="${sourceIndex + 1}"], [data-kind="entity-picker"][data-index="${sourceIndex + 1}"], [data-kind="entity-input"][data-index="${sourceIndex + 1}"]`); + this._duplicateEntityRow(sourceIndex); + return; + } + if (action === "toggle-entity-overrides") { + this._toggleEntityOverrideExpanded(Number(target.dataset.index)); + return; + } + if (action === "toggle-override-group") { + this._toggleOverrideGroupExpanded(Number(target.dataset.index), target.dataset.group); + return; + } + if (action === "toggle-card-group") { + this._toggleCardGroupExpanded(target.dataset.group); + return; + } + if (action === "remove-entity") { + this._removeEntityRow(Number(target.dataset.index)); + return; + } + if (action === "add-gradient-stop") { + this._queuePostRenderFocus("#gradient-draft-pos"); + this._commitGradientStopDraft({ type: "card" }); + return; + } + if (action === "remove-gradient-stop") { + const index = Number(target.dataset.index); + this._setGradientStops(this._getGradientStopsValue().filter((_, stopIndex) => stopIndex !== index), { rerender: true }); + return; + } + if (action === "add-entity-gradient-stop") { + const entityIndex = Number(target.dataset.index); + this._queuePostRenderFocus(`#entity-${entityIndex}-gradient-draft-pos`); + this._commitGradientStopDraft({ type: "entity", index: entityIndex }); + return; + } + if (action === "remove-entity-gradient-stop") { + const scope = { type: "entity", index: Number(target.dataset.index) }; + const stopIndex = Number(target.dataset.stopIndex); + this._setScopedGradientStops(scope, this._getScopedGradientStopsValue(scope).filter((_, index) => index !== stopIndex), { rerender: true }); + return; + } + if (action === "add-segment") { + this._queuePostRenderFocus("#segment-draft-from"); + this._commitSegmentDraft({ type: "card" }); + return; + } + if (action === "remove-segment") { + const index = Number(target.dataset.index); + this._setSegments(this._getSegmentsValue().filter((_, segmentIndex) => segmentIndex !== index), { rerender: true, sort: true }); + return; + } + if (action === "add-entity-segment") { + const entityIndex = Number(target.dataset.index); + this._queuePostRenderFocus(`#entity-${entityIndex}-segment-draft-from`); + this._commitSegmentDraft({ type: "entity", index: entityIndex }); + return; + } + if (action === "remove-entity-segment") { + const scope = { type: "entity", index: Number(target.dataset.index) }; + const segmentIndex = Number(target.dataset.segmentIndex); + this._setScopedSegments(scope, this._getScopedSegmentsValue(scope).filter((_, index) => index !== segmentIndex), { rerender: true, sort: true }); + return; + } + } + _handleChange(event) { + var _a, _b; + const kind = (_b = (_a = event.target) == null ? void 0 : _a.dataset) == null ? void 0 : _b.kind; + if (kind === "gradient-pos") { + const stopIndex = Number(event.target.dataset.index); + this._commitGradientStopPosEdit({ type: "card" }, stopIndex, event.target.value, event.target); + return; + } + if (kind === "entity-gradient-pos") { + const scope = { type: "entity", index: Number(event.target.dataset.index) }; + const stopIndex = Number(event.target.dataset.stopIndex); + this._commitGradientStopPosEdit(scope, stopIndex, event.target.value, event.target); + return; + } + if (kind === "segment-from" || kind === "segment-to") { + this._commitSegmentBoundaryEdit({ type: "card" }, Number(event.target.dataset.index), kind === "segment-from" ? "from" : "to", event.target.value, event.target); + return; + } + if (kind === "entity-segment-from" || kind === "entity-segment-to") { + this._commitSegmentBoundaryEdit({ type: "entity", index: Number(event.target.dataset.index) }, Number(event.target.dataset.segmentIndex), kind === "entity-segment-from" ? "from" : "to", event.target.value, event.target); + return; + } + this._handleFieldEvent(event); + } + _handleInput(event) { + var _a; + const target = event.target; + if (!target) return; + if (target.tagName === "HA-ENTITY-PICKER") return; + if (target.tagName === "INPUT" && target.type === "checkbox") return; + const kind = (_a = target.dataset) == null ? void 0 : _a.kind; + if (kind === "gradient-pos") { + this._setGradientStopPosText({ type: "card" }, Number(target.dataset.index), target.value); + return; + } + if (kind === "entity-gradient-pos") { + this._setGradientStopPosText({ type: "entity", index: Number(target.dataset.index) }, Number(target.dataset.stopIndex), target.value); + return; + } + if (kind === "segment-from" || kind === "segment-to") { + this._setSegmentBoundaryText({ type: "card" }, Number(target.dataset.index), kind === "segment-from" ? "from" : "to", target.value); + this._refreshSegmentUi({ type: "card" }); + return; + } + if (kind === "entity-segment-from" || kind === "entity-segment-to") { + const scope = { type: "entity", index: Number(target.dataset.index) }; + this._setSegmentBoundaryText(scope, Number(target.dataset.segmentIndex), kind === "entity-segment-from" ? "from" : "to", target.value); + this._refreshSegmentUi(scope); + return; + } + this._handleFieldEvent(event); + } + _handleValueChanged(event) { + var _a; + if (((_a = event.target) == null ? void 0 : _a.tagName) === "HA-ENTITY-PICKER") { + this._handleFieldEvent(event); + } + } + _handleKeydown(event) { + var _a, _b, _c, _d, _e, _f, _g, _h, _i, _j, _k, _l, _m, _n, _o, _p; + const kind = (_b = (_a = event.target) == null ? void 0 : _a.dataset) == null ? void 0 : _b.kind; + if (event.key === "Escape") { + if (kind === "gradient-draft-pos" || kind === "gradient-draft-color") { + (_c = event.preventDefault) == null ? void 0 : _c.call(event); + this._gradientStopsDrafts.set(this._getGradientStopsDraftKey({ type: "card" }), this._createGradientStopDraftState({ type: "card" })); + this._render(); + return; + } + if (kind === "entity-gradient-draft-pos" || kind === "entity-gradient-draft-color") { + (_d = event.preventDefault) == null ? void 0 : _d.call(event); + const scope = { type: "entity", index: Number(event.target.dataset.index) }; + this._gradientStopsDrafts.set(this._getGradientStopsDraftKey(scope), this._createGradientStopDraftState(scope)); + this._render(); + } + if (kind === "segment-draft-from" || kind === "segment-draft-to" || kind === "segment-draft-color") { + (_e = event.preventDefault) == null ? void 0 : _e.call(event); + this._segmentDrafts.set(this._getSegmentsScopeKey({ type: "card" }), this._createSegmentDraftState({ type: "card" })); + this._render(); + return; + } + if (kind === "entity-segment-draft-from" || kind === "entity-segment-draft-to" || kind === "entity-segment-draft-color") { + (_f = event.preventDefault) == null ? void 0 : _f.call(event); + const scope = { type: "entity", index: Number(event.target.dataset.index) }; + this._segmentDrafts.set(this._getSegmentsScopeKey(scope), this._createSegmentDraftState(scope)); + this._render(); + return; + } + return; + } + if (event.key !== "Enter") { + return; + } + if (kind === "gradient-pos") { + (_g = event.preventDefault) == null ? void 0 : _g.call(event); + const stopIndex = Number(event.target.dataset.index); + this._commitGradientStopPosEdit({ type: "card" }, stopIndex, event.target.value, event.target); + return; + } + if (kind === "entity-gradient-pos") { + (_h = event.preventDefault) == null ? void 0 : _h.call(event); + const scope = { type: "entity", index: Number(event.target.dataset.index) }; + const stopIndex = Number(event.target.dataset.stopIndex); + this._commitGradientStopPosEdit(scope, stopIndex, event.target.value, event.target); + return; + } + if (kind === "gradient-draft-pos") { + (_i = event.preventDefault) == null ? void 0 : _i.call(event); + this._commitGradientStopDraft({ type: "card" }); + return; + } + if (kind === "gradient-draft-color") { + (_j = event.preventDefault) == null ? void 0 : _j.call(event); + this._commitGradientStopDraft({ type: "card" }); + return; + } + if (kind === "entity-gradient-draft-pos") { + (_k = event.preventDefault) == null ? void 0 : _k.call(event); + this._commitGradientStopDraft({ type: "entity", index: Number(event.target.dataset.index) }); + return; + } + if (kind === "entity-gradient-draft-color") { + (_l = event.preventDefault) == null ? void 0 : _l.call(event); + this._commitGradientStopDraft({ type: "entity", index: Number(event.target.dataset.index) }); + return; + } + if (kind === "segment-from" || kind === "segment-to") { + (_m = event.preventDefault) == null ? void 0 : _m.call(event); + this._commitSegmentBoundaryEdit({ type: "card" }, Number(event.target.dataset.index), kind === "segment-from" ? "from" : "to", event.target.value, event.target); + return; + } + if (kind === "entity-segment-from" || kind === "entity-segment-to") { + (_n = event.preventDefault) == null ? void 0 : _n.call(event); + this._commitSegmentBoundaryEdit({ type: "entity", index: Number(event.target.dataset.index) }, Number(event.target.dataset.segmentIndex), kind === "entity-segment-from" ? "from" : "to", event.target.value, event.target); + return; + } + if (kind === "segment-draft-from" || kind === "segment-draft-to" || kind === "segment-draft-color") { + (_o = event.preventDefault) == null ? void 0 : _o.call(event); + this._commitSegmentDraft({ type: "card" }); + return; + } + if (kind === "entity-segment-draft-from" || kind === "entity-segment-draft-to" || kind === "entity-segment-draft-color") { + (_p = event.preventDefault) == null ? void 0 : _p.call(event); + this._commitSegmentDraft({ type: "entity", index: Number(event.target.dataset.index) }); + } + } + _handleFieldEvent(event) { + var _a, _b, _c, _d, _e, _f, _g; + const target = event.target; + const rawField = (_a = target == null ? void 0 : target.dataset) == null ? void 0 : _a.field; + const rawKind = (_b = target == null ? void 0 : target.dataset) == null ? void 0 : _b.kind; + const field = (rawField == null ? void 0 : rawField.endsWith("-text-fallback")) ? rawField.slice(0, -14) : rawField; + const kind = (rawKind == null ? void 0 : rawKind.endsWith("-text-fallback")) ? rawKind.slice(0, -14) : rawKind; + const detailValue = (_c = event.detail) == null ? void 0 : _c.value; + const value = detailValue != null ? detailValue : (target == null ? void 0 : target.type) === "checkbox" ? target.checked : target == null ? void 0 : target.value; + if (field === "title") return void this._setTitle(value); + if (field === "formatting-unit") return void this._setScopedFormattingUnit({ type: "card" }, value); + if (field === "formatting-decimal") return void this._setScopedFormattingDecimal({ type: "card" }, value); + if (field === "layout-label-position") return void this._setLayoutLabelPosition(value); + if (field === "layout-label-hero-size") return void this._setLayoutHeroSize(value); + if (field === "layout-height") return void this._setLayoutHeight(value); + if (field === "layout-label-width") return void this._setScopedLayoutLabelWidth({ type: "card" }, value); + if (field === "scale-min") return void this._setScaleBound("min", value); + if (field === "scale-max") return void this._setScaleBound("max", value); + if (field === "bar-fill-style") return void this._setBarFillStyle(value); + if (field === "bar-color") return void this._setBarColor(value); + if (field === "bar-solid-fill") return void this._setScopedBarSolidFill({ type: "card" }, value); + if (field === "bar-needle-mode") return void this._setScopedNeedleMode({ type: "card" }, value); + if (field === "bar-needle-color") return void this._setScopedNeedleColor({ type: "card" }, value); + if (field === "baseline-mode") return void this._setBaselineMode({ type: "card" }, value); + if (field === "baseline-value") { + return void this._setBaselineResolvablePart({ type: "card" }, "fixed", value); + } + if (field === "baseline-above-color") return void this._setBaselineDirectionalColor({ type: "card" }, "above", value); + if (field === "baseline-below-color") return void this._setBaselineDirectionalColor({ type: "card" }, "below", value); + if (field === "baseline-above-color-enabled") return void this._setBaselineDirectionalColorEnabled({ type: "card" }, "above", value); + if (field === "baseline-below-color-enabled") return void this._setBaselineDirectionalColorEnabled({ type: "card" }, "below", value); + if (field === "target-mode") return void this._setTargetMode({ type: "card" }, value); + if (field === "target-value") { + return void this._setTargetResolvablePart({ type: "card" }, "fixed", value); + } + if (field === "target-color") return void this._setTargetColor({ type: "card" }, value); + if (field === "target-label-show") return void this._setTargetLabelShow({ type: "card" }, value); + if (field === "target-above-fill-enabled") return void this._setTargetAboveFillEnabled({ type: "card" }, value); + if (field === "target-above-fill-color") return void this._setTargetAboveFillColor({ type: "card" }, value); + if (field === "peak-show") return void this._setPeakShow(value); + if (field === "peak-color") return void this._setScopedPeakColor({ type: "card" }, value); + if (kind === "entity-picker" || kind === "entity-input") { + const index = Number(target.dataset.index); + const nextEntities = this._getEntitiesValue().map((entry, entryIndex) => entryIndex === index ? { ...entry, entity: this._normalizeTextValue(value) } : entry); + const nextEntries = this._buildEntityConfigEntries(nextEntities); + if (Array.isArray(this._draftConfig.entities) || nextEntries.length > 1 || !this._draftConfig.entity) { + this._setValueAtPath(["entities"], nextEntries); + } else { + this._setValueAtPath(["entity"], (_e = (_d = nextEntities[0]) == null ? void 0 : _d.entity) != null ? _e : ""); + } + return; + } + if (kind === "entity-name") { + return void this._setEntityField(Number(target.dataset.index), "name", value); + } + if (kind === "entity-icon") { + return void this._setEntityField(Number(target.dataset.index), "icon", value); + } + if (kind === "scale-min-entity-source") { + return void this._setCanonicalResolvablePart({ type: "card" }, "min", "entity", value); + } + if (kind === "scale-max-entity-source") { + return void this._setCanonicalResolvablePart({ type: "card" }, "max", "entity", value); + } + if (kind === "baseline-entity-source") { + return void this._setBaselineResolvablePart({ type: "card" }, "entity", value); + } + if (kind === "target-entity-source") { + return void this._setTargetResolvablePart({ type: "card" }, "entity", value); + } + if (kind === "entity-scale-inherit") { + if (value) { + return void this._clearScaleOverride({ type: "entity", index: Number(target.dataset.index) }); + } + return; + } + if (kind === "entity-override-min") { + return void this._setCanonicalResolvablePart({ type: "entity", index: Number(target.dataset.index) }, "min", "fixed", value); + } + if (kind === "entity-override-max") { + return void this._setCanonicalResolvablePart({ type: "entity", index: Number(target.dataset.index) }, "max", "fixed", value); + } + if (kind === "entity-override-min-entity-source") { + return void this._setCanonicalResolvablePart({ type: "entity", index: Number(target.dataset.index) }, "min", "entity", value); + } + if (kind === "entity-override-max-entity-source") { + return void this._setCanonicalResolvablePart({ type: "entity", index: Number(target.dataset.index) }, "max", "entity", value); + } + if (kind === "entity-override-height") { + return void this._setScopedLayoutHeight({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-layout-inherit") { + if (value) { + return void this._clearLayoutOverride({ type: "entity", index: Number(target.dataset.index) }); + } + return; + } + if (kind === "entity-layout-label-position") { + return void this._setScopedLayoutLabelPosition({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-layout-label-hero-size") { + return void this._setScopedLayoutHeroSize({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-layout-label-width") { + return void this._setScopedLayoutLabelWidth({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-formatting-inherit") { + if (value) { + return void this._clearFormattingOverride({ type: "entity", index: Number(target.dataset.index) }); + } + return; + } + if (kind === "entity-formatting-unit") { + return void this._setScopedFormattingUnit({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-formatting-decimal") { + return void this._setScopedFormattingDecimal({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-peak-inherit") { + if (value) { + return void this._clearPeakOverride({ type: "entity", index: Number(target.dataset.index) }); + } + return; + } + if (kind === "entity-peak-enabled") { + return void this._setScopedPeakEnabled({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-peak-color") { + return void this._setScopedPeakColor({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-segments-inherit") { + if (value) { + return void this._clearSegmentsOverride({ type: "entity", index: Number(target.dataset.index) }); + } + return; + } + if (kind === "entity-gradient-stops-inherit") { + if (value) { + return void this._clearGradientStopsOverride({ type: "entity", index: Number(target.dataset.index) }); + } + return; + } + if (kind === "entity-bar-inherit") { + if (value) { + return void this._clearEntityBarAppearance({ type: "entity", index: Number(target.dataset.index) }); + } + return; + } + if (kind === "entity-needle-inherit") { + if (value) { + return void this._removeScopedNeedle({ type: "entity", index: Number(target.dataset.index) }); + } + return; + } + if (kind === "entity-bar-fill-style") { + return void this._setScopedBarFillStyle({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-bar-color") { + return void this._setScopedBarColor({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-bar-solid-fill") { + return void this._setScopedBarSolidFill({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-needle-mode") { + return void this._setScopedNeedleMode({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-needle-color") { + return void this._setScopedNeedleColor({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-baseline-mode") { + return void this._setBaselineMode({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-baseline-inherit") { + if (value) { + return void this._clearBaselineOverride({ type: "entity", index: Number(target.dataset.index) }); + } + return; + } + if (kind === "entity-baseline-value") { + return void this._setBaselineResolvablePart({ type: "entity", index: Number(target.dataset.index) }, "fixed", value); + } + if (kind === "entity-baseline-entity-source") { + return void this._setBaselineResolvablePart({ type: "entity", index: Number(target.dataset.index) }, "entity", value); + } + if (kind === "entity-baseline-above-color") { + return void this._setBaselineDirectionalColor({ type: "entity", index: Number(target.dataset.index) }, "above", value); + } + if (kind === "entity-baseline-below-color") { + return void this._setBaselineDirectionalColor({ type: "entity", index: Number(target.dataset.index) }, "below", value); + } + if (kind === "entity-baseline-above-color-enabled") { + return void this._setBaselineDirectionalColorEnabled({ type: "entity", index: Number(target.dataset.index) }, "above", value); + } + if (kind === "entity-baseline-below-color-enabled") { + return void this._setBaselineDirectionalColorEnabled({ type: "entity", index: Number(target.dataset.index) }, "below", value); + } + if (kind === "entity-target-mode") { + return void this._setTargetMode({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-target-inherit") { + if (value) { + return void this._clearTargetOverride({ type: "entity", index: Number(target.dataset.index) }); + } + return; + } + if (kind === "entity-target-value") { + return void this._setTargetResolvablePart({ type: "entity", index: Number(target.dataset.index) }, "fixed", value); + } + if (kind === "entity-target-entity-source") { + return void this._setTargetResolvablePart({ type: "entity", index: Number(target.dataset.index) }, "entity", value); + } + if (kind === "entity-target-color") { + return void this._setTargetColor({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-target-label-show") { + return void this._setTargetLabelShow({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-target-above-fill-enabled") { + return void this._setTargetAboveFillEnabled({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "entity-target-above-fill-color") { + return void this._setTargetAboveFillColor({ type: "entity", index: Number(target.dataset.index) }, value); + } + if (kind === "gradient-draft-pos") { + this._setGradientStopsDraftField({ type: "card" }, "pos", value); + return; + } + if (kind === "gradient-draft-color") { + this._setGradientStopsDraftField({ type: "card" }, "color", value); + return; + } + if (kind === "entity-gradient-draft-pos") { + this._setGradientStopsDraftField({ type: "entity", index: Number(target.dataset.index) }, "pos", value); + return; + } + if (kind === "entity-gradient-draft-color") { + this._setGradientStopsDraftField({ type: "entity", index: Number(target.dataset.index) }, "color", value); + return; + } + if (kind === "segment-draft-from") { + this._setSegmentDraftField({ type: "card" }, "from", value); + return; + } + if (kind === "segment-draft-to") { + this._setSegmentDraftField({ type: "card" }, "to", value); + return; + } + if (kind === "segment-draft-color") { + this._setSegmentDraftField({ type: "card" }, "color", value); + return; + } + if (kind === "entity-segment-draft-from") { + this._setSegmentDraftField({ type: "entity", index: Number(target.dataset.index) }, "from", value); + return; + } + if (kind === "entity-segment-draft-to") { + this._setSegmentDraftField({ type: "entity", index: Number(target.dataset.index) }, "to", value); + return; + } + if (kind === "entity-segment-draft-color") { + this._setSegmentDraftField({ type: "entity", index: Number(target.dataset.index) }, "color", value); + return; + } + if (kind == null ? void 0 : kind.startsWith("gradient-")) { + const index = Number(target.dataset.index); + const currentStops = this._sanitizeGradientStopsForEmit(this._getGradientStopsValue()); + const nextStops = this._getGradientStopsValue().map((stop, stopIndex) => { + var _a2; + if (stopIndex !== index) return stop; + const nextPos = this._normalizeGradientStopPosValue(stop == null ? void 0 : stop.pos); + return { + ...stop, + pos: nextPos != null ? nextPos : 0, + color: kind === "gradient-color" ? value : (_a2 = stop == null ? void 0 : stop.color) != null ? _a2 : "#4a9eff" + }; + }); + if (this._serializeConfig(nextStops) !== this._serializeConfig(currentStops)) { + this._setGradientStops(nextStops); + } + return; + } + if (kind == null ? void 0 : kind.startsWith("entity-gradient-")) { + const scope = { type: "entity", index: Number(target.dataset.index) }; + const stopIndex = Number(target.dataset.stopIndex); + const currentStops = this._sanitizeGradientStopsForEmit(this._getScopedGradientStopsValue(scope)); + const nextStops = this._getScopedGradientStopsValue(scope).map((stop, currentStopIndex) => { + var _a2; + if (currentStopIndex !== stopIndex) return stop; + const nextPos = this._normalizeGradientStopPosValue(stop == null ? void 0 : stop.pos); + return { + ...stop, + pos: nextPos != null ? nextPos : 0, + color: kind === "entity-gradient-color" ? value : (_a2 = stop == null ? void 0 : stop.color) != null ? _a2 : "#4a9eff" + }; + }); + if (this._serializeConfig(nextStops) !== this._serializeConfig(currentStops)) { + this._setScopedGradientStops(scope, nextStops); + } + return; + } + if (kind === "segment-color") { + const index = Number(target.dataset.index); + const nextSegments = ((_f = this._getSegmentsUiRows({ type: "card" })) != null ? _f : this._getSegmentsValue()).map((segment, segmentIndex) => { + if (segmentIndex !== index) return segment; + return { + ...segment, + color: value + }; + }); + this._setSegments(nextSegments, { sort: false }); + return; + } + if (kind === "entity-segment-color") { + const scope = { type: "entity", index: Number(target.dataset.index) }; + const segmentIndex = Number(target.dataset.segmentIndex); + const nextSegments = ((_g = this._getSegmentsUiRows(scope)) != null ? _g : this._getScopedSegmentsValue(scope)).map((segment, currentSegmentIndex) => { + if (currentSegmentIndex !== segmentIndex) return segment; + return { + ...segment, + color: value + }; + }); + this._setScopedSegments(scope, nextSegments, { sort: false }); + return; + } + } + }; + } + }); + + // src/sensor-bar-card-plus.js + var require_sensor_bar_card_plus = __commonJS({ + "src/sensor-bar-card-plus.js"() { + init_SensorBarCard(); + init_SensorBarCardPlusEditor(); + customElements.define("sensor-bar-card-plus", SensorBarCard); + customElements.define("sensor-bar-card-plus-editor", SensorBarCardPlusEditor); + window.customCards = window.customCards || []; + window.customCards.push({ + type: "sensor-bar-card-plus", + name: "Sensor Bar Card Plus", + description: "Animated, colour-coded horizontal bar card for Home Assistant with extended target and layout features." + }); + } + }); + require_sensor_bar_card_plus(); +})(); diff --git a/www/community/sensor-bar-card-plus/sensor-bar-card-plus.js.gz b/www/community/sensor-bar-card-plus/sensor-bar-card-plus.js.gz new file mode 100644 index 0000000..6089085 Binary files /dev/null and b/www/community/sensor-bar-card-plus/sensor-bar-card-plus.js.gz differ diff --git a/www/commute_snapshot.jpg b/www/commute_snapshot.jpg new file mode 100644 index 0000000..bf2439c Binary files /dev/null and b/www/commute_snapshot.jpg differ diff --git a/www/nest.jpg b/www/nest.jpg new file mode 100644 index 0000000..5ccc755 Binary files /dev/null and b/www/nest.jpg differ diff --git a/www/temp/nest_snapshot.jpg b/www/temp/nest_snapshot.jpg new file mode 100644 index 0000000..5ccc755 Binary files /dev/null and b/www/temp/nest_snapshot.jpg differ diff --git a/www/thermostat-pro-timeline.js b/www/thermostat-pro-timeline.js new file mode 100644 index 0000000..ac38266 --- /dev/null +++ b/www/thermostat-pro-timeline.js @@ -0,0 +1,22112 @@ +// Note: avoid ESM imports so the card works when loaded as "javascript" resource in HA. +// ---- thermostat-timeline i18n (namespaced) ---- +const TT_I18N = { + en: { + // Card + 'card.title_default': 'Thermostat Timeline', + 'err.config_entities': "Config must have 'entities: [climate.xxx, ...]'.", + // Modal / editor (inline) + 'ui.edit_block': 'Edit block', + 'ui.add_block': 'Add block', + 'ui.from': 'From', + 'ui.to': 'To', + 'ui.temperature': 'Temperature', + 'ui.delete': 'Delete', + 'ui.cancel': 'Cancel', + 'ui.close': 'Close', + 'ui.unsaved_confirm': 'You have unsaved changes. Close anyway?', + 'ui.discard_changes': 'Discard changes', + 'ui.keep_editing': 'Keep editing', + 'ui.save': 'Save', + 'ui.error_prefix': 'Error: ', + 'ui.overlap_msg': 'You selected overlapping times — overlap is {start} – {end}.', + 'ui.auto_fix': 'Auto-fix', + 'ui.fix_start_to': 'Set start to {time}', + 'ui.fix_end_to': 'Set end to {time}', + 'ui.current_temp': 'Current temperature:', + 'ui.copy_plan': 'Copy plan', + 'ui.paste_plan': 'Paste plan', + 'ui.copy_day': 'Copy day', + 'ui.paste_day': 'Paste day', + + 'editor.title_label': 'Title', + 'editor.add_entity': 'Add room', + 'editor.entity_placeholder': 'Select an entity', + 'editor.drag_reorder': 'Drag to reorder', + 'editor.remove': 'Remove', + 'editor.default_c': 'Default °C', + 'editor.row_height_px': 'Row height (px)', + 'editor.max_c': 'Max °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Auto apply', + 'editor.auto_apply.desc': 'When enabled, the card tries to set temperature on the selected climate entities to match the scheduled value for “now”.', + 'editor.apply_edit.title': 'On edit', + 'editor.apply_edit.desc': 'When you edit blocks and it affects the current time, apply the new temperature immediately.', + 'editor.apply_default.title': 'On default temperature change', + 'editor.apply_default.desc': 'When “Default °C” changes and it affects the current period, apply the new temperature immediately.', + 'editor.perroom.title': 'Default °C per room', + 'editor.perroom.desc': 'Shows a Default °C field on each row. When off, the global Default °C is used for all rooms.', + + 'editor.show_room_temp.title': 'Show room temperature', + 'editor.show_room_temp.desc': 'Show the current room temperature bubble on each row in the timeline header.', + + 'editor.room_mode.title': 'Room entity type', + 'editor.room_mode.desc': 'When enabled, select an input_number (target temperature). When disabled, select a climate entity.', + + 'editor.room_temp_sensor_override.title': 'Use room temperature sensor', + 'editor.room_temp_sensor_override.desc': 'When enabled, the shown room temperature (and boiler logic) uses the selected temperature sensor instead of climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Temperature sensor', + + 'editor.turn_on.title': 'Send turn_on command', + 'editor.turn_on.desc': 'Some thermostats require a turn_on command in addition to set_temperature.', + 'editor.turn_on.order': 'Order', + 'editor.turn_on.order.before': 'Before set_temperature', + 'editor.turn_on.order.after': 'After set_temperature', + + 'editor.merge_label': 'Merge with (add extra thermostat)', + 'editor.display_name_optional': 'Room name (optional)', + 'editor.merged_with': 'Merged with', + + 'editor.store_enable.title': 'Shared storage + background control', + 'editor.store_enable.short': 'Stores schedules in the selected sensor and enables background control.', + 'editor.store_enable.desc': 'Stores schedules in the selected sensor and enables set_temperature in the background. Thermostats are also updated when the card is closed. Shares schedules and card settings across users/devices. When off: commands are only sent while the card is visible on a device and must remain open.', + 'editor.store_missing': 'The integration “Thermostat Pro Timeline Sync” is not installed. Shared storage is unavailable.', + 'editor.open_in_hacs': 'Open in HACS', + 'editor.clear_store': 'Clear storage', + 'editor.clear_store_confirm': 'This will delete all stored schedules. Continue?', + 'editor.migrate_to_store': 'Transfer data to storage', + 'editor.migrate_confirm': 'Transfer your browser-saved schedules to the selected sensor? This overwrites existing schedules in storage.', + 'editor.migrate_pull_confirm': 'Import schedules from storage and overwrite local browser data?', + 'editor.select_storage_first': 'Select a storage sensor first.', + 'editor.no_local_data': 'No local browser data was found.', + 'editor.test_tool': 'Test tool', + 'editor.test_select_entity': 'Select entity', + 'editor.test_empty': 'Select an entity to see available commands…', + 'editor.test_export': 'Export .md', + 'editor.test_no_fields': 'No fields', + 'editor.test_select_placeholder': '— Select —', + 'editor.error_prefix': 'Error: ', + 'editor.clear_all': 'Clear all data', + 'editor.clear_all_confirm': 'This deletes both storage and local/browser schedules. Continue?', + 'editor.clear_storage_only': 'Clear storage data only', + 'editor.clear_storage_only_confirm': 'This deletes schedules from the selected storage sensor. Continue?', + 'editor.clear_local_only': 'Clear ALL local data (local only)', + 'editor.clear_local_only_confirm': 'This deletes only schedules saved in your browser. Continue?', + 'editor.reset.desc': 'What this does:\n\n• Clear all data: Resets backend storage (.storage) for Thermostat Timeline and removes ALL local browser caches/drafts.\n• Clear ALL local data (local only): Only clears data stored in this browser (localStorage).\n\nTip: After a full reset, reload the page to avoid old drafts reappearing.', + + 'editor.select_entities': 'Rooms', + 'editor.colors.empty': 'No rooms added yet. Add a room to define colors.', + 'editor.colors.go_add': 'Add room', + + 'week.enable': 'Enable weekdays', + 'week.enable.desc': 'Enable per-weekday planning. When off, the same plan is used every day.', + 'week.mode': 'Weekday mode', + // removed same_all option from UI + 'week.mode.weekday_weekend': 'Weekdays + Weekend', + 'week.mode.weekday_sat_sun': 'Weekdays + Saturday + Sunday', + 'week.mode.all_7': 'All days', + 'week.button': 'Weekdays', + 'week.title': 'Weekdays', + 'week.group.weekdays': 'Weekdays', + 'week.group.weekend': 'Weekend', + 'week.days_full.mon': 'Monday', + 'week.days_full.tue': 'Tuesday', + 'week.days_full.wed': 'Wednesday', + 'week.days_full.thu': 'Thursday', + 'week.days_full.fri': 'Friday', + 'week.days_full.sat': 'Saturday', + 'week.days_full.sun': 'Sunday', + 'week.days.mon': 'Mon', + 'week.days.tue': 'Tue', + 'week.days.wed': 'Wed', + 'week.days.thu': 'Thu', + 'week.days.fri': 'Fri', + 'week.days.sat': 'Sat', + 'week.days.sun': 'Sun', + 'week.editor.title': 'Weekday schedule', + 'week.editor.preview_note': 'Preview only. Changes are applied when you press Save.', + 'week.open_editor.desc': 'Open the weekdays editor popup.', + 'week.open_editor.btn': 'Open', + 'week.view.title': 'Weekdays view', + 'week.view.rooms_one_day': 'All rooms • one day', + 'week.view.days_one_room': 'All days • one room', + 'week.view_switch.title': 'Switch view from timeline', + 'week.view_switch.desc': 'Show a toggle in the timeline header to switch between the two Weekdays view modes.', + 'week.copy_from_main.title': 'Copy main schedule', + 'week.copy_from_main.desc': 'You enabled weekdays. Do you want to copy the main schedule into weekdays? Choose the weekday mode to copy into.', + 'week.copy_from_main.mode_label': 'Weekday mode', + 'week.copy_from_main.target_label': 'Copy to', + 'week.copy_from_main.copy_btn': 'Copy', + 'week.copy_from_main.skip_btn': 'Don’t copy', + 'week.select_room': 'Select room', + 'week.copy_to_rooms': 'Copy weekdays to rooms', + 'week.copy_to_rooms.title': 'Copy weekdays to rooms', + 'week.copy_to_rooms.select': 'Select rooms', + 'week.copy_to_rooms.apply': 'Apply to rooms', + + 'editor.timefmt.title': 'Time format', + 'editor.timefmt.desc': 'Choose 24-hour or 12-hour (AM/PM) display for times.', + 'editor.tempfmt.title': 'Temperature unit', + 'editor.tempfmt.desc': 'Choose Celsius (°C) or Fahrenheit (°F) for display in the card and editors.', + 'editor.timesrc.title': 'Time source', + 'editor.timesrc.desc': 'Choose whether the card uses the browser time or the Home Assistant time zone for “now”.', + 'editor.timesrc.browser': 'Browser', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'Configuration ID', + 'editor.instance.desc': 'Keep multiple independent schedules/settings under different IDs.', + 'editor.instance.new_id': 'New ID', + + 'editor.tabs.settings': 'Settings', + 'editor.tabs.pause': 'Pause', + 'editor.tabs.weekdays': 'Weekdays', + 'editor.tabs.rooms': 'Rooms', + 'editor.tabs.boiler': 'Boiler control', + 'editor.tabs.colors': 'Colors', + 'editor.tabs.presence_sensor': 'Presence sensor', + 'editor.tabs.owd': 'Open Window Detection', + 'editor.tabs.away': 'Away from home', + 'editor.tabs.reset': 'Reset', + 'editor.tabs.sync': 'Sync Engine', + 'editor.tabs.backup': 'Backup', + + 'editor.presence_sensor.enable.title': 'Presence sensor', + 'editor.presence_sensor.enable.desc': 'Enable per-room presence sensor configuration.', + + 'editor.presence_sensor.entity.title': 'Presence entity', + 'editor.presence_sensor.entity.desc': 'Select the binary_sensor that indicates presence in this room.', + + 'editor.presence_sensor.on_temp.title': 'Temperature when presence is ON', + 'editor.presence_sensor.on_temp.desc': 'If the sensor is ON, this room will use this temperature instead of the schedule.', + + 'editor.presence_sensor.on_delay.title': 'Delay before applying presence', + 'editor.presence_sensor.on_delay.desc': 'How long the sensor must stay ON before the override temperature is used. Value uses the selected unit.', + 'editor.presence_sensor.off_delay.title': 'Delay before resuming schedule', + 'editor.presence_sensor.off_delay.desc': 'How long the sensor must stay OFF before the schedule is resumed. Value uses the selected unit.', + + 'owd.enable': 'Open Window Detection', + 'owd.enable.desc': 'Turns off thermostats in a room when selected window/door sensors are open.', + 'owd.sensors': 'Window/Door sensors', + 'owd.delays.title': 'Delays', + 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).', + 'owd.open_delay': 'After open (min)', + 'owd.close_delay': 'After closed (min)', + + 'boiler.switch': 'Boiler switch', + 'boiler.switch_type': 'Entity type', + 'boiler.switch_type.switch': 'Switch', + 'boiler.switch_type.input_boolean': 'Input boolean', + 'boiler.rooms': 'Rooms included', + 'boiler.rooms.desc': 'Select which rooms should be included in boiler control.', + 'boiler.multi_enable': 'Enable multiple boilers', + 'boiler.multi_enable.desc': 'Assign rooms to different boilers and set per-room offsets.', + 'boiler.multi_rooms': 'Boiler per room', + 'boiler.multi_rooms.desc': 'Select boiler switch and offsets for each room.', + 'boiler.room.enable': 'Include room', + 'boiler.room.enable.desc': 'Include this room in boiler control.', + 'boiler.enable': 'Enable boiler control', + 'boiler.enable.desc': 'Show boiler settings in this card editor.', + 'boiler.temp_sensor': 'Boiler temperature sensor', + 'boiler.temp_sensor.desc': 'Temperature sensor that controls the boiler switch.', + 'boiler.min_temp': 'Min boiler temp (°C)', + 'boiler.max_temp': 'Max boiler temp (°C)', + 'boiler.offsets': 'Offsets', + 'boiler.offsets.desc': 'ON offset: how far below the scheduled target before turning ON. OFF offset: how far above the scheduled target before turning OFF.', + 'boiler.on_offset': 'ON offset (°C)', + 'boiler.off_offset': 'OFF offset (°C)', + + 'sync.running': 'Sync in progress…', + 'sync.completed': 'Sync completed', + + 'backup.title': 'Backup', + 'backup.desc': 'Copy the full content of your sync sensor to the backup sensor and restore it when needed.', + 'backup.now': 'Backup now', + 'backup.restore': 'Restore', + 'backup.restore.confirm_overwrite': 'Restore from backup? This will overwrite current schedules and settings.', + 'backup.delete': 'Delete backup', + 'backup.delete.confirm': 'Delete selected backup?', + 'backup.new': 'New backup', + 'backup.auto': 'Auto backup', + 'backup.auto.desc': 'Create backups automatically at a fixed interval.', + 'backup.interval': 'Interval (days)', + 'backup.interval.desc': 'How many days between automatic backups (min 1).', + 'backup.sensor': 'Backup sensor', + 'backup.last': 'Last backup', + 'backup.slot.title': 'Select backup', + 'backup.slot.desc': 'Select which backup slot to restore.', + 'backup.slot.empty': 'No backups yet', + 'backup.select': 'Select what to back up', + 'backup.select.desc': 'Checked items are included in backup/restore', + 'backup.opt.all': 'All', + 'backup.opt.main': 'Main schedules', + 'backup.opt.weekday': 'Weekday schedules', + 'backup.opt.presence': 'Presence schedules', + 'backup.opt.settings': 'Editor settings', + 'backup.opt.holiday': 'Holiday schedules', + 'backup.opt.colors': 'Color blocks', + + 'backup.import.title': 'Import thermostat_timeline.json', + 'backup.import.desc': 'Upload a thermostat_timeline.json file and import it. A new backup is created first.', + 'backup.import.btn': 'Import file', + 'backup.export.title': 'Export backup file', + 'backup.export.desc': 'Download a full thermostat_timeline_backup.json with all slots and data.', + 'backup.export.btn': 'Download file', + 'backup.imported_suffix': ' - imported', + 'backup.importbk.title': 'Import backup file', + 'backup.importbk.desc': 'Upload a thermostat_timeline_backup.json file to append its backups to the list.', + 'backup.importbk.btn': 'Import backup', + 'backup.importbk.err_invalid': 'Invalid backup file', + 'backup.file.choose': 'Choose imported backup', + 'backup.file.add_selected': 'Import backup', + 'backup.file.add_all': 'Import all backups', + 'backup.file.loaded_n': 'File loaded ({n} backups)', + 'backup.file.imported_ok': 'Imported ({n})', + 'backup.import.err_no_hass': 'Home Assistant not ready', + 'backup.import.err_storage_off': 'Shared storage is OFF. Turn it on first.', + 'backup.import.err_no_integration': 'Integration thermostat_timeline not available', + 'backup.import.err_json': 'Invalid JSON', + 'backup.import.err_invalid': 'Invalid file: missing data.schedules', + + 'away.enable': 'Enable Away mode', + 'away.desc': 'When none of the selected persons are home, thermostats are automatically lowered to the specified temperature.', + 'away.temp': 'Away temperature', + 'away.persons': 'Persons', + 'away.add_person': 'Add person', + 'away.resume_schedule': 'Resume schedule', + 'away.resume_away': 'Resume away temperature', + + 'editor.heat_colors.title': 'Colors for heat blocks', + 'editor.heat_colors.add': 'Add color interval', + 'editor.colors.col_from': 'From °C', + 'editor.colors.col_to': 'To °C', + 'editor.colors.col_color': 'Color', + 'editor.colors.refresh': 'Refresh schedule', + 'editor.colors.clear_local': 'Clear local color data', + 'editor.colors.clear_local_confirm': 'This deletes color intervals stored in your browser for this card. Continue?', + 'editor.colors.clear_shared': 'Clear shared color storage', + 'editor.colors.clear_shared_confirm': 'This deletes color intervals in the selected storage sensor. Continue?', + 'editor.colors.clear_all': 'Clear all color data', + 'editor.colors.clear_all_confirm': 'This deletes all color intervals both locally and in the selected storage sensor. Continue?', + 'editor.colors.mode': 'Color mode', + 'editor.colors.mode.desc': 'Choose per-room colors or one set for all rooms.', + 'editor.colors.mode_per_room': 'Per room', + 'editor.colors.mode_global': 'All rooms', + 'editor.colors.global_title': 'All rooms', + + 'editor.reload_editor': 'Reload editor', + 'editor.reload_dashboard': 'Reload dashboard', + + 'onboard.title': 'Welcome to Thermostat Timeline', + 'onboard.next': 'Next', + 'onboard.prev': 'Back', + 'onboard.skip': 'Skip', + 'onboard.done': 'Done', + 'onboard.dont_show': 'Don’t show again', + 'onboard.video_not_found': 'Video not found', + 'onboard.image_not_found': 'Image not found', + 'onboard.step_main': 'This is the main timeline. It shows target temperatures over 24 hours for each room.', + 'onboard.step_add': 'Add a heat block via + Add block or by double-clicking the timeline.', + 'onboard.step_edit': 'Edit or delete a block by double-clicking it. Use Weekdays for different days.', + 'onboard.reset': 'Show guide again', + 'onboard.reset_ok': 'The guide will show next time.', + + 'pause.menu_title': 'Pause auto-apply', + 'pause.off_1h': 'Pause for 1 hour', + 'pause.off_5h': 'Pause for 5 hours', + 'pause.off_custom': 'Custom duration…', + 'pause.prompt_minutes': 'Enter duration (minutes):', + 'pause.off_indef': 'Pause until I resume', + 'pause.resume': 'Resume now', + 'pause.close': 'Close', + 'pause.active_until': 'Paused until {time}', + 'pause.active_indef': 'Paused', + + 'editor.pausebtn.title': 'Pause button', + 'editor.pausebtn.desc': 'Show a global pause button in the card header. When pause is active, all set_temperature commands are suppressed.', + 'editor.pause_sensor.title': 'Pause via binary sensor', + 'editor.pause_sensor.desc': 'Automatically pause while the selected binary sensor is on.', + 'editor.pause_sensor.entity_title': 'Pause sensor', + 'editor.pause_sensor.entity_desc': 'Binary sensor that controls pause (on = pause, off = resume).', + 'editor.integration.connected': 'Connected to integration', + 'editor.integration.disconnected': 'Not connected to integration', + + 'profiles.enable': 'Enable profiles (override)', + 'profiles.enable.desc': 'Allow named day schedules that can be activated to override the main plan.', + 'season.enable.title': 'Enable summer/winter schedules', + 'season.enable.desc': 'When enabled, each schedule has separate Summer and Winter versions that you can switch between.', + 'season.mode.title': 'Season', + 'season.mode.winter': 'Winter', + 'season.mode.summer': 'Summer', + 'profiles.button': 'Profiles', + 'profiles.editor.title': 'Profiles', + 'profiles.add': 'Add profile', + 'profiles.rename': 'Rename', + 'profiles.delete': 'Delete', + 'profiles.activate': 'Activate', + 'profiles.deactivate': 'Deactivate', + 'profiles.active': 'Active', + 'profiles.none': 'None', + 'profiles.name_placeholder': 'Profile name', + 'profiles.name_exists': 'Name exists', + 'profiles.edit': 'Edit', + 'profiles.preview_note': 'Preview only. Profiles become active when you activate them.', + 'profiles.active_label': 'Active profile:', + 'profiles.save_new': 'Save new profile', + 'profiles.save_existing': 'Save changes to selected profile', + + 'editor.tabs.holidays': 'Holidays', + 'holidays.enable': 'Enable holidays', + 'holidays.desc': 'Use a special day schedule on holidays.', + 'holidays.source': 'Holiday source', + 'holidays.source.calendar': 'Calendar', + 'holidays.source.manual': 'Manual dates', + 'holidays.entity': 'Holiday entity', + 'holidays.entity_desc': 'Select a calendar that only provides on/off. It must be on on holidays and off on other days.', + 'holidays.dates': 'Holiday dates', + 'holidays.add_date': 'Add date', + 'holidays.add_range': 'Add range', + 'holidays.date_in_range': 'Date already covered by a range', + 'holidays.ranges': 'Holiday ranges', + 'holidays.single_dates': 'Single dates', + 'holidays.editor.title': 'Holiday schedule', + 'holidays.edit': 'Edit holiday schedule', + 'holidays.button': 'Holidays', + // Holiday copy-from popup + 'holidays.copy_from': 'Copy from…', + 'holidays.copy.title': 'Copy into holiday', + 'holidays.copy.select': 'Select source', + 'holidays.copy.apply': 'Insert', + 'holidays.copy.replace_confirm': 'Replace existing holiday blocks?', + 'holidays.copy.group.main': 'Main plan', + 'holidays.copy.group.weekdays': 'Weekdays', + 'holidays.copy.group.profiles': 'Profiles', + 'holidays.copy.group.presence': 'Presence', + + 'presence.button': 'Presence schedules', + 'presence.editor.title': 'Presence schedules', + 'presence.enable_advanced': 'Enable advanced presence schedules', + 'presence.enable_advanced.desc': 'Use combinations and schedules based on who is home/away.', + 'presence.live_header': 'Show live presence in header', + 'presence.live_header.desc': 'Chips are shown in the right corner.', + // Away delay + 'away.delay_enable': 'Delay Away actions', + 'away.delay_desc': 'Wait before applying Away changes.', + 'away.delay_value': 'Delay', + 'away.delay_unit': 'Unit', + 'away.delay_unit.seconds': 'Seconds', + 'away.delay_unit.minutes': 'Minutes', + 'presence.combos': 'Combinations', + 'presence.enable_combo': 'Enable this combination', + 'presence.all_home': 'All home', + 'presence.none_home': 'Nobody home', + 'presence.home': 'Home: {names}', + 'presence.away': 'Away: {names}', + 'presence.manage': 'Manage combinations', + 'presence.who_home': 'Who is home', + 'presence.who_away': 'Who is away' + }, + da: { + // Card + 'card.title_default': 'Thermostat Timeline', + 'err.config_entities': "Config skal have 'entities: [climate.xxx, ...]'.", + // Modal / editor (inline) + 'ui.edit_block': 'Edit block', + 'ui.add_block': 'Add block', + 'ui.from': 'From', + 'ui.to': 'To', + 'ui.temperature': 'Temperatur', + 'ui.delete': 'Slet', + 'ui.cancel': 'Annuller', + 'ui.close': 'Luk', + 'ui.unsaved_confirm': 'Du har ændringer, der ikke er gemt. Vil du lukke alligevel?', + 'ui.discard_changes': 'Forkast ændringer', + 'ui.keep_editing': 'Fortsæt redigering', + 'ui.save': 'Gem', + 'ui.error_prefix': 'Fejl: ', + 'ui.overlap_msg': 'Du har valgt overlappende tider — overlap er {start} – {end}.', + 'ui.auto_fix': 'Ret automatisk', + 'ui.fix_start_to': 'Sæt start til {time}', + 'ui.fix_end_to': 'Sæt slut til {time}', + 'ui.current_temp': 'Nuværende temperatur:', + 'ui.copy_plan': 'Kopiér plan', + 'ui.paste_plan': 'Indsæt plan', + 'ui.copy_day': 'Kopiér dag', + 'ui.paste_day': 'Indsæt dag', + 'editor.title_label': 'Titel', + 'editor.store_enable.title': 'Delt lager + baggrundsstyring', + 'editor.store_enable.short': 'Gemmer tidsplaner i den valgte sensor og aktiverer baggrundsstyring.', + 'editor.store_enable.desc': 'Gemmer tidsplaner i den valgte sensor og aktiverer set_temperature i baggrunden. Termostater opdateres også når kortet er lukket. Deler tidsplaner og kortindstillinger på tværs af brugere/enheder. Når slået fra: kommandoer sendes kun mens kortet er synligt på en enhed og skal forblive åbent.', + 'editor.store_missing': 'Integrationen “Thermostat Pro Timeline Sync” er ikke installeret. Delt lager er ikke tilgængeligt.', + 'editor.clear_store': 'Ryd lager', + 'editor.clear_store_confirm': 'Dette vil slette alle gemte tidsplaner. Vil du fortsætte?', + 'editor.migrate_to_store': 'Overfør data til lager', + 'editor.migrate_confirm': 'Overfør dine browser-gemte tidsplaner til den valgte sensor? Dette overskriver eksisterende planer i lageret.', + 'editor.migrate_pull_confirm': 'Importér tidsplaner fra lager og overskriv lokale browserdata?', + 'editor.select_storage_first': 'Vælg først en lagersensor.', + 'editor.no_local_data': 'Ingen lokale browserdata blev fundet.', + 'editor.clear_all': 'Ryd alt data', + 'editor.clear_all_confirm': 'Dette sletter både lager og lokale/browser tidsplaner. Vil du fortsætte?', + 'editor.clear_storage_only': 'Ryd kun lagerdata', + 'editor.clear_storage_only_confirm': 'Dette sletter tidsplaner fra den valgte lagersensor. Vil du fortsætte?', + 'editor.clear_local_only': 'Ryd ALLE lokale data (kun lokalt)', + 'editor.clear_local_only_confirm': 'Dette sletter kun tidsplaner gemt i din browser. Vil du fortsætte?', + 'editor.reset.desc': 'Hvad gør det her:\n\n• Ryd alt data: Nulstiller backend storage (.storage) for Thermostat Timeline og fjerner ALLE lokale caches/drafts i denne browser.\n• Ryd ALLE lokale data (kun lokalt): Rydder kun data gemt i denne browser (localStorage).\n\nTip: Efter en fuld reset kan en genindlæsning forhindre at gamle drafts dukker op igen.', + 'editor.temp_sensor': 'Temperatursensor', + 'editor.test_tool': 'Testværktøj', + 'editor.test_select_entity': 'Vælg entitet', + 'editor.test_empty': 'Vælg en entitet for at se tilgængelige kommandoer…', + 'editor.test_export': 'Export .md', + 'editor.test_no_fields': 'Ingen felter', + 'editor.test_select_placeholder': '— Vælg —', + 'editor.error_prefix': 'Fejl: ', + 'editor.open_in_hacs': 'Åbn i HACS', + 'editor.add_entity': 'Tilføj rum', + 'editor.entity_placeholder': 'Vælg en entitet', + 'editor.drag_reorder': 'Træk for at ændre rækkefølge', + 'editor.remove': 'Fjern', + 'editor.default_c': 'Standard °C', + 'editor.row_height_px': 'Rækkehøjde (px)', + 'editor.min_c': 'Min °C', + 'editor.max_c': 'Maks °C', + 'editor.auto_apply.title': 'Auto apply', + 'editor.auto_apply.desc': 'Når slået til forsøger kortet at sætte temperaturen på de valgte climate‑entiteter til den planlagte værdi for “nu”.', + 'editor.apply_edit.title': 'Ved redigering', + 'editor.apply_edit.desc': 'Når du redigerer blokke og det påvirker nuværende tidspunkt, så anvend den nye temperatur med det samme.', + 'editor.apply_default.title': 'Ved ændring af Standard °C', + 'editor.apply_default.desc': 'Når “Standard °C” ændres og det påvirker den aktuelle periode, så anvend den nye temperatur med det samme.', + 'editor.perroom.title': 'Standard °C pr. rum', + 'editor.perroom.desc': 'Vis et Standard °C‑felt på hver række. Når slået fra, bruges global Standard °C for alle rum.', + + 'editor.show_room_temp.title': 'Vis rumtemperatur', + 'editor.show_room_temp.desc': 'Vis boblen med nuværende rumtemperatur på hver række i tidslinjens header.', + 'editor.room_mode.title': 'Rumtype (entitet)', + 'editor.room_mode.desc': 'Når slået til vælger du en input_number (måltemperatur). Når slået fra vælger du en climate‑entitet.', + + 'editor.room_temp_sensor_override.title': 'Brug temperatur-sensor til rumtemp', + 'editor.room_temp_sensor_override.desc': 'Når slået til bruges den valgte temperatursensor til vist rumtemp (og kedel-logik) i stedet for climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Temperatursensor', + + 'editor.turn_on.title': 'Send turn_on kommando', + 'editor.turn_on.desc': 'Nogle termostater kræver en turn_on kommando ud over set_temperature.', + 'editor.turn_on.order': 'Rækkefølge', + 'editor.turn_on.order.before': 'Før set_temperature', + 'editor.turn_on.order.after': 'Efter set_temperature', + 'editor.display_name_optional': 'Rum navn (valgfri)', + 'editor.merge_label': 'Flet med (tilføj ekstra termostat)', + 'editor.merged_with': 'Flettet med', + 'editor.colors.empty': 'Der er endnu ikke oprettet rum. Tilføj et rum for at definere farver.', + 'editor.colors.go_add': 'Tilføj rum', + 'editor.select_entities': 'Rum', + 'week.enable': 'Aktivér ugedage', + 'week.mode': 'Ugedags-tilstand', + 'week.mode.weekday_weekend': 'Hverdage + Weekend', + 'week.mode.weekday_sat_sun': 'Hverdage + Lørdag + Søndag', + 'week.mode.all_7': 'Alle dage', + 'week.button': 'Ugedage', + 'week.title': 'Ugedage', + 'week.group.weekdays': 'Hverdage', + 'week.group.weekend': 'Weekend', + 'week.days_full.mon': 'Mandag', + 'week.days_full.tue': 'Tirsdag', + 'week.days_full.wed': 'Onsdag', + 'week.days_full.thu': 'Torsdag', + 'week.days_full.fri': 'Fredag', + 'week.days_full.sat': 'Lørdag', + 'week.days_full.sun': 'Søndag', + 'week.days.mon': 'Man', + 'week.days.tue': 'Tir', + 'week.days.wed': 'Ons', + 'week.days.thu': 'Tor', + 'week.days.fri': 'Fre', + 'week.days.sat': 'Lør', + 'week.days.sun': 'Søn', + 'week.editor.title': 'Ugedags-tidsplan', + 'week.editor.preview_note': 'Kun forhåndsvisning. Ændringer gemmes først når du trykker Gem.', + 'week.open_editor.desc': 'Åbn ugedags-editoren.', + 'week.open_editor.btn': 'Åbn', + 'week.view.title': 'Ugedage-visning', + 'week.view.rooms_one_day': 'Alle rum • én dag', + 'week.view.days_one_room': 'Alle dage • ét rum', + 'week.view_switch.title': 'Skift visning fra tidslinjen', + 'week.view_switch.desc': 'Vis en toggle i tidslinjens header, så du kan skifte ugedags-visning uden at åbne indstillinger.', + 'week.copy_from_main.title': 'Kopiér main‑skema', + 'week.copy_from_main.desc': 'Du har slået ugedage til. Vil du kopiere main‑skemaet over i ugedage? Vælg ugedags‑mode der kopieres til.', + 'week.copy_from_main.mode_label': 'Ugedags‑mode', + 'week.copy_from_main.target_label': 'Kopiér til', + 'week.copy_from_main.copy_btn': 'Kopiér', + 'week.copy_from_main.skip_btn': 'Kopiér ikke', + 'week.select_room': 'Vælg rum', + 'week.copy_to_rooms': 'Kopiér ugedage til rum', + 'week.copy_to_rooms.title': 'Kopiér ugedage til rum', + 'week.copy_to_rooms.select': 'Vælg rum', + 'week.copy_to_rooms.apply': 'Indsæt i rum', + 'week.enable.desc': 'Aktivér plan per ugedag. Når slået fra, bruges samme plan hver dag.', + 'editor.timefmt.title': 'Tidsformat', + 'editor.timefmt.desc': 'Vælg 24-timers eller 12-timers (AM/PM) visning for tider på kortet.', + 'editor.tempfmt.title': 'Temperaturenhed', + 'editor.tempfmt.desc': 'Vælg Celsius (°C) eller Fahrenheit (°F) til visning i kortet og editorer.', + 'editor.timesrc.title': 'Tidskilde', + 'editor.timesrc.desc': 'Vælg om kortet skal bruge browserens tid eller Home Assistant-tidszone til “nu”.', + 'editor.timesrc.browser': 'Browser', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'Konfigurations-ID', + 'editor.instance.desc': 'Hold flere uafhængige tidsplaner/indstillinger under forskellige ID’er.', + 'editor.instance.new_id': 'Nyt ID', + 'editor.tabs.settings': 'Indstillinger', + 'editor.tabs.pause': 'Pause', + 'editor.tabs.weekdays': 'Ugedage', + 'editor.tabs.rooms': 'Rum', + 'editor.tabs.boiler': 'Kedelstyring', + 'editor.tabs.colors': 'Farver', + 'editor.tabs.presence_sensor': 'Tilstedeværelsessensor', + 'editor.presence_sensor.enable.title': 'Tilstedeværelsessensor', + 'editor.presence_sensor.enable.desc': 'Aktivér konfiguration af tilstedeværelsessensor per rum.', + 'editor.presence_sensor.entity.title': 'Tilstedeværelses-entity', + 'editor.presence_sensor.entity.desc': 'Vælg den binary_sensor der indikerer tilstedeværelse i dette rum.', + 'editor.presence_sensor.on_temp.title': 'Temperatur når der er tilstedeværelse', + 'editor.presence_sensor.on_temp.desc': 'Hvis sensoren er ON, bruger rummet denne temperatur i stedet for skemaet.', + 'editor.presence_sensor.on_delay.title': 'Forsinkelse før tilstedeværelse bruges', + 'editor.presence_sensor.on_delay.desc': 'Hvor længe sensoren skal være ON før overstyringstemperaturen bruges. Værdien bruger valgt enhed.', + 'editor.presence_sensor.off_delay.title': 'Forsinkelse før skema genoptages', + 'editor.presence_sensor.off_delay.desc': 'Hvor længe sensoren skal være OFF før skemaet genoptages. Værdien bruger valgt enhed.', + 'editor.tabs.owd': 'Open Window Detection', + 'owd.enable': 'Open Window Detection', + 'owd.enable.desc': 'Slår termostaterne fra i et rum når valgte vindue-/dørsensorer er åbne.', + 'owd.sensors': 'Vindue-/dørsensorer', + 'owd.delays.title': 'Forsinkelser', + 'owd.delays.desc': 'Minutter før der slukkes (åben) og før skemaet genoptages (lukket).', + 'owd.open_delay': 'Efter åbnet (min)', + 'owd.close_delay': 'Efter lukket (min)', + 'editor.tabs.away': 'Væk fra hjemme', + 'editor.tabs.reset': 'Nulstil', + 'editor.tabs.sync': 'Sync Engine', + 'boiler.switch': 'Boiler switch', + 'boiler.switch_type': 'Entitetstype', + 'boiler.switch_type.switch': 'Switch', + 'boiler.switch_type.input_boolean': 'Input boolean', + 'boiler.rooms': 'Rum der er med', + 'boiler.rooms.desc': 'Vælg hvilke rum der skal være med i kedelstyringen.', + 'boiler.multi_enable': 'Slå flere boilers til', + 'boiler.multi_enable.desc': 'Vælg hvilken kedel hvert rum skal bruge, og angiv offsets pr. rum.', + 'boiler.multi_rooms': 'Kedel pr. rum', + 'boiler.multi_rooms.desc': 'Vælg kedel‑switch og offsets for hvert rum.', + 'boiler.room.enable': 'Medtag rum', + 'boiler.room.enable.desc': 'Medtag dette rum i kedelstyringen.', + 'boiler.enable': 'Aktivér kedelstyring', + 'boiler.enable.desc': 'Vis kedelindstillinger i denne cards editor.', + 'boiler.temp_sensor': 'Kedel temperatursensor', + 'boiler.temp_sensor.desc': 'Temperatursensor som styrer kedel‑switchen.', + 'boiler.min_temp': 'Min kedeltemp (°C)', + 'boiler.max_temp': 'Maks kedeltemp (°C)', + 'boiler.offsets': 'Offset', + 'boiler.offsets.desc': 'ON offset: hvor langt under skema-temperaturen den må gå før den tænder. OFF offset: hvor langt over skema-temperaturen den må gå før den slukker.', + 'boiler.on_offset': 'ON offset (°C)', + 'boiler.off_offset': 'OFF offset (°C)', + 'sync.running': 'Igangværende sync…', + 'sync.completed': 'Sync gennemført', + 'editor.tabs.backup': 'Backup', + 'backup.title': 'Backup', + 'backup.desc': 'Kopiér hele indholdet af din sync‑sensor til backup‑sensoren og gendan den igen ved behov.', + 'backup.now': 'Backup nu', + 'backup.restore': 'Gendan', + 'backup.restore.confirm_overwrite': 'Gendan fra backup? Dette overskriver nuværende tidsplaner og indstillinger.', + 'backup.delete': 'Slet backup', + 'backup.delete.confirm': 'Slet valgt backup?', + 'backup.new': 'Ny backup', + 'backup.auto': 'Auto backup', + 'backup.auto.desc': 'Lav automatisk backup med faste intervaller.', + 'backup.interval': 'Interval (dage)', + 'backup.interval.desc': 'Hvor mange dage der skal gå mellem automatiske backups (min. 1).', + 'backup.sensor': 'Backup sensor', + 'backup.last': 'Sidste backup', + 'backup.slot.title': 'Vælg backup', + 'backup.slot.desc': 'Vælg hvilken backup-slot der skal gendannes.', + 'backup.slot.empty': 'Ingen backups endnu', + 'backup.select': 'Vælg hvad der skal tages backup af', + 'backup.select.desc': 'Markerede punkter tages med i backup/gendannelse', + 'backup.opt.all': 'Alt', + 'backup.opt.main': 'Hovedskemaer', + 'backup.opt.weekday': 'Ugedagsskemaer', + 'backup.opt.presence': 'Tilstede skemaer', + 'backup.opt.settings': 'Editor-indstillinger', + 'backup.opt.holiday': 'Helligdags-skemaer', + 'backup.opt.colors': 'Farveblokke', + 'backup.import.title': 'Importér thermostat_timeline.json', + 'backup.import.desc': 'Upload en thermostat_timeline.json fil og importér den. Der laves først en ny backup.', + 'backup.import.btn': 'Importér fil', + 'backup.export.title': 'Eksportér backupfil', + 'backup.export.desc': 'Download en komplet thermostat_timeline_backup.json med alle slots og data.', + 'backup.export.btn': 'Download fil', + 'backup.imported_suffix': ' - importeret', + 'backup.importbk.title': 'Importér backupfil', + 'backup.importbk.desc': 'Upload en thermostat_timeline_backup.json og tilføj dens backups til listen.', + 'backup.importbk.btn': 'Importér backup', + 'backup.importbk.err_invalid': 'Ugyldig backupfil', + 'backup.file.choose': 'Vælg importeret backup', + 'backup.file.add_selected': 'Importér backup', + 'backup.file.add_all': 'Importér alle backups', + 'backup.file.loaded_n': 'Fil indlæst ({n} backups)', + 'backup.file.imported_ok': 'Importeret ({n})', + 'backup.import.err_no_hass': 'Home Assistant er ikke klar', + 'backup.import.err_storage_off': 'Delt lager er slået fra. Slå det til først.', + 'backup.import.err_no_integration': 'Integrationen thermostat_timeline er ikke tilgængelig', + 'backup.import.err_json': 'Ugyldigt JSON', + 'backup.import.err_invalid': 'Ugyldig fil: mangler data.schedules', + 'away.enable': 'Aktivér væk-tilstand', + 'away.desc': 'Når ingen af de valgte personer er hjemme, sænkes termostaterne automatisk til den angivne temperatur.', + 'away.temp': 'Væk-temperatur', + 'away.persons': 'Personer', + 'away.add_person': 'Tilføj person', + 'away.resume_schedule': 'Genoptag skema', + 'away.resume_away': 'Genoptag væk‑temperatur', + 'editor.heat_colors.title': 'Farver for varmeblokke', + 'editor.heat_colors.add': 'Tilføj farveinterval', + 'editor.colors.col_from': 'Fra °C', + 'editor.colors.col_to': 'Til °C', + 'editor.colors.col_color': 'Farve', + 'editor.colors.refresh': 'Opdater skema', + 'editor.colors.clear_local': 'Ryd lokale farvedata', + 'editor.colors.clear_local_confirm': 'Dette sletter farveintervaller gemt i din browser for dette kort. Vil du fortsætte?', + 'editor.colors.clear_shared': 'Ryd delt farvelager', + 'editor.colors.clear_shared_confirm': 'Dette sletter farveintervaller i den valgte lagersensor. Vil du fortsætte?', + 'editor.colors.clear_all': 'Ryd alle farvedata', + 'editor.colors.clear_all_confirm': 'Dette sletter alle farveintervaller både lokalt og i den valgte lagersensor. Vil du fortsætte?', + 'editor.colors.mode': 'Farvetilstand', + 'editor.colors.mode.desc': 'Vælg om farver defineres pr. rum eller ens for alle rum.', + 'editor.colors.mode_per_room': 'Pr. rum', + 'editor.colors.mode_global': 'Samme for alle', + 'editor.colors.global_title': 'Alle rum', + 'editor.reload_editor': 'Genindlæs editor', + 'editor.reload_dashboard': 'Genindlæs dashboard', + 'onboard.title': 'Velkommen til Termostat Tidslinje', + 'onboard.next': 'Næste', + 'onboard.prev': 'Tilbage', + 'onboard.skip': 'Spring over', + 'onboard.done': 'Færdig', + 'onboard.dont_show': 'Vis ikke igen', + 'onboard.step_main': 'Dette er hovedtidslinjen. Den viser ønskede temperaturer over 24 timer for hvert rum.', + 'onboard.step_add': 'Tilføj en varmeblok via + Tilføj blok eller ved at dobbeltklikke på tidslinjen.', + 'onboard.step_edit': 'Redigér eller slet en blok ved dobbeltklik. Brug Ugedage til forskellige dage.', + 'onboard.reset': 'Vis guide igen', + 'onboard.reset_ok': 'Guiden vises næste gang.', + 'onboard.video_not_found': 'Video ikke fundet', + 'onboard.image_not_found': 'Billede ikke fundet', + 'pause.menu_title': 'Pause auto-apply', + 'pause.off_1h': 'Sluk i 1 time', + 'pause.off_5h': 'Sluk i 5 timer', + 'pause.off_custom': 'Tilpasset varighed…', + 'pause.prompt_minutes': 'Angiv varighed (minutter):', + 'pause.off_indef': 'Sluk til jeg tænder', + 'pause.resume': 'Tænd igen nu', + 'pause.close': 'Luk', + 'pause.active_until': 'Slukket til {time}', + 'pause.active_indef': 'Slukket', + 'editor.pausebtn.title': 'Pause‑knap', + 'editor.pausebtn.desc': 'Vis en global pauseknap i kortets header. Når pause er aktiv, undertrykkes alle set_temperature‑kommandoer.', + 'editor.pause_sensor.title': 'Pause via binary sensor', + 'editor.pause_sensor.desc': 'Sæt automatisk pause mens den valgte binary sensor er on.', + 'editor.pause_sensor.entity_title': 'Pause sensor', + 'editor.pause_sensor.entity_desc': 'Binary sensor som styrer pause (on = pause, off = genoptag).', + 'editor.integration.connected': 'Integration klar', + 'editor.integration.disconnected': 'Integration ikke tilgængelig', + 'profiles.enable': 'Aktivér skema overrule', + 'profiles.enable.desc': 'Tillad navngivne dagsskemaer som kan aktiveres og overrule hovedplanen.', + 'season.enable.title': 'Aktivér sommer-/vinterskema', + 'season.enable.desc': 'Når aktiv, får hver tidsplan både en Sommer- og en Vinter-version, som du kan skifte mellem.', + 'season.mode.title': 'Sæson', + 'season.mode.winter': 'Vinter', + 'season.mode.summer': 'Sommer', + 'profiles.button': 'Manuelt skema', + 'profiles.editor.title': 'Skemaer', + 'profiles.add': 'Tilføj skema', + 'profiles.rename': 'Omdøb', + 'profiles.delete': 'Slet', + 'profiles.activate': 'Aktivér', + 'profiles.deactivate': 'Deaktivér', + 'profiles.active': 'Aktiv', + 'profiles.none': 'Ingen', + 'profiles.name_placeholder': 'Skemanavn', + 'profiles.name_exists': 'Navnet findes allerede', + 'profiles.edit': 'Redigér', + 'profiles.preview_note': 'Kun forhåndsvisning. Skemaerne bliver først aktive, når profilen aktiveres.', + 'profiles.active_label': 'Manuelt skema aktivt:', + 'profiles.save_new': 'Gem ny profil', + 'profiles.save_existing': 'Gem ændringer til valgt profil', + 'editor.tabs.holidays': 'Helligdage', + 'holidays.enable': 'Aktivér helligdage', + 'holidays.desc': 'Brug et særligt dagsskema på helligdage.', + 'holidays.source': 'Kilde til helligdage', + 'holidays.source.calendar': 'Kalender', + 'holidays.source.manual': 'Manuelle datoer', + 'holidays.entity': 'Helligdags‑entitet', + 'holidays.entity_desc': 'Vælg en kalender, der kun giver on/off. Den skal være on på helligdage og off på andre dage.', + 'holidays.dates': 'Helligdagsdatoer', + 'holidays.add_date': 'Tilføj dato', + 'holidays.add_range': 'Tilføj periode', + 'holidays.date_in_range': 'Dato findes allerede i en periode', + 'holidays.ranges': 'Ferieperioder', + 'holidays.single_dates': 'Enkeltdatoer', + 'holidays.editor.title': 'Helligdags‑skema', + 'holidays.edit': 'Redigér helligdags‑skema', + 'holidays.button': 'Helligdage', + // Holiday copy-from popup + 'holidays.copy_from': 'Kopiér fra…', + 'holidays.copy.title': 'Kopiér til helligdags‑skema', + 'holidays.copy.select': 'Vælg kilde', + 'holidays.copy.apply': 'Indsæt', + 'holidays.copy.replace_confirm': 'Erstat eksisterende helligdags‑blokke?', + 'holidays.copy.group.main': 'Hovedskema', + 'holidays.copy.group.weekdays': 'Ugedage', + 'holidays.copy.group.profiles': 'Skemaer', + 'holidays.copy.group.presence': 'Tilstede skemaer', + 'presence.button': 'Tilstede skemaer', + 'presence.editor.title': 'Tilstede skemaer', + 'presence.enable_advanced': 'Aktivér avancerede tilstede‑skemaer', + 'presence.enable_advanced.desc': 'Brug kombinationer og skemaer baseret på hvem der er hjemme/ude.', + // Away delay + 'away.delay_enable': 'Forsink Away‑handlinger', + 'away.delay_desc': 'Vent før Away ændringer anvendes.', + 'away.delay_value': 'Forsinkelse', + 'away.delay_unit': 'Enhed', + 'away.delay_unit.seconds': 'Sekunder', + 'away.delay_unit.minutes': 'Minutter', + 'presence.live_header': 'Vis live personstatus i header', + 'presence.live_header.desc': 'Chips vises i højre hjørne.', + 'presence.combos': 'Kombinationer', + 'presence.enable_combo': 'Aktivér denne kombination', + 'presence.all_home': 'Alle hjemme', + 'presence.none_home': 'Ingen hjemme', + 'presence.home': 'Hjemme: {names}', + 'presence.away': 'Ude: {names}', + 'presence.manage': 'Administrér kombinationer', + 'presence.who_home': 'Hvem er hjemme', + 'presence.who_away': 'Hvem er ikke hjemme' + }, + sv: { + 'card.title_default': 'Termostat-tidslinje', + 'ui.edit_block': 'Redigera block', + 'ui.add_block': 'Lägg till block', + 'ui.from': 'Från', + 'ui.to': 'Till', + 'ui.temperature': 'Temperatur', + 'ui.delete': 'Ta bort', + 'ui.cancel': 'Avbryt', + 'ui.close': 'Stäng', + 'ui.unsaved_confirm': 'Du har osparade ändringar. Vill du stänga ändå?', + 'ui.discard_changes': 'Ignorera ändringar', + 'ui.keep_editing': 'Fortsätt redigera', + 'ui.save': 'Spara', + 'ui.error_prefix': 'Fel: ', + 'ui.overlap_msg': 'Du har valt överlappande tider — det överlappar under {start} – {end}.', + 'ui.auto_fix': 'Åtgärda automatiskt', + 'ui.fix_start_to': 'Justera start till {time}', + 'ui.fix_end_to': 'Justera slut till {time}', + // Inline meta + 'ui.current_temp': 'Nuvarande temperatur:', + + 'editor.title_label': 'Titel', + 'editor.add_entity': 'Lägg till rum', + 'editor.entity_placeholder': 'Välj en entitet', + 'editor.drag_reorder': 'Dra för att ändra ordning', + 'editor.remove': 'Ta bort', + 'editor.default_c': 'Standard °C', + 'editor.row_height_px': 'Radhöjd (px)', + 'editor.max_c': 'Max °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Automatisk tillämpning', + 'editor.auto_apply.desc': 'När det är aktiverat försöker kortet automatiskt ställa in temperaturen på valda climate-entiteter så att den matchar det planerade värdet för ”just nu”.', + 'editor.apply_edit.title': 'Vid redigering', + 'editor.apply_edit.desc': 'När du ändrar tidslinjeblock och ändringen påverkar aktuell tid, tillämpas den nya temperaturen omedelbart.', + 'editor.apply_default.title': 'Vid ändring av Standard °C', + 'editor.apply_default.desc': 'När ”Standard °C” ändras och det påverkar den aktuella perioden, tillämpas den nya temperaturen omedelbart.', + + 'editor.show_room_temp.title': 'Visa rumstemperatur', + 'editor.show_room_temp.desc': 'Visa bubblan med aktuell rumstemperatur på varje rad i tidslinjens header.', + 'editor.room_mode.title': 'Typ av rumentitet', + 'editor.room_mode.desc': 'När aktiverat, välj en input_number (måltemperatur). När av, välj en climate-entitet.', + + 'editor.room_temp_sensor_override.title': 'Använd rumstemperatursensor', + 'editor.room_temp_sensor_override.desc': 'När den är aktiverad används den valda temperatursensorn för visad rumstemperatur (och pannlogik) i stället för climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Temperatursensor', + 'ui.copy_plan': 'Kopiera schema', + 'ui.paste_plan': 'Klistra in schema', + 'ui.copy_day': 'Kopiera dag', + 'ui.paste_day': 'Klistra in dag', + 'editor.merge_label': 'Flet med (vælg en ekstra radiatortermostat)', + 'editor.display_name_optional': 'Rumsnamn (valfritt)', + 'editor.merged_with': 'Sammanslagen med', + 'editor.store_enable.title': 'Delad lagring + bakgrundsstyrning', + 'editor.store_enable.short': 'Sparar scheman i vald sensor och aktiverar bakgrundsstyrning.', + 'editor.store_enable.desc': 'Sparar scheman i vald sensor och aktiverar set_temperature i bakgrunden. Termostater uppdateras även när kortet är stängt. Delar också scheman (värmeblock) och kortinställningar mellan användare/enheter. När av: kommandon skickas bara när kortet är aktivt synligt på en enhet (surfplatta/dator/mobil) och måste förbli öppet.', + 'editor.store_missing': 'Integrationen "Thermostat Pro Timeline Sync" är inte installerad. Delad lagring är inte tillgänglig.', + 'editor.clear_store': 'Rensa lagring', + 'editor.clear_store_confirm': 'Detta tar bort alla sparade scheman. Vill du fortsätta?', + 'editor.migrate_to_store': 'Överför data till lagring', + 'editor.migrate_confirm': 'Överför dina webbläsar-sparade scheman till vald sensor? Detta skriver över befintliga scheman i lagringen.', + 'editor.migrate_pull_confirm': 'Importera scheman från lagring och skriv över lokala webbläsardata?', + 'editor.select_storage_first': 'Välj först en lagringssensor.', + 'editor.no_local_data': 'Inga lokala webbläsardata hittades.', + 'editor.clear_all': 'Rensa all data', + 'editor.clear_all_confirm': 'Detta tar bort både lagring och lokala webblässarscheman. Vill du fortsätta?', + 'editor.clear_storage_only': 'Rensa endast lagringsdata', + 'editor.clear_storage_only_confirm': 'Detta tar bort scheman från vald lagringssensor. Vill du fortsätta?', + 'editor.clear_local_only': 'Rensa endast lokala data', + 'editor.clear_local_only_confirm': 'Detta tar endast bort scheman som sparats i din webbläsare. Vill du fortsätta?', + 'editor.reset.desc': 'Det här gör:\n\n• Rensa all data: Återställer backend-lagring (.storage) för Thermostat Timeline och tar bort ALLA lokala cache/drafts i den här webbläsaren.\n• Rensa endast lokala data: Rensar bara data som är lagrad i den här webbläsaren (localStorage).\n\nTips: Efter en full reset kan en siduppdatering förhindra att gamla drafts kommer tillbaka.', + 'week.enable': 'Aktivera veckodagar', + 'week.mode': 'Veckoläge', + 'week.mode.same_all': 'Samma alla dagar (1)', + 'week.mode.weekday_weekend': 'Vardagar + Helg', + 'week.mode.weekday_sat_sun': 'Vardagar + Lördag + Söndag', + 'week.mode.all_7': 'Alla dagar', + 'week.button': 'Veckodagar', + 'week.days.mon': 'Mån', + 'week.days.tue': 'Tis', + 'week.days.wed': 'Ons', + 'week.days.thu': 'Tor', + 'week.days.fri': 'Fre', + 'week.days.sat': 'Lör', + 'week.days.sun': 'Sön', + 'week.editor.title': 'Veckoschema', + 'week.editor.preview_note': 'Endast förhandsvisning. Ändringar tillämpas när du trycker Spara.', + 'week.open_editor.desc': 'Öppna veckodagsredigeraren.', + 'week.open_editor.btn': 'Öppna', + 'week.copy_to_rooms': 'Kopiera vecka till rum', + 'week.copy_to_rooms.title': 'Kopiera vecka till rum', + 'week.copy_to_rooms.select': 'Välj rum', + 'week.copy_to_rooms.apply': 'Klistra in i rum', + 'week.group.weekdays': 'Vardagar', + 'week.group.weekend': 'Helg', + 'week.days_full.mon': 'Måndag', + 'week.days_full.tue': 'Tisdag', + 'week.days_full.wed': 'Onsdag', + 'week.days_full.thu': 'Torsdag', + 'week.days_full.fri': 'Fredag', + 'week.days_full.sat': 'Lördag', + 'week.days_full.sun': 'Söndag', + 'editor.timefmt.title': 'Tidsformat', + 'editor.timefmt.desc': 'Välj 24-timmars eller 12-timmars (AM/PM) visning på kortet.', + 'editor.tempfmt.title': 'Temperaturenhet', + 'editor.tempfmt.desc': 'Välj Celsius (°C) eller Fahrenheit (°F) för visning i kortet och editorer.', + 'editor.timesrc.title': 'Tidskälla', + 'editor.timesrc.desc': 'Välj om kortet ska använda webbläsarens tid eller Home Assistant-tidszon för ”nu”.', + 'editor.timesrc.browser': 'Webbläsare', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'Konfigurations-ID', + 'editor.instance.desc': 'Ha flera oberoende scheman/inställningar under olika ID:n.', + 'editor.instance.new_id': 'Nytt ID', + 'editor.heat_colors.title': 'Färger för värmeblock', + 'editor.heat_colors.add': 'Lägg till färgintervall', + 'editor.colors.col_from': 'Från °C', + 'editor.colors.col_to': 'Till °C', + 'editor.colors.col_color': 'Färg', + 'editor.colors.empty': 'Inga rum tillagda ännu. Lägg till ett rum för att definiera färger.', + 'editor.colors.go_add': 'Lägg till rum', + 'editor.colors.refresh': 'Uppdatera schema', + 'editor.colors.clear_local': 'Rensa lokala färgdata', + 'editor.colors.clear_local_confirm': 'Detta tar bort färgintervall som sparats i din webbläsare för detta kort. Vill du fortsätta?', + 'editor.colors.clear_shared': 'Rensa delad färglagring', + 'editor.colors.clear_shared_confirm': 'Detta tar bort färgintervall i den valda lagringssensorn. Vill du fortsätta?', + 'editor.colors.mode': 'Färgläge', + 'editor.colors.mode.desc': 'Välj färger per rum eller ett set för alla rum.', + 'editor.colors.mode_per_room': 'Per rum', + 'editor.colors.mode_global': 'Alla rum', + 'editor.colors.global_title': 'Alla rum', + 'editor.colors.clear_all': 'Rensa all färgdata', + 'editor.colors.clear_all_confirm': 'Detta tar bort alla färgintervall både lokalt och i vald lagringssensor. Vill du fortsätta?', + 'editor.reload_editor': 'Ladda om redigerare', + 'editor.reload_dashboard': 'Ladda om instrumentpanel', + 'onboard.title': 'Välkommen till Termostat‑tidslinje', + 'onboard.next': 'Nästa', + 'onboard.prev': 'Tillbaka', + 'onboard.skip': 'Hoppa över', + 'onboard.done': 'Klar', + 'onboard.dont_show': 'Visa inte igen', + 'onboard.step_main': 'Detta är huvudtidslinjen. Den visar önskad temperatur under dygnet för varje rum.', + 'onboard.step_add': 'Lägg till ett värmeblock via + Lägg till block eller dubbelklicka på tidslinjen.', + 'onboard.step_edit': 'Redigera eller ta bort ett block genom att dubbelklicka. Använd veckodagar för olika dagar.', + 'onboard.reset': 'Visa guiden igen', + 'onboard.reset_ok': 'Guiden visas nästa gång.', + 'editor.select_entities': 'Rum', + 'editor.tabs.rooms': 'Rum', + 'editor.test_tool': 'Testverktyg', + 'editor.test_select_entity': 'Välj enhet', + 'editor.test_empty': 'Välj en enhet för att se tillgängliga kommandon…', + 'editor.test_export': 'Exportera .md', + 'editor.test_no_fields': 'Inga fält', + 'editor.test_select_placeholder': '— Välj —', + 'editor.error_prefix': 'Fel: ', + 'editor.open_in_hacs': 'Öppna i HACS', + 'week.enable.desc': 'Aktivera schema per veckodag. När det är av, används samma schema varje dag.', + 'editor.tabs.sync': 'Sync Engine', + 'sync.running': 'Synkronisering pågår…', + 'sync.completed': 'Synkronisering klar', + 'editor.tabs.backup': 'Backup', + 'backup.title': 'Backup', + 'backup.desc': 'Kopiera innehållet i din sync‑sensor till backup‑sensorn och återställ vid behov.', + 'backup.now': 'Säkerhetskopiera nu', + 'backup.restore': 'Återställ', + 'backup.delete': 'Ta bort backup', + 'backup.delete.confirm': 'Ta bort vald backup?', + 'backup.new': 'Ny backup', + 'backup.auto': 'Automatisk backup', + 'backup.auto.desc': 'Gör automatiska säkerhetskopior med intervall.', + 'backup.interval': 'Intervall (dagar)', + 'backup.interval.desc': 'Hur många dagar mellan automatiska säkerhetskopior (min. 1).', + 'backup.sensor': 'Backup‑sensor', + 'backup.last': 'Senaste backup', + 'backup.export.title': 'Export backup file', + 'backup.export.desc': 'Download a full thermostat_timeline_backup.json with all slots and data.', + 'backup.export.btn': 'Download file', + 'backup.export.title': 'Exportera backupfil', + 'backup.export.desc': 'Ladda ner en fullständig thermostat_timeline_backup.json med alla platser och data.', + 'backup.export.btn': 'Ladda ner fil', + 'backup.file.add_selected': 'Importera backup', + 'backup.file.add_all': 'Importera alla backuper', + 'backup.file.loaded_n': 'Fil laddad ({n} backuper)', + 'backup.file.imported_ok': 'Importerad ({n})', + 'backup.slot.title': 'Välj backup', + 'backup.slot.desc': 'Välj vilken backupplats som ska återställas.', + 'backup.slot.empty': 'Inga backups ännu', + 'pause.menu_title': 'Pausa auto‑apply', + 'pause.off_1h': 'Pausa 1 timme', + 'pause.off_5h': 'Pausa 5 timmar', + 'pause.off_custom': 'Anpassad varaktighet…', + 'pause.off_indef': 'Pausa tills jag återupptar', + 'pause.resume': 'Återuppta nu', + 'pause.close': 'Stäng', + 'pause.active_until': 'Pausad till {time}', + 'pause.active_indef': 'Pausad', + 'editor.pausebtn.title': 'Pausknapp', + 'editor.pausebtn.desc': 'Visa en global pausknapp i kortets huvud. När paus är aktiv blockeras alla set_temperature‑kommandon.', + 'editor.integration.connected': 'Integration klar', + 'editor.integration.disconnected': 'Integration inte tillgänglig' + , 'profiles.button': 'Manuellt schema' + , 'profiles.editor.title': 'Profiler' + , 'profiles.add': 'Lägg till profil' + , 'profiles.rename': 'Byt namn' + , 'profiles.delete': 'Ta bort' + , 'profiles.activate': 'Aktivera' + , 'profiles.deactivate': 'Inaktivera' + , 'profiles.active': 'Aktiv' + , 'profiles.none': 'Ingen' + , 'profiles.name_placeholder': 'Profilnamn' + , 'profiles.edit': 'Redigera' + , 'profiles.preview_note': 'Endast förhandsvisning. Scheman blir aktiva när du aktiverar profilen.' + , 'editor.tabs.holidays': 'Helgdagar' + , 'holidays.enable': 'Aktivera helgdagar' + , 'holidays.desc': 'Använd ett särskilt dagsschema på helgdagar.' + , 'holidays.source': 'Källa för helgdagar' + , 'holidays.source.calendar': 'Kalender' + , 'holidays.source.manual': 'Manuella datum' + , 'holidays.entity': 'Helgdags‑entitet' + , 'holidays.entity_desc': 'Välj en kalender som bara ger on/off. Den ska vara on på helgdagar och off övriga dagar.' + , 'holidays.dates': 'Helgdagsdatum' + , 'holidays.add_date': 'Lägg till datum' + , 'holidays.editor.title': 'Helgdags‑schema' + , 'holidays.edit': 'Redigera helgdags‑schema' + , 'holidays.button': 'Helgdagar' + , 'editor.perroom.title': 'Standard °C per rum' + , 'editor.perroom.desc': 'Visa ett Standard °C‑fält på varje rad. När av, används global Standard °C för alla rum.' + , 'away.resume_schedule': 'Återuppta schema' + , 'away.resume_away': 'Återuppta bortatemperatur' + , 'presence.button': 'Närvaroscheman' + , 'presence.editor.title': 'Närvaroscheman' + , 'presence.enable_advanced': 'Aktivera avancerade närvaroscheman' + , 'presence.enable_advanced.desc': 'Använd kombinationer och scheman baserade på vem som är hemma/borta.' + , 'away.delay_enable': 'Fördröj borta‑åtgärder' + , 'away.delay_desc': 'Vänta innan borta‑ändringar tillämpas.' + , 'away.delay_value': 'Fördröjning' + , 'away.delay_unit': 'Enhet' + , 'away.delay_unit.seconds': 'Sekunder' + , 'away.delay_unit.minutes': 'Minuter' + , 'presence.combos': 'Kombinationer' + , 'presence.enable_combo': 'Aktivera denna kombination' + , 'presence.all_home': 'Alla hemma' + , 'presence.none_home': 'Ingen hemma' + , 'presence.home': 'Hemma: {names}' + , 'presence.away': 'Borta: {names}' + , 'presence.manage': 'Hantera kombinationer' + , 'presence.who_home': 'Vem är hemma' + , 'presence.who_away': 'Vem är inte hemma' + , 'away.enable': 'Aktivera bortaläge' + , 'away.desc': 'När ingen av de valda personerna är hemma, sänks termostaterna automatiskt till den angivna temperaturen.' + , 'away.temp': 'Bortatemperatur' + , 'away.persons': 'Personer' + , 'away.add_person': 'Lägg till person' + , 'backup.select': 'Välj vad som ska säkerhetskopieras' + , 'backup.select.desc': 'Markerade objekt inkluderas i backup/återställning' + , 'backup.opt.all': 'Alla' + , 'backup.opt.main': 'Huvudscheman' + , 'backup.opt.weekday': 'Veckodagsscheman' + , 'backup.opt.presence': 'Närvaroscheman' + , 'backup.opt.settings': 'Redigeringsinställningar' + , 'backup.opt.holiday': 'Helgdags‑scheman' + , 'backup.opt.colors': 'Färgblock' + , 'editor.tabs.settings': 'Inställningar' + , 'editor.tabs.pause': 'Paus' + , 'editor.tabs.weekdays': 'Veckodagar' + , 'editor.tabs.colors': 'Färger' + , 'editor.tabs.presence_sensor': 'Närvarosensor' + , 'editor.presence_sensor.enable.title': 'Närvarosensor' + , 'editor.presence_sensor.enable.desc': 'Aktivera konfiguration av närvarosensor per rum.' + , 'editor.presence_sensor.entity.title': 'Närvaroentitet' + , 'editor.presence_sensor.entity.desc': 'Välj den binary_sensor som indikerar närvaro i detta rum.' + , 'editor.presence_sensor.on_temp.title': 'Temperatur när närvaro är PÅ' + , 'editor.presence_sensor.on_temp.desc': 'Om sensorn är PÅ använder rummet denna temperatur istället för schemat.' + , 'editor.presence_sensor.on_delay.title': 'Fördröjning innan närvaro används' + , 'editor.presence_sensor.on_delay.desc': 'Hur länge sensorn måste vara PÅ innan överstyrningstemperaturen används. Värdet använder vald enhet.' + , 'editor.presence_sensor.off_delay.title': 'Fördröjning innan schema återupptas' + , 'editor.presence_sensor.off_delay.desc': 'Hur länge sensorn måste vara AV innan schemat återupptas. Värdet använder vald enhet.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Borta hemifrån' + , 'editor.tabs.reset': 'Återställ' + , 'editor.tabs.boiler': 'Pannstyrning' + , 'boiler.switch': 'Pannbrytare' + , 'boiler.switch_type': 'Entitetstyp' + , 'boiler.switch_type.switch': 'Switch' + , 'boiler.switch_type.input_boolean': 'Input boolean' + , 'boiler.rooms': 'Rum som ingår' + , 'boiler.rooms.desc': 'Välj vilka rum som ska ingå i pannstyrningen.' + , 'boiler.multi_enable': 'Aktivera flera pannor' + , 'boiler.multi_enable.desc': 'Tilldela rum till olika pannor och ange offsets per rum.' + , 'boiler.multi_rooms': 'Panna per rum' + , 'boiler.multi_rooms.desc': 'Välj pannbrytare och offsets för varje rum.' + , 'boiler.room.enable': 'Inkludera rum' + , 'boiler.room.enable.desc': 'Inkludera detta rum i pannstyrningen.' + , 'boiler.enable': 'Aktivera pannstyrning' + , 'boiler.enable.desc': 'Visa panninställningar i den här korteditorn.' + , 'boiler.temp_sensor': 'Pannans temperatursensor' + , 'boiler.temp_sensor.desc': 'Temperatursensor som styr pannbrytaren.' + , 'boiler.min_temp': 'Min pann‑temp (°C)' + , 'boiler.max_temp': 'Max pann‑temp (°C)' + , 'boiler.offsets': 'Offset' + , 'boiler.offsets.desc': 'ON‑offset: hur långt under schemamålet innan den slår PÅ. OFF‑offset: hur långt över schemamålet innan den slår AV.' + , 'boiler.on_offset': 'ON offset (°C)' + , 'boiler.off_offset': 'OFF offset (°C)' + , 'editor.temp_sensor': 'Temperatursensor' + , 'presence.live_header': 'Visa live‑närvaro i rubriken' + , 'presence.live_header.desc': 'Chips visas i högra hörnet.' + , 'profiles.enable': 'Aktivera profiler (åsidosättning)' + , 'profiles.enable.desc': 'Tillåt namngivna dagsscheman som kan aktiveras för att åsidosätta huvudplanen.' + , 'season.enable.title': 'Aktivera sommar-/vinterscheman' + , 'season.enable.desc': 'När aktiverat får varje schema en Sommar- och en Vinter-version som du kan växla mellan.' + , 'season.mode.title': 'Säsong' + , 'season.mode.winter': 'Vinter' + , 'season.mode.summer': 'Sommar' + , 'profiles.active_label': 'Manuell profil aktiv:' + , 'profiles.save_new': 'Spara ny profil' + , 'profiles.save_existing': 'Spara ändringar i vald profil' + , 'week.view.title': 'Veckovy' + , 'week.view.rooms_one_day': 'Alla rum • en dag' + , 'week.view.days_one_room': 'Alla dagar • ett rum' + , 'week.view_switch.title': 'Byt vy från tidslinjen' + , 'week.view_switch.desc': 'Visa en växlare i tidslinjens rubrik så att du kan byta mellan de två veckodagsvisningarna.' + , 'week.copy_from_main.title': 'Kopiera huvudschema' + , 'week.copy_from_main.desc': 'Du aktiverade veckodagar. Vill du kopiera huvudschemat till veckodagar? Välj veckodagsläge att kopiera till.' + , 'week.copy_from_main.mode_label': 'Veckodagsläge' + , 'week.copy_from_main.target_label': 'Kopiera till' + , 'week.copy_from_main.copy_btn': 'Kopiera' + , 'week.copy_from_main.skip_btn': 'Kopiera inte' + , 'week.title': 'Veckodagar' + , 'week.select_room': 'Välj rum' + , 'err.config_entities': "Konfigurationen måste ha 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Video hittades inte' + , 'onboard.image_not_found': 'Bild hittades inte' + , 'pause.prompt_minutes': 'Ange varaktighet (minuter):' + , 'editor.test_tool': 'Testverktyg' + , 'editor.test_select_entity': 'Välj en entitet' + , 'editor.test_empty': 'Inget att exportera' + , 'editor.test_export': 'Exportera' + , 'editor.test_no_fields': 'Inga fält att visa' + , 'editor.test_select_placeholder': 'Välj...' + , 'editor.error_prefix': 'Fel: ' + , 'backup.restore.confirm_overwrite': 'Återställ från säkerhetskopia? Detta kommer att skriva över nuvarande scheman och inställningar.' + , 'backup.import.err_no_hass': 'Home Assistant är inte redo' + , 'backup.import.err_storage_off': 'Delad lagring är AV. Slå på den först.' + , 'backup.import.err_no_integration': 'Integrationen thermostat_timeline är inte tillgänglig' + , 'backup.import.err_json': 'Ogiltig JSON' + , 'profiles.name_exists': 'Namnet finns redan' + }, + nb: { + 'card.title_default': 'Termostat tidslinje', + 'ui.edit_block': 'Rediger blokk', + 'ui.add_block': 'Legg til blokk', + 'ui.from': 'Fra', + 'ui.to': 'Til', + 'ui.temperature': 'Temperatur', + 'ui.delete': 'Slett', + 'ui.cancel': 'Avbryt', + 'ui.close': 'Lukk', + 'ui.unsaved_confirm': 'Du har ulagrede endringer. Vil du lukke likevel?', + 'ui.discard_changes': 'Forkast endringer', + 'ui.keep_editing': 'Fortsett redigering', + 'ui.save': 'Lagre', + 'ui.error_prefix': 'Feil: ', + 'ui.overlap_msg': 'Du har valgt overlappende tider — det overlapper i perioden {start} – {end}.', + 'ui.auto_fix': 'Rett automatisk', + 'ui.fix_start_to': 'Sett start til {time}', + 'ui.fix_end_to': 'Sett slutt til {time}', + // Inline meta + 'ui.current_temp': 'Aktuell temperatur:', + + 'editor.title_label': 'Tittel', + 'editor.add_entity': 'Legg til rom', + 'editor.entity_placeholder': 'Velg en entitet', + 'editor.drag_reorder': 'Dra for å endre rekkefølge', + 'editor.remove': 'Fjern', + 'editor.default_c': 'Standard °C', + 'editor.row_height_px': 'Radhøyde (px)', + 'editor.max_c': 'Maks °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Automatisk bruk', + 'editor.auto_apply.desc': 'Når aktivert forsøker kortet å sette temperaturen på valgte climate-entiteter slik at den samsvarer med planlagt verdi for «nå».', + 'editor.apply_edit.title': 'Ved redigering', + 'editor.apply_edit.desc': 'Når du endrer blokker i tidslinjen og endringen påvirker aktuell tid, tilpasses den nye temperaturen umiddelbart.', + 'editor.apply_default.title': 'Ved Standard °C-endring', + 'editor.apply_default.desc': 'Når «Standard °C» endres og det påvirker gjeldende periode, brukes den nye temperaturen umiddelbart.', + + 'editor.show_room_temp.title': 'Vis romtemperatur', + 'editor.show_room_temp.desc': 'Vis boblen med aktuell romtemperatur på hver rad i tidslinjens toppfelt.', + 'editor.room_mode.title': 'Type rom-entitet', + 'editor.room_mode.desc': 'Når aktivert, velg en input_number (måltemperatur). Når deaktivert, velg en climate-entitet.', + + 'editor.room_temp_sensor_override.title': 'Bruk romtemperatursensor', + 'editor.room_temp_sensor_override.desc': 'Når aktivert brukes den valgte temperatursensoren for vist romtemperatur (og kjel-logikk) i stedet for climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Temperatursensor', + 'ui.copy_plan': 'Kopier plan', + 'ui.paste_plan': 'Lim inn plan', + 'ui.copy_day': 'Kopier dag', + 'ui.paste_day': 'Lim inn dag', + 'editor.merge_label': 'Flet med (vælg en ekstra radiatortermostat)', + 'editor.display_name_optional': 'Romnavn (valgfritt)', + 'editor.merged_with': 'Flettet med', + 'editor.store_enable.title': 'Delt lagring + bakgrunnsstyring', + 'editor.store_enable.short': 'Lagrer tidsplaner i valgt sensor og aktiverer bakgrunnsstyring.', + 'editor.store_enable.desc': 'Lagrer tidsplaner i valgt sensor og aktiverer set_temperature i bakgrunnen. Termostater oppdateres også når kortet er lukket. Deler også tidsplaner (varmeblokker) og kortinnstillinger på tvers av brukere/enheter. Når av: kommandoer sendes kun mens kortet er aktivt synlig på en enhet (nettbrett/PC/mobil) og må forbli åpent.', + 'editor.store_missing': 'Integrasjonen "Thermostat Pro Timeline Sync" er ikke installert. Delt lagring er ikke tilgjengelig.', + 'editor.clear_store': 'Tøm lagring', + 'editor.clear_store_confirm': 'Dette vil slette alle lagrede tidsplaner. Fortsette?', + 'editor.migrate_to_store': 'Overfør data til lagring', + 'editor.migrate_confirm': 'Overfør nettleserlagrede tidsplaner til valgt sensor? Dette overskriver eksisterende tidsplaner i lagringen.', + 'editor.migrate_pull_confirm': 'Importer tidsplaner fra lagring og overskriv lokale nettleserdata?', + 'editor.select_storage_first': 'Velg først en lagringssensor.', + 'editor.no_local_data': 'Ingen lokale nettleserdata ble funnet.', + 'editor.clear_all': 'Tøm all data', + 'editor.clear_all_confirm': 'Dette sletter både lagring og lokale nettleserplaner. Fortsette?', + 'editor.clear_storage_only': 'Tøm kun lagringsdata', + 'editor.clear_storage_only_confirm': 'Dette sletter tidsplaner fra valgt lagringssenso r. Fortsette?', + 'editor.clear_local_only': 'Tøm kun lokale data', + 'editor.clear_local_only_confirm': 'Dette sletter kun tidsplaner som er lagret i nettleseren. Fortsette?', + 'editor.reset.desc': 'Dette gjør:\n\n• Tøm all data: Tilbakestiller backend-lagring (.storage) for Thermostat Timeline og sletter ALLE lokale cache/drafts i denne nettleseren.\n• Tøm kun lokale data: Tømmer kun data lagret i denne nettleseren (localStorage).\n\nTips: Etter en full reset kan en oppdatering av siden hindre at gamle drafts dukker opp igjen.', + 'week.enable': 'Aktiver ukedager', + 'week.mode': 'Ukedagsmodus', + 'week.mode.same_all': 'Samme hver dag (1)', + 'week.mode.weekday_weekend': 'Hverdager + Helg', + 'week.mode.weekday_sat_sun': 'Hverdager + Lørdag + Søndag', + 'week.mode.all_7': 'Alle dager', + 'week.button': 'Ukedager', + 'week.days.mon': 'Man', + 'week.days.tue': 'Tir', + 'week.days.wed': 'Ons', + 'week.days.thu': 'Tor', + 'week.days.fri': 'Fre', + 'week.days.sat': 'Lør', + 'week.days.sun': 'Søn', + 'week.editor.title': 'Ukeplan', + 'week.editor.preview_note': 'Kun forhåndsvisning. Endringer brukes når du trykker Lagre.', + 'week.open_editor.desc': 'Åpne ukedagseditoren.', + 'week.open_editor.btn': 'Åpne', + 'week.copy_to_rooms': 'Kopier ukeplan til rom', + 'week.copy_to_rooms.title': 'Kopier ukeplan til rom', + 'week.copy_to_rooms.select': 'Velg rom', + 'week.copy_to_rooms.apply': 'Sett inn i rom', + 'week.group.weekdays': 'Hverdager', + 'week.group.weekend': 'Helg', + 'week.days_full.mon': 'Mandag', + 'week.days_full.tue': 'Tirsdag', + 'week.days_full.wed': 'Onsdag', + 'week.days_full.thu': 'Torsdag', + 'week.days_full.fri': 'Fredag', + 'week.days_full.sat': 'Lørdag', + 'week.days_full.sun': 'Søndag', + 'editor.timefmt.title': 'Tidsformat', + 'editor.timefmt.desc': 'Velg 24-timers eller 12-timers (AM/PM) visning for tider på kortet.', + 'editor.tempfmt.title': 'Temperaturenhet', + 'editor.tempfmt.desc': 'Velg Celsius (°C) eller Fahrenheit (°F) for visning i kortet og editorer.', + 'editor.timesrc.title': 'Tidskilde', + 'editor.timesrc.desc': 'Velg om kortet skal bruke nettleserens tid eller Home Assistant-tidssone for «nå».', + 'editor.timesrc.browser': 'Nettleser', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'Konfigurasjons-ID', + 'editor.instance.desc': 'Behold flere uavhengige tidsplaner/innstillinger under forskjellige ID-er.', + 'editor.instance.new_id': 'Nytt ID', + 'editor.heat_colors.title': 'Farger for varmeblokker', + 'editor.heat_colors.add': 'Legg til fargeintervall', + 'editor.colors.col_from': 'Fra °C', + 'editor.colors.col_to': 'Til °C', + 'editor.colors.col_color': 'Farge', + 'editor.colors.empty': 'Ingen rom lagt til ennå. Legg til et rom for å definere farger.', + 'editor.colors.go_add': 'Legg til rom', + 'editor.colors.refresh': 'Oppdater tidsplan', + 'editor.colors.clear_local': 'Tøm lokale fargedata', + 'editor.colors.clear_local_confirm': 'Dette sletter fargeintervaller lagret i nettleseren for dette kortet. Fortsette?', + 'editor.colors.clear_shared': 'Tøm delt fargelagring', + 'editor.colors.clear_shared_confirm': 'Dette sletter fargeintervaller i den valgte lagringssensoren. Fortsette?', + 'editor.colors.mode': 'Fargemodus', + 'editor.colors.mode.desc': 'Velg farger per rom eller ett felles sett for alle rom.', + 'editor.colors.mode_per_room': 'Per rom', + 'editor.colors.mode_global': 'Alle rom', + 'editor.colors.global_title': 'Alle rom', + 'editor.colors.clear_all': 'Tøm alle fargedata', + 'editor.colors.clear_all_confirm': 'Dette sletter alle fargeintervaller både lokalt og i valgt lagringssenso r. Fortsette?', + 'editor.reload_editor': 'Last inn editor på nytt', + 'editor.reload_dashboard': 'Last inn dashboard på nytt', + 'onboard.title': 'Velkommen til Termostat tidslinje', + 'onboard.next': 'Neste', + 'onboard.prev': 'Tilbake', + 'onboard.skip': 'Hopp over', + 'onboard.done': 'Ferdig', + 'onboard.dont_show': 'Ikke vis igjen', + 'onboard.step_main': 'Dette er hovedtidslinjen. Den viser ønsket temperatur gjennom døgnet for hvert rom.', + 'onboard.step_add': 'Legg til en varmeblokk via + Legg til blokk eller ved å dobbeltklikke på tidslinjen.', + 'onboard.step_edit': 'Rediger eller slett en blokk ved å dobbeltklikke. Bruk ukedager for forskjellige dager.', + 'onboard.reset': 'Vis guiden på nytt', + 'onboard.reset_ok': 'Velkomst vises neste gang.', + 'editor.select_entities': 'Rom', + 'editor.tabs.rooms': 'Rum', + 'editor.test_tool': 'Testverktøy', + 'editor.test_select_entity': 'Velg enhet', + 'editor.test_empty': 'Velg en enhet for å se tilgjengelige kommandoer…', + 'editor.test_export': 'Eksporter .md', + 'editor.test_no_fields': 'Ingen felt', + 'editor.test_select_placeholder': '— Velg —', + 'editor.error_prefix': 'Feil: ', + 'editor.open_in_hacs': 'Åpne i HACS', + 'week.enable.desc': 'Aktivera schema per veckodag. När det är av, används samma schema varje dag.', + 'editor.tabs.sync': 'Sync Engine', + 'sync.running': 'Synkronisering pågår…', + 'sync.completed': 'Synkronisering fullført', + 'pause.menu_title': 'Pause auto‑apply', + 'pause.off_1h': 'Pause i 1 time', + 'pause.off_5h': 'Pause i 5 timer', + 'pause.off_custom': 'Egendefinert varighet…', + 'pause.off_indef': 'Pause til jeg gjenopptar', + 'pause.resume': 'Gjenoppta nå', + 'pause.close': 'Lukk', + 'pause.active_until': 'Pauset til {time}', + 'pause.active_indef': 'Pauset', + 'editor.pausebtn.title': 'Pauseknapp', + 'editor.pausebtn.desc': 'Vis en global pauseknapp i kortets header. Når pause er aktiv, undertrykkes alle set_temperature‑kommandoer.', + 'editor.tabs.sync': 'Synk‑motor', + 'editor.tabs.backup': 'Backup', + 'backup.title': 'Backup', + 'backup.desc': 'Kopier innholdet i synk‑sensoren til backup‑sensoren og gjenopprett ved behov.', + 'backup.now': 'Ta backup nå', + 'backup.restore': 'Gjenopprett', + 'backup.delete': 'Slett backup', + 'backup.delete.confirm': 'Slett valgt backup?', + 'backup.new': 'Ny backup', + 'backup.auto': 'Automatisk backup', + 'backup.auto.desc': 'Lag automatiske sikkerhetskopier med intervaller.', + 'backup.interval': 'Intervall (dager)', + 'backup.interval.desc': 'Antall dager mellom automatiske sikkerhetskopier (min. 1).', + 'backup.sensor': 'Backup‑sensor', + 'backup.last': 'Siste backup', + 'backup.export.title': 'Eksporter sikkerhetskopi', + 'backup.export.desc': 'Last ned en komplett thermostat_timeline_backup.json med alle slotter og data.', + 'backup.export.btn': 'Last ned fil', + 'backup.file.add_selected': 'Importer backup', + 'backup.file.add_all': 'Importer alle backuper', + 'backup.file.loaded_n': 'Fil lastet ({n} backuper)', + 'backup.file.imported_ok': 'Importert ({n})', + 'backup.slot.title': 'Velg sikkerhetskopi', + 'backup.slot.desc': 'Velg hvilken backup-slot som skal gjenopprettes.', + 'backup.slot.empty': 'Ingen sikkerhetskopier ennå', + 'editor.integration.connected': 'Integrasjon klar', + 'editor.integration.disconnected': 'Integrasjon utilgjengelig' + , 'profiles.button': 'Manuelt skjema' + , 'profiles.editor.title': 'Profiler' + , 'profiles.add': 'Legg til profil' + , 'profiles.rename': 'Gi nytt navn' + , 'profiles.delete': 'Slett' + , 'profiles.activate': 'Aktiver' + , 'profiles.deactivate': 'Deaktiver' + , 'profiles.active': 'Aktiv' + , 'profiles.none': 'Ingen' + , 'profiles.name_placeholder': 'Profilnavn' + , 'profiles.edit': 'Rediger' + , 'profiles.preview_note': 'Kun forhåndsvisning. Skjemaer blir aktive når profilen aktiveres.' + , 'editor.tabs.holidays': 'Helligdager' + , 'holidays.enable': 'Aktiver helligdager' + , 'holidays.desc': 'Bruk en egen dagsplan på helligdager.' + , 'holidays.source': 'Kilde for helligdager' + , 'holidays.source.calendar': 'Kalender' + , 'holidays.source.manual': 'Manuelle datoer' + , 'holidays.entity': 'Helligdags‑entitet' + , 'holidays.entity_desc': 'Velg en kalender som kun gir on/off. Den skal være on på helligdager og off ellers.' + , 'holidays.dates': 'Helligdagsdatoer' + , 'holidays.add_date': 'Legg til dato' + , 'holidays.editor.title': 'Helligdags‑skjema' + , 'holidays.edit': 'Rediger helligdags‑skjema' + , 'holidays.button': 'Helligdager' + , 'editor.perroom.title': 'Standard °C per rom' + , 'editor.perroom.desc': 'Vis et Standard °C‑felt på hver rad. Når av, brukes global Standard °C for alle rom.' + , 'away.resume_schedule': 'Gjenoppta skjema' + , 'away.resume_away': 'Gjenoppta borte‑temperatur' + , 'presence.button': 'Tilstede‑skjemaer' + , 'presence.editor.title': 'Tilstede‑skjemaer' + , 'presence.enable_advanced': 'Aktiver avanserte tilstede‑skjemaer' + , 'presence.enable_advanced.desc': 'Bruk kombinasjoner og skjemaer basert på hvem som er hjemme/borte.' + , 'away.delay_enable': 'Forsink borte‑handlinger' + , 'away.delay_desc': 'Vent før borte‑endringer brukes.' + , 'away.delay_value': 'Forsinkelse' + , 'away.delay_unit': 'Enhet' + , 'away.delay_unit.seconds': 'Sekunder' + , 'away.delay_unit.minutes': 'Minutter' + , 'presence.combos': 'Kombinasjoner' + , 'presence.enable_combo': 'Aktiver denne kombinasjonen' + , 'presence.all_home': 'Alle hjemme' + , 'presence.none_home': 'Ingen hjemme' + , 'presence.home': 'Hjemme: {names}' + , 'presence.away': 'Borte: {names}' + , 'presence.manage': 'Administrer kombinasjoner' + , 'presence.who_home': 'Hvem er hjemme' + , 'presence.who_away': 'Hvem er ikke hjemme' + , 'away.enable': 'Aktiver borte‑modus' + , 'away.desc': 'Når ingen av de valgte personene er hjemme, senkes termostatene automatisk til den angitte temperaturen.' + , 'away.temp': 'Borte‑temperatur' + , 'away.persons': 'Personer' + , 'away.add_person': 'Legg til person' + , 'backup.select': 'Velg hva som skal sikkerhetskopieres' + , 'backup.select.desc': 'Avmerkede elementer tas med i sikkerhetskopi/gjenoppretting' + , 'backup.opt.all': 'Alle' + , 'backup.opt.main': 'Hovedskjemaer' + , 'backup.opt.weekday': 'Ukedagsskjemaer' + , 'backup.opt.presence': 'Tilstede‑skjemaer' + , 'backup.opt.settings': 'Redigeringsinnstillinger' + , 'backup.opt.holiday': 'Helligdags‑skjemaer' + , 'backup.opt.colors': 'Fargeblokker' + , 'editor.tabs.settings': 'Innstillinger' + , 'editor.tabs.pause': 'Pause' + , 'editor.tabs.weekdays': 'Ukedager' + , 'editor.tabs.colors': 'Farger' + , 'editor.tabs.presence_sensor': 'Tilstedeværelsessensor' + , 'editor.presence_sensor.enable.title': 'Tilstedeværelsessensor' + , 'editor.presence_sensor.enable.desc': 'Aktiver konfigurasjon av tilstedeværelsessensor per rom.' + , 'editor.presence_sensor.entity.title': 'Tilstedeværelses‑entitet' + , 'editor.presence_sensor.entity.desc': 'Velg binary_sensor som indikerer tilstedeværelse i dette rommet.' + , 'editor.presence_sensor.on_temp.title': 'Temperatur når tilstedeværelse er PÅ' + , 'editor.presence_sensor.on_temp.desc': 'Hvis sensoren er PÅ, bruker rommet denne temperaturen i stedet for skjemaet.' + , 'editor.presence_sensor.on_delay.title': 'Forsinkelse før tilstedeværelse brukes' + , 'editor.presence_sensor.on_delay.desc': 'Hvor lenge sensoren må være PÅ før overstyringstemperaturen brukes. Verdien bruker valgt enhet.' + , 'editor.presence_sensor.off_delay.title': 'Forsinkelse før skjema gjenopptas' + , 'editor.presence_sensor.off_delay.desc': 'Hvor lenge sensoren må være AV før skjemaet gjenopptas. Verdien bruker valgt enhet.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Borte fra hjemmet' + , 'editor.tabs.reset': 'Tilbakestill' + , 'editor.tabs.boiler': 'Kjelstyring' + , 'boiler.switch': 'Kjelbryter' + , 'boiler.switch_type': 'Enhetstype' + , 'boiler.switch_type.switch': 'Switch' + , 'boiler.switch_type.input_boolean': 'Input boolean' + , 'boiler.rooms': 'Rom som er med' + , 'boiler.rooms.desc': 'Velg hvilke rom som skal være med i kjelstyringen.' + , 'boiler.multi_enable': 'Aktiver flere kjeler' + , 'boiler.multi_enable.desc': 'Tildel rom til ulike kjeler og sett forskyvninger per rom.' + , 'boiler.multi_rooms': 'Kjel per rom' + , 'boiler.multi_rooms.desc': 'Velg kjelbryter og forskyvninger for hvert rom.' + , 'boiler.room.enable': 'Inkluder rom' + , 'boiler.room.enable.desc': 'Inkluder dette rommet i kjelstyringen.' + , 'boiler.enable': 'Aktiver kjelstyring' + , 'boiler.enable.desc': 'Vis kjelinnstillinger i denne kort‑editoren.' + , 'boiler.temp_sensor': 'Kjelens temperatursensor' + , 'boiler.temp_sensor.desc': 'Temperatursensor som styrer kjelbryteren.' + , 'boiler.min_temp': 'Min kjeltemperatur (°C)' + , 'boiler.max_temp': 'Maks kjeltemperatur (°C)' + , 'boiler.offsets': 'Offset' + , 'boiler.offsets.desc': 'ON offset: hvor langt under planlagt temperatur før den slår PÅ. OFF offset: hvor langt over planlagt temperatur før den slår AV.' + , 'boiler.on_offset': 'ON offset (°C)' + , 'boiler.off_offset': 'OFF offset (°C)' + , 'editor.temp_sensor': 'Temperatursensor' + , 'presence.live_header': 'Vis live tilstedeværelse i headeren' + , 'presence.live_header.desc': 'Chips vises i høyre hjørne.' + , 'profiles.enable': 'Aktiver profiler (overstyring)' + , 'profiles.enable.desc': 'Tillat navngitte dagsskjemaer som kan aktiveres for å overstyre hovedplanen.' + , 'season.enable.title': 'Aktiver sommer-/vinterplaner' + , 'season.enable.desc': 'Når aktivert får hver plan en Sommer- og en Vinter-versjon som du kan bytte mellom.' + , 'season.mode.title': 'Sesong' + , 'season.mode.winter': 'Vinter' + , 'season.mode.summer': 'Sommer' + , 'profiles.active_label': 'Manuell profil aktiv:' + , 'profiles.save_new': 'Lagre ny profil' + , 'profiles.save_existing': 'Lagre endringer i valgt profil' + , 'week.view.title': 'Ukevisning' + , 'week.view.rooms_one_day': 'Alle rom • én dag' + , 'week.view.days_one_room': 'Alle dager • ett rom' + , 'week.view_switch.title': 'Bytt visning fra tidslinjen' + , 'week.view_switch.desc': 'Vis en bryter i tidslinjens toppfelt for å bytte mellom de to ukedagsvisningene.' + , 'week.copy_from_main.title': 'Kopier hovedplan' + , 'week.copy_from_main.desc': 'Du aktiverte ukedager. Vil du kopiere hovedplanen til ukedager? Velg ukedagsmodus å kopiere til.' + , 'week.copy_from_main.mode_label': 'Ukedagsmodus' + , 'week.copy_from_main.target_label': 'Kopier til' + , 'week.copy_from_main.copy_btn': 'Kopier' + , 'week.copy_from_main.skip_btn': 'Ikke kopier' + , 'week.title': 'Ukedager' + , 'week.select_room': 'Velg rom' + , 'err.config_entities': "Konfig må ha 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Video ble ikke funnet' + , 'onboard.image_not_found': 'Bilde ble ikke funnet' + , 'pause.prompt_minutes': 'Angi varighet (minutter):' + , 'editor.test_tool': 'Testverktøy' + , 'editor.test_select_entity': 'Velg en entitet' + , 'editor.test_empty': 'Ingenting å eksportere' + , 'editor.test_export': 'Eksporter' + , 'editor.test_no_fields': 'Ingen felter å vise' + , 'editor.test_select_placeholder': 'Velg...' + , 'editor.error_prefix': 'Feil: ' + , 'backup.restore.confirm_overwrite': 'Gjenopprett fra sikkerhetskopi? Dette vil overskrive nåværende tidsplaner og innstillinger.' + , 'backup.import.err_no_hass': 'Home Assistant er ikke klar' + , 'backup.import.err_storage_off': 'Delt lagring er AV. Slå den på først.' + , 'backup.import.err_no_integration': 'Integrasjonen thermostat_timeline er ikke tilgjengelig' + , 'backup.import.err_json': 'Ugyldig JSON' + , 'profiles.name_exists': 'Navnet finnes allerede' + }, + de: { + 'card.title_default': 'Thermostat-Zeitlinie', + 'ui.edit_block': 'Block bearbeiten', + 'ui.add_block': 'Block hinzufügen', + 'ui.from': 'Von', + 'ui.to': 'Bis', + 'ui.temperature': 'Temperatur', + 'ui.delete': 'Löschen', + 'ui.cancel': 'Abbrechen', + 'ui.close': 'Schließen', + 'ui.unsaved_confirm': 'Es gibt ungespeicherte Änderungen. Trotzdem schließen?', + 'ui.discard_changes': 'Änderungen verwerfen', + 'ui.keep_editing': 'Weiter bearbeiten', + 'ui.save': 'Speichern', + 'ui.error_prefix': 'Fehler: ', + 'ui.overlap_msg': 'Sie haben sich überschneidende Zeiten ausgewählt — Überschneidung zwischen {start} – {end}.', + 'ui.auto_fix': 'Automatisch korrigieren', + 'ui.fix_start_to': 'Beginn auf {time} korrigieren', + 'ui.fix_end_to': 'Ende auf {time} korrigieren', + // Inline meta + 'ui.current_temp': 'Aktuelle Temperatur:', + + 'editor.title_label': 'Titel', + 'editor.add_entity': 'Raum hinzufügen', + 'editor.entity_placeholder': 'Entität auswählen', + 'editor.drag_reorder': 'Zum Neuordnen ziehen', + 'editor.remove': 'Entfernen', + 'editor.default_c': 'Standard °C', + 'editor.row_height_px': 'Zeilenhöhe (px)', + 'editor.max_c': 'Max °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Automatisch anwenden', + 'editor.auto_apply.desc': 'Wenn aktiviert, versucht die Karte, die Temperatur der ausgewählten climate-Entitäten automatisch so einzustellen, dass sie dem geplanten Wert für „jetzt“ entspricht.', + 'editor.apply_edit.title': 'Beim Bearbeiten', + 'editor.apply_edit.desc': 'Wenn Sie Blöcke der Zeitlinie ändern und die Änderung die aktuelle Zeit betrifft, wird die neue Temperatur sofort angewendet.', + 'editor.apply_default.title': 'Bei Änderung von Standard °C', + 'editor.apply_default.desc': 'Wenn „Standard °C“ geändert wird und dies den aktuellen Zeitraum betrifft, wird die neue Temperatur sofort angewendet.', + + 'editor.show_room_temp.title': 'Raumtemperatur anzeigen', + 'editor.show_room_temp.desc': 'Zeigt die Blase mit der aktuellen Raumtemperatur pro Zeile in der Kopfzeile der Zeitlinie an.', + 'editor.room_mode.title': 'Raum-Entitätstyp', + 'editor.room_mode.desc': 'Wenn aktiviert, wählen Sie eine input_number (Zieltemperatur). Wenn deaktiviert, wählen Sie eine climate-Entität.', + + 'editor.room_temp_sensor_override.title': 'Raumtemperatursensor verwenden', + 'editor.room_temp_sensor_override.desc': 'Wenn aktiviert, verwendet die angezeigte Raumtemperatur (und die Kessel-Logik) den ausgewählten Temperatursensor statt climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Temperatursensor', + 'ui.copy_plan': 'Plan kopieren', + 'ui.paste_plan': 'Plan einfügen', + 'ui.copy_day': 'Tag kopieren', + 'ui.paste_day': 'Tag einfügen', + 'editor.merge_label': 'Mit zusammenführen (zusätzlichen Thermostat hinzufügen)', + 'editor.display_name_optional': 'Raumname (optional)', + 'editor.merged_with': 'Zusammengeführt mit', + 'editor.store_enable.title': 'Geteilter Speicher + Hintergrundsteuerung', + 'editor.store_enable.short': 'Speichert Pläne im ausgewählten Sensor und aktiviert die Hintergrundsteuerung.', + 'editor.store_enable.desc': 'Speichert Pläne im ausgewählten Sensor und aktiviert set_temperature im Hintergrund. Thermostate werden auch aktualisiert, wenn die Karte geschlossen ist. Teilt zudem Zeitpläne (Heizblöcke) und Karten-Einstellungen zwischen Benutzern/Geräten. Wenn aus: Befehle werden nur gesendet, solange die Karte auf einem Gerät (Tablet/PC/Mobil) aktiv sichtbar ist und geöffnet bleibt.', + 'editor.store_missing': 'Die Integration „Thermostat Pro Timeline Sync“ ist nicht installiert. Geteilter Speicher ist nicht verfügbar.', + 'editor.clear_store': 'Speicher leeren', + 'editor.clear_store_confirm': 'Dadurch werden alle gespeicherten Zeitpläne gelöscht. Fortfahren?', + 'editor.migrate_to_store': 'Daten in Speicher übertragen', + 'editor.migrate_confirm': 'Ihre im Browser gespeicherten Pläne in den ausgewählten Sensor übertragen? Vorhandene gespeicherte Pläne werden überschrieben.', + 'editor.migrate_pull_confirm': 'Pläne aus dem Speicher importieren und lokale Browserdaten überschreiben?', + 'editor.select_storage_first': 'Bitte wähle zuerst einen Speichersensor aus.', + 'editor.no_local_data': 'Keine lokalen Browserdaten gefunden.', + 'editor.clear_all': 'Alle Daten löschen', + 'editor.clear_all_confirm': 'Dies löscht sowohl Speicher- als auch lokale Browser-Zeitpläne. Fortfahren?', + 'editor.clear_storage_only': 'Nur Speicherdaten löschen', + 'editor.clear_storage_only_confirm': 'Dies löscht Zeitpläne vom ausgewählten Speichersensor. Fortfahren?', + 'editor.clear_local_only': 'Nur lokale Daten löschen', + 'editor.clear_local_only_confirm': 'Dies löscht nur im Browser gespeicherte Zeitpläne. Fortfahren?', + 'editor.reset.desc': 'Was das macht:\n\n• Alle Daten löschen: Setzt den Backend-Speicher (.storage) für Thermostat Timeline zurück und löscht ALLE lokalen Browser-Caches/Entwürfe.\n• Nur lokale Daten löschen: Löscht nur Daten, die in diesem Browser gespeichert sind (localStorage).\n\nTipp: Nach einem vollständigen Reset die Seite neu laden, damit alte Entwürfe nicht wieder auftauchen.', + 'week.enable': 'Wochentage aktivieren', + 'week.mode': 'Wochentagsmodus', + 'week.mode.same_all': 'Jeden Tag gleich (1)', + 'week.mode.weekday_weekend': 'Werktage + Wochenende', + 'week.mode.weekday_sat_sun': 'Werktage + Samstag + Sonntag', + 'week.mode.all_7': 'Alle Tage', + 'week.button': 'Wochentage', + 'week.days.mon': 'Mo', + 'week.days.tue': 'Di', + 'week.days.wed': 'Mi', + 'week.days.thu': 'Do', + 'week.days.fri': 'Fr', + 'week.days.sat': 'Sa', + 'week.days.sun': 'So', + 'week.editor.title': 'Wochenschema', + 'week.editor.preview_note': 'Nur Vorschau. Änderungen werden erst beim Speichern angewendet.', + 'week.open_editor.desc': 'Wochentags-Editor öffnen.', + 'week.open_editor.btn': 'Öffnen', + 'week.copy_to_rooms': 'Wochenschema in Räume kopieren', + 'week.copy_to_rooms.title': 'Wochenschema in Räume kopieren', + 'week.copy_to_rooms.select': 'Räume auswählen', + 'week.copy_to_rooms.apply': 'In Räume einfügen', + 'week.group.weekdays': 'Werktage', + 'week.group.weekend': 'Wochenende', + 'week.days_full.mon': 'Montag', + 'week.days_full.tue': 'Dienstag', + 'week.days_full.wed': 'Mittwoch', + 'week.days_full.thu': 'Donnerstag', + 'week.days_full.fri': 'Freitag', + 'week.days_full.sat': 'Samstag', + 'week.days_full.sun': 'Sonntag', + 'editor.timefmt.title': 'Zeitformat', + 'editor.timefmt.desc': '24-Stunden- oder 12-Stunden-Anzeige (AM/PM) für Zeiten auf der Karte wählen.', + 'editor.tempfmt.title': 'Temperatureinheit', + 'editor.tempfmt.desc': 'Celsius (°C) oder Fahrenheit (°F) für Anzeige in Karte und Editoren wählen.', + 'editor.timesrc.title': 'Zeitquelle', + 'editor.timesrc.desc': 'Wählen, ob die Karte die Browser‑Zeit oder die Home‑Assistant‑Zeitzone für „jetzt“ verwendet.', + 'editor.timesrc.browser': 'Browser', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'Konfigurations-ID', + 'editor.instance.desc': 'Mehrere unabhängige Zeitpläne/Einstellungen unter verschiedenen IDs verwalten.', + 'editor.instance.new_id': 'Neue ID', + 'editor.heat_colors.title': 'Farben für Heizblöcke', + 'editor.heat_colors.add': 'Farbintervall hinzufügen', + 'editor.colors.col_from': 'Von °C', + 'editor.colors.col_to': 'Bis °C', + 'editor.colors.col_color': 'Farbe', + 'editor.colors.empty': 'Noch keine Räume hinzugefügt. Fügen Sie einen Raum hinzu, um Farben zu definieren.', + 'editor.colors.go_add': 'Raum hinzufügen', + 'editor.colors.refresh': 'Zeitplan aktualisieren', + 'editor.colors.clear_local': 'Lokale Farb-Daten löschen', + 'editor.colors.clear_local_confirm': 'Dadurch werden in Ihrem Browser für diese Karte gespeicherte Farbbereiche gelöscht. Fortfahren?', + 'editor.colors.clear_shared': 'Geteilten Farbspeicher löschen', + 'editor.colors.clear_shared_confirm': 'Dadurch werden Farbbereiche im ausgewählten Speichersensor gelöscht. Fortfahren?', + 'editor.colors.mode': 'Farbmodus', + 'editor.colors.mode.desc': 'Farben pro Raum oder ein Satz für alle Räume wählen.', + 'editor.colors.mode_per_room': 'Pro Raum', + 'editor.colors.mode_global': 'Alle Räume', + 'editor.colors.global_title': 'Alle Räume', + 'editor.colors.clear_all': 'Alle Farbdaten löschen', + 'editor.colors.clear_all_confirm': 'Dies löscht alle Farbbereiche sowohl lokal als auch im ausgewählten Speichersensor. Fortfahren?', + 'editor.reload_editor': 'Editor neu laden', + 'editor.reload_dashboard': 'Dashboard neu laden', + 'onboard.title': 'Willkommen bei Thermostat‑Zeitlinie', + 'onboard.next': 'Weiter', + 'onboard.prev': 'Zurück', + 'onboard.skip': 'Überspringen', + 'onboard.done': 'Fertig', + 'onboard.dont_show': 'Nicht erneut anzeigen', + 'onboard.step_main': 'Dies ist die Hauptzeitlinie. Sie zeigt Solltemperaturen über 24 Stunden für jeden Raum.', + 'onboard.step_add': 'Fügen Sie einen Heizblock über + Block hinzufügen oder per Doppelklick hinzu.', + 'onboard.step_edit': 'Bearbeiten/Löschen per Doppelklick. Verwenden Sie Wochentage für verschiedene Tage.', + 'onboard.reset': 'Anleitung erneut anzeigen', + 'onboard.reset_ok': 'Willkommen wird beim nächsten Mal angezeigt.', + 'editor.select_entities': 'Räume', + 'editor.tabs.rooms': 'Räume', + 'editor.test_tool': 'Testwerkzeug', + 'editor.test_select_entity': 'Entität auswählen', + 'editor.test_empty': 'Wählen Sie eine Entität, um verfügbare Befehle anzuzeigen…', + 'editor.test_export': 'Exportieren .md', + 'editor.test_no_fields': 'Keine Felder', + 'editor.test_select_placeholder': '— Auswählen —', + 'editor.error_prefix': 'Fehler: ', + 'editor.open_in_hacs': 'In HACS öffnen', + 'week.enable.desc': 'Pro-Wochentag-Plan aktivieren. Wenn aus, wird jeden Tag derselbe Plan verwendet.', + 'editor.tabs.sync': 'Sync‑Engine', + 'sync.running': 'Synchronisierung läuft…', + 'sync.completed': 'Synchronisierung abgeschlossen', + 'editor.tabs.backup': 'Backup', + 'backup.title': 'Backup', + 'backup.desc': 'Kopiere den Inhalt deines Sync‑Sensors in den Backup‑Sensor und bei Bedarf wiederherstellen.', + 'backup.now': 'Jetzt sichern', + 'backup.restore': 'Wiederherstellen', + 'backup.delete': 'Backup löschen', + 'backup.delete.confirm': 'Ausgewähltes Backup löschen?', + 'backup.new': 'Neues Backup', + 'backup.auto': 'Automatisches Backup', + 'backup.auto.desc': 'Automatische Sicherungen in festen Intervallen erstellen.', + 'backup.interval': 'Intervall (Tage)', + 'backup.interval.desc': 'Anzahl der Tage zwischen automatischen Sicherungen (min. 1).', + 'backup.sensor': 'Backup‑Sensor', + 'backup.last': 'Letztes Backup', + 'backup.export.title': 'Backup-Datei exportieren', + 'backup.export.desc': 'Eine vollständige thermostat_timeline_backup.json mit allen Slots und Daten herunterladen.', + 'backup.export.btn': 'Datei herunterladen', + 'backup.file.add_selected': 'Backup importieren', + 'backup.file.add_all': 'Alle Backups importieren', + 'backup.file.loaded_n': 'Datei geladen ({n} Backups)', + 'backup.file.imported_ok': 'Importiert ({n})', + 'backup.slot.title': 'Backup auswählen', + 'backup.slot.desc': 'Wählen Sie, welcher Backup-Slot wiederhergestellt werden soll.', + 'backup.slot.empty': 'Noch keine Backups', + 'pause.menu_title': 'Auto‑Anwendung pausieren', + 'pause.off_1h': '1 Stunde pausieren', + 'pause.off_5h': '5 Stunden pausieren', + 'pause.off_custom': 'Benutzerdefinierte Dauer…', + 'pause.off_indef': 'Pausieren bis ich fortsetze', + 'pause.resume': 'Jetzt fortsetzen', + 'pause.close': 'Schließen', + 'pause.active_until': 'Pausiert bis {time}', + 'pause.active_indef': 'Pausiert', + 'editor.pausebtn.title': 'Pause‑Schaltfläche', + 'editor.pausebtn.desc': 'Zeigt eine globale Pause‑Schaltfläche in der Kopfzeile. Während der Pause werden alle set_temperature‑Befehle unterdrückt.', + 'editor.integration.connected': 'Integration bereit', + 'editor.integration.disconnected': 'Integration nicht verfügbar' + , 'profiles.button': 'Manueller Zeitplan' + , 'profiles.editor.title': 'Profile' + , 'profiles.add': 'Profil hinzufügen' + , 'profiles.rename': 'Umbenennen' + , 'profiles.delete': 'Löschen' + , 'profiles.activate': 'Aktivieren' + , 'profiles.deactivate': 'Deaktivieren' + , 'profiles.active': 'Aktiv' + , 'profiles.none': 'Keins' + , 'profiles.name_placeholder': 'Profilname' + , 'profiles.edit': 'Bearbeiten' + , 'profiles.preview_note': 'Nur Vorschau. Pläne werden aktiv, wenn Sie das Profil aktivieren.' + , 'editor.tabs.holidays': 'Feiertage' + , 'holidays.enable': 'Feiertage aktivieren' + , 'holidays.desc': 'An Feiertagen einen speziellen Tagesplan verwenden.' + , 'holidays.source': 'Feiertagsquelle' + , 'holidays.source.calendar': 'Kalender' + , 'holidays.source.manual': 'Manuelle Daten' + , 'holidays.entity': 'Feiertags‑Entität' + , 'holidays.entity_desc': 'Wähle einen Kalender, der nur Ein/Aus meldet. An Feiertagen sollte er „on“ sein, an anderen Tagen „off“.' + , 'holidays.dates': 'Feiertagsdaten' + , 'holidays.add_date': 'Datum hinzufügen' + , 'holidays.editor.title': 'Feiertags‑Zeitplan' + , 'holidays.edit': 'Feiertags‑Zeitplan bearbeiten' + , 'holidays.button': 'Feiertage' + , 'editor.perroom.title': 'Standard‑°C pro Raum' + , 'editor.perroom.desc': 'Zeigt ein Standard‑°C‑Feld in jeder Zeile. Wenn aus, wird die globale Standard‑°C für alle Räume verwendet.' + , 'away.resume_schedule': 'Zeitplan fortsetzen' + , 'away.resume_away': 'Abwesenheits‑Temperatur fortsetzen' + , 'presence.button': 'Anwesenheitspläne' + , 'presence.editor.title': 'Anwesenheitspläne' + , 'presence.enable_advanced': 'Erweiterte Anwesenheitspläne aktivieren' + , 'presence.enable_advanced.desc': 'Verwende Kombinationen und Zeitpläne basierend darauf, wer zu Hause/abwesend ist.' + , 'away.delay_enable': 'Abwesenheits‑Aktionen verzögern' + , 'away.delay_desc': 'Vor Anwendung der Abwesenheitsänderungen warten.' + , 'away.delay_value': 'Verzögerung' + , 'away.delay_unit': 'Einheit' + , 'away.delay_unit.seconds': 'Sekunden' + , 'away.delay_unit.minutes': 'Minuten' + , 'presence.combos': 'Kombinationen' + , 'presence.enable_combo': 'Diese Kombination aktivieren' + , 'presence.all_home': 'Alle zu Hause' + , 'presence.none_home': 'Niemand zu Hause' + , 'presence.home': 'Zu Hause: {names}' + , 'presence.away': 'Abwesend: {names}' + , 'presence.manage': 'Kombinationen verwalten' + , 'presence.who_home': 'Wer ist zu Hause' + , 'presence.who_away': 'Wer ist nicht zu Hause' + , 'away.enable': 'Abwesenheitsmodus aktivieren' + , 'away.desc': 'Wenn keine der ausgewählten Personen zu Hause ist, werden die Thermostate automatisch auf die eingestellte Temperatur abgesenkt.' + , 'away.temp': 'Abwesenheits‑Temperatur' + , 'away.persons': 'Personen' + , 'away.add_person': 'Person hinzufügen' + , 'backup.select': 'Auswählen, was gesichert wird' + , 'backup.select.desc': 'Markierte Elemente werden in Sicherung/Wiederherstellung einbezogen.' + , 'backup.opt.all': 'Alle' + , 'backup.opt.main': 'Hauptzeitpläne' + , 'backup.opt.weekday': 'Wochentags‑Zeitpläne' + , 'backup.opt.presence': 'Anwesenheitspläne' + , 'backup.opt.settings': 'Editor‑Einstellungen' + , 'backup.opt.holiday': 'Feiertags‑Zeitpläne' + , 'backup.opt.colors': 'Farbblöcke' + , 'editor.tabs.settings': 'Einstellungen' + , 'editor.tabs.pause': 'Pause' + , 'editor.tabs.weekdays': 'Wochentage' + , 'editor.tabs.colors': 'Farben' + , 'editor.tabs.presence_sensor': 'Anwesenheitssensor' + , 'editor.presence_sensor.enable.title': 'Anwesenheitssensor' + , 'editor.presence_sensor.enable.desc': 'Aktiviert die Anwesenheitssensor‑Konfiguration pro Raum.' + , 'editor.presence_sensor.entity.title': 'Anwesenheitsentität' + , 'editor.presence_sensor.entity.desc': 'Wähle den binary_sensor, der Anwesenheit in diesem Raum anzeigt.' + , 'editor.presence_sensor.on_temp.title': 'Temperatur bei Anwesenheit (EIN)' + , 'editor.presence_sensor.on_temp.desc': 'Wenn der Sensor EIN ist, nutzt dieser Raum diese Temperatur statt dem Zeitplan.' + , 'editor.presence_sensor.on_delay.title': 'Verzögerung bis Anwesenheit gilt' + , 'editor.presence_sensor.on_delay.desc': 'Wie lange der Sensor EIN sein muss, bevor die Override-Temperatur gilt. Wert verwendet die gewählte Einheit.' + , 'editor.presence_sensor.off_delay.title': 'Verzögerung bis Zeitplan fortgesetzt wird' + , 'editor.presence_sensor.off_delay.desc': 'Wie lange der Sensor AUS sein muss, bevor der Zeitplan fortgesetzt wird. Wert verwendet die gewählte Einheit.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Abwesend von zu Hause' + , 'editor.tabs.reset': 'Zurücksetzen' + , 'editor.tabs.boiler': 'Kesselsteuerung' + , 'boiler.switch': 'Kesselschalter' + , 'boiler.switch_type': 'Entitätstyp' + , 'boiler.switch_type.switch': 'Schalter' + , 'boiler.switch_type.input_boolean': 'Input Boolean' + , 'boiler.rooms': 'Einbezogene Räume' + , 'boiler.rooms.desc': 'Wähle, welche Räume in der Kesselsteuerung berücksichtigt werden.' + , 'boiler.multi_enable': 'Mehrere Kessel aktivieren' + , 'boiler.multi_enable.desc': 'Räume verschiedenen Kesseln zuweisen und Offsets pro Raum setzen.' + , 'boiler.multi_rooms': 'Kessel pro Raum' + , 'boiler.multi_rooms.desc': 'Kesselschalter und Offsets pro Raum auswählen.' + , 'boiler.room.enable': 'Raum einbeziehen' + , 'boiler.room.enable.desc': 'Diesen Raum in die Kesselsteuerung einbeziehen.' + , 'boiler.enable': 'Kesselsteuerung aktivieren' + , 'boiler.enable.desc': 'Kesseleinstellungen in diesem Karten‑Editor anzeigen.' + , 'boiler.temp_sensor': 'Kessel‑Temperatursensor' + , 'boiler.temp_sensor.desc': 'Temperatursensor zur Steuerung des Kesselschalters.' + , 'boiler.min_temp': 'Min. Kesseltemperatur (°C)' + , 'boiler.max_temp': 'Max. Kesseltemperatur (°C)' + , 'boiler.offsets': 'Offsets' + , 'boiler.offsets.desc': 'ON‑Offset: wie weit unter dem Zeitplan‑Sollwert bevor EIN geschaltet wird. OFF‑Offset: wie weit über dem Zeitplan‑Sollwert bevor AUS geschaltet wird.' + , 'boiler.on_offset': 'ON‑Offset (°C)' + , 'boiler.off_offset': 'OFF‑Offset (°C)' + , 'editor.temp_sensor': 'Temperatursensor' + , 'presence.live_header': 'Live‑Anwesenheit in Kopfzeile anzeigen' + , 'presence.live_header.desc': 'Chips werden in der rechten Ecke angezeigt.' + , 'profiles.enable': 'Profile aktivieren (Überschreiben)' + , 'profiles.enable.desc': 'Benannte Tagespläne erlauben, die den Hauptplan bei Aktivierung überschreiben.' + , 'season.enable.title': 'Sommer-/Winterpläne aktivieren' + , 'season.enable.desc': 'Wenn aktiviert, hat jeder Plan eine Sommer- und eine Winter-Version, zwischen denen du wechseln kannst.' + , 'season.mode.title': 'Saison' + , 'season.mode.winter': 'Winter' + , 'season.mode.summer': 'Sommer' + , 'profiles.active_label': 'Manuelles Profil aktiv:' + , 'profiles.save_new': 'Neues Profil speichern' + , 'profiles.save_existing': 'Änderungen im ausgewählten Profil speichern' + , 'week.view.title': 'Wochenansicht' + , 'week.view.rooms_one_day': 'Alle Räume • ein Tag' + , 'week.view.days_one_room': 'Alle Tage • ein Raum' + , 'week.view_switch.title': 'Ansicht in der Zeitleiste wechseln' + , 'week.view_switch.desc': 'Zeigt einen Umschalter in der Kopfzeile der Zeitleiste, um zwischen den beiden Wochentagsansichten zu wechseln.' + , 'week.copy_from_main.title': 'Hauptplan kopieren' + , 'week.copy_from_main.desc': 'Du hast Wochentage aktiviert. Soll der Hauptplan in die Wochentage kopiert werden? Wähle den Wochentagsmodus zum Kopieren.' + , 'week.copy_from_main.mode_label': 'Wochentagsmodus' + , 'week.copy_from_main.target_label': 'Kopieren nach' + , 'week.copy_from_main.copy_btn': 'Kopieren' + , 'week.copy_from_main.skip_btn': 'Nicht kopieren' + , 'week.title': 'Wochentage' + , 'week.select_room': 'Raum auswählen' + , 'err.config_entities': "Die Konfiguration muss 'entities: [climate.xxx, ...]' enthalten." + , 'onboard.video_not_found': 'Video nicht gefunden' + , 'onboard.image_not_found': 'Bild nicht gefunden' + , 'pause.prompt_minutes': 'Dauer eingeben (Minuten):' + , 'editor.test_tool': 'Testwerkzeug' + , 'editor.test_select_entity': 'Entität auswählen' + , 'editor.test_empty': 'Nichts zu exportieren' + , 'editor.test_export': 'Exportieren' + , 'editor.test_no_fields': 'Keine Felder zum Anzeigen' + , 'editor.test_select_placeholder': 'Auswählen...' + , 'editor.error_prefix': 'Fehler: ' + , 'backup.restore.confirm_overwrite': 'Aus Backup wiederherstellen? Dadurch werden aktuelle Zeitpläne und Einstellungen überschrieben.' + , 'backup.import.err_no_hass': 'Home Assistant ist nicht bereit' + , 'backup.import.err_storage_off': 'Gemeinsamer Speicher ist AUS. Bitte zuerst aktivieren.' + , 'backup.import.err_no_integration': 'Integration thermostat_timeline ist nicht verfügbar' + , 'backup.import.err_json': 'Ungültiges JSON' + , 'profiles.name_exists': 'Name existiert bereits' + }, + es: { + 'card.title_default': 'Línea de tiempo del termostato', + 'ui.edit_block': 'Editar bloque', + 'ui.add_block': 'Agregar bloque', + 'ui.from': 'Desde', + 'ui.to': 'Hasta', + 'ui.temperature': 'Temperatura', + 'ui.delete': 'Eliminar', + 'ui.cancel': 'Cancelar', + 'ui.close': 'Cerrar', + 'ui.unsaved_confirm': 'Tienes cambios sin guardar. ¿Cerrar igualmente?', + 'ui.discard_changes': 'Descartar cambios', + 'ui.keep_editing': 'Seguir editando', + 'ui.save': 'Guardar', + 'ui.error_prefix': 'Error: ', + 'ui.overlap_msg': 'Has seleccionado horas superpuestas — se solapa durante {start} – {end}.', + 'ui.auto_fix': 'Corregir automáticamente', + 'ui.fix_start_to': 'Ajustar inicio a {time}', + 'ui.fix_end_to': 'Ajustar fin a {time}', + // Inline meta + 'ui.current_temp': 'Temperatura actual:', + + 'editor.title_label': 'Título', + 'editor.add_entity': 'Añadir habitación', + 'editor.entity_placeholder': 'Selecciona una entidad', + 'editor.drag_reorder': 'Arrastrar para reordenar', + 'editor.remove': 'Eliminar', + 'editor.default_c': 'Predeterminado °C', + 'editor.row_height_px': 'Altura de fila (px)', + 'editor.max_c': 'Máx °C', + 'editor.min_c': 'Mín °C', + 'editor.auto_apply.title': 'Aplicar automáticamente', + 'editor.auto_apply.desc': 'Cuando está activado, la tarjeta intentará establecer la temperatura en las entidades climate seleccionadas para que coincida con el valor planificado para “ahora mismo”.', + 'editor.apply_edit.title': 'Al editar', + 'editor.apply_edit.desc': 'Cuando cambias bloques de la línea de tiempo y el cambio afecta a la hora actual, la nueva temperatura se aplica inmediatamente.', + 'editor.apply_default.title': 'Al cambiar Predeterminado °C', + 'editor.apply_default.desc': 'Cuando “Predeterminado °C” cambia y afecta al período actual, la nueva temperatura se aplica inmediatamente.', + + 'editor.show_room_temp.title': 'Mostrar temperatura de la habitación', + 'editor.show_room_temp.desc': 'Muestra la burbuja de temperatura actual de la habitación en cada fila del encabezado de la línea de tiempo.', + 'editor.room_mode.title': 'Tipo de entidad de habitación', + 'editor.room_mode.desc': 'Cuando está activado, selecciona un input_number (temperatura objetivo). Cuando está desactivado, selecciona una entidad climate.', + + 'editor.room_temp_sensor_override.title': 'Usar sensor de temperatura de la habitación', + 'editor.room_temp_sensor_override.desc': 'Cuando está activado, la temperatura mostrada de la habitación (y la lógica de la caldera) usa el sensor de temperatura seleccionado en lugar de climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Sensor de temperatura', + 'ui.copy_plan': 'Copiar plan', + 'ui.paste_plan': 'Pegar plan', + 'ui.copy_day': 'Copiar día', + 'ui.paste_day': 'Pegar día', + 'editor.merge_label': 'Combinar con (añadir termostato extra)', + 'editor.display_name_optional': 'Nombre de la habitación (opcional)', + 'editor.merged_with': 'Combinado con', + 'editor.store_enable.title': 'Almacenamiento compartido + control en segundo plano', + 'editor.store_enable.short': 'Guarda horarios en el sensor seleccionado y habilita el control en segundo plano.', + 'editor.store_enable.desc': 'Guarda horarios en el sensor seleccionado y habilita set_temperature en segundo plano. Los termostatos se actualizan incluso con la tarjeta cerrada. Además comparte horarios (bloques de calor) y la configuración de la tarjeta entre usuarios/dispositivos. Si está desactivado: los comandos se envían solo mientras la tarjeta está visible en un dispositivo (tableta/PC/móvil) y debe permanecer abierta.', + 'editor.store_missing': 'La integración "Thermostat Pro Timeline Sync" no está instalada. El almacenamiento compartido no está disponible.', + 'editor.clear_store': 'Vaciar almacenamiento', + 'editor.clear_store_confirm': 'Esto eliminará todos los horarios guardados. ¿Continuar?', + 'editor.migrate_to_store': 'Transferir datos al almacenamiento', + 'editor.migrate_confirm': '¿Transferir los horarios guardados en tu navegador al sensor seleccionado? Esto sobrescribirá los horarios existentes en el almacenamiento.', + 'editor.migrate_pull_confirm': '¿Importar horarios desde el almacenamiento y sobrescribir los datos locales del navegador?', + 'editor.select_storage_first': 'Selecciona primero un sensor de almacenamiento.', + 'editor.no_local_data': 'No se encontraron datos locales del navegador.', + 'editor.clear_all': 'Borrar todos los datos', + 'editor.clear_all_confirm': 'Esto eliminará los horarios tanto del almacenamiento como del navegador local. ¿Continuar?', + 'editor.clear_storage_only': 'Borrar solo datos de almacenamiento', + 'editor.clear_storage_only_confirm': 'Esto eliminará los horarios del sensor de almacenamiento seleccionado. ¿Continuar?', + 'editor.clear_local_only': 'Borrar solo datos locales', + 'editor.clear_local_only_confirm': 'Esto eliminará solo los horarios guardados en tu navegador. ¿Continuar?', + 'editor.reset.desc': 'Qué hace esto:\n\n• Borrar todos los datos: Restablece el almacenamiento del backend (.storage) de Thermostat Timeline y borra TODA la caché/borradores locales del navegador.\n• Borrar solo datos locales: Borra solo los datos guardados en este navegador (localStorage).\n\nConsejo: Después de un reset completo, recarga la página para evitar que reaparezcan borradores antiguos.', + 'week.enable': 'Habilitar días de la semana', + 'week.mode': 'Modo por días', + 'week.mode.same_all': 'Igual todos los días (1)', + 'week.mode.weekday_weekend': 'Laborables + Fin de semana', + 'week.mode.weekday_sat_sun': 'Laborables + Sábado + Domingo', + 'week.mode.all_7': 'Todos los días', + 'week.button': 'Días de la semana', + 'week.days.mon': 'Lun', + 'week.days.tue': 'Mar', + 'week.days.wed': 'Mié', + 'week.days.thu': 'Jue', + 'week.days.fri': 'Vie', + 'week.days.sat': 'Sáb', + 'week.days.sun': 'Dom', + 'week.editor.title': 'Programación por días', + 'week.editor.preview_note': 'Solo vista previa. Los cambios se aplican al pulsar Guardar.', + 'week.open_editor.desc': 'Abrir el editor de días de la semana.', + 'week.open_editor.btn': 'Abrir', + 'week.copy_to_rooms': 'Copiar semana a habitaciones', + 'week.copy_to_rooms.title': 'Copiar semana a habitaciones', + 'week.copy_to_rooms.select': 'Seleccionar habitaciones', + 'week.copy_to_rooms.apply': 'Insertar en habitaciones', + 'week.group.weekdays': 'Laborables', + 'week.group.weekend': 'Fin de semana', + 'week.days_full.mon': 'Lunes', + 'week.days_full.tue': 'Martes', + 'week.days_full.wed': 'Miércoles', + 'week.days_full.thu': 'Jueves', + 'week.days_full.fri': 'Viernes', + 'week.days_full.sat': 'Sábado', + 'week.days_full.sun': 'Domingo', + 'editor.timefmt.title': 'Formato de hora', + 'editor.timefmt.desc': 'Elige visualización de 24 horas o 12 horas (AM/PM) para la tarjeta.', + 'editor.tempfmt.title': 'Unidad de temperatura', + 'editor.tempfmt.desc': 'Elige Celsius (°C) o Fahrenheit (°F) para mostrar en la tarjeta y editores.', + 'editor.timesrc.title': 'Fuente de tiempo', + 'editor.timesrc.desc': 'Elige si la tarjeta usa la hora del navegador o la zona horaria de Home Assistant para “ahora”.', + 'editor.timesrc.browser': 'Navegador', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'ID de configuración', + 'editor.instance.desc': 'Mantén varios horarios/ajustes independientes bajo IDs diferentes.', + 'editor.instance.new_id': 'Nuevo ID', + 'editor.heat_colors.title': 'Colores de bloques de calor', + 'editor.heat_colors.add': 'Añadir intervalo de color', + 'editor.colors.col_from': 'Desde °C', + 'editor.colors.col_to': 'Hasta °C', + 'editor.colors.col_color': 'Color', + 'editor.colors.empty': 'Aún no se han añadido habitaciones. Añade una para definir colores.', + 'editor.colors.go_add': 'Añadir habitación', + 'editor.colors.refresh': 'Actualizar programación', + 'editor.colors.clear_local': 'Borrar datos de color locales', + 'editor.colors.clear_local_confirm': 'Esto eliminará los intervalos de color guardados en tu navegador para esta tarjeta. ¿Continuar?', + 'editor.colors.clear_shared': 'Borrar almacenamiento de color compartido', + 'editor.colors.clear_shared_confirm': 'Esto eliminará los intervalos de color en el sensor de almacenamiento seleccionado. ¿Continuar?', + 'editor.colors.mode': 'Modo de color', + 'editor.colors.mode.desc': 'Elige colores por habitación o un conjunto para todas.', + 'editor.colors.mode_per_room': 'Por habitación', + 'editor.colors.mode_global': 'Todas las habitaciones', + 'editor.colors.global_title': 'Todas las habitaciones', + 'editor.colors.clear_all': 'Borrar todos los datos de color', + 'editor.colors.clear_all_confirm': 'Esto borrará todos los intervalos de color tanto locales como del sensor de almacenamiento seleccionado. ¿Continuar?', + 'editor.reload_editor': 'Recargar editor', + 'editor.reload_dashboard': 'Recargar panel', + 'onboard.title': 'Bienvenido a la línea de tiempo del termostato', + 'onboard.next': 'Siguiente', + 'onboard.prev': 'Atrás', + 'onboard.skip': 'Omitir', + 'onboard.done': 'Listo', + 'onboard.dont_show': 'No mostrar de nuevo', + 'onboard.step_main': 'Esta es la línea principal. Muestra las temperaturas deseadas a lo largo del día por habitación.', + 'onboard.step_add': 'Añade un bloque con + Agregar bloque o con doble clic en la línea de tiempo.', + 'onboard.step_edit': 'Edita o elimina un bloque con doble clic. Usa días de la semana para distintos días.', + 'onboard.reset': 'Mostrar la guía otra vez', + 'onboard.reset_ok': 'La bienvenida se mostrará la próxima vez.', + 'editor.select_entities': 'Habitaciones', + 'editor.tabs.rooms': 'Habitaciones', + 'editor.test_tool': 'Herramienta de prueba', + 'editor.test_select_entity': 'Seleccionar entidad', + 'editor.test_empty': 'Seleccione una entidad para ver los comandos disponibles…', + 'editor.test_export': 'Exportar .md', + 'editor.test_no_fields': 'Sin campos', + 'editor.test_select_placeholder': '— Seleccionar —', + 'editor.error_prefix': 'Error: ', + 'editor.open_in_hacs': 'Abrir en HACS', + 'week.enable.desc': 'Habilitar programación por día. Si está desactivado, se usa el mismo plan todos los días.', + 'editor.tabs.sync': 'Motor de sincronización', + 'sync.running': 'Sincronización en curso…', + 'sync.completed': 'Sincronización completada', + 'editor.tabs.backup': 'Copia de seguridad', + 'backup.title': 'Copia de seguridad', + 'backup.desc': 'Copia el contenido de tu sensor de sincronización al sensor de copia de seguridad y restaura cuando sea necesario.', + 'backup.now': 'Hacer copia ahora', + 'backup.restore': 'Restaurar', + 'backup.delete': 'Eliminar copia de seguridad', + 'backup.delete.confirm': '¿Eliminar la copia seleccionada?', + 'backup.new': 'Nueva copia de seguridad', + 'backup.auto': 'Copia de seguridad automática', + 'backup.auto.desc': 'Realiza copias de seguridad automáticas a intervalos.', + 'backup.interval': 'Intervalo (días)', + 'backup.interval.desc': 'Cuántos días entre copias de seguridad automáticas (mín. 1).', + 'backup.sensor': 'Sensor de copia de seguridad', + 'backup.last': 'Última copia de seguridad', + 'backup.export.title': 'Exportar archivo de copia', + 'backup.export.desc': 'Descarga un thermostat_timeline_backup.json completo con todos los slots y datos.', + 'backup.export.btn': 'Descargar archivo', + 'backup.file.add_selected': 'Importar copia de seguridad', + 'backup.file.add_all': 'Importar todas las copias de seguridad', + 'backup.file.loaded_n': 'Archivo cargado ({n} copias)', + 'backup.file.imported_ok': 'Importado ({n})', + 'backup.slot.title': 'Seleccionar copia de seguridad', + 'backup.slot.desc': 'Selecciona qué ranura de copia de seguridad restaurar.', + 'backup.slot.empty': 'Aún no hay copias de seguridad', + 'pause.menu_title': 'Pausar auto‑aplicación', + 'pause.off_1h': 'Pausar 1 hora', + 'pause.off_5h': 'Pausar 5 horas', + 'pause.off_custom': 'Duración personalizada…', + 'pause.off_indef': 'Pausar hasta reanudar', + 'pause.resume': 'Reanudar ahora', + 'pause.close': 'Cerrar', + 'pause.active_until': 'Pausado hasta {time}', + 'pause.active_indef': 'Pausado', + 'editor.pausebtn.title': 'Botón de pausa', + 'editor.pausebtn.desc': 'Muestra un botón de pausa global en la cabecera. Mientras está pausado, se suprimen todos los comandos set_temperature.', + 'editor.integration.connected': 'Integración lista', + 'editor.integration.disconnected': 'Integración no disponible' + , 'profiles.button': 'Horario manual' + , 'profiles.editor.title': 'Perfiles' + , 'profiles.add': 'Agregar perfil' + , 'profiles.rename': 'Renombrar' + , 'profiles.delete': 'Eliminar' + , 'profiles.activate': 'Activar' + , 'profiles.deactivate': 'Desactivar' + , 'profiles.active': 'Activo' + , 'profiles.none': 'Ninguno' + , 'profiles.name_placeholder': 'Nombre del perfil' + , 'profiles.edit': 'Editar' + , 'profiles.preview_note': 'Solo vista previa. Los horarios se activan al activar el perfil.' + , 'editor.tabs.holidays': 'Festivos' + , 'holidays.enable': 'Habilitar festivos' + , 'holidays.desc': 'Usa un horario especial en días festivos.' + , 'holidays.source': 'Fuente de festivos' + , 'holidays.source.calendar': 'Calendario' + , 'holidays.source.manual': 'Fechas manuales' + , 'holidays.entity': 'Entidad de festivos' + , 'holidays.entity_desc': 'Selecciona un calendario que solo informe on/off. Debe estar en on en festivos y en off el resto de días.' + , 'holidays.dates': 'Fechas de festivos' + , 'holidays.add_date': 'Agregar fecha' + , 'holidays.editor.title': 'Horario de festivos' + , 'holidays.edit': 'Editar horario de festivos' + , 'holidays.button': 'Festivos' + , 'editor.perroom.title': '°C predeterminado por habitación' + , 'editor.perroom.desc': 'Muestra un campo de °C predeterminado en cada fila. Si está desactivado, se usa el °C predeterminado global para todas las habitaciones.' + , 'away.resume_schedule': 'Reanudar horario' + , 'away.resume_away': 'Reanudar temperatura de ausencia' + , 'presence.button': 'Horarios de presencia' + , 'presence.editor.title': 'Horarios de presencia' + , 'presence.enable_advanced': 'Habilitar horarios avanzados de presencia' + , 'presence.enable_advanced.desc': 'Usa combinaciones y horarios según quién esté en casa/ausente.' + , 'away.delay_enable': 'Retrasar acciones de ausencia' + , 'away.delay_desc': 'Espera antes de aplicar cambios de ausencia.' + , 'away.delay_value': 'Retraso' + , 'away.delay_unit': 'Unidad' + , 'away.delay_unit.seconds': 'Segundos' + , 'away.delay_unit.minutes': 'Minutos' + , 'presence.combos': 'Combinaciones' + , 'presence.enable_combo': 'Habilitar esta combinación' + , 'presence.all_home': 'Todos en casa' + , 'presence.none_home': 'Nadie en casa' + , 'presence.home': 'En casa: {names}' + , 'presence.away': 'Fuera: {names}' + , 'presence.manage': 'Gestionar combinaciones' + , 'presence.who_home': 'Quién está en casa' + , 'presence.who_away': 'Quién no está en casa' + , 'away.enable': 'Habilitar modo ausente' + , 'away.desc': 'Cuando ninguna de las personas seleccionadas está en casa, los termostatos bajan automáticamente a la temperatura establecida.' + , 'away.temp': 'Temperatura de ausencia' + , 'away.persons': 'Personas' + , 'away.add_person': 'Agregar persona' + , 'backup.select': 'Elegir qué copiar en la copia de seguridad' + , 'backup.select.desc': 'Los elementos marcados se incluyen en la copia/restauración' + , 'backup.opt.all': 'Todo' + , 'backup.opt.main': 'Horarios principales' + , 'backup.opt.weekday': 'Horarios por día' + , 'backup.opt.presence': 'Horarios de presencia' + , 'backup.opt.settings': 'Ajustes del editor' + , 'backup.opt.holiday': 'Horarios de festivos' + , 'backup.opt.colors': 'Bloques de color' + , 'editor.tabs.settings': 'Ajustes' + , 'editor.tabs.pause': 'Pausa' + , 'editor.tabs.weekdays': 'Días de la semana' + , 'editor.tabs.colors': 'Colores' + , 'editor.tabs.presence_sensor': 'Sensor de presencia' + , 'editor.presence_sensor.enable.title': 'Sensor de presencia' + , 'editor.presence_sensor.enable.desc': 'Habilita la configuración de sensor de presencia por habitación.' + , 'editor.presence_sensor.entity.title': 'Entidad de presencia' + , 'editor.presence_sensor.entity.desc': 'Selecciona el binary_sensor que indica presencia en esta habitación.' + , 'editor.presence_sensor.on_temp.title': 'Temperatura cuando hay presencia (ENCENDIDO)' + , 'editor.presence_sensor.on_temp.desc': 'Si el sensor está ENCENDIDO, esta habitación usa esta temperatura en lugar del horario.' + , 'editor.presence_sensor.on_delay.title': 'Retraso antes de aplicar presencia' + , 'editor.presence_sensor.on_delay.desc': 'Tiempo que el sensor debe permanecer ENCENDIDO antes de usar la temperatura de presencia. El valor usa la unidad seleccionada.' + , 'editor.presence_sensor.off_delay.title': 'Retraso antes de reanudar el horario' + , 'editor.presence_sensor.off_delay.desc': 'Tiempo que el sensor debe permanecer APAGADO antes de reanudar el horario. El valor usa la unidad seleccionada.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Fuera de casa' + , 'editor.tabs.reset': 'Restablecer' + , 'editor.tabs.boiler': 'Control de caldera' + , 'boiler.switch': 'Interruptor de caldera' + , 'boiler.switch_type': 'Tipo de entidad' + , 'boiler.switch_type.switch': 'Interruptor' + , 'boiler.switch_type.input_boolean': 'Booleano de entrada' + , 'boiler.rooms': 'Habitaciones incluidas' + , 'boiler.rooms.desc': 'Selecciona qué habitaciones deben incluirse en el control de la caldera.' + , 'boiler.multi_enable': 'Habilitar varias calderas' + , 'boiler.multi_enable.desc': 'Asigna habitaciones a distintas calderas y define offsets por habitación.' + , 'boiler.multi_rooms': 'Caldera por habitación' + , 'boiler.multi_rooms.desc': 'Selecciona el interruptor de caldera y los offsets para cada habitación.' + , 'boiler.room.enable': 'Incluir habitación' + , 'boiler.room.enable.desc': 'Incluir esta habitación en el control de la caldera.' + , 'boiler.enable': 'Activar control de caldera' + , 'boiler.enable.desc': 'Mostrar ajustes de caldera en el editor de esta tarjeta.' + , 'boiler.temp_sensor': 'Sensor de temperatura de caldera' + , 'boiler.temp_sensor.desc': 'Sensor de temperatura usado para controlar el interruptor de la caldera.' + , 'boiler.min_temp': 'Temp. mínima de caldera (°C)' + , 'boiler.max_temp': 'Temp. máxima de caldera (°C)' + , 'boiler.offsets': 'Offsets' + , 'boiler.offsets.desc': 'Offset ON: cuánto por debajo del objetivo programado antes de encender. Offset OFF: cuánto por encima del objetivo programado antes de apagar.' + , 'boiler.on_offset': 'Offset ON (°C)' + , 'boiler.off_offset': 'Offset OFF (°C)' + , 'editor.temp_sensor': 'Sensor de temperatura' + , 'presence.live_header': 'Mostrar presencia en vivo en la cabecera' + , 'presence.live_header.desc': 'Las fichas se muestran en la esquina derecha.' + , 'profiles.enable': 'Habilitar perfiles (sobrescribir)' + , 'profiles.enable.desc': 'Permitir horarios diarios con nombre que puedas activar para sobrescribir el plan principal.' + , 'season.enable.title': 'Habilitar horarios de verano/invierno' + , 'season.enable.desc': 'Cuando está habilitado, cada horario tiene versiones de Verano e Invierno que puedes alternar.' + , 'season.mode.title': 'Temporada' + , 'season.mode.winter': 'Invierno' + , 'season.mode.summer': 'Verano' + , 'profiles.active_label': 'Perfil manual activo:' + , 'profiles.save_new': 'Guardar perfil nuevo' + , 'profiles.save_existing': 'Guardar cambios en el perfil seleccionado' + , 'week.view.title': 'Vista semanal' + , 'week.view.rooms_one_day': 'Todas las habitaciones • un día' + , 'week.view.days_one_room': 'Todos los días • una habitación' + , 'week.view_switch.title': 'Cambiar vista desde la línea de tiempo' + , 'week.view_switch.desc': 'Muestra un selector en el encabezado de la línea de tiempo para alternar entre los dos modos de vista de los días de la semana.' + , 'week.copy_from_main.title': 'Copiar plan principal' + , 'week.copy_from_main.desc': 'Activaste los días de la semana. ¿Quieres copiar el plan principal a los días de la semana? Elige el modo para copiar.' + , 'week.copy_from_main.mode_label': 'Modo de días de la semana' + , 'week.copy_from_main.target_label': 'Copiar a' + , 'week.copy_from_main.copy_btn': 'Copiar' + , 'week.copy_from_main.skip_btn': 'No copiar' + , 'week.title': 'Días de la semana' + , 'week.select_room': 'Seleccionar habitación' + , 'err.config_entities': "La configuración debe tener 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Video no encontrado' + , 'onboard.image_not_found': 'Imagen no encontrada' + , 'pause.prompt_minutes': 'Introduce la duración (minutos):' + , 'editor.test_tool': 'Herramienta de prueba' + , 'editor.test_select_entity': 'Selecciona una entidad' + , 'editor.test_empty': 'Nada que exportar' + , 'editor.test_export': 'Exportar' + , 'editor.test_no_fields': 'No hay campos para mostrar' + , 'editor.test_select_placeholder': 'Seleccionar...' + , 'editor.error_prefix': 'Error: ' + , 'backup.restore.confirm_overwrite': '¿Restaurar desde una copia de seguridad? Esto sobrescribirá los horarios y ajustes actuales.' + , 'backup.import.err_no_hass': 'Home Assistant no está listo' + , 'backup.import.err_storage_off': 'El almacenamiento compartido está DESACTIVADO. Actívalo primero.' + , 'backup.import.err_no_integration': 'La integración thermostat_timeline no está disponible' + , 'backup.import.err_json': 'JSON no válido' + , 'profiles.name_exists': 'El nombre ya existe' + }, + fr: { + 'card.title_default': 'Chronologie du thermostat', + 'ui.edit_block': 'Modifier le bloc', + 'ui.add_block': 'Ajouter un bloc', + 'ui.from': 'De', + 'ui.to': 'À', + 'ui.temperature': 'Température', + 'ui.delete': 'Supprimer', + 'ui.cancel': 'Annuler', + 'ui.close': 'Fermer', + 'ui.unsaved_confirm': 'Des modifications non enregistrées existent. Fermer quand même ?', + 'ui.discard_changes': 'Ignorer les modifications', + 'ui.keep_editing': 'Continuer l’édition', + 'ui.save': 'Enregistrer', + 'ui.error_prefix': 'Erreur : ', + 'ui.overlap_msg': 'Vous avez sélectionné des heures qui se chevauchent — chevauchement entre {start} – {end}.', + 'ui.auto_fix': 'Corriger automatiquement', + 'ui.fix_start_to': 'Ajuster le début à {time}', + 'ui.fix_end_to': 'Ajuster la fin à {time}', + // Inline meta + 'ui.current_temp': 'Température actuelle :', + + 'editor.title_label': 'Titre', + 'editor.add_entity': 'Ajouter une pièce', + 'editor.entity_placeholder': 'Sélectionnez une entité', + 'editor.room_mode.title': 'Type d’entité de pièce', + 'editor.room_mode.desc': 'Activé : sélectionnez un input_number (température cible). Désactivé : sélectionnez une entité climate.', + + 'editor.room_temp_sensor_override.title': 'Utiliser un capteur de température de la pièce', + 'editor.room_temp_sensor_override.desc': 'Lorsque c’est activé, la température de la pièce affichée (et la logique de la chaudière) utilise le capteur de température sélectionné au lieu de climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Capteur de température', + 'editor.drag_reorder': 'Glisser pour réorganiser', + 'editor.remove': 'Supprimer', + 'editor.default_c': 'Par défaut °C', + 'editor.row_height_px': 'Hauteur de ligne (px)', + 'editor.max_c': 'Max °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Application automatique', + 'editor.auto_apply.desc': 'Lorsqu’il est activé, la carte essaie de régler la température des entités climate sélectionnées pour qu’elle corresponde à la valeur prévue pour « maintenant »', + 'editor.apply_edit.title': 'Lors de l’édition', + 'editor.apply_edit.desc': 'Quand vous modifiez des blocs de la chronologie et que la modification affecte l’heure actuelle, la nouvelle température est appliquée immédiatement.', + 'editor.apply_default.title': 'Lors d’un changement de °C par défaut', + 'editor.apply_default.desc': 'Lorsque « °C par défaut » change et que cela affecte la période en cours, la nouvelle température est appliquée immédiatement.', + + 'editor.show_room_temp.title': 'Afficher la température de la pièce', + 'editor.show_room_temp.desc': 'Affiche la bulle de température actuelle de la pièce sur chaque ligne dans l’en-tête de la frise.', + 'ui.copy_plan': 'Copier le plan', + 'ui.paste_plan': 'Coller le plan', + 'ui.copy_day': 'Copier le jour', + 'ui.paste_day': 'Coller le jour', + 'editor.merge_label': 'Fusionner avec (ajouter un thermostat supplémentaire)', + 'editor.display_name_optional': 'Nom de la pièce (facultatif)', + 'editor.merged_with': 'Fusionné avec', + 'editor.store_enable.title': 'Stockage partagé + contrôle en arrière‑plan', + 'editor.store_enable.short': 'Enregistre les plannings dans le capteur sélectionné et active le contrôle en arrière‑plan.', + 'editor.store_enable.desc': 'Enregistre les plannings dans le capteur sélectionné et active set_temperature en arrière‑plan. Les thermostats se mettent à jour même lorsque la carte est fermée. Partage également les plannings (blocs de chauffe) et les paramètres de la carte entre utilisateurs/appareils. Désactivé : commandes envoyées uniquement lorsque la carte est visible sur un appareil (tablette/PC/mobile) et elle doit rester ouverte.', + 'editor.store_missing': 'L’intégration « Thermostat Pro Timeline Sync » n’est pas installée. Le stockage partagé n’est pas disponible.', + 'editor.clear_store': 'Vider le stockage', + 'editor.clear_store_confirm': 'Cela supprimera tous les plannings enregistrés. Continuer ?', + 'editor.migrate_to_store': 'Transférer les données vers le stockage', + 'editor.migrate_confirm': 'Transférer vos plannings enregistrés dans le navigateur vers le capteur sélectionné ? Cela remplacera les plannings existants dans le stockage.', + 'editor.migrate_pull_confirm': 'Importer les plannings depuis le stockage et remplacer les données locales du navigateur ?', + 'editor.select_storage_first': 'Veuillez d’abord sélectionner un capteur de stockage.', + 'editor.no_local_data': 'Aucune donnée locale du navigateur trouvée.', + 'editor.clear_all': 'Effacer toutes les données', + 'editor.clear_all_confirm': 'Cela supprimera les plannings du stockage et du navigateur local. Continuer ?', + 'editor.clear_storage_only': 'Effacer uniquement les données du stockage', + 'editor.clear_storage_only_confirm': 'Cela supprimera les plannings du capteur de stockage sélectionné. Continuer ?', + 'editor.clear_local_only': 'Effacer uniquement les données locales', + 'editor.clear_local_only_confirm': 'Cela supprimera uniquement les plannings enregistrés dans votre navigateur. Continuer ?', + 'editor.reset.desc': 'Ce que cela fait :\n\n• Effacer toutes les données : Réinitialise le stockage backend (.storage) de Thermostat Timeline et supprime TOUS les caches/brouillons locaux du navigateur.\n• Effacer uniquement les données locales : Supprime uniquement les données enregistrées dans ce navigateur (localStorage).\n\nAstuce : Après une réinitialisation complète, rechargez la page pour éviter que d’anciens brouillons réapparaissent.', + 'week.enable': 'Activer les jours de semaine', + 'week.mode': 'Mode jours de semaine', + 'week.mode.same_all': 'Identique chaque jour (1)', + 'week.mode.weekday_weekend': 'Jours ouvrés + Week-end', + 'week.mode.weekday_sat_sun': 'Jours ouvrés + Samedi + Dimanche', + 'week.mode.all_7': 'Tous les jours', + 'week.button': 'Jours de semaine', + 'week.days.mon': 'Lun', + 'week.days.tue': 'Mar', + 'week.days.wed': 'Mer', + 'week.days.thu': 'Jeu', + 'week.days.fri': 'Ven', + 'week.days.sat': 'Sam', + 'week.days.sun': 'Dim', + 'week.editor.title': 'Planification par jour', + 'week.editor.preview_note': 'Aperçu uniquement. Les modifications s’appliquent lors de l’enregistrement.', + 'week.open_editor.desc': 'Ouvrir l’éditeur des jours.', + 'week.open_editor.btn': 'Ouvrir', + 'week.copy_to_rooms': 'Copier la semaine vers des pièces', + 'week.copy_to_rooms.title': 'Copier la semaine vers des pièces', + 'week.copy_to_rooms.select': 'Sélectionner des pièces', + 'week.copy_to_rooms.apply': 'Insérer dans les pièces', + 'week.group.weekdays': 'Jours ouvrés', + 'week.group.weekend': 'Week-end', + 'week.days_full.mon': 'Lundi', + 'week.days_full.tue': 'Mardi', + 'week.days_full.wed': 'Mercredi', + 'week.days_full.thu': 'Jeudi', + 'week.days_full.fri': 'Vendredi', + 'week.days_full.sat': 'Samedi', + 'week.days_full.sun': 'Dimanche', + 'editor.timefmt.title': 'Format de l’heure', + 'editor.timefmt.desc': 'Choisir affichage 24 h ou 12 h (AM/PM) pour les heures sur la carte.', + 'editor.tempfmt.title': 'Unité de température', + 'editor.tempfmt.desc': 'Choisir Celsius (°C) ou Fahrenheit (°F) pour l’affichage dans la carte et les éditeurs.', + 'editor.timesrc.title': 'Source de l’heure', + 'editor.timesrc.desc': 'Choisir si la carte utilise l’heure du navigateur ou le fuseau horaire de Home Assistant pour « maintenant ».', + 'editor.timesrc.browser': 'Navigateur', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'ID de configuration', + 'editor.instance.desc': 'Conservez plusieurs plannings/paramètres indépendants sous différents identifiants.', + 'editor.instance.new_id': 'Nouvel ID', + 'editor.heat_colors.title': 'Couleurs des blocs de chauffe', + 'editor.heat_colors.add': 'Ajouter un intervalle de couleur', + 'editor.colors.col_from': 'De °C', + 'editor.colors.col_to': 'À °C', + 'editor.colors.col_color': 'Couleur', + 'editor.colors.empty': 'Aucune pièce ajoutée. Ajoutez une pièce pour définir des couleurs.', + 'editor.colors.go_add': 'Ajouter une pièce', + 'editor.colors.refresh': 'Mettre à jour le planning', + 'editor.colors.clear_local': 'Effacer les données de couleur locales', + 'editor.colors.clear_local_confirm': 'Cela supprimera les intervalles de couleur enregistrés dans votre navigateur pour cette carte. Continuer ?', + 'editor.colors.clear_shared': 'Effacer le stockage de couleur partagé', + 'editor.colors.clear_shared_confirm': 'Cela supprimera les intervalles de couleur dans le capteur de stockage sélectionné. Continuer ?', + 'editor.colors.mode': 'Mode de couleur', + 'editor.colors.mode.desc': 'Choisir des couleurs par pièce ou un jeu pour toutes les pièces.', + 'editor.colors.mode_per_room': 'Par pièce', + 'editor.colors.mode_global': 'Toutes les pièces', + 'editor.colors.global_title': 'Toutes les pièces', + 'editor.colors.clear_all': 'Effacer toutes les données de couleur', + 'editor.colors.clear_all_confirm': 'Ceci supprimera tous les intervalles de couleur à la fois localement et sur le capteur de stockage sélectionné. Continuer ?', + 'editor.reload_editor': 'Recharger l’éditeur', + 'editor.reload_dashboard': 'Recharger le tableau de bord', + 'onboard.title': 'Bienvenue dans la Chronologie du thermostat', + 'onboard.next': 'Suivant', + 'onboard.prev': 'Précédent', + 'onboard.skip': 'Ignorer', + 'onboard.done': 'Terminer', + 'onboard.dont_show': 'Ne plus afficher', + 'onboard.step_main': 'Voici la chronologie principale. Elle affiche les températures souhaitées sur 24 h pour chaque pièce.', + 'onboard.step_add': 'Ajoutez un bloc via + Ajouter un bloc ou double‑cliquez sur la chronologie.', + 'onboard.step_edit': 'Modifiez/Supprimez un bloc par double‑clic. Utilisez les jours de semaine pour des jours différents.', + 'onboard.reset': 'Afficher le guide à nouveau', + 'onboard.reset_ok': 'La présentation s’affichera la prochaine fois.', + 'editor.select_entities': 'Pièces', + 'editor.tabs.rooms': 'Pièces', + 'week.enable.desc': 'Activer la planification par jour de semaine. Désactivé : même plan chaque jour.', + 'editor.tabs.sync': 'Moteur de synchro', + 'sync.running': 'Synchronisation en cours…', + 'sync.completed': 'Synchronisation terminée', + 'editor.tabs.backup': 'Sauvegarde', + 'backup.title': 'Sauvegarde', + 'backup.desc': 'Copie le contenu de votre capteur de synchro vers le capteur de sauvegarde et restaure si besoin.', + 'backup.now': 'Sauvegarder maintenant', + 'backup.restore': 'Restaurer', + 'backup.delete': 'Supprimer la sauvegarde', + 'backup.delete.confirm': 'Supprimer la sauvegarde sélectionnée ?', + 'backup.new': 'Nouvelle sauvegarde', + 'backup.auto': 'Sauvegarde automatique', + 'backup.auto.desc': 'Effectuer des sauvegardes automatiques à intervalle régulier.', + 'backup.interval': 'Intervalle (jours)', + 'backup.interval.desc': 'Nombre de jours entre les sauvegardes automatiques (min. 1).', + 'backup.sensor': 'Capteur de sauvegarde', + 'backup.last': 'Dernière sauvegarde', + 'backup.export.title': 'Exporter le fichier de sauvegarde', + 'backup.export.desc': 'Télécharger un thermostat_timeline_backup.json complet avec tous les emplacements et données.', + 'backup.export.btn': 'Télécharger le fichier', + 'backup.file.add_selected': 'Importer la sauvegarde', + 'backup.file.add_all': 'Importer toutes les sauvegardes', + 'backup.file.loaded_n': 'Fichier chargé ({n} sauvegardes)', + 'backup.file.imported_ok': 'Importé ({n})', + 'backup.slot.title': 'Sélectionner une sauvegarde', + 'backup.slot.desc': 'Sélectionnez l’emplacement de sauvegarde à restaurer.', + 'backup.slot.empty': 'Aucune sauvegarde pour l’instant', + 'pause.menu_title': 'Suspendre l’application automatique', + 'pause.off_1h': 'Suspendre 1 heure', + 'pause.off_5h': 'Suspendre 5 heures', + 'pause.off_custom': 'Durée personnalisée…', + 'pause.off_indef': 'Suspendre jusqu’à reprise', + 'pause.resume': 'Reprendre maintenant', + 'pause.close': 'Fermer', + 'pause.active_until': 'Suspendu jusqu’à {time}', + 'pause.active_indef': 'Suspendu', + 'editor.pausebtn.title': 'Bouton Pause', + 'editor.pausebtn.desc': 'Affiche un bouton de pause global dans l’en‑tête de la carte. En pause, toutes les commandes set_temperature sont bloquées.', + 'editor.integration.connected': 'Intégration prête', + 'editor.integration.disconnected': 'Intégration indisponible' + , 'editor.open_in_hacs': 'Ouvrir dans HACS' + , 'profiles.button': 'Programme manuel' + , 'profiles.editor.title': 'Profils' + , 'profiles.add': 'Ajouter un profil' + , 'profiles.rename': 'Renommer' + , 'profiles.delete': 'Supprimer' + , 'profiles.activate': 'Activer' + , 'profiles.deactivate': 'Désactiver' + , 'profiles.active': 'Actif' + , 'profiles.none': 'Aucun' + , 'profiles.name_placeholder': 'Nom du profil' + , 'profiles.edit': 'Modifier' + , 'profiles.preview_note': 'Aperçu uniquement. Les plannings deviennent actifs lorsque vous activez le profil.' + , 'editor.tabs.holidays': 'Jours fériés' + , 'holidays.enable': 'Activer les jours fériés' + , 'holidays.desc': 'Utiliser un planning spécifique les jours fériés.' + , 'holidays.source': 'Source des jours fériés' + , 'holidays.source.calendar': 'Calendrier' + , 'holidays.source.manual': 'Dates manuelles' + , 'holidays.entity': 'Entité jours fériés' + , 'holidays.entity_desc': 'Sélectionnez un calendrier ne fournissant que on/off. Il doit être « on » les jours fériés et « off » les autres jours.' + , 'holidays.dates': 'Dates des jours fériés' + , 'holidays.add_date': 'Ajouter une date' + , 'holidays.editor.title': 'Planning jours fériés' + , 'holidays.edit': 'Modifier le planning jours fériés' + , 'holidays.button': 'Jours fériés' + , 'editor.perroom.title': '°C par défaut par pièce' + , 'editor.perroom.desc': 'Affiche un champ °C par défaut sur chaque ligne. Désactivé : utilise le °C par défaut global pour toutes les pièces.' + , 'away.resume_schedule': 'Reprendre le programme' + , 'away.resume_away': 'Reprendre la température d’absence' + , 'presence.button': 'Programmes de présence' + , 'presence.editor.title': 'Programmes de présence' + , 'presence.enable_advanced': 'Activer les plannings de présence avancés' + , 'presence.enable_advanced.desc': 'Utilisez des combinaisons et des plannings selon qui est présent/absent.' + , 'away.delay_enable': 'Retarder les actions d’absence' + , 'away.delay_desc': 'Attendre avant d’appliquer les changements d’absence.' + , 'away.delay_value': 'Délai' + , 'away.delay_unit': 'Unité' + , 'away.delay_unit.seconds': 'Secondes' + , 'away.delay_unit.minutes': 'Minutes' + , 'presence.combos': 'Combinaisons' + , 'presence.enable_combo': 'Activer cette combinaison' + , 'presence.all_home': 'Tous à la maison' + , 'presence.none_home': 'Personne à la maison' + , 'presence.home': 'À la maison : {names}' + , 'presence.away': 'Absents : {names}' + , 'presence.manage': 'Gérer les combinaisons' + , 'presence.who_home': 'Qui est à la maison' + , 'presence.who_away': 'Qui n’est pas à la maison' + , 'away.enable': 'Activer le mode absence' + , 'away.desc': 'Lorsque aucune des personnes sélectionnées n’est à la maison, les thermostats sont automatiquement abaissés à la température définie.' + , 'away.temp': 'Température d’absence' + , 'away.persons': 'Personnes' + , 'away.add_person': 'Ajouter une personne' + , 'backup.select': 'Choisir quoi sauvegarder' + , 'backup.select.desc': 'Les éléments cochés sont inclus dans la sauvegarde/restauration' + , 'backup.opt.all': 'Tout' + , 'backup.opt.main': 'Plannings principaux' + , 'backup.opt.weekday': 'Plannings par jour' + , 'backup.opt.presence': 'Plannings de présence' + , 'backup.opt.settings': 'Paramètres de l’éditeur' + , 'backup.opt.holiday': 'Plannings jours fériés' + , 'backup.opt.colors': 'Blocs de couleur' + , 'editor.tabs.settings': 'Paramètres' + , 'editor.tabs.pause': 'Pause' + , 'editor.tabs.weekdays': 'Jours de semaine' + , 'editor.tabs.colors': 'Couleurs' + , 'editor.tabs.presence_sensor': 'Capteur de présence' + , 'editor.presence_sensor.enable.title': 'Capteur de présence' + , 'editor.presence_sensor.enable.desc': 'Activer la configuration des capteurs de présence par pièce.' + , 'editor.presence_sensor.entity.title': 'Entité de présence' + , 'editor.presence_sensor.entity.desc': 'Sélectionnez le binary_sensor qui indique la présence dans cette pièce.' + , 'editor.presence_sensor.on_temp.title': 'Température quand présence est activée' + , 'editor.presence_sensor.on_temp.desc': 'Si le capteur est activé, cette pièce utilise cette température au lieu du planning.' + , 'editor.presence_sensor.on_delay.title': 'Délai avant d’appliquer la présence' + , 'editor.presence_sensor.on_delay.desc': 'Durée pendant laquelle le capteur doit rester activé avant d’utiliser la température de présence. La valeur utilise l’unité sélectionnée.' + , 'editor.presence_sensor.off_delay.title': 'Délai avant de reprendre le planning' + , 'editor.presence_sensor.off_delay.desc': 'Durée pendant laquelle le capteur doit rester désactivé avant de reprendre le planning. La valeur utilise l’unité sélectionnée.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Absence du domicile' + , 'editor.tabs.reset': 'Réinitialiser' + , 'editor.tabs.boiler': 'Contrôle chaudière' + , 'boiler.switch': 'Interrupteur de chaudière' + , 'boiler.switch_type': "Type d’entité" + , 'boiler.switch_type.switch': 'Interrupteur' + , 'boiler.switch_type.input_boolean': "Booléen d’entrée" + , 'boiler.rooms': 'Pièces incluses' + , 'boiler.rooms.desc': 'Sélectionnez les pièces à inclure dans le contrôle de la chaudière.' + , 'boiler.multi_enable': 'Activer plusieurs chaudières' + , 'boiler.multi_enable.desc': 'Assignez des pièces à différentes chaudières et définissez des offsets par pièce.' + , 'boiler.multi_rooms': 'Chaudière par pièce' + , 'boiler.multi_rooms.desc': 'Sélectionnez l’interrupteur de chaudière et les offsets pour chaque pièce.' + , 'boiler.room.enable': 'Inclure la pièce' + , 'boiler.room.enable.desc': 'Inclure cette pièce dans le contrôle de la chaudière.' + , 'boiler.enable': 'Activer le contrôle chaudière' + , 'boiler.enable.desc': 'Afficher les réglages de chaudière dans l’éditeur de cette carte.' + , 'boiler.temp_sensor': 'Capteur de température chaudière' + , 'boiler.temp_sensor.desc': 'Capteur de température utilisé pour piloter l’interrupteur de la chaudière.' + , 'boiler.min_temp': 'Temp. min. chaudière (°C)' + , 'boiler.max_temp': 'Temp. max. chaudière (°C)' + , 'boiler.offsets': 'Décalages' + , 'boiler.offsets.desc': 'Décalage ON : à combien en dessous de la consigne planifiée avant d’allumer. Décalage OFF : à combien au‑dessus de la consigne planifiée avant d’éteindre.' + , 'boiler.on_offset': 'Décalage ON (°C)' + , 'boiler.off_offset': 'Décalage OFF (°C)' + , 'editor.temp_sensor': 'Capteur de température' + , 'presence.live_header': 'Afficher la présence en direct dans l’en‑tête' + , 'presence.live_header.desc': 'Les puces sont affichées dans le coin droit.' + , 'profiles.enable': 'Activer les profils (remplacement)' + , 'profiles.enable.desc': 'Autoriser des plannings journaliers nommés pouvant remplacer le plan principal lorsque vous les activez.' + , 'season.enable.title': 'Activer les plannings été/hiver' + , 'season.enable.desc': 'Lorsque activé, chaque planning possède une version Été et Hiver que vous pouvez alterner.' + , 'season.mode.title': 'Saison' + , 'season.mode.winter': 'Hiver' + , 'season.mode.summer': 'Été' + , 'profiles.active_label': 'Profil manuel actif :' + , 'profiles.save_new': 'Enregistrer un nouveau profil' + , 'profiles.save_existing': 'Enregistrer les modifications du profil sélectionné' + , 'week.view.title': 'Vue semaine' + , 'week.view.rooms_one_day': 'Toutes les pièces • un jour' + , 'week.view.days_one_room': 'Tous les jours • une pièce' + , 'week.view_switch.title': 'Changer la vue depuis la frise' + , 'week.view_switch.desc': 'Affiche un sélecteur dans l’en-tête de la frise pour basculer entre les deux modes de vue des jours de semaine.' + , 'week.copy_from_main.title': 'Copier le planning principal' + , 'week.copy_from_main.desc': 'Vous avez activé les jours de semaine. Voulez-vous copier le planning principal vers les jours de semaine ? Choisissez le mode à copier.' + , 'week.copy_from_main.mode_label': 'Mode jours de semaine' + , 'week.copy_from_main.target_label': 'Copier vers' + , 'week.copy_from_main.copy_btn': 'Copier' + , 'week.copy_from_main.skip_btn': 'Ne pas copier' + , 'week.title': 'Jours de semaine' + , 'week.select_room': 'Sélectionner une pièce' + , 'err.config_entities': "La configuration doit contenir 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Vidéo introuvable' + , 'onboard.image_not_found': 'Image introuvable' + , 'pause.prompt_minutes': 'Saisir la durée (minutes) :' + , 'editor.test_tool': 'Outil de test' + , 'editor.test_select_entity': 'Sélectionnez une entité' + , 'editor.test_empty': 'Rien à exporter' + , 'editor.test_export': 'Exporter' + , 'editor.test_no_fields': 'Aucun champ à afficher' + , 'editor.test_select_placeholder': 'Sélectionner...' + , 'editor.error_prefix': 'Erreur : ' + , 'backup.restore.confirm_overwrite': 'Restaurer depuis une sauvegarde ? Cela écrasera les plannings et paramètres actuels.' + , 'backup.import.err_no_hass': "Home Assistant n'est pas prêt" + , 'backup.import.err_storage_off': 'Le stockage partagé est DÉSACTIVÉ. Activez-le d’abord.' + , 'backup.import.err_no_integration': "L'intégration thermostat_timeline n'est pas disponible" + , 'backup.import.err_json': 'JSON invalide' + , 'profiles.name_exists': 'Le nom existe déjà' + }, + it: { + 'card.title_default': 'Cronologia del termostato', + 'ui.edit_block': 'Modifica blocco', + 'ui.add_block': 'Aggiungi blocco', + 'ui.from': 'Da', + 'ui.to': 'A', + 'ui.temperature': 'Temperatura', + 'ui.delete': 'Elimina', + 'ui.cancel': 'Annulla', + 'ui.close': 'Chiudi', + 'ui.unsaved_confirm': 'Hai modifiche non salvate. Chiudere comunque?', + 'ui.discard_changes': 'Scarta modifiche', + 'ui.keep_editing': 'Continua a modificare', + 'ui.save': 'Salva', + 'ui.error_prefix': 'Errore: ', + 'ui.overlap_msg': 'Hai selezionato orari sovrapposti — si sovrappongono tra {start} e {end}.', + 'ui.auto_fix': 'Correzione automatica', + 'ui.fix_start_to': 'Allinea inizio a {time}', + 'ui.fix_end_to': 'Allinea fine a {time}', + // Inline meta + 'ui.current_temp': 'Temperatura attuale:', + + 'editor.title_label': 'Titolo', + 'editor.add_entity': 'Aggiungi stanza', + 'editor.entity_placeholder': 'Seleziona un\'entità', + 'editor.room_mode.title': 'Tipo di entità stanza', + 'editor.room_mode.desc': 'Quando è attivo, seleziona un input_number (temperatura obiettivo). Quando è disattivo, seleziona un\'entità climate.', + + 'editor.room_temp_sensor_override.title': 'Usa sensore temperatura stanza', + 'editor.room_temp_sensor_override.desc': 'Quando abilitato, la temperatura stanza mostrata (e la logica della caldaia) usa il sensore di temperatura selezionato invece di climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Sensore di temperatura', + 'editor.drag_reorder': 'Trascina per riordinare', + 'editor.remove': 'Rimuovi', + 'editor.default_c': '°C predefinita', + 'editor.perroom.title': '°C predefinita per stanza', + 'editor.perroom.desc': 'Mostra un campo °C predefinita su ogni riga. Se disattivato, viene usata la °C predefinita globale per tutte le stanze.', + 'editor.row_height_px': 'Altezza riga (px)', + 'editor.max_c': '°C max', + 'editor.min_c': '°C min', + 'editor.auto_apply.title': 'Applicazione automatica', + 'editor.auto_apply.desc': 'Quando è attivo, la scheda prova a impostare la temperatura sulle entità climate selezionate così da corrispondere al valore pianificato per "adesso".', + 'editor.apply_edit.title': 'Durante la modifica', + 'editor.apply_edit.desc': 'Quando modifichi i blocchi della timeline e la modifica influisce sull\'ora corrente, la nuova temperatura viene applicata immediatamente.', + 'editor.apply_default.title': 'Al cambio di °C predefinita', + 'editor.apply_default.desc': 'Quando la "°C predefinita" cambia e interessa il periodo corrente, la nuova temperatura viene applicata subito.', + + 'editor.show_room_temp.title': 'Mostra temperatura stanza', + 'editor.show_room_temp.desc': 'Mostra la bolla della temperatura attuale della stanza su ogni riga nell’intestazione della timeline.', + 'ui.copy_plan': 'Copia pianificazione', + 'ui.paste_plan': 'Incolla pianificazione', + 'ui.copy_day': 'Copia giorno', + 'ui.paste_day': 'Incolla giorno', + 'editor.merge_label': 'Unisci con (aggiungi un altro termostato)', + 'editor.display_name_optional': 'Nome stanza (facoltativo)', + 'editor.merged_with': 'Unito con', + 'editor.store_enable.title': 'Archiviazione condivisa + controllo in background', + 'editor.store_enable.short': 'Salva i programmi nel sensore selezionato e abilita il controllo in background.', + 'editor.store_enable.desc': 'Salva i programmi nel sensore selezionato e abilita i comandi set_temperature in background tramite l\'integrazione Thermostat Timeline. I termostati si aggiornano anche quando la scheda è chiusa. Condivide anche programmi (blocchi di calore) e impostazioni tra utenti/dispositivi. Quando è disattivo, i comandi vengono inviati solo mentre la scheda è visibile su un dispositivo (tablet/PC/mobile) e deve restare aperta.', + 'editor.store_missing': 'L\'integrazione "Thermostat Pro Timeline Sync" non è installata. L\'archiviazione condivisa non è disponibile.', + 'editor.clear_store': 'Svuota archiviazione', + 'editor.clear_store_confirm': 'Questo eliminerà tutti i programmi salvati. Continuare?', + 'editor.migrate_to_store': 'Trasferisci dati nell\'archiviazione', + 'editor.migrate_confirm': 'Trasferire i programmi salvati nel browser al sensore selezionato? Eventuali programmi esistenti verranno sovrascritti.', + 'editor.migrate_pull_confirm': 'Importare i programmi dall\'archiviazione e sovrascrivere i dati locali del browser?', + 'editor.select_storage_first': 'Seleziona prima un sensore di archiviazione.', + 'editor.no_local_data': 'Nessun dato locale del browser trovato.', + 'editor.clear_all': 'Cancella tutti i dati', + 'editor.clear_all_confirm': 'Questo eliminerà sia l\'archiviazione sia i programmi salvati nel browser locale. Continuare?', + 'editor.clear_storage_only': 'Cancella solo i dati dell\'archiviazione', + 'editor.clear_storage_only_confirm': 'Questo eliminerà i programmi dal sensore di archiviazione selezionato. Continuare?', + 'editor.clear_local_only': 'Cancella solo i dati locali', + 'editor.clear_local_only_confirm': 'Questo eliminerà solo i programmi salvati nel tuo browser. Continuare?', + 'editor.reset.desc': 'Cosa fa:\n\n• Cancella tutti i dati: Reimposta lo storage backend (.storage) di Thermostat Timeline ed elimina TUTTE le cache/bozze locali del browser.\n• Cancella solo i dati locali: Elimina solo i dati salvati in questo browser (localStorage).\n\nSuggerimento: Dopo un reset completo, ricarica la pagina per evitare che vecchie bozze ricompaiano.', + 'editor.temp_sensor': 'Sensore di temperatura', + 'editor.test_tool': 'Strumento di test', + 'editor.test_select_entity': 'Seleziona entità', + 'editor.test_empty': 'Seleziona un\'entità per vedere i comandi disponibili…', + 'editor.test_export': 'Esporta .md', + 'editor.test_no_fields': 'Nessun campo', + 'editor.test_select_placeholder': '— Seleziona —', + 'editor.error_prefix': 'Errore: ', + 'editor.open_in_hacs': 'Apri in HACS' + , 'week.enable': 'Abilita giorni della settimana' + , 'week.mode': 'Modalità giorni' + , 'week.mode.weekday_weekend': 'Feriali + Weekend' + , 'week.mode.weekday_sat_sun': 'Feriali + Sabato + Domenica' + , 'week.mode.all_7': 'Tutti i giorni' + , 'week.button': 'Giorni feriali' + , 'week.group.weekdays': 'Feriali' + , 'week.group.weekend': 'Weekend' + , 'week.days_full.mon': 'Lunedì' + , 'week.days_full.tue': 'Martedì' + , 'week.days_full.wed': 'Mercoledì' + , 'week.days_full.thu': 'Giovedì' + , 'week.days_full.fri': 'Venerdì' + , 'week.days_full.sat': 'Sabato' + , 'week.days_full.sun': 'Domenica' + , 'week.days.mon': 'Lun' + , 'week.days.tue': 'Mar' + , 'week.days.wed': 'Mer' + , 'week.days.thu': 'Gio' + , 'week.days.fri': 'Ven' + , 'week.days.sat': 'Sab' + , 'week.days.sun': 'Dom' + , 'week.editor.title': 'Programma settimanale' + , 'week.editor.preview_note': 'Solo anteprima. Le modifiche si applicano quando premi Salva.' + , 'week.open_editor.desc': 'Apri l’editor dei giorni della settimana.' + , 'week.open_editor.btn': 'Apri' + , 'week.view.title': 'Vista settimanale' + , 'week.view.rooms_one_day': 'Tutte le stanze • un giorno' + , 'week.view.days_one_room': 'Tutti i giorni • una stanza' + , 'week.view_switch.title': 'Cambia vista dalla timeline' + , 'week.view_switch.desc': 'Mostra un selettore nell’intestazione della timeline per passare tra le due modalità di visualizzazione dei giorni feriali.' + , 'week.copy_from_main.title': 'Copia programma principale' + , 'week.copy_from_main.desc': 'Hai attivato i giorni feriali. Vuoi copiare il programma principale nei giorni feriali? Scegli la modalità da copiare.' + , 'week.copy_from_main.mode_label': 'Modalità giorni feriali' + , 'week.copy_from_main.target_label': 'Copia in' + , 'week.copy_from_main.copy_btn': 'Copia' + , 'week.copy_from_main.skip_btn': 'Non copiare' + , 'week.title': 'Giorni feriali' + , 'week.select_room': 'Seleziona stanza' + , 'week.copy_to_rooms': 'Copia settimana nelle stanze' + , 'week.copy_to_rooms.title': 'Copia settimana nelle stanze' + , 'week.copy_to_rooms.select': 'Seleziona stanze' + , 'week.copy_to_rooms.apply': 'Inserisci nelle stanze' + , 'week.enable.desc': 'Abilita la pianificazione per giorno. Quando è disattivo viene usato lo stesso piano ogni giorno.' + , 'editor.timefmt.title': 'Formato orario' + , 'editor.timefmt.desc': 'Scegli 24 ore o 12 ore (AM/PM) per gli orari mostrati sulla scheda.' + , 'editor.tempfmt.title': 'Unità di temperatura' + , 'editor.tempfmt.desc': 'Scegli Celsius (°C) o Fahrenheit (°F) per la scheda e gli editor.' + , 'editor.timesrc.title': 'Origine orario' + , 'editor.timesrc.desc': 'Scegli se la scheda usa l\'ora del browser o il fuso di Home Assistant per "adesso".' + , 'editor.timesrc.browser': 'Browser' + , 'editor.timesrc.ha': 'Home Assistant' + , 'editor.instance.title': 'ID configurazione' + , 'editor.instance.desc': 'Mantieni più pianificazioni/impostazioni indipendenti con ID diversi.' + , 'editor.instance.new_id': 'Nuovo ID' + , 'editor.tabs.settings': 'Impostazioni' + , 'editor.tabs.pause': 'Pausa' + , 'editor.tabs.weekdays': 'Giorni feriali' + , 'editor.tabs.rooms': 'Stanze' + , 'editor.tabs.colors': 'Colori' + , 'editor.tabs.presence_sensor': 'Sensore di presenza' + , 'editor.presence_sensor.enable.title': 'Sensore di presenza' + , 'editor.presence_sensor.enable.desc': 'Abilita la configurazione del sensore di presenza per stanza.' + , 'editor.presence_sensor.entity.title': 'Entità di presenza' + , 'editor.presence_sensor.entity.desc': 'Seleziona il binary_sensor che indica la presenza in questa stanza.' + , 'editor.presence_sensor.on_temp.title': 'Temperatura quando presenza è ON' + , 'editor.presence_sensor.on_temp.desc': 'Se il sensore è ON, questa stanza usa questa temperatura invece del programma.' + , 'editor.presence_sensor.on_delay.title': 'Ritardo prima di applicare presenza' + , 'editor.presence_sensor.on_delay.desc': 'Quanto tempo il sensore deve restare ON prima di usare la temperatura di presenza. Il valore usa l’unità selezionata.' + , 'editor.presence_sensor.off_delay.title': 'Ritardo prima di riprendere il programma' + , 'editor.presence_sensor.off_delay.desc': 'Quanto tempo il sensore deve restare OFF prima di riprendere il programma. Il valore usa l’unità selezionata.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Fuori casa' + , 'editor.tabs.reset': 'Reimposta' + , 'editor.tabs.boiler': 'Controllo caldaia' + , 'boiler.switch': 'Interruttore caldaia' + , 'boiler.switch_type': 'Tipo di entità' + , 'boiler.switch_type.switch': 'Interruttore' + , 'boiler.switch_type.input_boolean': 'Booleano di input' + , 'boiler.rooms': 'Stanze incluse' + , 'boiler.rooms.desc': 'Seleziona quali stanze includere nel controllo della caldaia.' + , 'boiler.multi_enable': 'Abilita più caldaie' + , 'boiler.multi_enable.desc': 'Assegna le stanze a caldaie diverse e imposta offset per stanza.' + , 'boiler.multi_rooms': 'Caldaia per stanza' + , 'boiler.multi_rooms.desc': 'Seleziona l’interruttore della caldaia e gli offset per ogni stanza.' + , 'boiler.room.enable': 'Includi stanza' + , 'boiler.room.enable.desc': 'Includi questa stanza nel controllo caldaia.' + , 'boiler.enable': 'Attiva controllo caldaia' + , 'boiler.enable.desc': 'Mostra le impostazioni caldaia nell’editor di questa scheda.' + , 'boiler.temp_sensor': 'Sensore temperatura caldaia' + , 'boiler.temp_sensor.desc': 'Sensore di temperatura usato per controllare l’interruttore della caldaia.' + , 'boiler.min_temp': 'Temperatura min. caldaia (°C)' + , 'boiler.max_temp': 'Temperatura max. caldaia (°C)' + , 'boiler.offsets': 'Offset' + , 'boiler.offsets.desc': 'Offset ON: quanto sotto l’obiettivo programmato prima di accendere. Offset OFF: quanto sopra l’obiettivo programmato prima di spegnere.' + , 'boiler.on_offset': 'Offset ON (°C)' + , 'boiler.off_offset': 'Offset OFF (°C)' + , 'editor.tabs.sync': 'Motore di sincronizzazione' + , 'sync.running': 'Sincronizzazione in corso…' + , 'sync.completed': 'Sincronizzazione completata' + , 'away.enable': 'Abilita modalità assenza' + , 'away.desc': 'Quando nessuna delle persone selezionate è in casa, abbassa automaticamente i termostati alla temperatura impostata.' + , 'away.temp': 'Temperatura assenza' + , 'away.persons': 'Persone' + , 'away.add_person': 'Aggiungi persona' + , 'away.resume_schedule': 'Riprendi programma' + , 'away.resume_away': 'Ripristina temperatura assenza' + , 'editor.heat_colors.title': 'Colori dei blocchi di calore' + , 'editor.heat_colors.add': 'Aggiungi intervallo colore' + , 'editor.colors.col_from': 'Da °C' + , 'editor.colors.col_to': 'A °C' + , 'editor.colors.col_color': 'Colore' + , 'editor.colors.empty': 'Nessuna stanza aggiunta. Aggiungi una stanza per definire i colori.' + , 'editor.colors.go_add': 'Aggiungi stanza' + , 'editor.select_entities': 'Stanze' + , 'editor.colors.refresh': 'Aggiorna programma' + , 'editor.colors.clear_local': 'Pulisci dati colore locali' + , 'editor.colors.clear_local_confirm': 'Questo eliminerà gli intervalli colore salvati nel tuo browser per questa scheda. Continuare?' + , 'editor.colors.clear_shared': 'Pulisci archivio colori condiviso' + , 'editor.colors.clear_shared_confirm': 'Questo eliminerà gli intervalli colore nel sensore di archiviazione selezionato. Continuare?' + , 'editor.colors.clear_all': 'Pulisci tutti i dati colore' + , 'editor.colors.clear_all_confirm': 'Questo eliminerà tutti gli intervalli colore sia locali sia nel sensore selezionato. Continuare?' + , 'editor.colors.mode': 'Modalità colore' + , 'editor.colors.mode.desc': 'Scegli colori per stanza o un set per tutte le stanze.' + , 'editor.colors.mode_per_room': 'Per stanza' + , 'editor.colors.mode_global': 'Tutte le stanze' + , 'editor.colors.global_title': 'Tutte le stanze' + , 'editor.reload_editor': 'Ricarica editor' + , 'editor.reload_dashboard': 'Ricarica dashboard' + , 'onboard.title': 'Benvenuto in Thermostat Timeline' + , 'onboard.next': 'Avanti' + , 'onboard.prev': 'Indietro' + , 'onboard.skip': 'Salta' + , 'onboard.done': 'Fine' + , 'onboard.dont_show': 'Non mostrare più' + , 'onboard.step_main': 'Questa è la timeline principale. Mostra le temperature desiderate sulle 24 ore per ogni stanza.' + , 'onboard.step_add': 'Aggiungi un blocco di calore con + Aggiungi blocco o facendo doppio clic sulla timeline.' + , 'onboard.step_edit': 'Modifica o elimina un blocco con un doppio clic. Usa Giorni feriali per pianificare giorni diversi.' + , 'onboard.reset': 'Mostra di nuovo la guida' + , 'onboard.reset_ok': 'La schermata di benvenuto verrà mostrata la prossima volta.' + , 'pause.menu_title': 'Pausa applicazione automatica' + , 'pause.off_1h': 'Pausa 1 ora' + , 'pause.off_5h': 'Pausa 5 ore' + , 'pause.off_custom': 'Durata personalizzata…' + , 'pause.off_indef': 'Pausa finché non riprendo' + , 'pause.resume': 'Riprendi ora' + , 'pause.close': 'Chiudi' + , 'pause.active_until': 'In pausa fino alle {time}' + , 'pause.active_indef': 'In pausa' + , 'editor.pausebtn.title': 'Pulsante Pausa' + , 'editor.pausebtn.desc': 'Mostra un pulsante di pausa globale nell\'intestazione della scheda. Durante la pausa, tutti i comandi set_temperature sono sospesi.' + , 'editor.integration.connected': 'Integrazione pronta' + , 'editor.integration.disconnected': 'Integrazione non disponibile' + , 'profiles.enable': 'Abilita profili (override)' + , 'profiles.enable.desc': 'Consenti programmi giornalieri nominati che possono sostituire il piano principale.' + , 'season.enable.title': 'Abilita programmi estivi/invernali' + , 'season.enable.desc': 'Quando attivo, ogni programma ha una versione Estate e Inverno tra cui puoi passare.' + , 'season.mode.title': 'Stagione' + , 'season.mode.winter': 'Inverno' + , 'season.mode.summer': 'Estate' + , 'profiles.button': 'Programma manuale' + , 'profiles.editor.title': 'Profili' + , 'profiles.add': 'Aggiungi profilo' + , 'profiles.rename': 'Rinomina' + , 'profiles.delete': 'Elimina' + , 'profiles.activate': 'Attiva' + , 'profiles.deactivate': 'Disattiva' + , 'profiles.active': 'Attivo' + , 'profiles.none': 'Nessuno' + , 'profiles.name_placeholder': 'Nome profilo' + , 'profiles.edit': 'Modifica' + , 'profiles.preview_note': 'Solo anteprima. I programmi diventano attivi quando attivi il profilo.' + , 'profiles.active_label': 'Profilo manuale attivo:' + , 'profiles.save_new': 'Salva nuovo profilo' + , 'profiles.save_existing': 'Salva modifiche al profilo selezionato' + , 'editor.tabs.holidays': 'Festività' + , 'editor.tabs.backup': 'Backup' + , 'backup.title': 'Backup' + , 'backup.desc': 'Copia programmi e impostazioni nel backup e ripristinali al bisogno.' + , 'backup.now': 'Esegui backup ora' + , 'backup.restore': 'Ripristina' + , 'backup.delete': 'Elimina backup' + , 'backup.delete.confirm': 'Eliminare il backup selezionato?' + , 'backup.new': 'Nuovo backup' + , 'backup.auto': 'Backup automatico' + , 'backup.auto.desc': 'Esegui backup automatici pianificati.' + , 'backup.interval': 'Intervallo (giorni)' + , 'backup.interval.desc': 'Numero di giorni tra i backup automatici (min. 1).' + , 'backup.sensor': 'Sensore di backup' + , 'backup.last': 'Ultimo backup' + , 'backup.export.title': 'Esporta file di backup' + , 'backup.export.desc': 'Scarica un file thermostat_timeline_backup.json completo con tutti gli slot e i dati.' + , 'backup.export.btn': 'Scarica file' + , 'backup.file.add_selected': 'Importa backup' + , 'backup.file.add_all': 'Importa tutti i backup' + , 'backup.file.loaded_n': 'File caricato ({n} backup)' + , 'backup.file.imported_ok': 'Importato ({n})' + , 'backup.slot.title': 'Seleziona backup' + , 'backup.slot.desc': 'Seleziona quale slot di backup ripristinare.' + , 'backup.slot.empty': 'Nessun backup ancora' + , 'backup.select': 'Scegli cosa includere nel backup' + , 'backup.select.desc': 'Gli elementi selezionati sono inclusi nel backup/ripristino' + , 'backup.opt.all': 'Tutto' + , 'backup.opt.main': 'Programmi principali' + , 'backup.opt.weekday': 'Programmi feriali' + , 'backup.opt.presence': 'Programmi presenza' + , 'backup.opt.settings': 'Impostazioni editor' + , 'backup.opt.holiday': 'Programmi festività' + , 'backup.opt.colors': 'Blocchi colore' + , 'holidays.enable': 'Abilita festività' + , 'holidays.desc': 'Usa un programma giornaliero speciale nei giorni festivi.' + , 'holidays.source': 'Fonte festività' + , 'holidays.source.calendar': 'Calendario' + , 'holidays.source.manual': 'Date manuali' + , 'holidays.entity': 'Entità festività' + , 'holidays.entity_desc': 'Seleziona un calendario che segnala solo acceso/spento. Deve essere acceso nei giorni festivi e spento negli altri.' + , 'holidays.dates': 'Date festività' + , 'holidays.add_date': 'Aggiungi data' + , 'holidays.editor.title': 'Programma festività' + , 'holidays.edit': 'Modifica programma festività' + , 'holidays.button': 'Festività' + , 'presence.button': 'Programmi presenza' + , 'presence.editor.title': 'Programmi presenza' + , 'presence.enable_advanced': 'Abilita pianificazioni di presenza avanzate' + , 'presence.enable_advanced.desc': 'Usa combinazioni e pianificazioni in base a chi è in casa/fuori.' + , 'away.delay_enable': 'Ritarda azioni di assenza' + , 'away.delay_desc': 'Attendi prima di applicare i cambi di assenza.' + , 'away.delay_value': 'Ritardo' + , 'away.delay_unit': 'Unità' + , 'away.delay_unit.seconds': 'Secondi' + , 'away.delay_unit.minutes': 'Minuti' + , 'presence.live_header': 'Mostra presenza live nell\'intestazione' + , 'presence.live_header.desc': 'Le chip vengono visualizzate nell’angolo destro.' + , 'presence.combos': 'Combinazioni' + , 'presence.enable_combo': 'Abilita questa combinazione' + , 'presence.all_home': 'Tutti a casa' + , 'presence.none_home': 'Nessuno a casa' + , 'presence.home': 'A casa: {names}' + , 'presence.away': 'Fuori: {names}' + , 'presence.manage': 'Gestisci combinazioni' + , 'presence.who_home': 'Chi è a casa' + , 'presence.who_away': 'Chi non è a casa' + , 'err.config_entities': "La configurazione deve avere 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Video non trovato' + , 'onboard.image_not_found': 'Immagine non trovata' + , 'pause.prompt_minutes': 'Inserisci la durata (minuti):' + , 'editor.test_tool': 'Strumento di test' + , 'editor.test_select_entity': "Seleziona un'entità" + , 'editor.test_empty': 'Niente da esportare' + , 'editor.test_export': 'Esporta' + , 'editor.test_no_fields': 'Nessun campo da mostrare' + , 'editor.test_select_placeholder': 'Seleziona...' + , 'editor.error_prefix': 'Errore: ' + , 'backup.restore.confirm_overwrite': 'Ripristinare da backup? Questo sovrascriverà pianificazioni e impostazioni correnti.' + , 'backup.import.err_no_hass': 'Home Assistant non è pronto' + , 'backup.import.err_storage_off': "L'archiviazione condivisa è DISATTIVATA. Attivala prima." + , 'backup.import.err_no_integration': "L'integrazione thermostat_timeline non è disponibile" + , 'backup.import.err_json': 'JSON non valido' + , 'profiles.name_exists': 'Il nome esiste già' + }, + fi: { + 'card.title_default': 'Termostaatin aikajana', + 'ui.edit_block': 'Muokkaa lohkoa', + 'ui.add_block': 'Lisää lohko', + 'ui.from': 'Alkaen', + 'ui.to': 'Asti', + 'ui.temperature': 'Lämpötila', + 'ui.delete': 'Poista', + 'ui.cancel': 'Peruuta', + 'ui.close': 'Sulje', + 'ui.unsaved_confirm': 'Sinulla on tallentamattomia muutoksia. Suljetaanko silti?', + 'ui.discard_changes': 'Hylkää muutokset', + 'ui.keep_editing': 'Jatka muokkausta', + 'ui.save': 'Tallenna', + 'ui.error_prefix': 'Virhe: ', + 'ui.overlap_msg': 'Valitsit päällekkäisiä aikoja — ne menevät päällekkäin välillä {start} – {end}.', + 'ui.auto_fix': 'Korjaa automaattisesti', + 'ui.fix_start_to': 'Korjaa alku ajaksi {time}', + 'ui.fix_end_to': 'Korjaa loppu ajaksi {time}', + // Inline meta + 'ui.current_temp': 'Nykyinen lämpötila:', + + 'editor.title_label': 'Otsikko', + 'editor.add_entity': 'Lisää huone', + 'editor.entity_placeholder': 'Valitse entiteetti', + 'editor.drag_reorder': 'Vedä järjestelläksesi', + 'editor.remove': 'Poista', + 'editor.default_c': 'Oletus °C', + 'editor.row_height_px': 'Rivin korkeus (px)', + 'editor.max_c': 'Maksi °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Automaattinen käyttö', + 'editor.auto_apply.desc': 'Kun käytössä, kortti yrittää asettaa valittujen climate-entiteettien lämpötilan vastaamaan suunniteltua arvoa tälle hetkelle.', + 'editor.apply_edit.title': 'Muokkauksen yhteydessä', + 'editor.apply_edit.desc': 'Kun muutat aikajanan lohkoja ja muutos koskee nykyhetkeä, uusi lämpötila otetaan käyttöön heti.', + 'editor.apply_default.title': 'Kun Oletus °C muuttuu', + 'editor.apply_default.desc': 'Kun ”Oletus °C” muuttuu ja se vaikuttaa meneillään olevaan ajanjaksoon, uusi lämpötila otetaan käyttöön heti.', + + 'editor.show_room_temp.title': 'Näytä huonelämpötila', + 'editor.show_room_temp.desc': 'Näyttää nykyisen huonelämpötilan kuplan jokaisella rivillä aikajanan otsikossa.', + 'editor.room_mode.title': 'Huoneen entiteettityyppi', + 'editor.room_mode.desc': 'Kun käytössä, valitse input_number (tavoitelämpötila). Kun pois päältä, valitse climate-entiteetti.', + + 'editor.room_temp_sensor_override.title': 'Käytä huonelämpötila-anturia', + 'editor.room_temp_sensor_override.desc': 'Kun käytössä, näytetty huonelämpötila (ja kattilalogiikka) käyttää valittua lämpötila-anturia climate current_temperature -arvon sijaan.', + 'editor.room_temp_sensor_override.entity': 'Lämpötila-anturi', + 'ui.copy_plan': 'Kopioi aikataulu', + 'ui.paste_plan': 'Liitä aikataulu', + 'ui.copy_day': 'Kopioi päivä', + 'ui.paste_day': 'Liitä päivä', + 'editor.merge_label': 'Yhdistä (lisää toinen termostaatti)', + 'editor.display_name_optional': 'Huoneen nimi (valinnainen)', + 'editor.merged_with': 'Yhdistetty kohteeseen', + 'editor.store_enable.title': 'Jaettu tallennus + taustasäätö', + 'editor.store_enable.short': 'Tallentaa aikataulut valittuun sensoriin ja ottaa käyttöön taustasäädön.', + 'editor.store_enable.desc': 'Tallentaa aikataulut valittuun sensoriin ja ottaa käyttöön taustalla tapahtuvan set_temperature‑ohjauksen. Termostaatit päivittyvät myös, kun kortti ei ole auki. Jakaa myös aikataulut (lämpöblokit) ja kortin asetukset käyttäjien/laitteiden välillä. Kun pois päältä: komennot lähetetään vain, kun kortti on näkyvissä laitteella (tabletti/tietokone/puhelin) ja sen on pysyttävä auki.', + 'editor.store_missing': 'Integraatiota "Thermostat Pro Timeline Sync" ei ole asennettu. Jaettu tallennus ei ole käytettävissä.', + 'editor.clear_store': 'Tyhjennä tallennus', + 'editor.clear_store_confirm': 'Tämä poistaa kaikki tallennetut aikataulut. Jatketaanko?', + 'editor.migrate_to_store': 'Siirrä tiedot tallennukseen', + 'editor.migrate_confirm': 'Siirretäänkö selaimeen tallennetut aikataulut valittuun sensoriin? Tämä korvaa nykyiset aikataulut tallennuksessa.', + 'editor.migrate_pull_confirm': 'Tuo aikataulut tallennuksesta ja korvaa paikalliset selaintiedot?', + 'editor.select_storage_first': 'Valitse ensin tallennussensori.', + 'editor.no_local_data': 'Paikallisia selaintietoja ei löytynyt.', + 'editor.clear_all': 'Tyhjennä kaikki tiedot', + 'editor.clear_all_confirm': 'Tämä poistaa sekä tallennuksesta että selaimesta aikataulut. Jatketaanko?', + 'editor.clear_storage_only': 'Tyhjennä vain tallennusdata', + 'editor.clear_storage_only_confirm': 'Tämä poistaa aikataulut valitusta sensorista. Jatketaanko?', + 'editor.clear_local_only': 'Tyhjennä vain paikalliset tiedot', + 'editor.clear_local_only_confirm': 'Tämä poistaa vain selaimeen tallennetut aikataulut. Jatketaanko?', + 'editor.reset.desc': 'Mitä tämä tekee:\n\n• Tyhjennä kaikki tiedot: Nollaa Thermostat Timeline -integraation backend-tallennuksen (.storage) ja poistaa KAIKKI paikalliset välimuistit/luonnokset tältä selaimelta.\n• Tyhjennä vain paikalliset tiedot: Tyhjentää vain tässä selaimessa tallennetut tiedot (localStorage).\n\nVinkki: Täyden resetoinnin jälkeen lataa sivu uudelleen, jotta vanhat luonnokset eivät ilmesty takaisin.', + 'week.enable': 'Ota viikonpäivät käyttöön', + 'week.mode': 'Viikkotila', + 'week.mode.same_all': 'Sama joka päivä (1)', + 'week.mode.weekday_weekend': 'Arkipäivät + Viikonloppu', + 'week.mode.weekday_sat_sun': 'Arkipäivät + Lauantai + Sunnuntai', + 'week.mode.all_7': 'Kaikki päivät', + 'week.button': 'Viikonpäivät', + 'week.days.mon': 'Ma', + 'week.days.tue': 'Ti', + 'week.days.wed': 'Ke', + 'week.days.thu': 'To', + 'week.days.fri': 'Pe', + 'week.days.sat': 'La', + 'week.days.sun': 'Su', + 'week.editor.title': 'Viikkosuunnitelma', + 'week.editor.preview_note': 'Vain esikatselu. Muutokset otetaan käyttöön, kun painat Tallenna.', + 'week.open_editor.desc': 'Avaa viikonpäiväeditori.', + 'week.open_editor.btn': 'Avaa', + 'week.copy_to_rooms': 'Kopioi viikkosuunnitelma huoneisiin', + 'week.copy_to_rooms.title': 'Kopioi viikkosuunnitelma huoneisiin', + 'week.copy_to_rooms.select': 'Valitse huoneet', + 'week.copy_to_rooms.apply': 'Lisää huoneisiin', + 'week.group.weekdays': 'Arkipäivät', + 'week.group.weekend': 'Viikonloppu', + 'week.days_full.mon': 'Maanantai', + 'week.days_full.tue': 'Tiistai', + 'week.days_full.wed': 'Keskiviikko', + 'week.days_full.thu': 'Torstai', + 'week.days_full.fri': 'Perjantai', + 'week.days_full.sat': 'Lauantai', + 'week.days_full.sun': 'Sunnuntai', + 'editor.timefmt.title': 'Aikamuoto', + 'editor.timefmt.desc': 'Valitse 24 tunnin tai 12 tunnin (AM/PM) näyttö kortin ajoille.', + 'editor.tempfmt.title': 'Lämpötilayksikkö', + 'editor.tempfmt.desc': 'Valitse Celsius (°C) tai Fahrenheit (°F) kortin ja editorien näyttöön.', + 'editor.timesrc.title': 'Ajan lähde', + 'editor.timesrc.desc': 'Valitse käytetäänkö “nyt”‑hetkeen selaimen aikaa vai Home Assistantin aikavyöhykettä.', + 'editor.timesrc.browser': 'Selain', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'Kokoonpano-ID', + 'editor.instance.desc': 'Pidä useita erillisiä aikatauluja/asetuksia eri tunnisteilla.', + 'editor.instance.new_id': 'Uusi ID', + 'editor.heat_colors.title': 'Lämmityslohkojen värit', + 'editor.heat_colors.add': 'Lisää värihaarukka', + 'editor.colors.col_from': 'Alkaen °C', + 'editor.colors.col_to': 'Asti °C', + 'editor.colors.col_color': 'Väri', + 'editor.colors.empty': 'Huoneita ei ole lisätty vielä. Lisää huone määrittääksesi värit.', + 'editor.colors.go_add': 'Lisää huone', + 'editor.colors.refresh': 'Päivitä aikataulu', + 'editor.colors.clear_local': 'Tyhjennä paikalliset väritiedot', + 'editor.colors.clear_local_confirm': 'Tämä poistaa tähän korttiin selaimeen tallennetut värihaarukat. Jatketaanko?', + 'editor.colors.clear_shared': 'Tyhjennä jaettu värivarasto', + 'editor.colors.clear_shared_confirm': 'Tämä poistaa värihaarukat valitusta tallennussensorista. Jatketaanko?', + 'editor.colors.mode': 'Väritila', + 'editor.colors.mode.desc': 'Valitse värit huoneittain tai yksi yhteinen kaikille.', + 'editor.colors.mode_per_room': 'Huoneittain', + 'editor.colors.mode_global': 'Kaikille', + 'editor.colors.global_title': 'Kaikki huoneet', + 'editor.colors.clear_all': 'Tyhjennä kaikki väritiedot', + 'editor.colors.clear_all_confirm': 'Tämä poistaa kaikki värihaarukat sekä paikallisesti että valitusta tallennussensorista. Jatketaanko?', + 'editor.reload_editor': 'Lataa editori uudelleen', + 'editor.reload_dashboard': 'Lataa koontinäyttö uudelleen', + 'onboard.title': 'Tervetuloa Termostaatin aikajanaan', + 'onboard.next': 'Seuraava', + 'onboard.prev': 'Edellinen', + 'onboard.skip': 'Ohita', + 'onboard.done': 'Valmis', + 'onboard.dont_show': 'Älä näytä uudelleen', + 'onboard.step_main': 'Tämä on pääaikajana. Se näyttää toivotut lämpötilat 24 tunnin aikana huoneittain.', + 'onboard.step_add': 'Lisää lämpölohko painamalla + Lisää lohko tai kaksoisnapsauttamalla aikajanaa.', + 'onboard.step_edit': 'Muokkaa tai poista lohko kaksoisnapsauttamalla. Käytä viikonpäiviä eri päiville.', + 'onboard.reset': 'Näytä opas uudelleen', + 'onboard.reset_ok': 'Tervetuloa-näyttö avautuu seuraavalla kerralla.', + 'editor.select_entities': 'Huoneet', + 'editor.tabs.rooms': 'Huoneet', + 'editor.test_tool': 'Testityökalu', + 'editor.test_select_entity': 'Valitse entiteetti', + 'editor.test_empty': 'Valitse entiteetti nähdäksesi käytettävissä olevat komennot…', + 'editor.test_export': 'Vie .md', + 'editor.test_no_fields': 'Ei kenttiä', + 'editor.test_select_placeholder': '— Valitse —', + 'editor.error_prefix': 'Virhe: ', + 'editor.open_in_hacs': 'Avaa HACSissa', + 'week.enable.desc': 'Ota viikonpäiväkohtainen aikataulu käyttöön. Kun pois päältä, käytetään samaa aikataulua joka päivä.', + 'editor.tabs.sync': 'Synkronointi‑moottori', + 'sync.running': 'Synkronointi käynnissä…', + 'sync.completed': 'Synkronointi valmis', + 'editor.tabs.backup': 'Varmuuskopio', + 'backup.title': 'Varmuuskopio', + 'backup.desc': 'Kopioi sync‑anturin sisältö varmuuskopioanturiin ja palauta tarvittaessa.', + 'backup.now': 'Varmuuskopioi nyt', + 'backup.restore': 'Palauta', + 'backup.delete': 'Poista varmuuskopio', + 'backup.delete.confirm': 'Poistetaanko valittu varmuuskopio?', + 'backup.new': 'Uusi varmuuskopio', + 'backup.auto': 'Automaattinen varmuuskopiointi', + 'backup.auto.desc': 'Tee varmuuskopioita automaattisesti tietyin välein.', + 'backup.interval': 'Väli (päivää)', + 'backup.interval.desc': 'Kuinka monta päivää automaattisten varmuuskopioiden välillä (min. 1).', + 'backup.sensor': 'Varmuuskopioanturi', + 'backup.last': 'Viimeisin varmuuskopio', + 'backup.export.title': 'Vie varmuuskopiotiedosto', + 'backup.export.desc': 'Lataa täydellinen thermostat_timeline_backup.json, jossa on kaikki slotit ja tiedot.', + 'backup.export.btn': 'Lataa tiedosto', + 'backup.file.add_selected': 'Tuo varmuuskopio', + 'backup.file.add_all': 'Tuo kaikki varmuuskopiot', + 'backup.file.loaded_n': 'Tiedosto ladattu ({n} varmuuskopiota)', + 'backup.file.imported_ok': 'Tuotu ({n})', + 'backup.slot.title': 'Valitse varmuuskopio', + 'backup.slot.desc': 'Valitse mikä varmuuskopiointipaikka palautetaan.', + 'backup.slot.empty': 'Ei varmuuskopioita vielä', + 'pause.menu_title': 'Keskeytä automaattinen asetus', + 'pause.off_1h': 'Keskeytä 1 tunniksi', + 'pause.off_5h': 'Keskeytä 5 tunniksi', + 'pause.off_custom': 'Mukautettu kesto…', + 'pause.off_indef': 'Keskeytä kunnes jatkan', + 'pause.resume': 'Jatka nyt', + 'pause.close': 'Sulje', + 'pause.active_until': 'Keskeytetty asti {time}', + 'pause.active_indef': 'Keskeytetty', + 'editor.pausebtn.title': 'Taukopainike', + 'editor.pausebtn.desc': 'Näytä globaali taukopainike kortin yläreunassa. Tauon aikana kaikki set_temperature‑komennot estetään.', + 'editor.integration.connected': 'Integraatio valmis', + 'editor.integration.disconnected': 'Integraatio ei käytettävissä' + , 'profiles.button': 'Manuaalinen aikataulu' + , 'profiles.editor.title': 'Profiilit' + , 'profiles.add': 'Lisää profiili' + , 'profiles.rename': 'Nimeä uudelleen' + , 'profiles.delete': 'Poista' + , 'profiles.activate': 'Aktivoi' + , 'profiles.deactivate': 'Poista käytöstä' + , 'profiles.active': 'Aktiivinen' + , 'profiles.none': 'Ei mikään' + , 'profiles.name_placeholder': 'Profiilin nimi' + , 'profiles.edit': 'Muokkaa' + , 'profiles.preview_note': 'Vain esikatselu. Aikataulut aktivoituvat, kun aktivoit profiilin.' + , 'editor.tabs.holidays': 'Pyhäpäivät' + , 'holidays.enable': 'Ota pyhäpäivät käyttöön' + , 'holidays.desc': 'Käytä erityistä päiväaikataulua pyhäpäivinä.' + , 'holidays.source': 'Pyhäpäivien lähde' + , 'holidays.source.calendar': 'Kalenteri' + , 'holidays.source.manual': 'Manuaaliset päivämäärät' + , 'holidays.entity': 'Pyhäpäivä‑entiteetti' + , 'holidays.entity_desc': 'Valitse kalenteri, joka ilmoittaa vain on/off. Sen tulee olla on pyhäpäivinä ja off muina päivinä.' + , 'holidays.dates': 'Pyhäpäivien päivämäärät' + , 'holidays.add_date': 'Lisää päivämäärä' + , 'holidays.editor.title': 'Pyhäpäivä‑aikataulu' + , 'holidays.edit': 'Muokkaa pyhäpäivä‑aikataulua' + , 'holidays.button': 'Pyhäpäivät' + , 'editor.perroom.title': 'Oletus °C huoneittain' + , 'editor.perroom.desc': 'Näytä oletus °C -kenttä jokaisella rivillä. Kun pois päältä, käytetään globaalia oletus °C:ta kaikissa huoneissa.' + , 'away.resume_schedule': 'Jatka aikataulua' + , 'away.resume_away': 'Jatka poissaololämpötilaa' + , 'presence.button': 'Läsnäoloaikataulut' + , 'presence.editor.title': 'Läsnäoloaikataulut' + , 'presence.enable_advanced': 'Ota käyttöön kehittyneet läsnäoloaikataulut' + , 'presence.enable_advanced.desc': 'Käytä yhdistelmiä ja aikatauluja sen mukaan kuka on kotona/poissa.' + , 'away.delay_enable': 'Viivästytä poissaolo‑toimia' + , 'away.delay_desc': 'Odotetaan ennen poissaolomuutosten soveltamista.' + , 'away.delay_value': 'Viive' + , 'away.delay_unit': 'Yksikkö' + , 'away.delay_unit.seconds': 'Sekunnit' + , 'away.delay_unit.minutes': 'Minuutit' + , 'presence.combos': 'Yhdistelmät' + , 'presence.enable_combo': 'Ota tämä yhdistelmä käyttöön' + , 'presence.all_home': 'Kaikki kotona' + , 'presence.none_home': 'Ei ketään kotona' + , 'presence.home': 'Kotona: {names}' + , 'presence.away': 'Poissa: {names}' + , 'presence.manage': 'Hallinnoi yhdistelmiä' + , 'presence.who_home': 'Kuka on kotona' + , 'presence.who_away': 'Kuka ei ole kotona' + , 'away.enable': 'Ota poissaolotila käyttöön' + , 'away.desc': 'Kun yksikään valituista henkilöistä ei ole kotona, termostaatit lasketaan automaattisesti asetettuun lämpötilaan.' + , 'away.temp': 'Poissaololämpötila' + , 'away.persons': 'Henkilöt' + , 'away.add_person': 'Lisää henkilö' + , 'backup.select': 'Valitse, mitä varmuuskopioidaan' + , 'backup.select.desc': 'Valitut kohteet sisällytetään varmuuskopiointiin/palautukseen' + , 'backup.opt.all': 'Kaikki' + , 'backup.opt.main': 'Pääaikataulut' + , 'backup.opt.weekday': 'Viikonpäiväaikataulut' + , 'backup.opt.presence': 'Läsnäoloaikataulut' + , 'backup.opt.settings': 'Editorin asetukset' + , 'backup.opt.holiday': 'Pyhäpäiväaikataulut' + , 'backup.opt.colors': 'Värilohkot' + , 'editor.tabs.settings': 'Asetukset' + , 'editor.tabs.pause': 'Tauko' + , 'editor.tabs.weekdays': 'Viikonpäivät' + , 'editor.tabs.colors': 'Värit' + , 'editor.tabs.presence_sensor': 'Läsnäoloanturi' + , 'editor.presence_sensor.enable.title': 'Läsnäoloanturi' + , 'editor.presence_sensor.enable.desc': 'Ota käyttöön läsnäoloanturin määritys huoneittain.' + , 'editor.presence_sensor.entity.title': 'Läsnäoloentiteetti' + , 'editor.presence_sensor.entity.desc': 'Valitse binary_sensor, joka ilmaisee läsnäolon tässä huoneessa.' + , 'editor.presence_sensor.on_temp.title': 'Lämpötila, kun läsnäolo on PÄÄLLÄ' + , 'editor.presence_sensor.on_temp.desc': 'Jos sensori on PÄÄLLÄ, tämä huone käyttää tätä lämpötilaa aikataulun sijaan.' + , 'editor.presence_sensor.on_delay.title': 'Viive ennen läsnäolon käyttöä' + , 'editor.presence_sensor.on_delay.desc': 'Kuinka kauan sensorin täytyy olla PÄÄLLÄ ennen kuin läsnäololämpötila otetaan käyttöön. Arvo käyttää valittua yksikköä.' + , 'editor.presence_sensor.off_delay.title': 'Viive ennen aikatauluun palaamista' + , 'editor.presence_sensor.off_delay.desc': 'Kuinka kauan sensorin täytyy olla POIS PÄÄLTÄ ennen kuin aikataulu palautuu. Arvo käyttää valittua yksikköä.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Poissa kotoa' + , 'editor.tabs.reset': 'Palauta' + , 'editor.tabs.boiler': 'Kattilan ohjaus' + , 'boiler.switch': 'Kattilakytkin' + , 'boiler.switch_type': 'Entiteettityyppi' + , 'boiler.switch_type.switch': 'Kytkin' + , 'boiler.switch_type.input_boolean': 'Input boolean' + , 'boiler.rooms': 'Mukana olevat huoneet' + , 'boiler.rooms.desc': 'Valitse, mitkä huoneet otetaan mukaan kattilan ohjaukseen.' + , 'boiler.multi_enable': 'Ota käyttöön useita kattiloita' + , 'boiler.multi_enable.desc': 'Määritä huoneet eri kattiloille ja aseta offsetit huonekohtaisesti.' + , 'boiler.multi_rooms': 'Kattila per huone' + , 'boiler.multi_rooms.desc': 'Valitse kattilakytkin ja offsetit jokaiselle huoneelle.' + , 'boiler.room.enable': 'Sisällytä huone' + , 'boiler.room.enable.desc': 'Sisällytä tämä huone kattilan ohjaukseen.' + , 'boiler.enable': 'Ota kattilan ohjaus käyttöön' + , 'boiler.enable.desc': 'Näytä kattila-asetukset tämän kortin editorissa.' + , 'boiler.temp_sensor': 'Kattilan lämpötila‑anturi' + , 'boiler.temp_sensor.desc': 'Lämpötila‑anturi, joka ohjaa kattilakytkintä.' + , 'boiler.min_temp': 'Kattilan minimi (°C)' + , 'boiler.max_temp': 'Kattilan maksimi (°C)' + , 'boiler.offsets': 'Offsetit' + , 'boiler.offsets.desc': 'ON‑offset: kuinka paljon alle aikataulun tavoitteen ennen kuin kytkeytyy päälle. OFF‑offset: kuinka paljon yli aikataulun tavoitteen ennen kuin kytkeytyy pois.' + , 'boiler.on_offset': 'ON offset (°C)' + , 'boiler.off_offset': 'OFF offset (°C)' + , 'editor.temp_sensor': 'Lämpötila‑anturi' + , 'presence.live_header': 'Näytä live‑läsnäolo yläreunassa' + , 'presence.live_header.desc': 'Siruja näytetään oikeassa kulmassa.' + , 'profiles.enable': 'Ota profiilit käyttöön (ohitus)' + , 'profiles.enable.desc': 'Salli nimettyjä päiväaikatauluja, jotka voit aktivoida ohittaaksesi pääsuunnitelman.' + , 'season.enable.title': 'Ota käyttöön kesä-/talviaikataulut' + , 'season.enable.desc': 'Kun käytössä, jokaisella aikataululla on erillinen Kesä- ja Talvi-versio, joiden välillä voit vaihtaa.' + , 'season.mode.title': 'Kausi' + , 'season.mode.winter': 'Talvi' + , 'season.mode.summer': 'Kesä' + , 'profiles.active_label': 'Manuaalinen profiili aktiivinen:' + , 'profiles.save_new': 'Tallenna uusi profiili' + , 'profiles.save_existing': 'Tallenna muutokset valittuun profiiliin' + , 'week.view.title': 'Viikkonäkymä' + , 'week.view.rooms_one_day': 'Kaikki huoneet • yksi päivä' + , 'week.view.days_one_room': 'Kaikki päivät • yksi huone' + , 'week.view_switch.title': 'Vaihda näkymää aikajanasta' + , 'week.view_switch.desc': 'Näyttää valitsimen aikajanan otsikossa, jolla voi vaihtaa kahden viikonpäivänäkymän välillä.' + , 'week.copy_from_main.title': 'Kopioi pääaikataulu' + , 'week.copy_from_main.desc': 'Otit viikonpäivät käyttöön. Haluatko kopioida pääaikataulun viikonpäiviin? Valitse tila, johon kopioidaan.' + , 'week.copy_from_main.mode_label': 'Viikonpäivätila' + , 'week.copy_from_main.target_label': 'Kopioi kohteeseen' + , 'week.copy_from_main.copy_btn': 'Kopioi' + , 'week.copy_from_main.skip_btn': 'Älä kopioi' + , 'week.title': 'Viikonpäivät' + , 'week.select_room': 'Valitse huone' + , 'err.config_entities': "Määrityksessä täytyy olla 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Videota ei löytynyt' + , 'onboard.image_not_found': 'Kuvaa ei löytynyt' + , 'pause.prompt_minutes': 'Anna kesto (minuuttia):' + , 'editor.test_tool': 'Testityökalu' + , 'editor.test_select_entity': 'Valitse entiteetti' + , 'editor.test_empty': 'Ei mitään vietävää' + , 'editor.test_export': 'Vie' + , 'editor.test_no_fields': 'Ei kenttiä näytettäväksi' + , 'editor.test_select_placeholder': 'Valitse...' + , 'editor.error_prefix': 'Virhe: ' + , 'backup.restore.confirm_overwrite': 'Palautetaanko varmuuskopiosta? Tämä korvaa nykyiset aikataulut ja asetukset.' + , 'backup.import.err_no_hass': 'Home Assistant ei ole valmis' + , 'backup.import.err_storage_off': 'Jaettu tallennus on POIS PÄÄLTÄ. Ota se ensin käyttöön.' + , 'backup.import.err_no_integration': 'Integraatio thermostat_timeline ei ole käytettävissä' + , 'backup.import.err_json': 'Virheellinen JSON' + , 'profiles.name_exists': 'Nimi on jo olemassa' + }, + cs: { + // Karta + 'card.title_default': 'Časová osa termostatu', + // Modal / editor (inline) + 'ui.edit_block': 'Upravit blok', + 'ui.add_block': 'Přidat blok', + 'ui.from': 'Od', + 'ui.to': 'Do', + 'ui.temperature': 'Teplota', + 'ui.delete': 'Smazat', + 'ui.cancel': 'Zrušit', + 'ui.close': 'Zavřít', + 'ui.unsaved_confirm': 'Máte neuložené změny. Opravdu zavřít?', + 'ui.discard_changes': 'Zahodit změny', + 'ui.keep_editing': 'Pokračovat v úpravách', + 'ui.save': 'Uložit', + 'ui.error_prefix': 'Chyba: ', + 'ui.overlap_msg': 'Vybrali jste překrývající se časy — překrývá se mezi {start} – {end}.', + 'ui.auto_fix': 'Opravit automaticky', + 'ui.fix_start_to': 'Upravit začátek na {time}', + 'ui.fix_end_to': 'Upravit konec na {time}', + // Inline meta + 'ui.current_temp': 'Aktuální teplota:', + + // GUI editor panel + 'editor.title_label': 'Nadpis', + 'editor.add_entity': 'Přidat místnost', + 'editor.entity_placeholder': 'Vyberte entitu', + 'editor.room_mode.title': 'Typ entity místnosti', + 'editor.room_mode.desc': 'Když je zapnuto, vyberte input_number (cílová teplota). Když je vypnuto, vyberte entitu climate.', + + 'editor.room_temp_sensor_override.title': 'Použít senzor teploty v místnosti', + 'editor.room_temp_sensor_override.desc': 'Je-li zapnuto, zobrazená teplota v místnosti (a logika kotle) používá vybraný teplotní senzor místo climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Senzor teploty', + 'editor.drag_reorder': 'Přetažením změňte pořadí', + 'editor.remove': 'Odebrat', + 'editor.default_c': 'Výchozí °C', + 'editor.row_height_px': 'Výška řádku (px)', + 'editor.max_c': 'Max °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Automatické použití', + 'editor.auto_apply.desc': 'Je-li zapnuto, karta se pokusí nastavit teplotu vybraných climate entit tak, aby odpovídala plánované hodnotě pro „právě teď“.', + 'editor.apply_edit.title': 'Při úpravě', + 'editor.apply_edit.desc': 'Když změníte bloky v časové ose a změna ovlivní aktuální čas, nová teplota se použije okamžitě.', + 'editor.apply_default.title': 'Při změně Výchozí °C', + 'editor.apply_default.desc': 'Když se změní „Výchozí °C“ a ovlivní aktuální období, nová teplota se použije okamžitě.', + + 'editor.show_room_temp.title': 'Zobrazit teplotu v místnosti', + 'editor.show_room_temp.desc': 'Zobrazí bublinu s aktuální teplotou v místnosti na každém řádku v záhlaví časové osy.', + 'ui.copy_plan': 'Kopírovat plán', + 'ui.paste_plan': 'Vložit plán', + 'ui.copy_day': 'Kopírovat den', + 'ui.paste_day': 'Vložit den', + 'editor.merge_label': 'Sloučit s (přidat další termostat)', + 'editor.display_name_optional': 'Název místnosti (volitelné)', + 'editor.merged_with': 'Sloučeno s', + 'editor.store_enable.title': 'Sdílené úložiště + řízení na pozadí', + 'editor.store_enable.short': 'Ukládá rozvrhy do zvoleného senzoru a umožňuje řízení na pozadí.', + 'editor.store_enable.desc': 'Ukládá rozvrhy do zvoleného senzoru a zapíná set_temperature na pozadí přes integraci Thermostat Timeline. Termostaty se aktualizují i když karta není otevřená. Také sdílí rozvrhy (tepelné bloky) a nastavení karty mezi uživateli/zařízeními. Když je vypnuto, příkazy se posílají jen když je karta aktivně viditelná na zařízení (tablet/PC/mobil) — musí zůstat otevřená na obrazovce.', + 'editor.store_missing': 'Integrace „Thermostat Pro Timeline Sync“ není nainstalována. Sdílené úložiště není k dispozici.', + 'editor.clear_store': 'Vymazat úložiště', + 'editor.clear_store_confirm': 'Tímto smažete všechny uložené rozvrhy. Pokračovat?', + 'editor.migrate_to_store': 'Převést data do úložiště', + 'editor.migrate_confirm': 'Převést vaše rozvrhy uložené v prohlížeči do zvoleného senzoru? Tím se přepíší existující rozvrhy v úložišti.', + 'editor.migrate_pull_confirm': 'Importovat rozvrhy z úložiště a přepsat lokální data v prohlížeči?', + 'editor.select_storage_first': 'Nejprve vyberte senzor úložiště.', + 'editor.no_local_data': 'Nebyla nalezena žádná lokální data v prohlížeči.', + 'editor.clear_all': 'Smazat všechna data', + 'editor.clear_all_confirm': 'Tímto smažete rozvrhy jak v úložišti, tak v lokálním prohlížeči. Pokračovat?', + 'editor.clear_storage_only': 'Smazat jen data v úložišti', + 'editor.clear_storage_only_confirm': 'Tímto smažete rozvrhy z vybraného senzoru úložiště. Pokračovat?', + 'editor.clear_local_only': 'Smazat jen lokální data', + 'editor.clear_local_only_confirm': 'Tímto smažete pouze rozvrhy uložené ve vašem prohlížeči. Pokračovat?', + 'editor.reset.desc': 'Co to dělá:\n\n• Smazat všechna data: Resetuje backend úložiště (.storage) pro Thermostat Timeline a smaže VŠECHNY lokální cache/návrhy v tomto prohlížeči.\n• Smazat jen lokální data: Smaže pouze data uložená v tomto prohlížeči (localStorage).\n\nTip: Po plném resetu stránku obnovte, aby se znovu neobjevily staré návrhy.', + 'editor.temp_sensor': 'Teplotní senzor', + + // Týdny + 'week.enable': 'Povolit pracovní dny', + 'week.mode': 'Režim dní v týdnu', + // removed same_all option from UI + 'week.mode.weekday_weekend': 'Pracovní dny + Víkend', + 'week.mode.weekday_sat_sun': 'Pracovní dny + Sobota + Neděle', + 'week.mode.all_7': 'Všechny dny', + 'week.button': 'Dny v týdnu', + 'week.group.weekdays': 'Pracovní dny', + 'week.group.weekend': 'Víkend', + 'week.days_full.mon': 'Pondělí', + 'week.days_full.tue': 'Úterý', + 'week.days_full.wed': 'Středa', + 'week.days_full.thu': 'Čtvrtek', + 'week.days_full.fri': 'Pátek', + 'week.days_full.sat': 'Sobota', + 'week.days_full.sun': 'Neděle', + 'week.days.mon': 'Po', + 'week.days.tue': 'Út', + 'week.days.wed': 'St', + 'week.days.thu': 'Čt', + 'week.days.fri': 'Pá', + 'week.days.sat': 'So', + 'week.days.sun': 'Ne', + 'week.editor.title': 'Týdenní rozvrh', + 'week.editor.preview_note': 'Pouze náhled. Změny se použijí po stisknutí Uložit.', + 'week.open_editor.desc': 'Otevřít editor dnů v týdnu.', + 'week.open_editor.btn': 'Otevřít', + 'week.copy_to_rooms': 'Kopírovat týden do místností', + 'week.copy_to_rooms.title': 'Kopírovat týden do místností', + 'week.copy_to_rooms.select': 'Vyberte místnosti', + 'week.copy_to_rooms.apply': 'Vložit do místností', + 'week.enable.desc': 'Povolit plán podle dní v týdnu. Když je vypnuto, používá se stejný plán každý den.', + + // Formáty + 'editor.timefmt.title': 'Formát času', + 'editor.timefmt.desc': 'Zvolte 24hodinový nebo 12hodinový (AM/PM) formát pro časy na kartě.', + 'editor.tempfmt.title': 'Jednotka teploty', + 'editor.tempfmt.desc': 'Zvolte Celsia (°C) nebo Fahrenheit (°F) pro zobrazení na kartě a v editorech.', + 'editor.timesrc.title': 'Zdroj času', + 'editor.timesrc.desc': 'Zvolte, zda má karta používat čas prohlížeče nebo časové pásmo Home Assistantu pro „nyní“.', + 'editor.timesrc.browser': 'Prohlížeč', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'ID konfigurace', + 'editor.instance.desc': 'Udržujte více nezávislých plánů/nastavení pod různými ID.', + 'editor.instance.new_id': 'Nové ID', + + // Záložky + 'editor.tabs.settings': 'Nastavení', + 'editor.tabs.pause': 'Pauza', + 'editor.tabs.rooms': 'Místnosti', + 'editor.tabs.boiler': 'Ovládání kotle', + 'boiler.switch': 'Spínač kotle', + 'boiler.switch_type': 'Typ entity', + 'boiler.switch_type.switch': 'Spínač', + 'boiler.switch_type.input_boolean': 'Vstupní boolean', + 'boiler.rooms': 'Zahrnuté místnosti', + 'boiler.rooms.desc': 'Vyberte, které místnosti mají být zahrnuty do ovládání kotle.', + 'boiler.multi_enable': 'Povolit více kotlů', + 'boiler.multi_enable.desc': 'Přiřaďte místnosti různým kotlům a nastavte odchylky pro každou místnost.', + 'boiler.multi_rooms': 'Kotel pro místnost', + 'boiler.multi_rooms.desc': 'Vyberte spínač kotle a odchylky pro každou místnost.', + 'boiler.room.enable': 'Zahrnout místnost', + 'boiler.room.enable.desc': 'Zahrnout tuto místnost do řízení kotle.', + 'editor.tabs.presence_sensor': 'Senzor přítomnosti', + 'boiler.enable': 'Povolit ovládání kotle', + 'boiler.enable.desc': 'Zobrazit nastavení kotle v editoru této karty.', + 'boiler.temp_sensor': 'Teplotní senzor kotle', + 'boiler.temp_sensor.desc': 'Teplotní senzor používaný k ovládání spínače kotle.', + + 'editor.presence_sensor.enable.title': 'Senzor přítomnosti', + 'editor.presence_sensor.enable.desc': 'Povolit konfiguraci senzoru přítomnosti pro jednotlivé místnosti.', + + 'editor.presence_sensor.entity.title': 'Entita přítomnosti', + 'editor.presence_sensor.entity.desc': 'Vyberte binary_sensor, který indikuje přítomnost v této místnosti.', + 'editor.presence_sensor.on_temp.title': 'Teplota při přítomnosti (ZAP)', + 'editor.presence_sensor.on_temp.desc': 'Pokud je senzor ZAP, tato místnost použije tuto teplotu místo rozvrhu.', + 'editor.presence_sensor.on_delay.title': 'Zpoždění před použitím přítomnosti', + 'editor.presence_sensor.on_delay.desc': 'Jak dlouho musí být senzor ZAP, než se použije teplota přítomnosti. Hodnota používá vybranou jednotku.', + 'editor.presence_sensor.off_delay.title': 'Zpoždění před návratem na rozvrh', + 'editor.presence_sensor.off_delay.desc': 'Jak dlouho musí být senzor VYP, než se obnoví rozvrh. Hodnota používá vybranou jednotku.', + 'boiler.min_temp': 'Min. teplota kotle (°C)', + 'boiler.max_temp': 'Max. teplota kotle (°C)', + 'boiler.offsets': 'Offsety', + 'boiler.offsets.desc': 'ON offset: o kolik pod plánovanou teplotu před zapnutím. OFF offset: o kolik nad plánovanou teplotu před vypnutím.', + 'boiler.on_offset': 'ON offset (°C)', + 'boiler.off_offset': 'OFF offset (°C)', + 'editor.test_tool': 'Testovací nástroj', + 'editor.test_select_entity': 'Vybrat entitu', + 'editor.test_empty': 'Vyberte entitu pro zobrazení dostupných příkazů…', + 'editor.test_export': 'Exportovat .md', + 'editor.test_no_fields': 'Žádná pole', + 'editor.test_select_placeholder': '— Vybrat —', + 'editor.error_prefix': 'Chyba: ', + 'editor.open_in_hacs': 'Otevřít v HACS', + 'editor.tabs.colors': 'Barvy', + 'editor.tabs.owd': 'Open Window Detection', + 'owd.enable': 'Open Window Detection', + 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.', + 'owd.sensors': 'Window/Door sensors', + 'owd.delays.title': 'Delays', + 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).', + 'owd.open_delay': 'After open (min)', + 'owd.close_delay': 'After closed (min)', + 'editor.tabs.away': 'Mimo domov', + 'editor.tabs.reset': 'Obnovit', + 'editor.tabs.sync': 'Sync engine', + 'sync.running': 'Probíhá synchronizace…', + 'sync.completed': 'Synchronizace dokončena', + 'editor.tabs.backup': 'Záloha', + 'backup.title': 'Záloha', + 'backup.desc': 'Zkopíruje obsah vašeho sync senzoru do záložního senzoru a v případě potřeby obnoví.', + 'backup.now': 'Zálohovat nyní', + 'backup.restore': 'Obnovit', + 'backup.delete': 'Smazat zálohu', + 'backup.delete.confirm': 'Smazat vybranou zálohu?', + 'backup.new': 'Nová záloha', + 'backup.auto': 'Automatická záloha', + 'backup.auto.desc': 'Provádět automatické zálohy v intervalech.', + 'backup.interval': 'Interval (dny)', + 'backup.interval.desc': 'Počet dnů mezi automatickými zálohami (min. 1).', + 'backup.sensor': 'Záložní senzor', + 'backup.last': 'Poslední záloha', + 'backup.export.title': 'Exportovat soubor zálohy', + 'backup.export.desc': 'Stáhnout kompletní thermostat_timeline_backup.json se všemi sloty a daty.', + 'backup.export.btn': 'Stáhnout soubor', + 'backup.file.add_selected': 'Importovat zálohu', + 'backup.file.add_all': 'Importovat všechny zálohy', + 'backup.file.loaded_n': 'Soubor načten ({n} záloh)', + 'backup.file.imported_ok': 'Importováno ({n})', + 'backup.slot.title': 'Vybrat zálohu', + 'backup.slot.desc': 'Vyberte, kterou zálohu obnovit.', + 'backup.slot.empty': 'Zatím žádné zálohy', + + // Away + 'away.enable': 'Povolit režim mimo domov', + 'away.desc': 'Když žádní z vybraných lidí nejsou doma, automaticky snižuje teploty na nastavenou hodnotu.', + 'away.temp': 'Teplota mimo domov', + 'away.persons': 'Osoby', + 'away.add_person': 'Přidat osobu', + + // Barvy tepla + 'editor.heat_colors.title': 'Barvy tepelných bloků', + 'editor.heat_colors.add': 'Přidat rozsah barev', + 'editor.colors.col_from': 'Od °C', + 'editor.colors.col_to': 'Do °C', + 'editor.colors.col_color': 'Barva', + 'editor.colors.empty': 'Zatím nebyly přidány žádné místnosti. Přidejte místnost pro definici barev.', + 'editor.colors.go_add': 'Přidat místnost', + 'editor.select_entities': 'Místnosti', + 'editor.colors.refresh': 'Aktualizovat rozvrh', + 'editor.colors.clear_local': 'Smazat lokální barevná data', + 'editor.colors.clear_local_confirm': 'Tímto smažete barevné rozsahy uložené ve vašem prohlížeči pro tuto kartu. Pokračovat?', + 'editor.colors.clear_shared': 'Smazat sdílené barevné úložiště', + 'editor.colors.clear_shared_confirm': 'Tímto smažete barevné rozsahy ve vybraném senzoru úložiště. Pokračovat?', + 'editor.colors.clear_all': 'Smazat všechna barevná data', + 'editor.colors.clear_all_confirm': 'Tímto smažete všechny barevné rozsahy jak lokálně, tak ve vybraném senzoru úložiště. Pokračovat?', + 'editor.colors.mode': 'Režim barev', + 'editor.colors.mode.desc': 'Zvolte barvy pro každou místnost nebo jeden set pro všechny.', + 'editor.colors.mode_per_room': 'Podle místnosti', + 'editor.colors.mode_global': 'Všechny místnosti', + 'editor.colors.global_title': 'Všechny místnosti', + 'editor.reload_editor': 'Znovu načíst editor', + 'editor.reload_dashboard': 'Znovu načíst dashboard', + + // Onboarding + 'onboard.title': 'Vítejte v Časové ose termostatu', + 'onboard.next': 'Další', + 'onboard.prev': 'Zpět', + 'onboard.skip': 'Přeskočit', + 'onboard.done': 'Hotovo', + 'onboard.dont_show': 'Nezobrazovat znovu', + 'onboard.step_main': 'Toto je hlavní časová osa. Zobrazuje požadované teploty během 24 hodin pro každou místnost.', + 'onboard.step_add': 'Přidejte tepelný blok přes + Přidat blok nebo dvojitým kliknutím na časovou osu.', + 'onboard.step_edit': 'Upravte nebo smažte blok dvojitým kliknutím. Použijte Dny v týdnu pro různé dny.', + 'onboard.reset': 'Zobrazit průvodce znovu', + 'onboard.reset_ok': 'Vítání se zobrazí příště.', + + // Pause + 'pause.menu_title': 'Pozastavit auto‑apply', + 'pause.off_1h': 'Pozastavit na 1 hodinu', + 'pause.off_5h': 'Pozastavit na 5 hodin', + 'pause.off_custom': 'Vlastní délka…', + 'pause.off_indef': 'Pozastavit do obnovení', + 'pause.resume': 'Obnovit nyní', + 'pause.close': 'Zavřít', + 'pause.active_until': 'Pozastaveno do {time}', + 'pause.active_indef': 'Pozastaveno', + 'editor.pausebtn.title': 'Tlačítko pozastavení', + 'editor.pausebtn.desc': 'Zobrazí globální tlačítko pozastavení v záhlaví karty. Během pauzy jsou všechny příkazy set_temperature potlačeny.', + 'editor.integration.connected': 'Integrace připravena', + 'editor.integration.disconnected': 'Integrace není k dispozici', + + // Profily + 'profiles.enable': 'Povolit profily (přepis)', + 'profiles.enable.desc': 'Povolit pojmenované denní rozvrhy, které můžete aktivovat pro přepsání hlavního plánu.', + 'season.enable.title': 'Povolit letní/zimní rozvrhy', + 'season.enable.desc': 'Když je povoleno, každý rozvrh má verzi Léto a Zima, mezi kterými můžete přepínat.', + 'season.mode.title': 'Sezóna', + 'season.mode.winter': 'Zima', + 'season.mode.summer': 'Léto', + 'profiles.button': 'Manuální plán', + 'profiles.editor.title': 'Profily', + 'profiles.add': 'Přidat profil', + 'profiles.rename': 'Přejmenovat', + 'profiles.delete': 'Smazat', + 'profiles.activate': 'Aktivovat', + 'profiles.deactivate': 'Deaktivovat', + 'profiles.active': 'Aktivní', + 'profiles.none': 'Žádný', + 'profiles.name_placeholder': 'Název profilu', + 'profiles.edit': 'Upravit', + 'profiles.preview_note': 'Pouze náhled. Rozvrhy se stanou aktivními po aktivaci profilu.', + 'profiles.active_label': 'Manuální profil aktivní:', + 'profiles.save_new': 'Uložit nový profil', + 'profiles.save_existing': 'Uložit změny do vybraného profilu', + + // Svátky + 'editor.tabs.holidays': 'Svátky', + 'holidays.enable': 'Povolit svátky', + 'holidays.desc': 'Použít speciální denní rozvrh o svátcích.', + 'holidays.source': 'Zdroj svátků', + 'holidays.source.calendar': 'Kalendář', + 'holidays.source.manual': 'Manuální data', + 'holidays.entity': 'Entita svátků', + 'holidays.entity_desc': 'Vyberte kalendář, který hlásí pouze on/off. Má být on ve svátky a off v ostatní dny.', + 'holidays.dates': 'Data svátků', + 'holidays.add_date': 'Přidat datum', + 'holidays.editor.title': 'Rozvrh o svátcích', + 'holidays.edit': 'Upravit rozvrh o svátcích', + 'holidays.button': 'Svátky', + 'editor.perroom.title': 'Výchozí °C pro místnost', + 'editor.perroom.desc': 'Zobrazit pole výchozí °C v každém řádku. Při vypnutí se použije globální výchozí °C pro všechny místnosti.', + 'away.resume_schedule': 'Obnovit plán', + 'away.resume_away': 'Obnovit teplotu mimo domov', + 'presence.button': 'Plány přítomnosti', + 'presence.editor.title': 'Plány přítomnosti', + 'presence.enable_advanced': 'Povolit pokročilé rozvrhy přítomnosti', + 'presence.enable_advanced.desc': 'Použijte kombinace a rozvrhy podle toho, kdo je doma/pryč.', + 'away.delay_enable': 'Zpozdit akce Nepřítomnosti', + 'away.delay_desc': 'Před použitím změn nepřítomnosti vyčkejte.', + 'away.delay_value': 'Zpoždění', + 'away.delay_unit': 'Jednotka', + 'away.delay_unit.seconds': 'Sekundy', + 'away.delay_unit.minutes': 'Minuty', + 'presence.combos': 'Kombinace', + 'presence.enable_combo': 'Povolit tuto kombinaci', + 'presence.all_home': 'Všichni doma', + 'presence.none_home': 'Nikdo doma', + 'presence.home': 'Doma: {names}', + 'presence.away': 'Mimo domov: {names}', + 'presence.manage': 'Spravovat kombinace', + 'presence.who_home': 'Kdo je doma', + 'presence.who_away': 'Kdo není doma' + , 'backup.select': 'Vyberte, co zálohovat' + , 'backup.select.desc': 'Zaškrtnuté položky jsou zahrnuty do zálohy/obnovy' + , 'backup.opt.all': 'Vše' + , 'backup.opt.main': 'Hlavní rozvrhy' + , 'backup.opt.weekday': 'Rozvrhy podle dní' + , 'backup.opt.presence': 'Plány přítomnosti' + , 'backup.opt.settings': 'Nastavení editoru' + , 'backup.opt.holiday': 'Sváteční rozvrhy' + , 'backup.opt.colors': 'Barevné bloky' + , 'editor.tabs.weekdays': 'Dny v týdnu' + , 'presence.live_header': 'Zobrazit živou přítomnost v záhlaví' + , 'presence.live_header.desc': 'Štítky jsou zobrazeny v pravém rohu.' + , 'week.view.title': 'Pohled na týden' + , 'week.view.rooms_one_day': 'Všechny místnosti • jeden den' + , 'week.view.days_one_room': 'Všechny dny • jedna místnost' + , 'week.view_switch.title': 'Přepnout zobrazení z časové osy' + , 'week.view_switch.desc': 'Zobrazí přepínač v záhlaví časové osy pro přepínání mezi dvěma režimy zobrazení dnů v týdnu.' + , 'week.copy_from_main.title': 'Kopírovat hlavní rozvrh' + , 'week.copy_from_main.desc': 'Zapnuli jste dny v týdnu. Chcete zkopírovat hlavní rozvrh do dnů v týdnu? Vyberte režim, do kterého se kopíruje.' + , 'week.copy_from_main.mode_label': 'Režim dnů v týdnu' + , 'week.copy_from_main.target_label': 'Kopírovat do' + , 'week.copy_from_main.copy_btn': 'Kopírovat' + , 'week.copy_from_main.skip_btn': 'Nekopírovat' + , 'week.title': 'Dny v týdnu' + , 'week.select_room': 'Vyberte místnost' + , 'err.config_entities': "Konfigurace musí obsahovat 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Video nebylo nalezeno' + , 'onboard.image_not_found': 'Obrázek nebyl nalezen' + , 'pause.prompt_minutes': 'Zadejte dobu trvání (minuty):' + , 'editor.test_tool': 'Testovací nástroj' + , 'editor.test_select_entity': 'Vyberte entitu' + , 'editor.test_empty': 'Nic k exportu' + , 'editor.test_export': 'Exportovat' + , 'editor.test_no_fields': 'Žádná pole k zobrazení' + , 'editor.test_select_placeholder': 'Vybrat...' + , 'editor.error_prefix': 'Chyba: ' + , 'backup.restore.confirm_overwrite': 'Obnovit ze zálohy? Tím se přepíší aktuální plány a nastavení.' + , 'backup.import.err_no_hass': 'Home Assistant není připraven' + , 'backup.import.err_storage_off': 'Sdílené úložiště je VYPNUTÉ. Nejprve jej zapněte.' + , 'backup.import.err_no_integration': 'Integrace thermostat_timeline není dostupná' + , 'backup.import.err_json': 'Neplatný JSON' + , 'profiles.name_exists': 'Název již existuje' + }, + sl: { + 'card.title_default': 'Časovnica termostata', + 'ui.edit_block': 'Uredi blok', + 'ui.add_block': 'Dodaj blok', + 'ui.from': 'Od', + 'ui.to': 'Do', + 'ui.temperature': 'Temperatura', + 'ui.delete': 'Izbriši', + 'ui.cancel': 'Prekliči', + 'ui.close': 'Zapri', + 'ui.unsaved_confirm': 'Imate neshranjene spremembe. Želite vseeno zapreti?', + 'ui.discard_changes': 'Zavrzi spremembe', + 'ui.keep_editing': 'Nadaljuj z urejanjem', + 'ui.save': 'Shrani', + 'ui.error_prefix': 'Napaka: ', + 'ui.overlap_msg': 'Izbrali ste prekrivajoče se čase — prekrivanje med {start} in {end}.', + 'ui.auto_fix': 'Samodejno popravi', + 'ui.fix_start_to': 'Nastavi začetek na {time}', + 'ui.fix_end_to': 'Nastavi konec na {time}', + // Inline meta + 'ui.current_temp': 'Trenutna temperatura:', + + 'editor.title_label': 'Naslov', + 'editor.add_entity': 'Dodaj prostor', + 'editor.entity_placeholder': 'Izberite entiteto', + 'editor.room_mode.title': 'Vrsta entitete prostora', + 'editor.room_mode.desc': 'Ko je omogočeno, izberite input_number (ciljna temperatura). Ko je onemogočeno, izberite climate entiteto.', + + 'editor.room_temp_sensor_override.title': 'Uporabi tipalo temperature prostora', + 'editor.room_temp_sensor_override.desc': 'Ko je omogočeno, prikazana temperatura prostora (in logika kotla) uporablja izbrano tipalo temperature namesto climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Tipalo temperature', + 'editor.drag_reorder': 'Povleci za spremembo vrstnega reda', + 'editor.remove': 'Odstrani', + 'editor.default_c': 'Privzeta °C', + 'editor.perroom.title': 'Privzeta °C po prostoru', + 'editor.perroom.desc': 'Prikaže polje Privzeta °C v vsaki vrstici. Ko je izklopljeno, se globalna Privzeta °C uporabi za vse prostore.', + 'editor.row_height_px': 'Višina vrstice (px)', + 'editor.max_c': 'Največ °C', + 'editor.min_c': 'Najmanj °C', + 'editor.auto_apply.title': 'Samodejna uporaba', + 'editor.auto_apply.desc': 'Ko je omogočeno, kartica poskuša nastaviti temperaturo na izbranih climate entitetah tako, da ustreza načrtovani vrednosti za "trenutno".', + 'editor.apply_edit.title': 'Ob urejanju', + 'editor.apply_edit.desc': 'Ko spremenite bloke na časovnici in sprememba vpliva na trenutni čas, se nova temperatura uporabi takoj.', + 'editor.apply_default.title': 'Ob spremembi privzete °C', + 'editor.apply_default.desc': 'Ko se "Privzeta °C" spremeni in to vpliva na trenutno obdobje, se nova temperatura uporabi takoj.', + + 'editor.show_room_temp.title': 'Prikaži temperaturo prostora', + 'editor.show_room_temp.desc': 'Prikaže mehurček s trenutno temperaturo prostora na vsaki vrstici v glavi časovnice.', + 'ui.copy_plan': 'Kopiraj načrt', + 'ui.paste_plan': 'Prilepi načrt', + 'ui.copy_day': 'Kopiraj dan', + 'ui.paste_day': 'Prilepi dan', + 'editor.merge_label': 'Združi z (dodaj dodatni termostat)', + 'editor.display_name_optional': 'Ime prostora (neobvezno)', + 'editor.merged_with': 'Združeno z', + 'editor.store_enable.title': 'Deljena shramba + ozadni nadzor', + 'editor.store_enable.short': 'Shrani urnike v izbrani senzor in omogoči nadzor v ozadju.', + 'editor.store_enable.desc': 'Shrani urnike v izbrani senzor in omogoči ozadnje ukaze set_temperature prek integracije Thermostat Timeline. Termostati se posodobijo tudi, ko kartica ni odprta. Prav tako deli urnike (toplotne bloke) in nastavitve kartice med uporabniki/napravami. Ko je izklopljeno, se ukazi pošiljajo le, ko je kartica vidna na napravi (tablica/PC/mobilnik) in mora ostati odprta.', + 'editor.store_missing': 'Integracija "Thermostat Pro Timeline Sync" ni nameščena. Deljena shramba ni na voljo.', + 'editor.clear_store': 'Počisti shrambo', + 'editor.clear_store_confirm': 'To bo izbrisalo vse shranjene urnike. Nadaljujem?', + 'editor.migrate_to_store': 'Prenesi podatke v shrambo', + 'editor.migrate_confirm': 'Prenesem urnike, shranjene v brskalniku, v izbrani senzor? To bo prepisalo obstoječe urnike v shrambi.', + 'editor.migrate_pull_confirm': 'Uvozim urnike iz shrambe in prepišem lokalne podatke v brskalniku?', + 'editor.select_storage_first': 'Najprej izberite senzor shrambe.', + 'editor.no_local_data': 'Lokalnih podatkov v brskalniku ni.', + 'editor.clear_all': 'Počisti vse podatke', + 'editor.clear_all_confirm': 'To bo izbrisalo urnike iz shrambe in lokalnega brskalnika. Nadaljujem?', + 'editor.clear_storage_only': 'Počisti samo shrambo', + 'editor.clear_storage_only_confirm': 'To bo izbrisalo urnike iz izbranega senzora shrambe. Nadaljujem?', + 'editor.clear_local_only': 'Počisti samo lokalne podatke', + 'editor.clear_local_only_confirm': 'To bo izbrisalo le urnike, shranjene v vašem brskalniku. Nadaljujem?', + 'editor.reset.desc': 'Kaj to naredi:\n\n• Počisti vse podatke: Ponastavi backend shrambo (.storage) za Thermostat Timeline in izbriše VSE lokalne predpomnilnike/osnutke v tem brskalniku.\n• Počisti samo lokalne podatke: Počisti samo podatke, shranjene v tem brskalniku (localStorage).\n\nNamig: Po popolni ponastavitvi osveži stran, da se stari osnutki ne pojavijo znova.', + 'editor.temp_sensor': 'Senzor temperature', + 'editor.test_tool': 'Testno orodje', + 'editor.test_select_entity': 'Izberite entiteto', + 'editor.test_empty': 'Izberite entiteto za prikaz razpoložljivih ukazov…', + 'editor.test_export': 'Izvozi .md', + 'editor.test_no_fields': 'Ni polj', + 'editor.test_select_placeholder': '— Izberi —', + 'editor.error_prefix': 'Napaka: ', + 'editor.open_in_hacs': 'Odpri v HACS' + , 'week.enable': 'Omogoči dneve v tednu' + , 'week.mode': 'Način tedna' + , 'week.mode.weekday_weekend': 'Delavnik + Vikend' + , 'week.mode.weekday_sat_sun': 'Delavnik + Sobota + Nedelja' + , 'week.mode.all_7': 'Vsi dnevi' + , 'week.button': 'Dnevi v tednu' + , 'week.group.weekdays': 'Delavniki' + , 'week.group.weekend': 'Vikend' + , 'week.days_full.mon': 'Ponedeljek' + , 'week.days_full.tue': 'Torek' + , 'week.days_full.wed': 'Sreda' + , 'week.days_full.thu': 'Četrtek' + , 'week.days_full.fri': 'Petek' + , 'week.days_full.sat': 'Sobota' + , 'week.days_full.sun': 'Nedelja' + , 'week.days.mon': 'Pon' + , 'week.days.tue': 'Tor' + , 'week.days.wed': 'Sre' + , 'week.days.thu': 'Čet' + , 'week.days.fri': 'Pet' + , 'week.days.sat': 'Sob' + , 'week.days.sun': 'Ned' + , 'week.editor.title': 'Urnik po dnevih' + , 'week.editor.preview_note': 'Samo predogled. Spremembe se uporabijo, ko pritisnete Shrani.' + , 'week.open_editor.desc': 'Odpri urejevalnik urnika po dnevih.' + , 'week.open_editor.btn': 'Odpri' + , 'week.view.title': 'Pogled tedna' + , 'week.view.rooms_one_day': 'Vsi prostori • en dan' + , 'week.view.days_one_room': 'Vsi dnevi • en prostor' + , 'week.view_switch.title': 'Preklopi pogled na časovnici' + , 'week.view_switch.desc': 'Prikaže preklopnik v glavi časovnice za preklapljanje med obema načinoma pogleda za dneve v tednu.' + , 'week.copy_from_main.title': 'Kopiraj glavni urnik' + , 'week.copy_from_main.desc': 'Omogočili ste dneve v tednu. Želite kopirati glavni urnik v dneve v tednu? Izberite način, v katerega se kopira.' + , 'week.copy_from_main.mode_label': 'Način dni v tednu' + , 'week.copy_from_main.target_label': 'Kopiraj v' + , 'week.copy_from_main.copy_btn': 'Kopiraj' + , 'week.copy_from_main.skip_btn': 'Ne kopiraj' + , 'week.title': 'Dnevi v tednu' + , 'week.select_room': 'Izberi prostor' + , 'week.copy_to_rooms': 'Kopiraj teden v prostore' + , 'week.copy_to_rooms.title': 'Kopiraj teden v prostore' + , 'week.copy_to_rooms.select': 'Izberi prostore' + , 'week.copy_to_rooms.apply': 'Vstavi v prostore' + , 'week.enable.desc': 'Omogoči načrtovanje po posameznem dnevu. Ko je izklopljeno, se vsak dan uporabi isti načrt.' + , 'editor.timefmt.title': 'Oblika časa' + , 'editor.timefmt.desc': 'Izberite 24-urni ali 12-urni (AM/PM) prikaz časov na kartici.' + , 'editor.tempfmt.title': 'Enota temperature' + , 'editor.tempfmt.desc': 'Izberite Celsius (°C) ali Fahrenheit (°F) za prikaz na kartici in v urejevalnikih.' + , 'editor.timesrc.title': 'Vir časa' + , 'editor.timesrc.desc': 'Izberite, ali kartica za "zdaj" uporablja čas brskalnika ali časovni pas Home Assistanta.' + , 'editor.timesrc.browser': 'Brskalnik' + , 'editor.timesrc.ha': 'Home Assistant' + , 'editor.instance.title': 'ID konfiguracije' + , 'editor.instance.desc': 'Ohranite več neodvisnih urnikov/nastavitev pod različnimi ID-ji.' + , 'editor.instance.new_id': 'Nov ID' + , 'editor.tabs.settings': 'Nastavitve' + , 'editor.tabs.pause': 'Premor' + , 'editor.tabs.weekdays': 'Dnevi v tednu' + , 'editor.tabs.rooms': 'Prostori' + , 'editor.tabs.colors': 'Barve' + , 'editor.tabs.presence_sensor': 'Senzor prisotnosti' + , 'editor.presence_sensor.enable.title': 'Senzor prisotnosti' + , 'editor.presence_sensor.enable.desc': 'Omogoči nastavitev senzorja prisotnosti po posameznih prostorih.' + , 'editor.presence_sensor.entity.title': 'Entiteta prisotnosti' + , 'editor.presence_sensor.entity.desc': 'Izberi binary_sensor, ki označuje prisotnost v tem prostoru.' + , 'editor.presence_sensor.on_temp.title': 'Temperatura, ko je prisotnost VKLOPLJENA' + , 'editor.presence_sensor.on_temp.desc': 'Če je senzor VKLOPLJEN, ta soba uporablja to temperaturo namesto urnika.' + , 'editor.presence_sensor.on_delay.title': 'Zamik pred uporabo prisotnosti' + , 'editor.presence_sensor.on_delay.desc': 'Kako dolgo mora biti senzor VKLOPLJEN, preden se uporabi temperatura prisotnosti. Vrednost uporablja izbrano enoto.' + , 'editor.presence_sensor.off_delay.title': 'Zamik pred nadaljevanjem urnika' + , 'editor.presence_sensor.off_delay.desc': 'Kako dolgo mora biti senzor IZKLOPLJEN, preden se urnik nadaljuje. Vrednost uporablja izbrano enoto.' + , 'editor.tabs.owd': 'Open Window Detection' + , 'owd.enable': 'Open Window Detection' + , 'owd.enable.desc': 'Turn off thermostats in a room when selected window/door sensors are open.' + , 'owd.sensors': 'Window/Door sensors' + , 'owd.delays.title': 'Delays' + , 'owd.delays.desc': 'Minutes to wait before turning off (open) and resuming schedule (closed).' + , 'owd.open_delay': 'After open (min)' + , 'owd.close_delay': 'After closed (min)' + , 'editor.tabs.away': 'Odsotnost' + , 'editor.tabs.reset': 'Ponastavi' + , 'editor.tabs.boiler': 'Krmiljenje kotla' + , 'boiler.switch': 'Stikalo kotla' + , 'boiler.switch_type': 'Vrsta entitete' + , 'boiler.switch_type.switch': 'Stikalo' + , 'boiler.switch_type.input_boolean': 'Vhodni boolean' + , 'boiler.rooms': 'Vključeni prostori' + , 'boiler.rooms.desc': 'Izberite, kateri prostori naj bodo vključeni v krmiljenje kotla.' + , 'boiler.multi_enable': 'Omogoči več kotlov' + , 'boiler.multi_enable.desc': 'Dodeli sobe različnim kotlom in nastavi odmike za posamezno sobo.' + , 'boiler.multi_rooms': 'Kotel na sobo' + , 'boiler.multi_rooms.desc': 'Izberi stikalo kotla in odmike za vsako sobo.' + , 'boiler.room.enable': 'Vključi sobo' + , 'boiler.room.enable.desc': 'Vključi to sobo v krmiljenje kotla.' + , 'boiler.enable': 'Omogoči krmiljenje kotla' + , 'boiler.enable.desc': 'Prikaži nastavitve kotla v urejevalniku te kartice.' + , 'boiler.temp_sensor': 'Temperaturni senzor kotla' + , 'boiler.temp_sensor.desc': 'Temperaturni senzor za krmiljenje stikala kotla.' + , 'boiler.min_temp': 'Min. temperatura kotla (°C)' + , 'boiler.max_temp': 'Maks. temperatura kotla (°C)' + , 'boiler.offsets': 'Odmiki' + , 'boiler.offsets.desc': 'ON odmik: kako daleč pod načrtovanim ciljem, preden se vklopi. OFF odmik: kako daleč nad načrtovanim ciljem, preden se izklopi.' + , 'boiler.on_offset': 'ON odmik (°C)' + , 'boiler.off_offset': 'OFF odmik (°C)' + , 'editor.tabs.sync': 'Sinhronizacijski pogon' + , 'sync.running': 'Sinhronizacija poteka…' + , 'sync.completed': 'Sinhronizacija končana' + , 'away.enable': 'Omogoči način odsotnosti' + , 'away.desc': 'Ko nihče iz izbranih oseb ni doma, termostati samodejno spustijo na nastavljeno temperaturo.' + , 'away.temp': 'Temperatura odsotnosti' + , 'away.persons': 'Osebe' + , 'away.add_person': 'Dodaj osebo' + , 'away.resume_schedule': 'Nadaljuj urnik' + , 'away.resume_away': 'Nadaljuj temperaturo odsotnosti' + , 'editor.heat_colors.title': 'Barve toplotnih blokov' + , 'editor.heat_colors.add': 'Dodaj barvno območje' + , 'editor.colors.col_from': 'Od °C' + , 'editor.colors.col_to': 'Do °C' + , 'editor.colors.col_color': 'Barva' + , 'editor.colors.empty': 'Ni še dodanih prostorov. Dodajte prostor za določitev barv.' + , 'editor.colors.go_add': 'Dodaj prostor' + , 'editor.select_entities': 'Prostori' + , 'editor.colors.refresh': 'Osveži urnik' + , 'editor.colors.clear_local': 'Počisti lokalne barvne podatke' + , 'editor.colors.clear_local_confirm': 'To bo izbrisalo barvna območja, shranjena v vašem brskalniku za to kartico. Nadaljujem?' + , 'editor.colors.clear_shared': 'Počisti deljeno barvno shrambo' + , 'editor.colors.clear_shared_confirm': 'To bo izbrisalo barvna območja v izbranem senzoru shrambe. Nadaljujem?' + , 'editor.colors.clear_all': 'Počisti vse barvne podatke' + , 'editor.colors.clear_all_confirm': 'To bo izbrisalo vsa barvna območja iz lokalne in deljene shrambe. Nadaljujem?' + , 'editor.colors.mode': 'Način barv' + , 'editor.colors.mode.desc': 'Izberite barve po prostorih ali en nabor za vse prostore.' + , 'editor.colors.mode_per_room': 'Po prostorih' + , 'editor.colors.mode_global': 'Vsi prostori' + , 'editor.colors.global_title': 'Vsi prostori' + , 'editor.reload_editor': 'Ponovno naloži urejevalnik' + , 'editor.reload_dashboard': 'Ponovno naloži nadzorno ploščo' + , 'onboard.title': 'Dobrodošli v Thermostat Timeline' + , 'onboard.next': 'Naprej' + , 'onboard.prev': 'Nazaj' + , 'onboard.skip': 'Preskoči' + , 'onboard.done': 'Končano' + , 'onboard.dont_show': 'Ne pokaži več' + , 'onboard.step_main': 'To je glavna časovnica. Prikazuje želene temperature za 24 ur v vsakem prostoru.' + , 'onboard.step_add': 'Toplotni blok dodate z + Dodaj blok ali z dvojnim klikom na časovnico.' + , 'onboard.step_edit': 'Blok uredite ali izbrišete z dvojnim klikom. Uporabite Dneve v tednu za načrtovanje različnih dni.' + , 'onboard.reset': 'Prikaži vodič znova' + , 'onboard.reset_ok': 'Pozdrav se bo prikazal naslednjič.' + , 'pause.menu_title': 'Premor samodejne uporabe' + , 'pause.off_1h': 'Premor 1 ura' + , 'pause.off_5h': 'Premor 5 ur' + , 'pause.off_custom': 'Poljubno trajanje…' + , 'pause.off_indef': 'Premor do nadaljnjega' + , 'pause.resume': 'Nadaljuj zdaj' + , 'pause.close': 'Zapri' + , 'pause.active_until': 'V premoru do {time}' + , 'pause.active_indef': 'V premoru' + , 'editor.pausebtn.title': 'Gumb premora' + , 'editor.pausebtn.desc': 'V glavi kartice prikaži globalni gumb premora. Med premorom so vsi ukazi set_temperature blokirani.' + , 'editor.integration.connected': 'Integracija pripravljena' + , 'editor.integration.disconnected': 'Integracija ni na voljo' + , 'profiles.enable': 'Omogoči profile (prepis)' + , 'profiles.enable.desc': 'Dovoli poimenovane dnevne urnike, ki lahko preglasijo glavni načrt.' + , 'season.enable.title': 'Omogoči poletne/zimne urnike' + , 'season.enable.desc': 'Ko je omogočeno, ima vsak urnik ločeni poletni in zimski različici, med katerimi lahko preklapljate.' + , 'season.mode.title': 'Sezona' + , 'season.mode.winter': 'Zima' + , 'season.mode.summer': 'Poletje' + , 'profiles.button': 'Ročni urnik' + , 'profiles.editor.title': 'Profili' + , 'profiles.add': 'Dodaj profil' + , 'profiles.rename': 'Preimenuj' + , 'profiles.delete': 'Izbriši' + , 'profiles.activate': 'Aktiviraj' + , 'profiles.deactivate': 'Deaktiviraj' + , 'profiles.active': 'Aktiven' + , 'profiles.none': 'Noben' + , 'profiles.name_placeholder': 'Ime profila' + , 'profiles.edit': 'Uredi' + , 'profiles.preview_note': 'Samo predogled. Urniki postanejo aktivni, ko profil aktivirate.' + , 'profiles.active_label': 'Ročni profil aktiven:' + , 'profiles.save_new': 'Shrani nov profil' + , 'profiles.save_existing': 'Shrani spremembe v izbran profil' + , 'editor.tabs.holidays': 'Prazniki' + , 'editor.tabs.backup': 'Varnostna kopija' + , 'backup.title': 'Varnostna kopija' + , 'backup.desc': 'Kopiraj urnike in nastavitve v varnostno kopijo ter jih po potrebi obnovi.' + , 'backup.now': 'Ustvari kopijo zdaj' + , 'backup.restore': 'Obnovi' + , 'backup.delete': 'Izbriši varnostno kopijo' + , 'backup.delete.confirm': 'Izbrišem izbrano varnostno kopijo?' + , 'backup.new': 'Nova varnostna kopija' + , 'backup.auto': 'Samodejna kopija' + , 'backup.auto.desc': 'Samodejno ustvarjaj varnostne kopije po urniku.' + , 'backup.interval': 'Interval (dni)' + , 'backup.interval.desc': 'Število dni med samodejnimi kopijami (min. 1).' + , 'backup.sensor': 'Senzor varnostne kopije' + , 'backup.last': 'Zadnja kopija' + , 'backup.export.title': 'Izvozi datoteko varnostne kopije' + , 'backup.export.desc': 'Prenesi popoln thermostat_timeline_backup.json z vsemi mesti (slot) in podatki.' + , 'backup.export.btn': 'Prenesi datoteko' + , 'backup.file.add_selected': 'Uvozi varnostno kopijo' + , 'backup.file.add_all': 'Uvozi vse varnostne kopije' + , 'backup.file.loaded_n': 'Datoteka naložena ({n} varnostnih kopij)' + , 'backup.file.imported_ok': 'Uvoženo ({n})' + , 'backup.slot.title': 'Izberi varnostno kopijo' + , 'backup.slot.desc': 'Izberite, kateri slot varnostne kopije želite obnoviti.' + , 'backup.slot.empty': 'Še ni varnostnih kopij' + , 'backup.select': 'Izberi, kaj kopirati' + , 'backup.select.desc': 'Označene postavke so vključene v kopiranje/obnovo' + , 'backup.opt.all': 'Vse' + , 'backup.opt.main': 'Glavni urniki' + , 'backup.opt.weekday': 'Urniki po dnevih' + , 'backup.opt.presence': 'Urniki prisotnosti' + , 'backup.opt.settings': 'Nastavitve urejevalnika' + , 'backup.opt.holiday': 'Praznični urniki' + , 'backup.opt.colors': 'Barvni bloki' + , 'holidays.enable': 'Omogoči praznike' + , 'holidays.desc': 'Uporabi poseben dnevni urnik ob praznikih.' + , 'holidays.source': 'Vir praznikov' + , 'holidays.source.calendar': 'Koledar' + , 'holidays.source.manual': 'Ročni datumi' + , 'holidays.entity': 'Entiteta za praznike' + , 'holidays.entity_desc': 'Izberite koledar, ki poroča le vklop/izklop. Na praznike mora biti vklopljen, sicer izklopljen.' + , 'holidays.dates': 'Datumi praznikov' + , 'holidays.add_date': 'Dodaj datum' + , 'holidays.editor.title': 'Praznični urnik' + , 'holidays.edit': 'Uredi praznični urnik' + , 'holidays.button': 'Prazniki' + , 'presence.button': 'Urniki prisotnosti' + , 'presence.editor.title': 'Urniki prisotnosti' + , 'presence.enable_advanced': 'Omogoči napredne urnike prisotnosti' + , 'presence.enable_advanced.desc': 'Uporabi kombinacije in urnike glede na to, kdo je doma/odsoten.' + , 'away.delay_enable': 'Zakasni dejanja odsotnosti' + , 'away.delay_desc': 'Počakaj preden uporabiš spremembe odsotnosti.' + , 'away.delay_value': 'Zamik' + , 'away.delay_unit': 'Enota' + , 'away.delay_unit.seconds': 'Sekunde' + , 'away.delay_unit.minutes': 'Minute' + , 'presence.live_header': 'Prikaži živo prisotnost v glavi' + , 'presence.live_header.desc': 'Oznake so prikazane v desnem kotu.' + , 'presence.combos': 'Kombinacije' + , 'presence.enable_combo': 'Omogoči to kombinacijo' + , 'presence.all_home': 'Vsi doma' + , 'presence.none_home': 'Nihče doma' + , 'presence.home': 'Doma: {names}' + , 'presence.away': 'Odsotni: {names}' + , 'presence.manage': 'Upravljaj kombinacije' + , 'presence.who_home': 'Kdo je doma' + , 'presence.who_away': 'Kdo ni doma' + , 'err.config_entities': "Konfiguracija mora imeti 'entities: [climate.xxx, ...]'." + , 'onboard.video_not_found': 'Videoposnetka ni bilo mogoče najti' + , 'onboard.image_not_found': 'Slike ni bilo mogoče najti' + , 'pause.prompt_minutes': 'Vnesite trajanje (minute):' + , 'editor.test_tool': 'Orodje za testiranje' + , 'editor.test_select_entity': 'Izberite entiteto' + , 'editor.test_empty': 'Ni ničesar za izvoz' + , 'editor.test_export': 'Izvozi' + , 'editor.test_no_fields': 'Ni polj za prikaz' + , 'editor.test_select_placeholder': 'Izberite...' + , 'editor.error_prefix': 'Napaka: ' + , 'backup.restore.confirm_overwrite': 'Obnovi iz varnostne kopije? To bo prepisalo trenutne urnike in nastavitve.' + , 'backup.import.err_no_hass': 'Home Assistant ni pripravljen' + , 'backup.import.err_storage_off': 'Skupna shramba je IZKLOPLJENA. Najprej jo vklopite.' + , 'backup.import.err_no_integration': 'Integracija thermostat_timeline ni na voljo' + , 'backup.import.err_json': 'Neveljaven JSON' + , 'profiles.name_exists': 'Ime že obstaja' + }, + ro: { + 'card.title_default': 'Cronologia termostatului', + 'err.config_entities': "Configurația trebuie să conțină 'entities: [climate.xxx, ...]'.", + 'ui.edit_block': 'Editează blocul', + 'ui.add_block': 'Adaugă bloc', + 'ui.from': 'De la', + 'ui.to': 'Până la', + 'ui.temperature': 'Temperatură', + 'ui.delete': 'Șterge', + 'ui.cancel': 'Anulează', + 'ui.close': 'Închide', + 'ui.unsaved_confirm': 'Ai modificări nesalvate. Închizi oricum?', + 'ui.discard_changes': 'Renunță la modificări', + 'ui.keep_editing': 'Continuă editarea', + 'ui.save': 'Salvează', + 'ui.error_prefix': 'Eroare: ', + 'ui.overlap_msg': 'Ai selectat intervale care se suprapun — suprapunerea este {start} – {end}.', + 'ui.auto_fix': 'Remediere automată', + 'ui.fix_start_to': 'Setează începutul la {time}', + 'ui.fix_end_to': 'Setează sfârșitul la {time}', + 'ui.current_temp': 'Temperatura curentă:', + 'ui.copy_plan': 'Copiază planul', + 'ui.paste_plan': 'Lipește planul', + 'ui.copy_day': 'Copiază ziua', + 'ui.paste_day': 'Lipește ziua', + + 'editor.title_label': 'Titlu', + 'editor.add_entity': 'Adaugă cameră', + 'editor.entity_placeholder': 'Selectează o entitate', + 'editor.drag_reorder': 'Trage pentru reordonare', + 'editor.remove': 'Elimină', + 'editor.default_c': 'Implicit °C', + 'editor.row_height_px': 'Înălțime rând (px)', + 'editor.max_c': 'Max °C', + 'editor.min_c': 'Min °C', + 'editor.auto_apply.title': 'Aplicare automată', + 'editor.auto_apply.desc': 'Când este activat, cardul încearcă să seteze temperatura pe entitățile climate selectate pentru a se potrivi cu valoarea programată pentru „acum”.', + 'editor.apply_edit.title': 'La editare', + 'editor.apply_edit.desc': 'Când editezi blocurile și asta afectează timpul curent, aplică imediat noua temperatură.', + 'editor.apply_default.title': 'La schimbarea temperaturii implicite', + 'editor.apply_default.desc': 'Când „Implicit °C” se schimbă și afectează perioada curentă, aplică imediat noua temperatură.', + 'editor.perroom.title': 'Implicit °C per cameră', + 'editor.perroom.desc': 'Afișează un câmp Implicit °C pe fiecare rând. Când este oprit, Implicit °C global este folosit pentru toate camerele.', + + 'editor.show_room_temp.title': 'Afișează temperatura camerei', + 'editor.show_room_temp.desc': 'Afișează bula cu temperatura curentă a camerei pe fiecare rând în antetul cronologiei.', + + 'editor.room_mode.title': 'Tip entitate cameră', + 'editor.room_mode.desc': 'Când este activat, selectează un input_number (temperatură țintă). Când este dezactivat, selectează o entitate climate.', + + 'editor.room_temp_sensor_override.title': 'Folosește senzorul de temperatură al camerei', + 'editor.room_temp_sensor_override.desc': 'Când este activat, temperatura camerei afișată (și logica boilerului) folosește senzorul de temperatură selectat în loc de climate current_temperature.', + 'editor.room_temp_sensor_override.entity': 'Senzor de temperatură', + + 'editor.turn_on.title': 'Trimite comanda turn_on', + 'editor.turn_on.desc': 'Unele termostate necesită o comandă turn_on pe lângă set_temperature.', + 'editor.turn_on.order': 'Ordine', + 'editor.turn_on.order.before': 'Înainte de set_temperature', + 'editor.turn_on.order.after': 'După set_temperature', + + 'editor.merge_label': 'Unește cu (adaugă termostat suplimentar)', + 'editor.display_name_optional': 'Nume cameră (opțional)', + 'editor.merged_with': 'Combinat cu', + + 'editor.store_enable.title': 'Stocare partajată + control în fundal', + 'editor.store_enable.short': 'Stochează programele în senzorul selectat și activează controlul în fundal.', + 'editor.store_enable.desc': 'Stochează programele în senzorul selectat și activează set_temperature în fundal. Termostatele sunt actualizate și când cardul este închis. Partajează programele și setările cardului între utilizatori/dispozitive. Când este oprit: comenzile sunt trimise doar cât timp cardul este vizibil pe un dispozitiv și trebuie să rămână deschis.', + 'editor.store_missing': 'Integrarea „Thermostat Pro Timeline Sync” nu este instalată. Stocarea partajată nu este disponibilă.', + 'editor.open_in_hacs': 'Deschide în HACS', + 'editor.clear_store': 'Șterge stocarea', + 'editor.clear_store_confirm': 'Aceasta va șterge toate programele stocate. Continui?', + 'editor.migrate_to_store': 'Transferă datele în stocare', + 'editor.migrate_confirm': 'Transferi programele salvate în browser către senzorul selectat? Aceasta va suprascrie programele existente în stocare.', + 'editor.migrate_pull_confirm': 'Importi programele din stocare și suprascrii datele locale din browser?', + 'editor.select_storage_first': 'Selectează mai întâi un senzor de stocare.', + 'editor.no_local_data': 'Nu s-au găsit date locale în browser.', + 'editor.test_tool': 'Instrument de test', + 'editor.test_select_entity': 'Selectează entitatea', + 'editor.test_empty': 'Selectează o entitate pentru a vedea comenzile disponibile…', + 'editor.test_export': 'Export .md', + 'editor.test_no_fields': 'Fără câmpuri', + 'editor.test_select_placeholder': '— Selectează —', + 'editor.error_prefix': 'Eroare: ', + 'editor.clear_all': 'Șterge toate datele', + 'editor.clear_all_confirm': 'Aceasta șterge atât stocarea, cât și programele locale din browser. Continui?', + 'editor.clear_storage_only': 'Șterge doar datele din stocare', + 'editor.clear_storage_only_confirm': 'Aceasta șterge programele din senzorul de stocare selectat. Continui?', + 'editor.clear_local_only': 'Șterge TOATE datele locale (doar local)', + 'editor.clear_local_only_confirm': 'Aceasta șterge doar programele salvate în browserul tău. Continui?', + 'editor.reset.desc': 'Ce face:\n\n• Șterge toate datele: Resetează stocarea backend (.storage) pentru Thermostat Timeline și elimină TOATE cache-urile/schițele locale din browser.\n• Șterge TOATE datele locale (doar local): Șterge doar datele stocate în acest browser (localStorage).\n\nSfat: După o resetare completă, reîncarcă pagina pentru a evita reapariția schițelor vechi.', + + 'editor.select_entities': 'Camere', + 'editor.colors.empty': 'Nu au fost adăugate camere încă. Adaugă o cameră pentru a defini culorile.', + 'editor.colors.go_add': 'Adaugă cameră', + + 'week.enable': 'Activează zilele săptămânii', + 'week.enable.desc': 'Activează planificarea pe zile ale săptămânii. Când este oprit, același plan este folosit în fiecare zi.', + 'week.mode': 'Mod zile ale săptămânii', + 'week.mode.weekday_weekend': 'Zile lucrătoare + weekend', + 'week.mode.weekday_sat_sun': 'Zile lucrătoare + sâmbătă + duminică', + 'week.mode.all_7': 'Toate zilele', + 'week.button': 'Zilele săptămânii', + 'week.title': 'Zilele săptămânii', + 'week.group.weekdays': 'Zile lucrătoare', + 'week.group.weekend': 'Weekend', + 'week.days_full.mon': 'Luni', + 'week.days_full.tue': 'Marți', + 'week.days_full.wed': 'Miercuri', + 'week.days_full.thu': 'Joi', + 'week.days_full.fri': 'Vineri', + 'week.days_full.sat': 'Sâmbătă', + 'week.days_full.sun': 'Duminică', + 'week.days.mon': 'Lun', + 'week.days.tue': 'Mar', + 'week.days.wed': 'Mie', + 'week.days.thu': 'Joi', + 'week.days.fri': 'Vin', + 'week.days.sat': 'Sâm', + 'week.days.sun': 'Dum', + 'week.editor.title': 'Program pe zile ale săptămânii', + 'week.editor.preview_note': 'Doar previzualizare. Modificările se aplică atunci când apeși Salvează.', + 'week.open_editor.desc': 'Deschide fereastra editorului pentru zilele săptămânii.', + 'week.open_editor.btn': 'Deschide', + 'week.view.title': 'Vizualizare zilele săptămânii', + 'week.view.rooms_one_day': 'Toate camerele • o zi', + 'week.view.days_one_room': 'Toate zilele • o cameră', + 'week.view_switch.title': 'Schimbă vizualizarea din cronologie', + 'week.view_switch.desc': 'Afișează un comutator în antetul cronologiei pentru a schimba între cele două moduri de vizualizare ale zilelor săptămânii.', + 'week.copy_from_main.title': 'Copiază programul principal', + 'week.copy_from_main.desc': 'Ai activat zilele săptămânii. Vrei să copiezi programul principal în zilele săptămânii? Alege modul în care să copiezi.', + 'week.copy_from_main.mode_label': 'Mod zile ale săptămânii', + 'week.copy_from_main.target_label': 'Copiază în', + 'week.copy_from_main.copy_btn': 'Copiază', + 'week.copy_from_main.skip_btn': 'Nu copia', + 'week.select_room': 'Selectează cameră', + 'week.copy_to_rooms': 'Copiază zilele săptămânii în camere', + 'week.copy_to_rooms.title': 'Copiază zilele săptămânii în camere', + 'week.copy_to_rooms.select': 'Selectează camere', + 'week.copy_to_rooms.apply': 'Aplică la camere', + + 'editor.timefmt.title': 'Format oră', + 'editor.timefmt.desc': 'Alege afișarea orei în format 24h sau 12h (AM/PM).', + 'editor.tempfmt.title': 'Unitate temperatură', + 'editor.tempfmt.desc': 'Alege Celsius (°C) sau Fahrenheit (°F) pentru afișare în card și editor.', + 'editor.timesrc.title': 'Sursa timpului', + 'editor.timesrc.desc': 'Alege dacă cardul folosește ora din browser sau fusul orar Home Assistant pentru „acum”.', + 'editor.timesrc.browser': 'Browser', + 'editor.timesrc.ha': 'Home Assistant', + + 'editor.instance.title': 'ID configurație', + 'editor.instance.desc': 'Păstrează mai multe programe/setări independente sub ID-uri diferite.', + 'editor.instance.new_id': 'ID nou', + + 'editor.tabs.settings': 'Setări', + 'editor.tabs.pause': 'Pauză', + 'editor.tabs.weekdays': 'Zilele săptămânii', + 'editor.tabs.rooms': 'Camere', + 'editor.tabs.boiler': 'Control boiler', + 'editor.tabs.colors': 'Culori', + 'editor.tabs.presence_sensor': 'Senzor de prezență', + 'editor.tabs.owd': 'Detectare fereastră deschisă', + 'editor.tabs.away': 'Plecat de acasă', + 'editor.tabs.reset': 'Resetare', + 'editor.tabs.sync': 'Motor de sincronizare', + 'editor.tabs.backup': 'Backup', + + 'editor.presence_sensor.enable.title': 'Senzor de prezență', + 'editor.presence_sensor.enable.desc': 'Activează configurarea senzorului de prezență per cameră.', + 'editor.presence_sensor.entity.title': 'Entitate prezență', + 'editor.presence_sensor.entity.desc': 'Selectează binary_sensor care indică prezența în această cameră.', + 'editor.presence_sensor.on_temp.title': 'Temperatură când prezența este ON', + 'editor.presence_sensor.on_temp.desc': 'Dacă senzorul este ON, această cameră va folosi această temperatură în locul programului.', + 'editor.presence_sensor.on_delay.title': 'Întârziere înainte de aplicarea prezenței', + 'editor.presence_sensor.on_delay.desc': 'Cât timp trebuie să rămână ON senzorul înainte de a folosi temperatura de suprascriere. Valoarea folosește unitatea selectată.', + 'editor.presence_sensor.off_delay.title': 'Întârziere înainte de reluarea programului', + 'editor.presence_sensor.off_delay.desc': 'Cât timp trebuie să rămână OFF senzorul înainte de reluarea programului. Valoarea folosește unitatea selectată.', + + 'owd.enable': 'Detectare fereastră deschisă', + 'owd.enable.desc': 'Oprește termostatele într-o cameră când senzorii de fereastră/ușă selectați sunt deschiși.', + 'owd.sensors': 'Senzori fereastră/ușă', + 'owd.delays.title': 'Întârzieri', + 'owd.delays.desc': 'Minute de așteptare înainte de oprire (deschis) și reluare program (închis).', + 'owd.open_delay': 'După deschidere (min)', + 'owd.close_delay': 'După închidere (min)', + + 'boiler.switch': 'Comutator boiler', + 'boiler.switch_type': 'Tip entitate', + 'boiler.switch_type.switch': 'Comutator', + 'boiler.switch_type.input_boolean': 'Input boolean', + 'boiler.rooms': 'Camere incluse', + 'boiler.rooms.desc': 'Selectează ce camere trebuie incluse în controlul boilerului.', + 'boiler.multi_enable': 'Activează mai multe boilere', + 'boiler.multi_enable.desc': 'Atribuie camere către boilere diferite și setează decalaje per cameră.', + 'boiler.multi_rooms': 'Boiler per cameră', + 'boiler.multi_rooms.desc': 'Selectează comutatorul boilerului și decalajele pentru fiecare cameră.', + 'boiler.room.enable': 'Include cameră', + 'boiler.room.enable.desc': 'Include această cameră în controlul boilerului.', + 'boiler.enable': 'Activează controlul boilerului', + 'boiler.enable.desc': 'Afișează setările boilerului în editorul acestui card.', + 'boiler.temp_sensor': 'Senzor temperatură boiler', + 'boiler.temp_sensor.desc': 'Senzor de temperatură care controlează comutatorul boilerului.', + 'boiler.min_temp': 'Temperatură minimă boiler (°C)', + 'boiler.max_temp': 'Temperatură maximă boiler (°C)', + 'boiler.offsets': 'Decalaje', + 'boiler.offsets.desc': 'Decalaj ON: cât de mult sub ținta programată înainte de a porni. Decalaj OFF: cât de mult peste ținta programată înainte de a opri.', + 'boiler.on_offset': 'Decalaj ON (°C)', + 'boiler.off_offset': 'Decalaj OFF (°C)', + + 'sync.running': 'Sincronizare în curs…', + 'sync.completed': 'Sincronizare finalizată', + + 'backup.title': 'Backup', + 'backup.desc': 'Copiază conținutul complet al senzorului de sincronizare în senzorul de backup și restaurează când este nevoie.', + 'backup.now': 'Backup acum', + 'backup.restore': 'Restaurare', + 'backup.restore.confirm_overwrite': 'Restaurezi din backup? Aceasta va suprascrie programele și setările curente.', + 'backup.delete': 'Șterge backupul', + 'backup.delete.confirm': 'Ștergi backupul selectat?', + 'backup.new': 'Backup nou', + 'backup.auto': 'Backup automat', + 'backup.auto.desc': 'Creează backupuri automat la un interval fix.', + 'backup.interval': 'Interval (zile)', + 'backup.interval.desc': 'Câte zile între backupurile automate (min 1).', + 'backup.sensor': 'Senzor backup', + 'backup.last': 'Ultimul backup', + 'backup.slot.title': 'Selectează backup', + 'backup.slot.desc': 'Selectează ce slot de backup să restaurezi.', + 'backup.slot.empty': 'Încă nu există backupuri', + 'backup.select': 'Selectează ce să fie salvat', + 'backup.select.desc': 'Elementele bifate sunt incluse în backup/restaurare', + 'backup.opt.all': 'Toate', + 'backup.opt.main': 'Programe principale', + 'backup.opt.weekday': 'Programe zile săptămânii', + 'backup.opt.presence': 'Programe de prezență', + 'backup.opt.settings': 'Setări editor', + 'backup.opt.holiday': 'Programe de sărbători', + 'backup.opt.colors': 'Blocuri de culori', + + 'backup.import.title': 'Importă thermostat_timeline.json', + 'backup.import.desc': 'Încarcă un fișier thermostat_timeline.json și importă-l. Se creează mai întâi un backup nou.', + 'backup.import.btn': 'Importă fișier', + 'backup.export.title': 'Exportă fișierul de backup', + 'backup.export.desc': 'Descarcă un thermostat_timeline_backup.json complet cu toate sloturile și datele.', + 'backup.export.btn': 'Descarcă fișier', + 'backup.imported_suffix': ' - importat', + 'backup.importbk.title': 'Importă fișier de backup', + 'backup.importbk.desc': 'Încarcă un fișier thermostat_timeline_backup.json pentru a adăuga backupurile lui în listă.', + 'backup.importbk.btn': 'Importă backup', + 'backup.importbk.err_invalid': 'Fișier de backup invalid', + 'backup.file.choose': 'Alege backupul importat', + 'backup.file.add_selected': 'Importă backup', + 'backup.file.add_all': 'Importă toate backupurile', + 'backup.file.loaded_n': 'Fișier încărcat ({n} backupuri)', + 'backup.file.imported_ok': 'Importat ({n})', + 'backup.import.err_no_hass': 'Home Assistant nu este pregătit', + 'backup.import.err_storage_off': 'Stocarea partajată este OPRITĂ. Pornește-o mai întâi.', + 'backup.import.err_no_integration': 'Integrarea thermostat_timeline nu este disponibilă', + 'backup.import.err_json': 'JSON invalid', + 'backup.import.err_invalid': 'Fișier invalid: lipsește data.schedules', + + 'away.enable': 'Activează modul Plecat', + 'away.desc': 'Când niciuna dintre persoanele selectate nu este acasă, termostatele sunt reduse automat la temperatura specificată.', + 'away.temp': 'Temperatură de plecare', + 'away.persons': 'Persoane', + 'away.add_person': 'Adaugă persoană', + 'away.resume_schedule': 'Reia programul', + 'away.resume_away': 'Reia temperatura de plecare', + + 'editor.heat_colors.title': 'Culori pentru blocurile de încălzire', + 'editor.heat_colors.add': 'Adaugă interval de culoare', + 'editor.colors.col_from': 'De la °C', + 'editor.colors.col_to': 'Până la °C', + 'editor.colors.col_color': 'Culoare', + 'editor.colors.refresh': 'Reîmprospătează programul', + 'editor.colors.clear_local': 'Șterge datele locale de culoare', + 'editor.colors.clear_local_confirm': 'Aceasta șterge intervalele de culoare stocate în browser pentru acest card. Continui?', + 'editor.colors.clear_shared': 'Șterge stocarea partajată a culorilor', + 'editor.colors.clear_shared_confirm': 'Aceasta șterge intervalele de culoare în senzorul de stocare selectat. Continui?', + 'editor.colors.clear_all': 'Șterge toate datele de culoare', + 'editor.colors.clear_all_confirm': 'Aceasta șterge toate intervalele de culoare atât local, cât și în senzorul de stocare selectat. Continui?', + 'editor.colors.mode': 'Mod de culoare', + 'editor.colors.mode.desc': 'Alege culori pe cameră sau un singur set pentru toate camerele.', + 'editor.colors.mode_per_room': 'Pe cameră', + 'editor.colors.mode_global': 'Toate camerele', + 'editor.colors.global_title': 'Toate camerele', + + 'editor.reload_editor': 'Reîncarcă editorul', + 'editor.reload_dashboard': 'Reîncarcă panoul', + + 'onboard.title': 'Bine ai venit la Thermostat Timeline', + 'onboard.next': 'Următorul', + 'onboard.prev': 'Înapoi', + 'onboard.skip': 'Sari peste', + 'onboard.done': 'Gata', + 'onboard.dont_show': 'Nu mai arăta', + 'onboard.video_not_found': 'Video negăsit', + 'onboard.image_not_found': 'Imagine negăsită', + 'onboard.step_main': 'Aceasta este cronologia principală. Afișează temperaturile țintă pe 24 de ore pentru fiecare cameră.', + 'onboard.step_add': 'Adaugă un bloc de încălzire prin + Adaugă bloc sau prin dublu clic pe cronologie.', + 'onboard.step_edit': 'Editează sau șterge un bloc prin dublu clic. Folosește Zilele săptămânii pentru zile diferite.', + 'onboard.reset': 'Afișează ghidul din nou', + 'onboard.reset_ok': 'Ghidul va fi afișat data viitoare.', + + 'pause.menu_title': 'Pune pe pauză aplicarea automată', + 'pause.off_1h': 'Pauză pentru 1 oră', + 'pause.off_5h': 'Pauză pentru 5 ore', + 'pause.off_custom': 'Durată personalizată…', + 'pause.prompt_minutes': 'Introdu durata (minute):', + 'pause.off_indef': 'Pauză până reiau', + 'pause.resume': 'Reia acum', + 'pause.close': 'Închide', + 'pause.active_until': 'Pauză până la {time}', + 'pause.active_indef': 'Pauză', + + 'editor.pausebtn.title': 'Buton de pauză', + 'editor.pausebtn.desc': 'Afișează un buton global de pauză în antetul cardului. Când pauza este activă, toate comenzile set_temperature sunt suprimate.', + 'editor.pause_sensor.title': 'Pauză prin binary sensor', + 'editor.pause_sensor.desc': 'Pune automat pe pauză când binary sensor-ul selectat este pornit.', + 'editor.pause_sensor.entity_title': 'Senzor de pauză', + 'editor.pause_sensor.entity_desc': 'Binary sensor care controlează pauza (on = pauză, off = reluare).', + 'editor.integration.connected': 'Conectat la integrare', + 'editor.integration.disconnected': 'Nu este conectat la integrare', + + 'profiles.enable': 'Activează profilele (suprascriere)', + 'profiles.enable.desc': 'Permite programe zilnice numite care pot fi activate pentru a suprascrie planul principal.', + 'season.enable.title': 'Activează programe de vară/iarnă', + 'season.enable.desc': 'Când este activat, fiecare program are versiuni separate de Vară și Iarnă între care poți comuta.', + 'season.mode.title': 'Sezon', + 'season.mode.winter': 'Iarnă', + 'season.mode.summer': 'Vară', + 'profiles.button': 'Profile', + 'profiles.editor.title': 'Profile', + 'profiles.add': 'Adaugă profil', + 'profiles.rename': 'Redenumește', + 'profiles.delete': 'Șterge', + 'profiles.activate': 'Activează', + 'profiles.deactivate': 'Dezactivează', + 'profiles.active': 'Activ', + 'profiles.none': 'Niciunul', + 'profiles.name_placeholder': 'Nume profil', + 'profiles.name_exists': 'Numele există', + 'profiles.edit': 'Editează', + 'profiles.preview_note': 'Doar previzualizare. Profilele devin active când le activezi.', + 'profiles.active_label': 'Profil activ:', + 'profiles.save_new': 'Salvează profil nou', + 'profiles.save_existing': 'Salvează modificările în profilul selectat', + + 'editor.tabs.holidays': 'Sărbători', + 'holidays.enable': 'Activează sărbătorile', + 'holidays.desc': 'Folosește un program special în zilele de sărbătoare.', + 'holidays.source': 'Sursa sărbătorilor', + 'holidays.source.calendar': 'Calendar', + 'holidays.source.manual': 'Date manuale', + 'holidays.entity': 'Entitate sărbători', + 'holidays.entity_desc': 'Selectează un calendar care oferă doar on/off. Trebuie să fie on în sărbători și off în celelalte zile.', + 'holidays.dates': 'Date de sărbătoare', + 'holidays.add_date': 'Adaugă dată', + 'holidays.add_range': 'Adaugă interval', + 'holidays.date_in_range': 'Data este deja acoperită de un interval', + 'holidays.ranges': 'Intervale de sărbători', + 'holidays.single_dates': 'Date individuale', + 'holidays.editor.title': 'Program de sărbătoare', + 'holidays.edit': 'Editează programul de sărbătoare', + 'holidays.button': 'Sărbători', + 'holidays.copy_from': 'Copiază din…', + 'holidays.copy.title': 'Copiază în sărbători', + 'holidays.copy.select': 'Selectează sursa', + 'holidays.copy.apply': 'Inserează', + 'holidays.copy.replace_confirm': 'Înlocuiești blocurile existente de sărbătoare?', + 'holidays.copy.group.main': 'Plan principal', + 'holidays.copy.group.weekdays': 'Zilele săptămânii', + 'holidays.copy.group.profiles': 'Profile', + 'holidays.copy.group.presence': 'Prezență', + + 'presence.button': 'Programe de prezență', + 'presence.editor.title': 'Programe de prezență', + 'presence.enable_advanced': 'Activează programe avansate de prezență', + 'presence.enable_advanced.desc': 'Folosește combinații și programe în funcție de cine este acasă/plecat.', + 'presence.live_header': 'Afișează prezența live în antet', + 'presence.live_header.desc': 'Insignele sunt afișate în colțul din dreapta.', + 'away.delay_enable': 'Întârzie acțiunile de plecare', + 'away.delay_desc': 'Așteaptă înainte de a aplica schimbările de plecare.', + 'away.delay_value': 'Întârziere', + 'away.delay_unit': 'Unitate', + 'away.delay_unit.seconds': 'Secunde', + 'away.delay_unit.minutes': 'Minute', + 'presence.combos': 'Combinații', + 'presence.enable_combo': 'Activează această combinație', + 'presence.all_home': 'Toți acasă', + 'presence.none_home': 'Nimeni acasă', + 'presence.home': 'Acasă: {names}', + 'presence.away': 'Plecați: {names}', + 'presence.manage': 'Gestionează combinațiile', + 'presence.who_home': 'Cine este acasă', + 'presence.who_away': 'Cine este plecat' + }, +}; +const TT_LANG_ALIAS = { no: 'nb', cz: 'cs', dk: 'da' }; +function ttGetLangFromHass(hass) { + // Home Assistant can report locales like "da_DK"; normalize to BCP-47 ("da-DK"). + const raw = + (typeof hass === 'string' ? hass : (hass?.locale?.language || hass?.language)) || + navigator.language || + 'en'; + return String(raw).replace(/_/g, '-').toLowerCase(); +} +function ttLocalize(key, langOrHass) { + const raw = typeof langOrHass === 'string' ? langOrHass : ttGetLangFromHass(langOrHass); + const lang = String(raw).replace(/_/g, '-').toLowerCase(); + const parts = lang.split('-'); + const candidates = [ + lang, + parts[0], + TT_LANG_ALIAS[lang], + TT_LANG_ALIAS[parts[0]], + 'en', + ].filter(Boolean); + for (const c of candidates) { + const dict = TT_I18N[c]; + if (dict && dict[key]) return dict[key]; + } + // Default/fallback language is English. + return TT_I18N.en[key] || key; +} +// ---- end i18n ---- + +/* eslint-disable no-undef */ + +// Simple runtime version to help with cache-busting diagnostics in HA. +// Update this when shipping changes so the version appears in the +// "Custom cards" panel and in logs. +const TT_CARD_VERSION = "2026.02.08-seasonal"; + +class ThermostatTimelineCard extends HTMLElement { + static get version() { return TT_CARD_VERSION; } + static getConfigElement() { return document.createElement("thermostat-timeline-card-editor"); } + static getStubConfig() { + return { + // Localize default title based on browser language; editor will further adapt to HA language + title: ttLocalize('card.title_default', navigator.language || 'en'), + entities: [], + time_12h: null, + time_source: 'browser', // 'browser' | 'ha' + color_global: false, // when true, use one global set of colors (key "*") for all rooms + now_update_ms: 60000, // kun til UI 'nu'-stregen + storage_enabled: true, // default: on -> uses file storage via API + // Optional per-card instance namespace (off by default for legacy compatibility) + instance_enabled: false, + // Pre-generate a unique ID so new cards don't inherit another card's instance. + // Safe chars only; will be normalized if user later edits it. + instance_id: `thermostat_pro_timeline_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,6)}`, + // Internal per-card uid to make local persistence stable even when multiple + // cards have identical entities/settings (and when Lovelace uses Shadow DOM). + instance_uid: `ttuid_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,10)}`, + storage_sync_min: 5, // minutes when mode = 'delay' (legacy) + backup_auto_enabled: false, + backup_interval_days: 1, + show_top_now: false, + now_extend_px: 76, + show_pause_button: true, + show_room_temp: true, + pause_sensor_enabled: false, + pause_sensor_entity: '', + labels: {}, + merges: {}, // { [primary_eid]: [linked_eid, ...] } + temp_sensors: {}, // { [primary_eid]: sensor.temperature_entity_id } + turn_on: {}, // { [primary_eid]: { enabled: bool, order: 'before'|'after' } } + boiler_enabled: false, // enable boiler controls in editor and runtime + boiler_switch: '', // optional switch.* entity used for boiler control + boiler_switch_domain: 'switch', // switch | input_boolean + boiler_rooms: null, // null = all rooms; array = selected climate rooms + boiler_on_offset: 0, // °C relative to target: ON when current < target - on_offset + boiler_off_offset: 0, // °C relative to target: OFF when current >= target + off_offset + boiler_temp_sensor: '', // optional sensor.* entity used for boiler temperature control + boiler_min_temp: 20, // boiler min temp (stored internally as °C) + boiler_max_temp: 25, // boiler max temp (stored internally as °C) + boiler_multi_enabled: false, // enable per-room boiler mapping + boiler_room_settings: {}, // { [room_eid]: { enabled, switch, switch_domain, on_offset, off_offset } } + weekdays_enabled: false, + seasonal_enabled: false, + seasonal_mode: 'winter', // 'winter' | 'summer' + weekdays_mode: 'weekday_weekend', // 'weekday_weekend' | 'weekday_sat_sun' | 'all_7' + weekdays_view: 'all_rooms_one_day', // 'all_rooms_one_day' | 'one_room_all_days' + weekdays_view_switch_in_timeline: false, + weekdays_selected_room: '', + profiles_enabled: false, + away: { enabled: false, persons: [], target_c: 17, advanced_enabled: false, combos: {} }, + // Open Window Detection (editor + shared settings) + open_window: { enabled: false, sensors: {}, open_delay_min: 2, close_delay_min: 5 }, + holidays_enabled: false, + holidays_source: 'calendar', // 'calendar' | 'manual' + holidays_entity: '', // calendar.* or binary_sensor.* that is on/off for holiday + holidays_dates: [], // [ 'YYYY-MM-DD', ... ] + holidays_groups: [], // UI groups for manual ranges: [{id, from, to, dates:[...] }] + presence_live_header: true // Show live presence chips in header + , room_use_input_number: [] // per-room toggle: show/select input_number instead of climate (matches entities[] index) + , room_use_temp_sensor: [] // per-room toggle: use temp_sensors[eid] as room temperature instead of climate.current_temperature + }; + } + + connectedCallback(){ + // Attach minimal listeners to avoid re-render while typing in row inputs + try { + this.shadowRoot?.addEventListener('focusin', (e)=>{ + try { + const t = e.target; + if (!t) return; + // Suspend while editing any number input in row header + if ((t.tagName === 'INPUT' || t.tagName === 'HA-TEXTFIELD') && t.closest && t.closest('.row-head')) { + this._suspendRender = true; + } + } catch {} + }); + this.shadowRoot?.addEventListener('focusout', ()=>{ setTimeout(()=>{ this._suspendRender = false; }, 150); }); + } catch {} + } + + // ---------- Home Assistant hook ---------- + set hass(hass) { + this._hass = hass; this._lang = ttGetLangFromHass(hass); + // Storage is now file-based via API, no sensor auto-detection needed + + // (Select-based global profile removed) + + + // Detect Away mode changes and apply immediately + try { + const curAway = this._isAwayActive(); + if (this._lastAwayActive === null) { + this._lastAwayActive = curAway; + } else if (this._initialized && curAway !== this._lastAwayActive) { + this._lastAwayActive = curAway; + // Reset manual away bypass on any away enter/exit + this._awayBypass = false; + if (this._config?.auto_apply) { + // Apply immediately on Away enter/exit + this._applyCurrentSetpoints(true); + // When Sync is ON, nudge backend to apply right away as well + try { if (this._config?.storage_enabled) this._nudgeBackgroundApplyNow(); } catch {} + this._scheduleNextApply(); + } + } + } catch {} + + // Hent fra sensor hvis version ændret + // Detect Presence combo changes and apply immediately + try { + const curKey = this._activePresenceComboKey ? this._activePresenceComboKey() : null; + if (this._lastPresenceKey === null) { + this._lastPresenceKey = curKey; + } else if (this._initialized && curKey !== this._lastPresenceKey) { + this._lastPresenceKey = curKey; + if (this._config?.auto_apply) { + this._applyCurrentSetpoints(true); + try { if (this._config?.storage_enabled) this._nudgeBackgroundApplyNow(); } catch {} + this._scheduleNextApply(); + } + } + } catch {} + + // Detect per-room presence sensor transitions and apply immediately + // (also schedules timers so delayed ON/OFF can take effect without new HA events) + try { + const curSnap = this._presenceSensorActiveSnapshot ? this._presenceSensorActiveSnapshot() : {}; + if (this._lastPresenceSensorSnap == null) { + this._lastPresenceSensorSnap = curSnap; + } else if (this._initialized && this._presenceSensorSnapChanged && this._presenceSensorSnapChanged(this._lastPresenceSensorSnap, curSnap)) { + this._lastPresenceSensorSnap = curSnap; + if (this._config?.auto_apply) { + if (this._config?.storage_enabled) this._nudgeBackgroundApplyNow(); + else this._applyCurrentSetpoints(true, true); + this._scheduleNextApply(); + } + } else { + this._lastPresenceSensorSnap = curSnap; + } + try { this._schedulePresenceDelayTimer && this._schedulePresenceDelayTimer(); } catch {} + } catch {} + + // Detect pause sensor transitions (UI responsiveness + frontend-only behavior when Sync is OFF) + try { + const cur = this._isPausedBySensor ? !!this._isPausedBySensor() : false; + if (this._lastPauseSensorOn === null || this._lastPauseSensorOn === undefined) { + this._lastPauseSensorOn = cur; + } else if (this._initialized && cur !== this._lastPauseSensorOn) { + this._lastPauseSensorOn = cur; + try { if (!this._inlineEditing && !this._editing) this._render(); } catch {} + if (this._config?.auto_apply) { + if (cur) { + // Pause: cancel any pending boundary timer + try { this._scheduleNextApply(); } catch {} + } else { + // Resume: reconcile once + schedule next + this._applyCurrentSetpoints(true, true); + try { if (this._config?.storage_enabled) this._nudgeBackgroundApplyNow(); } catch {} + this._scheduleNextApply(); + } + } + } + } catch {} + + // Version checking is now done via API polling in _startStoreWatchTimer + + // When Sync is OFF, do not override local editor toggles (profiles_enabled) with sensor values. + // Visibility of the Manual schedule button is driven by local config and localStorage only. + + try { + if (this._config?.storage_enabled) this._startStoreWatchTimer(); + else if (this._storeWatchTimer) { clearInterval(this._storeWatchTimer); this._storeWatchTimer = null; } + } catch {} + + // Boiler control (frontend only when Sync is OFF) + try { this._setupBoilerControl(); } catch {} + + // (Removed select/state subscription; card updates via version change as before) + + if (!this._initialized) { + this._initialized = true; + this._init(); + this._loadStore(true).then(async () => { + this._ensureSchedules(); + // Ensure labels/merges/temp_sensors from backend are reflected in config before first render + try { await this._hydrateUiNamesFromBackend(); } catch {} + // Restore delayed sync countdown if previously scheduled + try { this._restoreSyncDue(); } catch {} + this._render(); + // Open deferred Holiday editor right after first render if flagged by editor + try { this._maybeOpenDeferredHoliday && this._maybeOpenDeferredHoliday(); } catch {} + // Ved load sætter vi korrekt setpoint én gang + this._applyCurrentSetpoints(true); + this._scheduleNextApply(); + // Push any YAML-provided settings to storage/local after first load + // but avoid resetting a pending delayed sync countdown on hard refresh + try { + const hasPending = Number(this._storeDelayDue||0) > Date.now() || !!localStorage.getItem(this._syncDueKey()); + if (!(this._config?.storage_enabled && this._config?.storage_sync_mode === 'delay' && hasPending)) { + this._debouncedSaveStore(); + } + } catch {} + // (Removed polling safety net) + }); + } else { + if (!this._inlineEditing && !this._editing) this._render(); + } + } + + // Ensure UI-friendly names (labels) are hydrated from backend if missing locally + async _hydrateUiNamesFromBackend(){ + try { + if (!this._config) return; + const hasAnyLabel = this._config.labels && typeof this._config.labels==='object' && Object.keys(this._config.labels).length>0; + const hasAnyMerge = this._config.merges && typeof this._config.merges==='object' && Object.keys(this._config.merges).length>0; + const hasAnySensors = this._config.temp_sensors && typeof this._config.temp_sensors==='object' && Object.keys(this._config.temp_sensors).length>0; + const hasAnyTurnOn = this._config.turn_on && typeof this._config.turn_on==='object' && Object.keys(this._config.turn_on).length>0; + if (hasAnyLabel && hasAnyMerge && hasAnySensors && hasAnyTurnOn) return; + const api = await this._apiFetchState(); + const s = api?.settings || {}; + let changed = false; + if (!hasAnyLabel && s.labels && typeof s.labels==='object' && Object.keys(s.labels).length){ this._config.labels = { ...s.labels }; changed = true; } + if (!hasAnyMerge && s.merges && typeof s.merges==='object' && Object.keys(s.merges).length){ this._config.merges = { ...s.merges }; changed = true; } + if (!hasAnySensors && s.temp_sensors && typeof s.temp_sensors==='object' && Object.keys(s.temp_sensors).length){ this._config.temp_sensors = { ...s.temp_sensors }; changed = true; } + if (!hasAnyTurnOn && s.turn_on && typeof s.turn_on==='object' && Object.keys(s.turn_on).length){ this._config.turn_on = { ...s.turn_on }; changed = true; } + if (changed){ + try { const payload = this._makeStoragePayload(true); localStorage.setItem(this._localStoreKey(), JSON.stringify(payload)); } catch {} + } + } catch {} + } + + // ---------- Config ---------- + setConfig(config) { + if (!config || !Array.isArray(config.entities)) throw new Error(ttLocalize('err.config_entities', navigator.language || 'en')); + const prevDefault = Number(this._config?.default_temp); + const yamlSetsDefault = Object.prototype.hasOwnProperty.call(config, 'default_temp'); + let rowh = Number(config.row_height ?? 64); rowh = isNaN(rowh) ? 64 : Math.max(40, Math.min(120, Math.round(rowh))); + let deft = Number(config.default_temp ?? 20); deft = isNaN(deft) ? 20 : Math.max(5, Math.min(35, deft)); + let nowms = Number(config.now_update_ms ?? 60000); nowms = isNaN(nowms) ? 60000 : Math.max(200, nowms); + let maxt = Number(config.max_temp ?? 25); maxt = isNaN(maxt) ? 25 : Math.max(5, Math.min(120, maxt)); + let mint = Number(config.min_temp ?? 5); mint = isNaN(mint) ? 5 : Math.max(0, Math.min(45, mint)); + // Boiler limits default to 20/25°C. Keep previous values when not provided. + const prevBMin = (this._config && Object.prototype.hasOwnProperty.call(this._config, 'boiler_min_temp')) ? this._config.boiler_min_temp : null; + const prevBMax = (this._config && Object.prototype.hasOwnProperty.call(this._config, 'boiler_max_temp')) ? this._config.boiler_max_temp : null; + const clampBoiler = (n)=>{ const v = Number(n); return Number.isFinite(v) ? Math.max(0, Math.min(120, v)) : null; }; + let boilerMin = (config.boiler_min_temp === undefined) + ? (Number.isFinite(Number(prevBMin)) ? Number(prevBMin) : 20) + : ((config.boiler_min_temp === null || config.boiler_min_temp === '') ? 20 : clampBoiler(config.boiler_min_temp)); + let boilerMax = (config.boiler_max_temp === undefined) + ? (Number.isFinite(Number(prevBMax)) ? Number(prevBMax) : 25) + : ((config.boiler_max_temp === null || config.boiler_max_temp === '') ? 25 : clampBoiler(config.boiler_max_temp)); + const showTop = Boolean(config.show_top_now ?? false); + let extendPx = Number(config.now_extend_px ?? 76); extendPx = isNaN(extendPx) ? 76 : Math.max(48, Math.min(140, Math.round(extendPx))); + + // Mark which keys were explicitly provided in YAML so storage won't + // override them on first hass() load + this._yamlProvided = { + labels: Object.prototype.hasOwnProperty.call(config, 'labels'), + merges: Object.prototype.hasOwnProperty.call(config, 'merges'), + color_ranges: Object.prototype.hasOwnProperty.call(config, 'color_ranges'), + default_temp: Object.prototype.hasOwnProperty.call(config, 'default_temp'), + min_temp: Object.prototype.hasOwnProperty.call(config, 'min_temp'), + max_temp: Object.prototype.hasOwnProperty.call(config, 'max_temp'), + temp_sensors: Object.prototype.hasOwnProperty.call(config, 'temp_sensors'), + turn_on: Object.prototype.hasOwnProperty.call(config, 'turn_on'), + // Boiler settings are storage-driven; do not lock them from Lovelace/YAML defaults. + boiler_enabled: false, + boiler_switch: false, + boiler_switch_domain: false, + boiler_rooms: false, + boiler_on_offset: false, + boiler_off_offset: false, + boiler_temp_sensor: false, + boiler_min_temp: false, + boiler_max_temp: false, + show_pause_button: Object.prototype.hasOwnProperty.call(config, 'show_pause_button'), + show_room_temp: Object.prototype.hasOwnProperty.call(config, 'show_room_temp'), + pause_sensor_enabled: Object.prototype.hasOwnProperty.call(config, 'pause_sensor_enabled'), + pause_sensor_entity: Object.prototype.hasOwnProperty.call(config, 'pause_sensor_entity'), + away: Object.prototype.hasOwnProperty.call(config, 'away'), + time_12h: Object.prototype.hasOwnProperty.call(config, 'time_12h'), + time_source: Object.prototype.hasOwnProperty.call(config, 'time_source'), + temp_unit: Object.prototype.hasOwnProperty.call(config, 'temp_unit'), + // Holiday settings are storage-driven; do not lock them from Lovelace/YAML defaults. + holidays_enabled: false, + holidays_source: false, + holidays_entity: false, + holidays_dates: false, + presence_live_header: false, + presence_sensor_enabled: Object.prototype.hasOwnProperty.call(config, 'presence_sensor_enabled'), + presence_sensors: Object.prototype.hasOwnProperty.call(config, 'presence_sensors'), + presence_sensor_temps: Object.prototype.hasOwnProperty.call(config, 'presence_sensor_temps'), + presence_sensor_delays: Object.prototype.hasOwnProperty.call(config, 'presence_sensor_delays'), + presence_sensor_delay_units: Object.prototype.hasOwnProperty.call(config, 'presence_sensor_delay_units'), + }; + + this._config = { + // If no explicit title, localize a default based on HA/browser language + title: (config.title !== undefined && config.title !== null) + ? String(config.title) + : ttLocalize('card.title_default', this._hass || this._lang || navigator.language || 'en'), + entities: config.entities, + row_height: rowh, + default_temp: deft, + max_temp: maxt, + min_temp: mint, + temp_unit: (config.temp_unit === undefined || config.temp_unit === null || config.temp_unit === 'auto') + ? (this._detectPreferF() ? 'F' : 'C') + : (String(config.temp_unit).toUpperCase()==='F'?'F':'C'), + time_12h: (config.time_12h === undefined || config.time_12h === null || config.time_12h === 'auto') + ? this._detectPrefer12h() + : !!config.time_12h, + time_source: (config.time_source === 'ha' ? 'ha' : 'browser'), + now_update_ms: nowms, + // File-based storage is always used - no sensor entity configuration needed + storage_enabled: !!(config.storage_enabled ?? true), + instance_enabled: !!(config.instance_enabled ?? false), + instance_id: String(config.instance_id ?? ''), + storage_sync_mode: (config.storage_sync_mode === 'delay' ? 'delay' : 'instant'), + storage_sync_min: Number.isFinite(config.storage_sync_min) ? Math.max(0, Math.min(1440, Number(config.storage_sync_min))) : (this._config?.storage_sync_min ?? 5), + storage_sync_sec: Number.isFinite(config.storage_sync_sec) + ? Math.max(5, Math.min(86400, Math.round(config.storage_sync_sec))) + : (Number.isFinite(config.storage_sync_min) ? Math.max(5, Math.min(86400, Math.round(Number(config.storage_sync_min)*60))) : (this._config?.storage_sync_sec ?? 15)), + backup_auto_enabled: !!(config.backup_auto_enabled ?? this._config?.backup_auto_enabled ?? false), + backup_interval_days: Number.isFinite(config.backup_interval_days) ? Math.max(1, Math.min(365, Math.round(config.backup_interval_days))) : (this._config?.backup_interval_days ?? 1), + show_top_now: showTop, + now_extend_px: extendPx, + show_pause_button: !!(config.show_pause_button ?? true), + show_room_temp: !!(config.show_room_temp ?? true), + pause_sensor_enabled: !!(config.pause_sensor_enabled ?? this._config?.pause_sensor_enabled ?? false), + pause_sensor_entity: String(config.pause_sensor_entity ?? this._config?.pause_sensor_entity ?? ''), + presence_sensor_enabled: !!(config.presence_sensor_enabled ?? this._config?.presence_sensor_enabled ?? false), + presence_sensors: { ...(config.presence_sensors || {}) }, + presence_sensor_temps: (()=>{ + try { + const src = (config.presence_sensor_temps && typeof config.presence_sensor_temps === 'object') ? config.presence_sensor_temps : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const v = Number(src[k]); + if (Number.isFinite(v)) out[String(k)] = v; + } + return out; + } catch { return {}; } + })(), + presence_sensor_delays: (()=>{ + try { + const src = (config.presence_sensor_delays && typeof config.presence_sensor_delays === 'object') ? config.presence_sensor_delays : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const v = src?.[k]; + if (!v || typeof v !== 'object') continue; + const onS = Number(v.on_s); + const offS = Number(v.off_s); + const obj = {}; + if (Number.isFinite(onS) && onS >= 0) obj.on_s = onS; + if (Number.isFinite(offS) && offS >= 0) obj.off_s = offS; + if (Object.keys(obj).length) out[String(k)] = obj; + } + return out; + } catch { return {}; } + })(), + presence_sensor_delay_units: (()=>{ + try { + const src = (config.presence_sensor_delay_units && typeof config.presence_sensor_delay_units === 'object') ? config.presence_sensor_delay_units : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const u = String(src[k] || '').toLowerCase().trim(); + if (u === 'seconds' || u === 'minutes') out[String(k)] = u; + } + return out; + } catch { return {}; } + })(), + auto_apply: config.auto_apply ?? true, + apply_on_edit: config.apply_on_edit ?? true, + apply_on_default_change: config.apply_on_default_change ?? true, + labels: { ...(config.labels || {}) }, + merges: { ...(config.merges || {}) }, + temp_sensors: { ...(config.temp_sensors || {}) }, + turn_on: { ...(config.turn_on || {}) }, + boiler_enabled: !!(config.boiler_enabled ?? this._config?.boiler_enabled ?? false), + boiler_switch: String(config.boiler_switch ?? this._config?.boiler_switch ?? ''), + boiler_switch_domain: (()=>{ + try { + const sw = String(config.boiler_switch ?? this._config?.boiler_switch ?? ''); + const fromEid = (()=>{ + const dom = String(sw||'').split('.')[0]; + return (dom === 'input_boolean') ? 'input_boolean' : 'switch'; + })(); + const raw = String((config.boiler_switch_domain ?? this._config?.boiler_switch_domain ?? fromEid) || '').toLowerCase().trim(); + return (raw === 'input_boolean') ? 'input_boolean' : 'switch'; + } catch { return 'switch'; } + })(), + boiler_rooms: (()=>{ + try { + const ents = Array.isArray(config.entities) ? config.entities.filter(Boolean).map(String) : []; + const climateRooms = ents.filter(e=>String(e).startsWith('climate.')); + const raw = Object.prototype.hasOwnProperty.call(config, 'boiler_rooms') + ? config.boiler_rooms + : (this._config?.boiler_rooms ?? null); + if (Array.isArray(raw)) { + const want = new Set(raw.filter(Boolean).map(String)); + return climateRooms.filter(eid => want.has(eid)); + } + return null; + } catch { return null; } + })(), + boiler_on_offset: (()=>{ + try { + const prev = Number(this._config?.boiler_on_offset); + const raw = (config.boiler_on_offset === undefined) ? prev : Number(config.boiler_on_offset); + const v = Number.isFinite(raw) ? raw : (Number.isFinite(prev) ? prev : 0); + return Math.max(-10, Math.min(10, v)); + } catch { return 0; } + })(), + boiler_off_offset: (()=>{ + try { + const prev = Number(this._config?.boiler_off_offset); + const raw = (config.boiler_off_offset === undefined) ? prev : Number(config.boiler_off_offset); + const v = Number.isFinite(raw) ? raw : (Number.isFinite(prev) ? prev : 0); + return Math.max(-10, Math.min(10, v)); + } catch { return 0; } + })(), + boiler_temp_sensor: String(config.boiler_temp_sensor ?? this._config?.boiler_temp_sensor ?? ''), + boiler_min_temp: boilerMin, + boiler_max_temp: boilerMax, + boiler_multi_enabled: !!(config.boiler_multi_enabled ?? this._config?.boiler_multi_enabled ?? false), + boiler_room_settings: (()=>{ + try { + const src = (config.boiler_room_settings && typeof config.boiler_room_settings === 'object') + ? config.boiler_room_settings + : (this._config?.boiler_room_settings || {}); + return JSON.parse(JSON.stringify(src || {})); + } catch { return {}; } + })(), + color_ranges: { ...(config.color_ranges || {}) }, + color_global: !!(config.color_global ?? this._config?.color_global ?? false), + per_room_defaults: !!(config.per_room_defaults ?? this._config?.per_room_defaults ?? false), + presence_live_header: !!(config.presence_live_header ?? this._config?.presence_live_header ?? true), + weekdays_enabled: !!(config.weekdays_enabled ?? false), + seasonal_enabled: !!(config.seasonal_enabled ?? this._config?.seasonal_enabled ?? false), + seasonal_mode: (String(config.seasonal_mode || this._config?.seasonal_mode || 'winter') === 'summer') ? 'summer' : 'winter', + weekdays_mode: (config.weekdays_mode || 'weekday_weekend'), + weekdays_view: (config.weekdays_view || this._config?.weekdays_view || 'all_rooms_one_day'), + weekdays_view_switch_in_timeline: !!(config.weekdays_view_switch_in_timeline ?? this._config?.weekdays_view_switch_in_timeline ?? false), + weekdays_selected_room: String(config.weekdays_selected_room || this._config?.weekdays_selected_room || ''), + profiles_enabled: !!(config.profiles_enabled ?? false), + away: { + enabled: !!(config.away?.enabled ?? false), + persons: [...(config.away?.persons || [])].filter(Boolean).map(String), + target_c: Number.isFinite(config.away?.target_c) ? Number(config.away.target_c) : 17, + advanced_enabled: !!(config.away?.advanced_enabled ?? this._config?.away?.advanced_enabled ?? false), + combos: { ...(config.away?.combos || this._config?.away?.combos || {}) }, + delay_enabled: !!(config.away?.delay_enabled ?? this._config?.away?.delay_enabled ?? false), + delay_value: Number.isFinite(config.away?.delay_value) ? Number(config.away.delay_value) : (this._config?.away?.delay_value ?? 0), + delay_unit: String(config.away?.delay_unit || this._config?.away?.delay_unit || 'minutes') + } + , holidays_enabled: !!(config.holidays_enabled ?? this._config?.holidays_enabled ?? false) + , holidays_source: (config.holidays_source || this._config?.holidays_source || 'calendar') + , holidays_entity: String(config.holidays_entity || this._config?.holidays_entity || '') + , holidays_dates: Array.isArray(config.holidays_dates) ? [...config.holidays_dates] : ([...(this._config?.holidays_dates || [])]) + , holidays_groups: Array.isArray(config.holidays_groups) ? [...config.holidays_groups] : ([...(this._config?.holidays_groups || [])]) + }; + + // Guard: Away/Presence should not be active until at least one person is selected. + // Keep the user's enabled toggle, but disable advanced features if no persons are set. + try { + const ap = Array.isArray(this._config?.away?.persons) ? this._config.away.persons.filter(Boolean).map(String) : []; + this._config.away.persons = ap; + if (!ap.length) { + this._config.away.advanced_enabled = false; + } + } catch {} + + // If YAML explicitly changed default_temp, update rows that still use the old + // default value (do not overwrite rooms with a user-specific override) + try { + if (yamlSetsDefault && Number.isFinite(prevDefault)) { + for (const eid of (this._config.entities || [])) { + const r = this._schedules[eid]; + if (!r) continue; + if (!Number.isFinite(r.defaultTemp) || Math.abs(Number(r.defaultTemp) - prevDefault) < 0.001) { + r.defaultTemp = this._config.default_temp; + } + } + } + } catch {} + + if (this._initialized) { + this._ensureSchedules(); + // Extra safety: if YAML changed default_temp, propagate to rows that + // still used the previous global default + try { + if (yamlSetsDefault && Number.isFinite(prevDefault)) { + for (const eid of (this._config.entities || [])) { + const r = this._schedules[eid]; + if (!r) continue; + if (!Number.isFinite(r.defaultTemp) || Math.abs(Number(r.defaultTemp) - prevDefault) < 0.001) { + r.defaultTemp = this._config.default_temp; + } + } + } + } catch {} + this._startUiTimer(); + if (!this._inlineEditing && !this._editing) this._render(); + this._scheduleNextApply(); + // Persist shared settings to storage so other users/dashboards pick them up + this._debouncedSaveStore(); + + // Boiler control (frontend only when Sync is OFF) + try { this._setupBoilerControl(); } catch {} + } + // Mark config as ready + this._hasConfig = true; + + // Boiler control (frontend only when Sync is OFF) + try { this._setupBoilerControl(); } catch {} + } + + getCardSize() { return 3 + (this._config?.entities?.length || 0) * 2; } + + // ---------- Lifecycle ---------- + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this._initialized = false; + this._config = ThermostatTimelineCard.getStubConfig(); + // Track whether storage was loaded from backend to avoid early overwrites. + this._storeLoaded = false; + this._storeLoadedFromApi = false; + // Guard: wait for setConfig before any apply logic + this._hasConfig = false; + // Track which YAML keys were explicitly provided so storage doesn't + // override them on first load + this._yamlProvided = {}; + + // Pop-up overlays (block editor, weekday editor, copy-to-rooms, onboarding) + // Enable by default so Weekday and Add buttons are visible + this._disablePopups = false; + // Disable the onboarding guide popup explicitly + this._disableOnboard = true; + + // In-card settings popup state (draft; only saved on Save button) + this._settingsPopupTab = 'settings'; + this._settingsDraft = null; + + // Data + this._schedules = {}; // { [entity_id]: { defaultTemp:number, blocks:[{id,startMin,endMin,temp}] } } + + // UI state + this._uiTimer = null; // kun til at flytte "nu"-stregen + this._transitionTimer = null; // timer til næste setpoint-ændring + this._active = null; // { entity, id } + this._lastTap = { t: 0, target: null }; + this._editing = null; // { entity, blockId } + // When a weekly block is opened from main timeline, auto-save the weekly draft on block save + this._autoSaveWeeklyFromTimeline = false; + // When a profile block is opened from main timeline, auto-save the profile draft on block save + this._autoSaveProfileFromTimeline = false; + + // Inline edit hard-lock + this._inlineEditing = false; // er et inline input aktivt? + this._inlineEntity = null; // hvilken entitys Standard-felt? + this._inlineTempDraft = null; // den værdi man er i gang med at skrive + + // Sync/debounce + this._saving = false; + this._saveTimer = null; + this._saveDebounceMs = 300; + // Delayed sync scheduler + this._storeDelayTimer = null; + this._storeDelayDue = 0; + // Defer writes while an editor is open to avoid rapid file churn + this._pendingSaveAfterEdit = false; + this._markPendingSave = ()=>{ this._pendingSaveAfterEdit = true; try { this._syncSeasonalRowsToBucket(); } catch {} try { this._persistDraftNow(); } catch {} try { this._render(); } catch {} }; + this._lastVersion = 0; + this._lastProfileVersion = 0; + // Polling watcher to pick up sensor updates in case frontend misses events + this._storeWatchTimer = null; + this._storeWatchMs = 10000; // ms + this._storeWatchBusy = false; // avoid overlapping polls + // Track if user explicitly touched boiler toggle this session + this._boilerToggleTouched = false; + // Track explicit edits in Holidays tab so background saves don't overwrite backend values. + this._holidaysSourceTouched = false; + this._holidaysEntityTouched = false; + this._holidaysDatesTouched = false; + this._holidaysGroupsTouched = false; + this._awayTouched = false; + + // Cross-card sync (same dashboard + other tabs). Needed when storage is OFF, + // because multiple cards otherwise won't notice localStorage changes. + this._broadcastId = `ttbc_${Date.now().toString(36)}_${Math.random().toString(36).slice(2,10)}`; + this._bc = null; + this._onStoreUpdated = (ev)=>{ + try { + const d = ev?.detail || {}; + if (!d || d.origin === this._broadcastId) return; + if (this._inlineEditing || this._editing) return; + if (this._isSettingsPopupOpen && this._isSettingsPopupOpen()) return; + const key = String(d.key || ''); + const myKey = this._localStoreKey(); + if (!key || !myKey || key !== myKey) return; + this._loadStore(false).then(()=>{ + try { this._ensureSchedules(); } catch {} + try { if (!this._inlineEditing && !this._editing) this._render(); } catch {} + try { this._scheduleNextApply(); } catch {} + }); + } catch {} + }; + try { window.addEventListener('tt-store-updated', this._onStoreUpdated); } catch {} + try { + if (typeof BroadcastChannel !== 'undefined') { + this._bc = new BroadcastChannel('thermostat_timeline_store_updated'); + this._bc.onmessage = (m)=>{ try { this._onStoreUpdated({ detail: m?.data }); } catch {} }; + } + } catch { this._bc = null; } + + // Boiler hysteresis control tick (frontend only when Sync is OFF) + this._boilerTimer = null; + + // Auto apply guard + this._lastApplied = {}; // { [entity_id]: { min:number, temp:number } } + this._lastAwayActive = null; // Track last away state to trigger immediate apply on change + this._lastPresenceKey = null; // Track last active presence combo key + // Track per-row tools open state + this._toolsOpen = {}; // { [entity_id]: boolean } + // Seasonal schedule state + this._seasonAppliedKey = null; // 'winter' | 'summer' | null + + // Copy/paste schedule state + this._copyFrom = null; // entity id of source room + this._copyPayload = null; // deep-cloned schedule from source + + // Weekday modal state + this._weeklyOpen = false; + this._weeklyEntity = null; + this._weeklyDraft = null; // { mode, days: { mon:[], ... } } + this._weeklyDayKey = null; + this._weeklyOverlayHiddenForEditor = false; + // Weekdays timeline view state (browse day when showing one day across rooms) + this._weekdaysViewDayKey = null; + // Weekly copy/paste by group (weekdays/weekend/sat/sun/mon..sun) + this._weeklyCopyFromGroup = null; + this._weeklyCopyPayload = null; + // Pending copy-to-rooms targets (applied on Save) + this._weeklyCopyTargets = []; + + // Profiles modal state (per-room named daily schedules) + this._profilesOpen = false; + this._profilesEntity = null; // entity id for which profiles are managed + this._profilesSelected = null; // currently selected profile name + this._profilesOverlayHiddenForEditor = false; // track if hidden while block editor is open + this._profilesDraft = null; // { name: string, blocks: [...], defaultTemp:number } + this._profilesDirty = false; + this._profilesSaveLock = false; // debounce guard to prevent double-prompt on Enter + this._profilesRoom = null; // current room eid being edited in profiles modal + + // Global profile (shared via integration settings) + this._globalProfile = null; + // Track settings version even when storage is off + this._lastSettingsVersion = 0; + this._lastColorsVersion = 0; + this._lastWeekdayVersion = 0; + // HA event subscription (state_changed for storage sensor) + this._haEvtUnsub = null; + this._subStorageEid = null; + + // Holidays modal state + this._holidayOpen = false; + this._holidayEntity = null; // deprecated (use _holidayRoom) + this._holidayRoom = null; // current room eid being edited in holiday modal + this._holidayDraft = null; // { rooms: { [eid]: [blocks] } } + this._holidayOverlayHiddenForEditor = false; + // Holiday copy popup + this._holidayCopyOpen = false; + this._holidayCopySel = null; // value string like 'main::main', 'weekday::mon', 'profile::Name', 'presence::key' + + // External refresh hook (legacy). Reload store so other cards update immediately. + this._onExternalRefresh = () => { + try { + if (this._inlineEditing || this._editing) return; + this._loadStore(false).then(()=>{ + try { this._ensureSchedules(); } catch {} + try { this._render(); } catch {} + try { this._scheduleNextApply(); } catch {} + }); + } catch {} + }; + try { window.addEventListener('thermostat-timeline-refresh', this._onExternalRefresh); } catch {} + // External request to open holiday editor for a specific room + this._onOpenHoliday = (ev)=>{ try { const eid = ev?.detail?.eid || (this._config?.entities||[])[0] || null; if (eid) { this._openHolidayEditor(eid); } } catch {} }; + try { window.addEventListener('tt-open-holiday', this._onOpenHoliday); } catch {} + // External request to open presence editor + this._onOpenPresence = (ev)=>{ try { const eid = ev?.detail?.eid || (this._config?.entities||[])[0] || null; if (eid) this._openPresenceEditor(eid); } catch {} }; + try { window.addEventListener('tt-open-presence', this._onOpenPresence); } catch {} + + // Onboarding state + this._onboardOpen = false; + this._onboardStep = 0; + this._onboardLastUrl = null; + this._onboardSlides = [ + // Supports video (.mp4/.webm) or images (.webp/.png/.gif) + { key: 'onboard.step_main', img: this._assetUrl('media/thermostat-guide/step1.mp4'), fallback: '/local/media/thermostat-guide/step1.mp4' }, + { key: 'onboard.step_add', img: this._assetUrl('media/thermostat-guide/step2.mp4'), fallback: '/local/media/thermostat-guide/step2.mp4' }, + { key: 'onboard.step_edit', img: this._assetUrl('media/thermostat-guide/step3.mp4'), fallback: '/local/media/thermostat-guide/step3.mp4' }, + ]; + + // Global pause state (temporary suppression of set_temperature) + this._pauseIndef = false; // boolean + this._pauseUntilMs = 0; // epoch ms; > now => paused until time + this._resumeTimer = null; // timer to auto-resume + this._countdownTimer = null; // 1s updater for header countdown + // Sync countdown UI (for delayed sync) + this._syncTimer = null; + this._syncJustUntil = 0; // ms timestamp to show "done" message briefly + // Away bypass (manual override while nobody is home) + this._awayBypass = false; // when true and away is active, do not clamp to away.target_c + // Header tools open/close state + this._headToolsOpen = false; + + // Presence (advanced away) modal state + this._presenceOpen = false; + this._presenceCombo = null; // active combo key selected for editing + this._presenceRoom = null; // current room eid being edited in presence modal + this._presenceDraft = null; // { combo: string, rooms: { [eid]: [blocks] } } + } + + disconnectedCallback() { + if (this._uiTimer) clearInterval(this._uiTimer); + if (this._transitionTimer) clearTimeout(this._transitionTimer); + if (this._saveTimer) clearTimeout(this._saveTimer); + if (this._storeWatchTimer) clearInterval(this._storeWatchTimer); + try { if (this._onExternalRefresh) window.removeEventListener('thermostat-timeline-refresh', this._onExternalRefresh); } catch {} + try { if (this._onStoreUpdated) window.removeEventListener('tt-store-updated', this._onStoreUpdated); } catch {} + try { if (this._bc && this._bc.close) this._bc.close(); } catch {} + this._bc = null; + try { if (this._onOpenHoliday) window.removeEventListener('tt-open-holiday', this._onOpenHoliday); } catch {} + try { if (this._onOpenPresence) window.removeEventListener('tt-open-presence', this._onOpenPresence); } catch {} + try { if (this._haEvtUnsub) { this._haEvtUnsub(); this._haEvtUnsub = null; } } catch {} + } + + _broadcastStoreUpdated(key){ + try { + const payload = { key: String(key || ''), origin: this._broadcastId, ts: Date.now() }; + try { window.dispatchEvent(new CustomEvent('tt-store-updated', { detail: payload })); } catch {} + try { if (this._bc && this._bc.postMessage) this._bc.postMessage(payload); } catch {} + } catch {} + } + + // Resolve a static asset URL so it works in both dev (/local) and HACS (/hacsfiles/) installs. + // Place assets next to the JS file in your HACS repo (e.g. ./media/...), + // or in Home Assistant's config/www (refer using media/...). + _assetUrl(p){ + try { + const rel = String(p||'').replace(/^[\/\\]+/, ''); + // Resolve relative to the current JS module location (works under /local and /hacsfiles) + return new URL(rel, import.meta.url).href; + } catch (e) { + try { return `/local/${String(p||'').replace(/^[\/\\]+/, '')}`; } catch { return String(p||''); } + } + } + + // ---------- Storage (file-based via API) ---------- + // Stub function for backwards compatibility - always returns empty string (no sensor entities) + _storageEntity(kind = 'schedules') { return ''; } + + _normInstanceId(raw){ + try { + const s = String(raw ?? '').trim(); + if (!s) return 'default'; + const out = []; + for (const ch of s) { + const ok = (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch === '_' || ch === '-' || ch === '.'; + out.push(ok ? ch : '_'); + } + const s2 = out.join('').slice(0, 64); + return s2 || 'default'; + } catch { return 'default'; } + } + + _instanceId(){ + try { + if (!this._config?.instance_enabled) return 'default'; + return this._normInstanceId(this._config?.instance_id || 'default'); + } catch { return 'default'; } + } + + _localStoreKey(){ + try { + const iid = this._instanceId(); + return (this._config?.instance_enabled) ? `thermostat_timeline_store:${iid}` : 'thermostat_timeline_store'; + } catch { return 'thermostat_timeline_store'; } + } + + _hardResetKey(){ + // Marker to suppress importing legacy/local drafts after a full factory reset. + return 'tt_hard_reset_ts'; + } + _hardResetActive(maxAgeMs = 10 * 60 * 1000){ + try { + const raw = localStorage.getItem(this._hardResetKey()) || ''; + const ts = Number(raw); + if (!Number.isFinite(ts) || ts <= 0) return false; + return (Date.now() - ts) >= 0 && (Date.now() - ts) <= maxAgeMs; + } catch { return false; } + } + + _hashStr(s){ + try { + let h = 5381; + const str = String(s ?? ''); + for (let i = 0; i < str.length; i++) { + h = ((h << 5) + h) ^ str.charCodeAt(i); + } + return (h >>> 0).toString(36); + } catch { return '0'; } + } + + _instanceCfgLocalKeyLegacy(){ + // Legacy v1 key (shared across cards with the same entities) + try { + const ents = Array.isArray(this._config?.entities) ? this._config.entities.filter(Boolean).map(String) : []; + const base = `v1|${ents.slice().sort().join(',')}`; + return `tt_instance_cfg:${this._hashStr(base)}`; + } catch { return 'tt_instance_cfg:0'; } + } + + _instanceCfgGroupHash(){ + // Used to group similar cards; keep aligned with legacy v1 base. + try { + const ents = Array.isArray(this._config?.entities) ? this._config.entities.filter(Boolean).map(String) : []; + const base = `v1|${ents.slice().sort().join(',')}`; + return this._hashStr(base); + } catch { return '0'; } + } + + _pageHash(){ + try { + const loc = (window?.location?.pathname || '') + (window?.location?.search || '') + (window?.location?.hash || ''); + return this._hashStr(loc); + } catch { return '0'; } + } + + _cardOrdinalInGroup(){ + try { + const grp = this._instanceCfgGroupHash(); + const root = (typeof this.getRootNode === 'function' ? this.getRootNode() : null); + const scope = (root && typeof root.querySelectorAll === 'function') ? root : document; + const all = Array.from(scope.querySelectorAll('thermostat-timeline-card') || []); + const same = []; + for (const el of all) { + try { + const g = (typeof el?._instanceCfgGroupHash === 'function') ? el._instanceCfgGroupHash() : null; + if (g === grp) same.push(el); + } catch {} + } + const idx = same.indexOf(this); + return idx >= 0 ? idx : 0; + } catch { return 0; } + } + + _instanceCfgLocalKey(){ + // v3: stable per-card key + try { + const uid = this._normInstanceId(this._config?.instance_uid || ''); + if (uid && uid !== 'default') return `tt_instance_cfg:v3:${uid}`; + // Fallback (should rarely happen) + const page = this._pageHash(); + const grp = this._instanceCfgGroupHash(); + const ord = this._cardOrdinalInGroup(); + return `tt_instance_cfg:v2:${page}:${grp}:${ord}`; + } catch { return 'tt_instance_cfg:v3:0'; } + } + + _instanceCfgLocalKeyV2(){ + try { + const page = this._pageHash(); + const grp = this._instanceCfgGroupHash(); + const ord = this._cardOrdinalInGroup(); + return `tt_instance_cfg:v2:${page}:${grp}:${ord}`; + } catch { return 'tt_instance_cfg:v2:0:0:0'; } + } + + _genInstanceId(){ + try { + const suffix = `${Date.now().toString(36)}_${Math.random().toString(36).slice(2,6)}`; + return this._normInstanceId(`thermostat_pro_timeline_${suffix}`); + } catch { + return this._normInstanceId(`thermostat_pro_timeline_${Math.random().toString(36).slice(2,10)}`); + } + } + + _loadPersistedInstanceCfg(){ + try { + const key = this._instanceCfgLocalKey(); + const raw = localStorage.getItem(key) || ''; + if (raw) { + const obj = JSON.parse(raw); + if (!obj || typeof obj !== 'object') return null; + const enabled = !!obj.enabled; + const id = this._normInstanceId(obj.id || ''); + return { enabled, id }; + } + + // v2 fallback (older per-card key) + try { + const k2 = this._instanceCfgLocalKeyV2(); + const r2 = localStorage.getItem(k2) || ''; + if (r2) { + const o2 = JSON.parse(r2); + const enabled2 = !!o2?.enabled; + const id2 = this._normInstanceId(o2?.id || ''); + if (enabled2 && id2) { + try { this._persistInstanceCfg(true, id2); } catch {} + return { enabled: true, id: id2 }; + } + } + } catch {} + + // Migration from legacy shared key (prevents two identical cards getting the same ID) + const legacyKey = this._instanceCfgLocalKeyLegacy(); + const legacyRaw = localStorage.getItem(legacyKey) || ''; + if (!legacyRaw) return null; + const objL = JSON.parse(legacyRaw); + if (!objL || typeof objL !== 'object') return null; + const enabledL = !!objL.enabled; + const idL = this._normInstanceId(objL.id || ''); + if (!enabledL || !idL) return null; + + const ord = this._cardOrdinalInGroup(); + if (ord === 0) { + try { this._persistInstanceCfg(true, idL); } catch {} + return { enabled: true, id: idL }; + } + + // For subsequent cards with identical config, auto-generate a new id. + const newId = this._genInstanceId(); + try { this._persistInstanceCfg(true, newId); } catch {} + try { this._copyInstanceData(idL, newId); } catch {} + return { enabled: true, id: newId }; + } catch { return null; } + } + + _persistInstanceCfg(enabled, id){ + try { + const obj = { enabled: !!enabled, id: this._normInstanceId(id || '') }; + localStorage.setItem(this._instanceCfgLocalKey(), JSON.stringify(obj)); + } catch {} + } + + async _copyInstanceData(oldId, newId){ + try { + const oldI = this._normInstanceId(oldId); + const newI = this._normInstanceId(newId); + if (!oldI || !newI || oldI === newI) return; + + // Backend store copy (keeps old instance intact) + if (this._config?.storage_enabled && this._hass?.callApi && this._hass?.callService) { + try { + const api = await this._hass.callApi('GET', `thermostat_timeline/state?instance_id=${encodeURIComponent(oldI)}`); + const payload = { force: true, instance_id: newI, activate: false }; + if (api?.schedules) payload.schedules = api.schedules; + if (api?.settings) payload.settings = api.settings; + if (api?.colors) payload.colors = api.colors; + if (api?.weekdays) payload.weekdays = api.weekdays; + if (api?.profiles) payload.profiles = api.profiles; + await this._hass.callService('thermostat_timeline', 'set_store', payload); + return; + } catch {} + } + + // Local-only copy + try { this._copyInstanceLocalKeys(oldI, newI); } catch {} + } catch {} + } + + async _renameInstanceData(oldId, newId){ + try { + const oldI = this._normInstanceId(oldId); + const newI = this._normInstanceId(newId); + if (!oldI || !newI || oldI === newI) return false; + + // Backend rename/move (does not create a second instance) + if (this._config?.storage_enabled && this._hass?.callService) { + try { + await this._hass.callService('thermostat_timeline', 'rename_instance', { + old_instance_id: oldI, + new_instance_id: newI, + }); + return true; + } catch {} + } + + // Local-only rename/move + try { return !!this._moveInstanceLocalKeys(oldI, newI); } catch {} + } catch {} + return false; + } + + _hydrateInstanceCfgFromLocal(){ + try { + const curEnabled = !!this._config?.instance_enabled; + const curId = String(this._config?.instance_id || '').trim(); + if (curEnabled && curId) { + this._persistInstanceCfg(true, curId); + return; + } + const persisted = this._loadPersistedInstanceCfg(); + if (persisted && persisted.enabled && persisted.id) { + this._config.instance_enabled = true; + this._config.instance_id = persisted.id; + } + } catch {} + } + + _apiSupported(){ try { return !!(this._hass?.callApi) || !!(this._hass?.auth?.data?.access_token); } catch { return false; } } + async _apiGet(path){ + const url = path.startsWith('/') ? path : `/api/${path}`; + // 1) Prefer Home Assistant's callApi when available + if (this._hass?.callApi) { + try { + return await this._hass.callApi('GET', path); + } catch (e) {} + } + // 2) Fallback: direct fetch with bearer token (mobile/webview safety) + try { + const token = this._hass?.auth?.data?.access_token; + const resp = await fetch(url, { + method: 'GET', + headers: token ? { Authorization: `Bearer ${token}` } : {}, + credentials: 'same-origin' + }); + if (!resp.ok) throw new Error(`HTTP ${resp.status}`); + return await resp.json(); + } catch (e2) { return null; } + } + async _apiFetchState(){ + try { + const iid = this._instanceId(); + const q = (this._config?.instance_enabled && iid) ? `?instance_id=${encodeURIComponent(iid)}` : ''; + return await this._apiGet(`thermostat_timeline/state${q}`); + } catch { + return await this._apiGet('thermostat_timeline/state'); + } + } + async _apiFetchVersion(){ return await this._apiGet('thermostat_timeline/version'); } + + async _loadStore(preferSensor = true) { + const hardReset = this._hardResetActive(); + let stMain = null, stSet = null, stCol = null, stWeek = null, stProf = null; + // Load from integration API (file-based storage) + if (this._config?.storage_enabled) { + const api = await this._apiFetchState(); + if (api && typeof api === 'object') { + stMain = { state: Number(api.version || 0), attributes: { schedules: api.schedules, weekdays: api.weekdays, profiles: api.profiles, settings: api.settings, colors: api.colors } }; + stSet = { state: Number(api.settings_version || api.version || 0), attributes: { settings: api.settings, colors: api.colors } }; + stCol = { state: Number(api.colors_version || api.version || 0), attributes: { colors: api.colors } }; + stWeek = { state: Number(api.weekday_version || api.version || 0), attributes: { weekdays: api.weekdays } }; + stProf = { state: Number(api.profile_version || api.version || 0), attributes: { profiles: api.profiles, settings: api.settings } }; + } + } + + if (stMain || stSet || stCol || stWeek || stProf) { + try { + const attrsMain = stMain?.attributes || {}; + const attrsSet = stSet?.attributes || {}; + const attrsCol = stCol?.attributes || {}; + const attrsWeek = stWeek?.attributes || {}; + const attrsProf = stProf?.attributes || {}; + let storUnit = 'C'; + const schedSource = (attrsMain?.schedules && typeof attrsMain.schedules === "object") ? attrsMain : ((attrsSet?.schedules && typeof attrsSet.schedules === 'object') ? attrsSet : null); + if (schedSource) { + // Unwrap if an older/incorrect payload put the entire object under 'schedules' + let sch = JSON.parse(JSON.stringify(schedSource.schedules)); + if (sch && sch.schedules && typeof sch.schedules === 'object') sch = sch.schedules; + // Canonical storage is °C. For backwards compatibility, detect legacy stores that saved °F values. + try { + const su = String((schedSource?.settings?.storage_temp_unit || '')).toUpperCase(); + const legacyDisplayUnit = String((schedSource?.settings?.temp_unit || 'C')).toUpperCase(); + const maxT = this._maxTempInSchedules(sch); + const looksLikeF = Number.isFinite(maxT) && maxT > 45; + const legacyWasF = (legacyDisplayUnit === 'F'); + const storeWasF = (su === 'F') || (!su && (legacyWasF || looksLikeF)); + if (storeWasF) sch = this._convertSchedulesTemps(sch, 'C'); + } catch {} + this._schedules = sch; + try { this._seasonAppliedKey = null; } catch {} + // If there is a pending delayed sync (due stored locally), prefer draft schedules + try { + if (hardReset) throw new Error('hard reset active'); + const dueRaw = localStorage.getItem(this._syncDueKey()) || ''; + const due = Number(dueRaw); + if (Number.isFinite(due) && due > Date.now()) { + const dr = localStorage.getItem(this._draftKey()) || ''; + if (dr) { + const parsed = JSON.parse(dr); + const dsch = parsed?.schedules && parsed.schedules.schedules ? parsed.schedules.schedules : (parsed?.schedules || parsed || {}); + if (dsch && typeof dsch === 'object') { + this._schedules = dsch; + try { this._seasonAppliedKey = null; } catch {} + } + } + } + } catch {} + } + // Merge weekday-only payload into schedules (weekly + weekly_modes) + try { + const weekSrc = (attrsWeek?.weekdays && typeof attrsWeek.weekdays === 'object') ? attrsWeek.weekdays + : ((attrsMain?.weekdays && typeof attrsMain.weekdays === 'object') ? attrsMain.weekdays : null); + if (weekSrc && typeof weekSrc === 'object') { + for (const eid of Object.keys(weekSrc)) { + const wk = weekSrc[eid]; + if (!wk || typeof wk !== 'object') continue; + const row = this._schedules[eid] || {}; + if (wk.weekly) row.weekly = wk.weekly; + if (wk.weekly_modes) row.weekly_modes = wk.weekly_modes; + this._schedules[eid] = row; + } + } + } catch {} + // Merge profile-only payload into schedules (profiles + activeProfile) + try { + let profSrc = null; + if (attrsProf?.profiles && typeof attrsProf.profiles === 'object') profSrc = attrsProf.profiles; + else if (attrsMain?.profiles && typeof attrsMain.profiles === 'object') profSrc = attrsMain.profiles; + if (profSrc && typeof profSrc === 'object') { + // Canonical storage is °C. For backwards compatibility, detect legacy °F profile payloads. + try { + const su = String((attrsProf?.settings?.storage_temp_unit || attrsMain?.settings?.storage_temp_unit || '')).toUpperCase(); + const legacyDisplayUnit = String((attrsProf?.settings?.temp_unit || attrsMain?.settings?.temp_unit || 'C')).toUpperCase(); + const maxT = this._maxTempInSchedules(profSrc); + const looksLikeF = Number.isFinite(maxT) && maxT > 45; + const legacyWasF = (legacyDisplayUnit === 'F'); + const storeWasF = (su === 'F') || (!su && (legacyWasF || looksLikeF)); + if (storeWasF) profSrc = this._convertSchedulesTemps(profSrc, 'C'); + } catch {} + for (const eid of Object.keys(profSrc)) { + const src = profSrc[eid]; + if (!src || typeof src !== 'object') continue; + const row = this._schedules[eid] || {}; + if (src.profiles && typeof src.profiles === 'object') row.profiles = src.profiles; + if (typeof src.activeProfile === 'string' || src.activeProfile === null) row.activeProfile = src.activeProfile; + this._schedules[eid] = row; + } + } + } catch {} + // Optional shared settings (prefer dedicated settings sensor when provided) + let settingsSource = null; + if (attrsSet.settings && typeof attrsSet.settings === 'object') settingsSource = attrsSet.settings; + else if (attrsMain.settings && typeof attrsMain.settings === 'object') settingsSource = attrsMain.settings; + try { if (storUnit === 'C' && settingsSource?.temp_unit) storUnit = String(settingsSource.temp_unit).toUpperCase(); } catch {} + let colorsSource = null; + if (attrsCol.colors && typeof attrsCol.colors === 'object') colorsSource = attrsCol.colors; + else if (attrsSet.colors && typeof attrsSet.colors === 'object') colorsSource = attrsSet.colors; + else if (settingsSource && (settingsSource.color_ranges || settingsSource.color_global)) { + colorsSource = { color_ranges: settingsSource.color_ranges, color_global: settingsSource.color_global }; + } + if (settingsSource) { + try { + const s0 = settingsSource; + // Canonical storage is °C. For backwards compatibility, detect legacy stores that saved numeric settings in °F. + let s = s0; + try { + const su = String(s0?.storage_temp_unit || '').toUpperCase(); + const legacyDisplayUnit = String(s0?.temp_unit||'C').toUpperCase(); + const nums = [s0?.default_temp, s0?.min_temp, s0?.max_temp, s0?.boiler_min_temp, s0?.boiler_max_temp, s0?.away?.target_c].map(v=>Number(v)).filter(v=>Number.isFinite(v)); + const maxN = nums.length ? Math.max(...nums) : -Infinity; + const looksLikeF = Number.isFinite(maxN) && maxN > 45; + const legacyWasF = (legacyDisplayUnit === 'F'); + const storeWasF = (su === 'F') || (!su && (legacyWasF || looksLikeF)); + if (storeWasF) { + const conv = (n)=>{ const v=Number(n); return Number.isFinite(v)? this._fToC(v): n; }; + const so = { ...s0 }; + if (so.default_temp !== undefined) so.default_temp = conv(so.default_temp); + if (so.min_temp !== undefined) so.min_temp = conv(so.min_temp); + if (so.max_temp !== undefined) so.max_temp = conv(so.max_temp); + if (so.boiler_min_temp !== undefined) so.boiler_min_temp = conv(so.boiler_min_temp); + if (so.boiler_max_temp !== undefined) so.boiler_max_temp = conv(so.boiler_max_temp); + if (so.away && typeof so.away==='object') { + so.away = { ...so.away }; + if (so.away.target_c !== undefined) so.away.target_c = conv(so.away.target_c); + } + try { + if (so.presence_sensor_temps && typeof so.presence_sensor_temps === 'object') { + const out = {}; + for (const k of Object.keys(so.presence_sensor_temps||{})) out[String(k)] = conv(so.presence_sensor_temps[k]); + so.presence_sensor_temps = out; + } + } catch {} + s = so; + } + } catch {} + // Pick up format/unit from shared storage when not YAML-provided + if (typeof s.time_12h === 'boolean' && !this._yamlProvided?.time_12h) this._config.time_12h = !!s.time_12h; + if (typeof s.temp_unit === 'string' && !this._yamlProvided?.temp_unit) this._config.temp_unit = (String(s.temp_unit).toUpperCase()==='F' ? 'F' : 'C'); + // Pick up time_source from shared storage when not YAML-provided + if (typeof s.time_source === 'string' && !this._yamlProvided?.time_source) this._config.time_source = (s.time_source === 'ha' ? 'ha' : 'browser'); + if (s.color_ranges && typeof s.color_ranges==='object' && !this._yamlProvided?.color_ranges) this._config.color_ranges = { ...s.color_ranges }; + if (typeof s.color_global === 'boolean') this._config.color_global = !!s.color_global; + if (Number.isFinite(s.default_temp)) this._config.default_temp = Number(s.default_temp); + if (Number.isFinite(s.min_temp)) this._config.min_temp = Number(s.min_temp); + if (Number.isFinite(s.max_temp)) this._config.max_temp = Number(s.max_temp); + if (Number.isFinite(s.row_height)) this._config.row_height = Math.max(30, Math.min(200, Math.round(Number(s.row_height)))); + + // Rooms from shared storage (used by Settings popup) + try { + if (Array.isArray(s.entities)) { + const ents = s.entities.filter(Boolean).map(String); + this._config.entities = ents; + const rawModes = Array.isArray(s.room_use_input_number) ? s.room_use_input_number.map(v=>!!v) : []; + while (rawModes.length < ents.length) rawModes.push(false); + this._config.room_use_input_number = ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return true; + if (String(eid).startsWith('climate.')) return false; + return !!rawModes[idx]; + }); + + const rawTemp = Array.isArray(s.room_use_temp_sensor) ? s.room_use_temp_sensor.map(v=>!!v) : []; + while (rawTemp.length < ents.length) rawTemp.push(false); + this._config.room_use_temp_sensor = ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return false; + if (!String(eid).startsWith('climate.')) return false; + return !!rawTemp[idx]; + }); + } + } catch {} + + if (s.merges && typeof s.merges === 'object' && !this._yamlProvided?.merges) this._config.merges = { ...s.merges }; + if (s.labels && typeof s.labels === 'object' && !this._yamlProvided?.labels) this._config.labels = { ...s.labels }; + if (s.temp_sensors && typeof s.temp_sensors === 'object' && !this._yamlProvided?.temp_sensors) this._config.temp_sensors = { ...s.temp_sensors }; + if (s.turn_on && typeof s.turn_on === 'object' && !this._yamlProvided?.turn_on) this._config.turn_on = { ...s.turn_on }; + if (s.presence_sensors && typeof s.presence_sensors === 'object' && !this._yamlProvided?.presence_sensors) this._config.presence_sensors = { ...s.presence_sensors }; + try { + if (s.presence_sensor_temps && typeof s.presence_sensor_temps === 'object' && !this._yamlProvided?.presence_sensor_temps) { + const out = {}; + for (const k of Object.keys(s.presence_sensor_temps||{})) { + const v = Number(s.presence_sensor_temps[k]); + if (Number.isFinite(v)) out[String(k)] = v; + } + this._config.presence_sensor_temps = out; + } + } catch {} + try { + if (s.presence_sensor_delays && typeof s.presence_sensor_delays === 'object' && !this._yamlProvided?.presence_sensor_delays) { + const out = {}; + for (const k of Object.keys(s.presence_sensor_delays||{})) { + const v = s.presence_sensor_delays?.[k]; + if (!v || typeof v !== 'object') continue; + const onS = Number(v.on_s); + const offS = Number(v.off_s); + const obj = {}; + if (Number.isFinite(onS) && onS >= 0) obj.on_s = onS; + if (Number.isFinite(offS) && offS >= 0) obj.off_s = offS; + if (Object.keys(obj).length) out[String(k)] = obj; + } + this._config.presence_sensor_delays = out; + } + } catch {} + + try { + if (s.presence_sensor_delay_units && typeof s.presence_sensor_delay_units === 'object' && !this._yamlProvided?.presence_sensor_delay_units) { + const out = {}; + for (const k of Object.keys(s.presence_sensor_delay_units||{})) { + const u = String(s.presence_sensor_delay_units?.[k] || '').toLowerCase().trim(); + out[String(k)] = (u === 'seconds') ? 'seconds' : 'minutes'; + } + this._config.presence_sensor_delay_units = out; + } + } catch {} + try { + if (typeof s.presence_live_header === 'boolean') { + this._config.presence_live_header = !!s.presence_live_header; + } + } catch {} + + // Fallbacks for Rooms tab settings if backend didn't persist them: + // Merge from local browser copy to avoid losing user edits (e.g., custom room names) + try { + if (hardReset) throw new Error('hard reset active'); + const rawLocal = localStorage.getItem(this._localStoreKey()) || ''; + if (rawLocal) { + const parsed = JSON.parse(rawLocal); + const ls = parsed?.settings || {}; + // Only merge when YAML did not provide the key and backend didn't send it + const isEmptyObj = (o)=>{ try { return o && typeof o==='object' && Object.keys(o).length===0; } catch { return false; } }; + if (!this._yamlProvided?.labels && (!s.labels || typeof s.labels !== 'object' || isEmptyObj(s.labels))) { + if (ls.labels && typeof ls.labels === 'object') { + this._config.labels = { ...(this._config.labels || {}), ...ls.labels }; + } + } + if (!this._yamlProvided?.merges && (!s.merges || typeof s.merges !== 'object' || isEmptyObj(s.merges))) { + if (ls.merges && typeof ls.merges === 'object') { + this._config.merges = { ...(this._config.merges || {}), ...ls.merges }; + } + } + if (!this._yamlProvided?.temp_sensors && (!s.temp_sensors || typeof s.temp_sensors !== 'object' || isEmptyObj(s.temp_sensors))) { + if (ls.temp_sensors && typeof ls.temp_sensors === 'object') { + this._config.temp_sensors = { ...(this._config.temp_sensors || {}), ...ls.temp_sensors }; + } + } + if (!this._yamlProvided?.turn_on && (!s.turn_on || typeof s.turn_on !== 'object' || isEmptyObj(s.turn_on))) { + if (ls.turn_on && typeof ls.turn_on === 'object') { + this._config.turn_on = { ...(this._config.turn_on || {}), ...ls.turn_on }; + } + } + if (!this._yamlProvided?.presence_sensors && (!s.presence_sensors || typeof s.presence_sensors !== 'object' || isEmptyObj(s.presence_sensors))) { + if (ls.presence_sensors && typeof ls.presence_sensors === 'object') { + this._config.presence_sensors = { ...(this._config.presence_sensors || {}), ...ls.presence_sensors }; + } + } + if (!this._yamlProvided?.presence_sensor_temps && (!s.presence_sensor_temps || typeof s.presence_sensor_temps !== 'object' || isEmptyObj(s.presence_sensor_temps))) { + if (ls.presence_sensor_temps && typeof ls.presence_sensor_temps === 'object') { + this._config.presence_sensor_temps = { ...(this._config.presence_sensor_temps || {}), ...ls.presence_sensor_temps }; + } + } + if (!this._yamlProvided?.presence_sensor_delays && (!s.presence_sensor_delays || typeof s.presence_sensor_delays !== 'object' || isEmptyObj(s.presence_sensor_delays))) { + if (ls.presence_sensor_delays && typeof ls.presence_sensor_delays === 'object') { + this._config.presence_sensor_delays = { ...(this._config.presence_sensor_delays || {}), ...ls.presence_sensor_delays }; + } + } + if (!this._yamlProvided?.presence_sensor_delay_units && (!s.presence_sensor_delay_units || typeof s.presence_sensor_delay_units !== 'object' || isEmptyObj(s.presence_sensor_delay_units))) { + if (ls.presence_sensor_delay_units && typeof ls.presence_sensor_delay_units === 'object') { + this._config.presence_sensor_delay_units = { ...(this._config.presence_sensor_delay_units || {}), ...ls.presence_sensor_delay_units }; + } + } + if (!this._yamlProvided?.presence_live_header && typeof s.presence_live_header !== 'boolean') { + if (typeof ls.presence_live_header === 'boolean') { + this._config.presence_live_header = !!ls.presence_live_header; + } + } + if (Array.isArray(this._config.entities) && (!Array.isArray(s.entities) || !s.entities.length)) { + if (Array.isArray(ls.entities) && ls.entities.length) { + this._config.entities = ls.entities.filter(Boolean).map(String); + // room_use_input_number alignment + const rawModes = Array.isArray(ls.room_use_input_number) ? ls.room_use_input_number.map(v=>!!v) : []; + while (rawModes.length < this._config.entities.length) rawModes.push(false); + this._config.room_use_input_number = this._config.entities.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return true; + if (String(eid).startsWith('climate.')) return false; + return !!rawModes[idx]; + }); + + const rawTemp = Array.isArray(ls.room_use_temp_sensor) ? ls.room_use_temp_sensor.map(v=>!!v) : []; + while (rawTemp.length < this._config.entities.length) rawTemp.push(false); + this._config.room_use_temp_sensor = this._config.entities.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return false; + if (!String(eid).startsWith('climate.')) return false; + return !!rawTemp[idx]; + }); + } + } + + // Boiler settings fallbacks: if backend omitted these keys, pick last known values from localStorage + try { + const hasBool = (v)=> (typeof v === 'boolean' || typeof v === 'number'); + if (!this._yamlProvided?.boiler_enabled && !hasBool(s.boiler_enabled)) { + if (hasBool(ls.boiler_enabled)) this._config.boiler_enabled = !!ls.boiler_enabled; + } + if (!this._yamlProvided?.boiler_switch && typeof s.boiler_switch !== 'string') { + if (typeof ls.boiler_switch === 'string') this._config.boiler_switch = String(ls.boiler_switch || ''); + } + if (!this._yamlProvided?.boiler_rooms && !Array.isArray(s.boiler_rooms) && s.boiler_rooms !== null) { + if (Array.isArray(ls.boiler_rooms)) this._config.boiler_rooms = ls.boiler_rooms.filter(Boolean).map(String); + else if (ls.boiler_rooms === null) this._config.boiler_rooms = null; + } + const numOrNull = (v)=> (v === null || Number.isFinite(Number(v))); + if (!this._yamlProvided?.boiler_on_offset && !numOrNull(s.boiler_on_offset)) { + if (numOrNull(ls.boiler_on_offset)) { + const v = (ls.boiler_on_offset === null) ? 0 : Number(ls.boiler_on_offset); + if (Number.isFinite(v)) this._config.boiler_on_offset = Math.max(-10, Math.min(10, v)); + } + } + if (!this._yamlProvided?.boiler_off_offset && !numOrNull(s.boiler_off_offset)) { + if (numOrNull(ls.boiler_off_offset)) { + const v = (ls.boiler_off_offset === null) ? 0 : Number(ls.boiler_off_offset); + if (Number.isFinite(v)) this._config.boiler_off_offset = Math.max(-10, Math.min(10, v)); + } + } + if (!this._yamlProvided?.boiler_temp_sensor && typeof s.boiler_temp_sensor !== 'string') { + if (typeof ls.boiler_temp_sensor === 'string') this._config.boiler_temp_sensor = String(ls.boiler_temp_sensor || ''); + } + if (!this._yamlProvided?.boiler_min_temp && !numOrNull(s.boiler_min_temp)) { + if (numOrNull(ls.boiler_min_temp)) { + const v = (ls.boiler_min_temp === null) ? 20 : Number(ls.boiler_min_temp); + if (Number.isFinite(v)) this._config.boiler_min_temp = v; + } + } + if (!this._yamlProvided?.boiler_max_temp && !numOrNull(s.boiler_max_temp)) { + if (numOrNull(ls.boiler_max_temp)) { + const v = (ls.boiler_max_temp === null) ? 25 : Number(ls.boiler_max_temp); + if (Number.isFinite(v)) this._config.boiler_max_temp = v; + } + } + if (!this._yamlProvided?.boiler_multi_enabled && !hasBool(s.boiler_multi_enabled)) { + if (hasBool(ls.boiler_multi_enabled)) this._config.boiler_multi_enabled = !!ls.boiler_multi_enabled; + } + if (!this._yamlProvided?.boiler_room_settings && (!s.boiler_room_settings || typeof s.boiler_room_settings !== 'object')) { + if (ls.boiler_room_settings && typeof ls.boiler_room_settings === 'object') { + try { this._config.boiler_room_settings = JSON.parse(JSON.stringify(ls.boiler_room_settings)); } catch { this._config.boiler_room_settings = { ...(ls.boiler_room_settings||{}) }; } + } + } + } catch {} + } + } catch {} + if ((typeof s.boiler_enabled === 'boolean' || typeof s.boiler_enabled === 'number') && !this._yamlProvided?.boiler_enabled) this._config.boiler_enabled = !!s.boiler_enabled; + if (typeof s.boiler_switch === 'string') { + const curSw = String(this._config?.boiler_switch || ''); + if (!this._yamlProvided?.boiler_switch || !curSw) this._config.boiler_switch = s.boiler_switch || ''; + } + try { + const rawDom = String(s.boiler_switch_domain || '').toLowerCase().trim(); + const domFromSetting = (rawDom === 'input_boolean') ? 'input_boolean' : 'switch'; + const swDom = String(s.boiler_switch || '').split('.')[0]; + const domFromSwitch = (swDom === 'input_boolean' || swDom === 'switch') ? swDom : domFromSetting; + const curSw = String(this._config?.boiler_switch || ''); + if (!this._yamlProvided?.boiler_switch_domain || !curSw) this._config.boiler_switch_domain = domFromSwitch; + } catch {} + if ((typeof s.boiler_multi_enabled === 'boolean' || typeof s.boiler_multi_enabled === 'number') && !this._yamlProvided?.boiler_multi_enabled) this._config.boiler_multi_enabled = !!s.boiler_multi_enabled; + if (s.boiler_room_settings && typeof s.boiler_room_settings === 'object' && !this._yamlProvided?.boiler_room_settings) { + try { this._config.boiler_room_settings = JSON.parse(JSON.stringify(s.boiler_room_settings)); } catch { this._config.boiler_room_settings = { ...(s.boiler_room_settings||{}) }; } + } + if (!this._yamlProvided?.boiler_rooms) { + if (Array.isArray(s.boiler_rooms)) this._config.boiler_rooms = s.boiler_rooms.filter(Boolean).map(String); + else if (s.boiler_rooms === null) this._config.boiler_rooms = null; + } + if (!this._yamlProvided?.boiler_on_offset) { + if (s.boiler_on_offset === null) this._config.boiler_on_offset = 0; + else { const v = Number(s.boiler_on_offset); if (Number.isFinite(v)) this._config.boiler_on_offset = Math.max(-10, Math.min(10, v)); } + } + if (!this._yamlProvided?.boiler_off_offset) { + if (s.boiler_off_offset === null) this._config.boiler_off_offset = 0; + else { const v = Number(s.boiler_off_offset); if (Number.isFinite(v)) this._config.boiler_off_offset = Math.max(-10, Math.min(10, v)); } + } + if (typeof s.boiler_temp_sensor === 'string' && !this._yamlProvided?.boiler_temp_sensor) this._config.boiler_temp_sensor = s.boiler_temp_sensor || ''; + if (!this._yamlProvided?.boiler_min_temp) { + if (s.boiler_min_temp === null) this._config.boiler_min_temp = 20; + else { const v = Number(s.boiler_min_temp); if (Number.isFinite(v)) this._config.boiler_min_temp = v; } + } + if (!this._yamlProvided?.boiler_max_temp) { + if (s.boiler_max_temp === null) this._config.boiler_max_temp = 25; + else { const v = Number(s.boiler_max_temp); if (Number.isFinite(v)) this._config.boiler_max_temp = v; } + } + try { this._awayBypass = !!s.away_bypass; } catch {} + if (typeof s.per_room_defaults === 'boolean') this._config.per_room_defaults = !!s.per_room_defaults; + if (typeof s.show_pause_button === 'boolean') this._config.show_pause_button = !!s.show_pause_button; + if (typeof s.show_room_temp === 'boolean' && !this._yamlProvided?.show_room_temp) this._config.show_room_temp = !!s.show_room_temp; + if (typeof s.pause_sensor_enabled === 'boolean' && !this._yamlProvided?.pause_sensor_enabled) this._config.pause_sensor_enabled = !!s.pause_sensor_enabled; + if (typeof s.pause_sensor_entity === 'string' && !this._yamlProvided?.pause_sensor_entity) this._config.pause_sensor_entity = String(s.pause_sensor_entity || ''); + if ((typeof s.presence_sensor_enabled === 'boolean' || typeof s.presence_sensor_enabled === 'number') && !this._yamlProvided?.presence_sensor_enabled) this._config.presence_sensor_enabled = !!s.presence_sensor_enabled; + if (typeof s.profiles_enabled === 'boolean' && !this._yamlProvided?.profiles_enabled) this._config.profiles_enabled = !!s.profiles_enabled; + if (typeof s.auto_apply_enabled === 'boolean') this._config.auto_apply = !!s.auto_apply_enabled; + if (typeof s.apply_on_edit === 'boolean') this._config.apply_on_edit = !!s.apply_on_edit; + if (typeof s.apply_on_default_change === 'boolean') this._config.apply_on_default_change = !!s.apply_on_default_change; + // Final fallback: if backend omitted or defaulted boiler settings, prefer last local copy to keep UI consistent after hard refresh + try { + const rawLocal2 = localStorage.getItem(this._localStoreKey()) || ''; + if (rawLocal2) { + const parsed2 = JSON.parse(rawLocal2); + const ls2 = parsed2?.settings || {}; + const has = (obj, k)=> Object.prototype.hasOwnProperty.call(obj||{}, k); + if (!this._yamlProvided?.boiler_enabled && has(ls2, 'boiler_enabled')) this._config.boiler_enabled = !!ls2.boiler_enabled; + if (!this._yamlProvided?.boiler_switch && has(ls2, 'boiler_switch')) this._config.boiler_switch = String(ls2.boiler_switch || ''); + if (!this._yamlProvided?.boiler_rooms && (has(ls2, 'boiler_rooms'))) { + if (Array.isArray(ls2.boiler_rooms)) this._config.boiler_rooms = ls2.boiler_rooms.filter(Boolean).map(String); + else if (ls2.boiler_rooms === null) this._config.boiler_rooms = null; + } + if (!this._yamlProvided?.boiler_on_offset && has(ls2, 'boiler_on_offset')) { + const v = (ls2.boiler_on_offset === null) ? 0 : Number(ls2.boiler_on_offset); + if (Number.isFinite(v)) this._config.boiler_on_offset = Math.max(-10, Math.min(10, v)); + } + if (!this._yamlProvided?.boiler_off_offset && has(ls2, 'boiler_off_offset')) { + const v = (ls2.boiler_off_offset === null) ? 0 : Number(ls2.boiler_off_offset); + if (Number.isFinite(v)) this._config.boiler_off_offset = Math.max(-10, Math.min(10, v)); + } + if (!this._yamlProvided?.boiler_temp_sensor && has(ls2, 'boiler_temp_sensor')) this._config.boiler_temp_sensor = String(ls2.boiler_temp_sensor || ''); + if (!this._yamlProvided?.boiler_min_temp && has(ls2, 'boiler_min_temp')) { + const v = (ls2.boiler_min_temp === null) ? 20 : Number(ls2.boiler_min_temp); + if (Number.isFinite(v)) this._config.boiler_min_temp = v; + } + if (!this._yamlProvided?.boiler_max_temp && has(ls2, 'boiler_max_temp')) { + const v = (ls2.boiler_max_temp === null) ? 25 : Number(ls2.boiler_max_temp); + if (Number.isFinite(v)) this._config.boiler_max_temp = v; + } + if (!this._yamlProvided?.boiler_multi_enabled && has(ls2, 'boiler_multi_enabled')) this._config.boiler_multi_enabled = !!ls2.boiler_multi_enabled; + if (!this._yamlProvided?.boiler_room_settings && has(ls2, 'boiler_room_settings')) { + try { this._config.boiler_room_settings = JSON.parse(JSON.stringify(ls2.boiler_room_settings)); } catch { this._config.boiler_room_settings = { ...(ls2.boiler_room_settings||{}) }; } + } + } + } catch {} + // Open Window Detection from shared storage + try { if (s.open_window && typeof s.open_window === 'object') this._config.open_window = { ...s.open_window }; } catch {} + // Holidays shared settings + try { + if (typeof s.holidays_enabled === 'boolean') this._config.holidays_enabled = !!s.holidays_enabled; + if (typeof s.holidays_source === 'string' && !this._yamlProvided?.holidays_source) this._config.holidays_source = s.holidays_source === 'manual' ? 'manual' : 'calendar'; + if (typeof s.holidays_entity === 'string' && !this._yamlProvided?.holidays_entity) this._config.holidays_entity = s.holidays_entity || ''; + if (Array.isArray(s.holidays_dates) && !this._yamlProvided?.holidays_dates) this._config.holidays_dates = [...s.holidays_dates]; + if (Array.isArray(s.holidays_groups)) this._config.holidays_groups = [...s.holidays_groups]; + } catch {} + // Weekdays settings (shared) + if (this._config?.respect_storage_weekdays !== false) { + try { if (typeof s.weekdays_enabled === 'boolean') this._config.weekdays_enabled = !!s.weekdays_enabled; } catch {} + try { if (typeof s.weekdays_mode === 'string') this._config.weekdays_mode = s.weekdays_mode; } catch {} + try { if (typeof s.seasonal_enabled === 'boolean') this._config.seasonal_enabled = !!s.seasonal_enabled; } catch {} + try { if (typeof s.seasonal_mode === 'string') this._config.seasonal_mode = (s.seasonal_mode === 'summer' ? 'summer' : 'winter'); } catch {} + try { if (typeof s.weekdays_view === 'string') this._config.weekdays_view = s.weekdays_view; } catch {} + try { if (typeof s.weekdays_view_switch_in_timeline === 'boolean') this._config.weekdays_view_switch_in_timeline = !!s.weekdays_view_switch_in_timeline; } catch {} + try { if (typeof s.weekdays_selected_room === 'string') this._config.weekdays_selected_room = s.weekdays_selected_room; } catch {} + } + // Pick up a global profile selection from shared storage + try { this._globalProfile = (typeof s.global_profile === 'string' && s.global_profile) ? String(s.global_profile) : null; } catch {} + // Backup settings + try { + if (typeof s.backup_auto_enabled === 'boolean') this._config.backup_auto_enabled = !!s.backup_auto_enabled; + if (Number.isFinite(s.backup_interval_days)) this._config.backup_interval_days = Math.max(1, Math.min(365, Math.round(Number(s.backup_interval_days)))); + else if (Number.isFinite(s.backup_interval_min)) this._config.backup_interval_days = Math.max(1, Math.min(365, Math.ceil(Number(s.backup_interval_min)/1440))); + } catch {} + if (s.away && typeof s.away === 'object') { + try { + const a = s.away; + this._config.away = { + enabled: !!a.enabled, + persons: Array.isArray(a.persons) ? [...a.persons].filter(Boolean).map(String) : [], + target_c: Number.isFinite(a.target_c) ? Number(a.target_c) : (this._config.away?.target_c ?? 17), + advanced_enabled: !!(a.advanced_enabled ?? this._config?.away?.advanced_enabled ?? false), + combos: (a.combos && typeof a.combos === 'object') ? { ...a.combos } : (this._config?.away?.combos || {}), + delay_enabled: !!(a.delay_enabled ?? this._config?.away?.delay_enabled ?? false), + delay_value: Number.isFinite(a.delay_value) ? Number(a.delay_value) : (this._config?.away?.delay_value ?? 0), + delay_unit: String(a.delay_unit || this._config?.away?.delay_unit || 'minutes') + }; + } catch {} + } + // Guard: Away/Presence should not be active until at least one person is selected. + // Keep the enabled toggle; only disable advanced features when no persons are set. + try { + const ap = Array.isArray(this._config?.away?.persons) ? this._config.away.persons.filter(Boolean).map(String) : []; + this._config.away.persons = ap; + if (!ap.length) { this._config.away.advanced_enabled = false; } + } catch {} + // Pause flags from shared storage + try { + this._pauseIndef = !!s.pause_indef; + const pu = Number(s.pause_until_ms); + this._pauseUntilMs = Number.isFinite(pu) ? pu : 0; + } catch {} + // Delayed sync due time from shared storage (persist across reloads) + try { + const due = Number(s.sync_pending_until_ms); + if (Number.isFinite(due) && due > Date.now()) { + this._storeDelayDue = due; + try { localStorage.setItem(this._syncDueKey(), String(due)); } catch {} + // While a delayed sync is pending, prefer local browser draft for settings that affect UI immediately + try { + if (hardReset) throw new Error('hard reset active'); + const rawLocal = localStorage.getItem(this._localStoreKey()) || ''; + if (rawLocal) { + const parsed = JSON.parse(rawLocal); + const ls = parsed?.settings || {}; + if (typeof ls.holidays_enabled === 'boolean') this._config.holidays_enabled = !!ls.holidays_enabled; + if (typeof ls.holidays_source === 'string') this._config.holidays_source = (ls.holidays_source === 'manual') ? 'manual' : 'calendar'; + if (typeof ls.holidays_entity === 'string') this._config.holidays_entity = String(ls.holidays_entity||''); + if (Array.isArray(ls.holidays_dates)) this._config.holidays_dates = [...ls.holidays_dates]; + if (Array.isArray(ls.holidays_groups)) this._config.holidays_groups = [...ls.holidays_groups]; + } + } catch {} + } + } catch {} + } catch {} + } + if (colorsSource) { + try { + if (colorsSource.color_ranges && typeof colorsSource.color_ranges==='object' && !this._yamlProvided?.color_ranges) this._config.color_ranges = { ...colorsSource.color_ranges }; + if (typeof colorsSource.color_global === 'boolean') this._config.color_global = !!colorsSource.color_global; + } catch {} + } + this._lastVersion = Number(stMain?.state || stSet?.state || 0) || 0; + this._lastSettingsVersion = Number(stSet?.state || stMain?.state || 0) || this._lastVersion; + this._lastColorsVersion = Number(stCol?.state || this._lastSettingsVersion || this._lastVersion || 0) || 0; + this._lastWeekdayVersion = Number(stWeek?.state || this._lastVersion || 0) || 0; + this._lastProfileVersion = Number(stProf?.state || this._lastVersion || 0) || 0; + try { const payload = this._makeStoragePayload(true); localStorage.setItem(this._localStoreKey(), JSON.stringify(payload)); } catch {} + this._storeLoaded = true; + this._storeLoadedFromApi = true; + return; + } catch (e) { /* fallback */ } + } + try { + // During a hard reset window: do not import any local browser state. + if (hardReset) { this._schedules = {}; return; } + + let raw = localStorage.getItem(this._localStoreKey()) || ""; + // Migration fallback: if instances are enabled and no per-instance cache exists yet, reuse legacy cache. + if (!raw && this._config?.instance_enabled) { + const legacy = localStorage.getItem('thermostat_timeline_store') || ''; + if (legacy) { + raw = legacy; + try { localStorage.setItem(this._localStoreKey(), legacy); } catch {} + } + } + if (!raw) { this._schedules = {}; return; } + const parsed = JSON.parse(raw); + if (parsed && typeof parsed === 'object' && parsed.schedules) { + // Unwrap legacy nested structure if present + const sch = parsed.schedules && parsed.schedules.schedules ? parsed.schedules.schedules : parsed.schedules; + // If local copy was stored as °F, convert to °C for internal use + try { + const unit = String(parsed?.settings?.temp_unit||'C').toUpperCase(); + this._schedules = (unit==='F') ? this._convertSchedulesTemps(sch||{}, 'C') : (sch||{}); + } catch { this._schedules = sch || {}; } + try { + const wk = parsed.weekdays; + if (wk && typeof wk === 'object') { + for (const eid of Object.keys(wk)) { + const row = this._schedules[eid] || {}; + const src = wk[eid]; + if (src && typeof src === 'object') { + if (src.weekly) row.weekly = src.weekly; + if (src.weekly_modes) row.weekly_modes = src.weekly_modes; + this._schedules[eid] = row; + } + } + } + } catch {} + try { + let prof = parsed.profiles; + const unit = String(parsed?.settings?.temp_unit||'C').toUpperCase(); + if (prof && typeof prof === 'object' && unit === 'F') { + prof = this._convertSchedulesTemps(prof, 'C'); + } + if (prof && typeof prof === 'object') { + for (const eid of Object.keys(prof)) { + const src = prof[eid]; + if (!src || typeof src !== 'object') continue; + const row = this._schedules[eid] || {}; + if (src.profiles && typeof src.profiles === 'object') row.profiles = src.profiles; + if (Object.prototype.hasOwnProperty.call(src, 'activeProfile')) row.activeProfile = src.activeProfile; + this._schedules[eid] = row; + } + } + } catch {} + const s0 = parsed.settings || {}; + // Convert numeric settings from °F to °C for internal use + let s = s0; + try { + const unit = String(s0?.temp_unit||'C').toUpperCase(); + if (unit==='F') { + const conv = (n)=>{ const v=Number(n); return Number.isFinite(v)? this._fToC(v): n; }; + const so = { ...s0 }; + if (so.default_temp !== undefined) so.default_temp = conv(so.default_temp); + if (so.min_temp !== undefined) so.min_temp = conv(so.min_temp); + if (so.max_temp !== undefined) so.max_temp = conv(so.max_temp); + if (so.boiler_min_temp !== undefined) so.boiler_min_temp = conv(so.boiler_min_temp); + if (so.boiler_max_temp !== undefined) so.boiler_max_temp = conv(so.boiler_max_temp); + if (so.away && typeof so.away==='object') { so.away = { ...so.away }; if (so.away.target_c !== undefined) so.away.target_c = conv(so.away.target_c); } + try { + if (so.presence_sensor_temps && typeof so.presence_sensor_temps === 'object') { + const out = {}; + for (const k of Object.keys(so.presence_sensor_temps||{})) out[String(k)] = conv(so.presence_sensor_temps[k]); + so.presence_sensor_temps = out; + } + } catch {} + s = so; + } + } catch {} + // Pick up format/unit from local browser storage when not YAML-provided + if (typeof s.time_12h === 'boolean' && !this._yamlProvided?.time_12h) this._config.time_12h = !!s.time_12h; + if (typeof s.temp_unit === 'string' && !this._yamlProvided?.temp_unit) this._config.temp_unit = (String(s.temp_unit).toUpperCase()==='F' ? 'F' : 'C'); + if (s.color_ranges && typeof s.color_ranges==='object' && !this._yamlProvided?.color_ranges) this._config.color_ranges = { ...s.color_ranges }; + if (typeof s.color_global === 'boolean') this._config.color_global = !!s.color_global; + if (Number.isFinite(s.default_temp) && !this._yamlProvided?.default_temp) this._config.default_temp = Number(s.default_temp); + if (Number.isFinite(s.min_temp) && !this._yamlProvided?.min_temp) this._config.min_temp = Number(s.min_temp); + if (Number.isFinite(s.max_temp) && !this._yamlProvided?.max_temp) this._config.max_temp = Number(s.max_temp); + + // Rooms from local browser storage (used by Settings popup) + try { + if (Array.isArray(s.entities)) { + const ents = s.entities.filter(Boolean).map(String); + this._config.entities = ents; + const rawModes = Array.isArray(s.room_use_input_number) ? s.room_use_input_number.map(v=>!!v) : []; + while (rawModes.length < ents.length) rawModes.push(false); + this._config.room_use_input_number = ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return true; + if (String(eid).startsWith('climate.')) return false; + return !!rawModes[idx]; + }); + + const rawTemp = Array.isArray(s.room_use_temp_sensor) ? s.room_use_temp_sensor.map(v=>!!v) : []; + while (rawTemp.length < ents.length) rawTemp.push(false); + this._config.room_use_temp_sensor = ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return false; + if (!String(eid).startsWith('climate.')) return false; + return !!rawTemp[idx]; + }); + } + } catch {} + + if (s.merges && typeof s.merges==='object' && !this._yamlProvided?.merges) this._config.merges = { ...s.merges }; + if (s.labels && typeof s.labels==='object' && !this._yamlProvided?.labels) this._config.labels = { ...s.labels }; + if (s.temp_sensors && typeof s.temp_sensors==='object' && !this._yamlProvided?.temp_sensors) this._config.temp_sensors = { ...s.temp_sensors }; + if (s.presence_sensors && typeof s.presence_sensors==='object' && !this._yamlProvided?.presence_sensors) this._config.presence_sensors = { ...s.presence_sensors }; + try { + if (s.presence_sensor_temps && typeof s.presence_sensor_temps === 'object' && !this._yamlProvided?.presence_sensor_temps) { + const out = {}; + for (const k of Object.keys(s.presence_sensor_temps||{})) { + const v = Number(s.presence_sensor_temps[k]); + if (Number.isFinite(v)) out[String(k)] = v; + } + this._config.presence_sensor_temps = out; + } + } catch {} + try { + if (s.presence_sensor_delays && typeof s.presence_sensor_delays === 'object' && !this._yamlProvided?.presence_sensor_delays) { + const out = {}; + for (const k of Object.keys(s.presence_sensor_delays||{})) { + const v = s.presence_sensor_delays?.[k]; + if (!v || typeof v !== 'object') continue; + const onS = Number(v.on_s); + const offS = Number(v.off_s); + const obj = {}; + if (Number.isFinite(onS) && onS >= 0) obj.on_s = onS; + if (Number.isFinite(offS) && offS >= 0) obj.off_s = offS; + if (Object.keys(obj).length) out[String(k)] = obj; + } + this._config.presence_sensor_delays = out; + } + } catch {} + try { + if (s.presence_sensor_delay_units && typeof s.presence_sensor_delay_units === 'object' && !this._yamlProvided?.presence_sensor_delay_units) { + const out = {}; + for (const k of Object.keys(s.presence_sensor_delay_units||{})) { + const u = String(s.presence_sensor_delay_units?.[k] || '').toLowerCase().trim(); + out[String(k)] = (u === 'seconds') ? 'seconds' : 'minutes'; + } + this._config.presence_sensor_delay_units = out; + } + } catch {} + if ((typeof s.boiler_enabled === 'boolean' || typeof s.boiler_enabled === 'number') && !this._yamlProvided?.boiler_enabled) this._config.boiler_enabled = !!s.boiler_enabled; + if (typeof s.boiler_switch === 'string') { + const curSw = String(this._config?.boiler_switch || ''); + if (!this._yamlProvided?.boiler_switch || !curSw) this._config.boiler_switch = s.boiler_switch || ''; + } + try { + const rawDom = String(s.boiler_switch_domain || '').toLowerCase().trim(); + const domFromSetting = (rawDom === 'input_boolean') ? 'input_boolean' : 'switch'; + const swDom = String(s.boiler_switch || '').split('.')[0]; + const domFromSwitch = (swDom === 'input_boolean' || swDom === 'switch') ? swDom : domFromSetting; + const curSw = String(this._config?.boiler_switch || ''); + if (!this._yamlProvided?.boiler_switch_domain || !curSw) this._config.boiler_switch_domain = domFromSwitch; + } catch {} + if (!this._yamlProvided?.boiler_rooms) { + if (Array.isArray(s.boiler_rooms)) this._config.boiler_rooms = s.boiler_rooms.filter(Boolean).map(String); + else if (s.boiler_rooms === null) this._config.boiler_rooms = null; + } + if (!this._yamlProvided?.boiler_on_offset) { + if (s.boiler_on_offset === null) this._config.boiler_on_offset = 0; + else { const v = Number(s.boiler_on_offset); if (Number.isFinite(v)) this._config.boiler_on_offset = Math.max(-10, Math.min(10, v)); } + } + if (!this._yamlProvided?.boiler_off_offset) { + if (s.boiler_off_offset === null) this._config.boiler_off_offset = 0; + else { const v = Number(s.boiler_off_offset); if (Number.isFinite(v)) this._config.boiler_off_offset = Math.max(-10, Math.min(10, v)); } + } + if (typeof s.boiler_temp_sensor === 'string' && !this._yamlProvided?.boiler_temp_sensor) this._config.boiler_temp_sensor = s.boiler_temp_sensor || ''; + if (!this._yamlProvided?.boiler_min_temp) { + if (s.boiler_min_temp === null) this._config.boiler_min_temp = 20; + else { const v = Number(s.boiler_min_temp); if (Number.isFinite(v)) this._config.boiler_min_temp = v; } + } + if (!this._yamlProvided?.boiler_max_temp) { + if (s.boiler_max_temp === null) this._config.boiler_max_temp = 25; + else { const v = Number(s.boiler_max_temp); if (Number.isFinite(v)) this._config.boiler_max_temp = v; } + } + try { this._awayBypass = !!s.away_bypass; } catch {} + // Open Window Detection from local storage copy + try { if (s.open_window && typeof s.open_window==='object') this._config.open_window = { ...s.open_window }; } catch {} + // Weekdays flags and view (persist across refresh) — skip if editor preview forbids overrides + if (this._config?.respect_storage_weekdays !== false) { + try { if (typeof s.weekdays_enabled === 'boolean') this._config.weekdays_enabled = !!s.weekdays_enabled; } catch {} + try { if (typeof s.weekdays_mode === 'string') this._config.weekdays_mode = s.weekdays_mode; } catch {} + try { if (typeof s.seasonal_enabled === 'boolean') this._config.seasonal_enabled = !!s.seasonal_enabled; } catch {} + try { if (typeof s.seasonal_mode === 'string') this._config.seasonal_mode = (s.seasonal_mode === 'summer' ? 'summer' : 'winter'); } catch {} + try { if (typeof s.weekdays_view === 'string') this._config.weekdays_view = s.weekdays_view; } catch {} + try { if (typeof s.weekdays_view_switch_in_timeline === 'boolean') this._config.weekdays_view_switch_in_timeline = !!s.weekdays_view_switch_in_timeline; } catch {} + try { if (typeof s.weekdays_selected_room === 'string') this._config.weekdays_selected_room = s.weekdays_selected_room; } catch {} + } + if (typeof s.per_room_defaults === 'boolean') this._config.per_room_defaults = !!s.per_room_defaults; + if (typeof s.show_pause_button === 'boolean' && !this._yamlProvided?.show_pause_button) this._config.show_pause_button = !!s.show_pause_button; + if (typeof s.show_room_temp === 'boolean' && !this._yamlProvided?.show_room_temp) this._config.show_room_temp = !!s.show_room_temp; + if (typeof s.pause_sensor_enabled === 'boolean' && !this._yamlProvided?.pause_sensor_enabled) this._config.pause_sensor_enabled = !!s.pause_sensor_enabled; + if (typeof s.pause_sensor_entity === 'string' && !this._yamlProvided?.pause_sensor_entity) this._config.pause_sensor_entity = String(s.pause_sensor_entity || ''); + if (s.away && typeof s.away === 'object') { + try { + const a = s.away; + this._config.away = { + enabled: !!a.enabled, + persons: Array.isArray(a.persons) ? [...a.persons].filter(Boolean).map(String) : [], + target_c: Number.isFinite(a.target_c) ? Number(a.target_c) : (this._config.away?.target_c ?? 17), + advanced_enabled: !!(a.advanced_enabled ?? this._config?.away?.advanced_enabled ?? false), + combos: (a.combos && typeof a.combos === 'object') ? { ...a.combos } : (this._config?.away?.combos || {}), + delay_enabled: !!(a.delay_enabled ?? this._config?.away?.delay_enabled ?? false), + delay_value: Number.isFinite(a.delay_value) ? Number(a.delay_value) : (this._config?.away?.delay_value ?? 0), + delay_unit: String(a.delay_unit || this._config?.away?.delay_unit || 'minutes') + }; + } catch {} + } + // Guard: Away/Presence should not be active until at least one person is selected. + // Keep the enabled toggle; only disable advanced features when no persons are set. + try { + const ap = Array.isArray(this._config?.away?.persons) ? this._config.away.persons.filter(Boolean).map(String) : []; + this._config.away.persons = ap; + if (!ap.length) { this._config.away.advanced_enabled = false; } + } catch {} + // Load flags from local storage copy + try { this._pauseIndef = !!s.pause_indef; const pu = Number(s.pause_until_ms); this._pauseUntilMs = Number.isFinite(pu) ? pu : 0; } catch {} + try { if (typeof s.profiles_enabled === 'boolean' && !this._yamlProvided?.profiles_enabled) this._config.profiles_enabled = !!s.profiles_enabled; } catch {} + try { if ((typeof s.presence_sensor_enabled === 'boolean' || typeof s.presence_sensor_enabled === 'number') && !this._yamlProvided?.presence_sensor_enabled) this._config.presence_sensor_enabled = !!s.presence_sensor_enabled; } catch {} + try { if (typeof s.apply_on_edit === 'boolean') this._config.apply_on_edit = !!s.apply_on_edit; } catch {} + try { this._globalProfile = (typeof s.global_profile === 'string' && s.global_profile) ? String(s.global_profile) : null; } catch {} + try { if (typeof s.backup_auto_enabled === 'boolean') this._config.backup_auto_enabled = !!s.backup_auto_enabled; if (Number.isFinite(s.backup_interval_days)) this._config.backup_interval_days = Math.max(1, Math.min(365, Math.round(Number(s.backup_interval_days)))); else if (Number.isFinite(s.backup_interval_min)) this._config.backup_interval_days = Math.max(1, Math.min(365, Math.ceil(Number(s.backup_interval_min)/1440))); } catch {} + // Sync policy from storage + try { + if (typeof s.sync_mode === 'string') this._config.storage_sync_mode = (s.sync_mode === 'delay') ? 'delay' : 'instant'; + if (Number.isFinite(s.sync_delay_min)) this._config.storage_sync_min = Math.max(0, Math.min(1440, Number(s.sync_delay_min))); + if (Number.isFinite(s.sync_delay_sec)) this._config.storage_sync_sec = Math.max(5, Math.min(86400, Math.round(Number(s.sync_delay_sec)))); + else if (Number.isFinite(s.sync_delay_min)) this._config.storage_sync_sec = Math.max(5, Math.min(86400, Math.round(Number(s.sync_delay_min)*60))); + } catch {} + // Holidays from local storage + try { + if (typeof s.holidays_enabled === 'boolean') this._config.holidays_enabled = !!s.holidays_enabled; + if (typeof s.holidays_source === 'string' && !this._yamlProvided?.holidays_source) this._config.holidays_source = s.holidays_source === 'manual' ? 'manual' : 'calendar'; + if (typeof s.holidays_entity === 'string' && !this._yamlProvided?.holidays_entity) this._config.holidays_entity = s.holidays_entity || ''; + if (Array.isArray(s.holidays_dates) && !this._yamlProvided?.holidays_dates) this._config.holidays_dates = [...s.holidays_dates]; + } catch {} + } else { + // backward compat: schedules stored directly + this._schedules = parsed || {}; + try { this._seasonAppliedKey = null; } catch {} + } + this._storeLoaded = true; + this._storeLoadedFromApi = false; + } + catch { this._schedules = {}; } + } + + _isSettingsPopupOpen(){ + try { + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings'); + if (!ov) return false; + return !!ov.classList.contains('open') || !!this._settingsPopupSuspended; + } catch { return false; } + } + _editSessionActive(){ return !!(this._editing || this._inlineEditing || this._weeklyOpen || this._profilesOpen || this._holidayOpen || this._presenceOpen); } + _ensureEditModeOn(){ this._pendingSaveAfterEdit = true; } + _flushPendingSave(){ + try { + if (!this._pendingSaveAfterEdit) return; + if (this._editSessionActive()) return; + this._pendingSaveAfterEdit = false; + this._saveStore(true); + } catch {} + } + + async _saveStore(force = false) { + if (!force && this._editSessionActive()) { + this._markPendingSave(); + return; + } + if (!force && this._pendingSaveAfterEdit) return; + this._pendingSaveAfterEdit = false; + try { + const payload = this._makeStoragePayload(true); + localStorage.setItem(this._localStoreKey(), JSON.stringify(payload)); + } catch {} + try { this._broadcastStoreUpdated(this._localStoreKey()); } catch {} + if (this._config?.storage_enabled && !this._storeLoadedFromApi && !force) { + return; + } + // Always persist local browser copy immediately + const writeRemote = async () => { + if (!this._config?.storage_enabled) { + // When shared storage is turned off, still push a minimal settings payload to disable backend apply + try { + const basePayload = { + settings: { + auto_apply_enabled: false, + apply_on_edit: !!this._config.apply_on_edit, + profiles_enabled: !!this._config.profiles_enabled, + global_profile: this._globalProfile || null, + boiler_enabled: !!this._config.boiler_enabled, + boiler_switch: String(this._config.boiler_switch || ''), + boiler_switch_domain: String((this._config.boiler_switch_domain === 'input_boolean') ? 'input_boolean' : 'switch'), + boiler_rooms: Array.isArray(this._config.boiler_rooms) ? this._config.boiler_rooms.filter(Boolean).map(String) : null, + boiler_on_offset: (this._config.boiler_on_offset === null || this._config.boiler_on_offset === undefined) ? null : Number(this._config.boiler_on_offset), + boiler_off_offset: (this._config.boiler_off_offset === null || this._config.boiler_off_offset === undefined) ? null : Number(this._config.boiler_off_offset), + boiler_temp_sensor: String(this._config.boiler_temp_sensor || ''), + boiler_min_temp: (this._config.boiler_min_temp === null || this._config.boiler_min_temp === undefined) ? null : Number(this._config.boiler_min_temp), + boiler_max_temp: (this._config.boiler_max_temp === null || this._config.boiler_max_temp === undefined) ? null : Number(this._config.boiler_max_temp), + boiler_multi_enabled: !!this._config.boiler_multi_enabled, + boiler_room_settings: (()=>{ + try { + const src = (this._config.boiler_room_settings && typeof this._config.boiler_room_settings === 'object') ? this._config.boiler_room_settings : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const r = src[k]; if (!r || typeof r !== 'object') continue; + const enabled = !!r.enabled; + const sw = String(r.switch || ''); + const dom = String(r.switch_domain || '').toLowerCase().trim(); + const onOff = Number(r.on_offset); + const offOff = Number(r.off_offset); + const obj = { enabled, switch: sw, switch_domain: (dom === 'input_boolean') ? 'input_boolean' : 'switch' }; + if (Number.isFinite(onOff)) obj.on_offset = onOff; + if (Number.isFinite(offOff)) obj.off_offset = offOff; + if (enabled || sw || Number.isFinite(onOff) || Number.isFinite(offOff) || dom) out[String(k)] = obj; + } + return out; + } catch { return {}; } + })(), + show_pause_button: !!(this._config.show_pause_button ?? true), + show_room_temp: !!(this._config.show_room_temp ?? true), + pause_sensor_enabled: !!(this._config.pause_sensor_enabled ?? false), + pause_sensor_entity: String(this._config.pause_sensor_entity || ''), + pause_indef: !!this._pauseIndef, + pause_until_ms: Number(this._pauseUntilMs||0), + open_window: this._config.open_window || { enabled:false, sensors:{}, open_delay_min:2, close_delay_min:5 }, + holidays_enabled: !!this._config.holidays_enabled, + holidays_source: this._config.holidays_source || 'calendar', + holidays_entity: this._config.holidays_entity || '', + holidays_dates: Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : [] + }, + colors: { color_ranges: this._config.color_ranges, color_global: !!this._config.color_global } + }; + if (this._config?.instance_enabled) { + basePayload.instance_id = this._instanceId(); + basePayload.activate = false; + } + await this._hass.callService("thermostat_timeline", "set_store", basePayload); + } catch {} + return; + } + this._saving = true; + try { + let payload = this._makeStoragePayload(true); + // Merge with current backend settings to avoid wiping keys the editor/card doesn't currently include (e.g. labels) + try { + const state = await this._apiFetchState(); + const base = (state && typeof state === 'object' && state.settings && typeof state.settings === 'object') ? state.settings : {}; + // If no base available (early startup), avoid writing holidays_enabled unless user toggled + if ((!state || !state.settings) && !this._holidaysToggleTouched) { + try { if (payload?.settings && Object.prototype.hasOwnProperty.call(payload.settings,'holidays_enabled')) delete payload.settings.holidays_enabled; } catch {} + } + const incoming = payload.settings || {}; + const merged = { ...base, ...incoming }; + // If user didn't toggle holidays during this session, keep backend value explicitly + try { + if (!this._holidaysToggleTouched && typeof base.holidays_enabled === 'boolean') { + merged.holidays_enabled = base.holidays_enabled; + } + if (!this._holidaysSourceTouched && typeof base.holidays_source === 'string') { + merged.holidays_source = String(base.holidays_source || 'calendar') === 'manual' ? 'manual' : 'calendar'; + } + if (!this._holidaysEntityTouched && typeof base.holidays_entity === 'string') { + merged.holidays_entity = String(base.holidays_entity || ''); + } + if (!this._holidaysDatesTouched && Array.isArray(base.holidays_dates)) { + merged.holidays_dates = Array.from(base.holidays_dates); + } + if (!this._holidaysGroupsTouched && Array.isArray(base.holidays_groups)) { + merged.holidays_groups = Array.from(base.holidays_groups); + } + } catch {} + // Heuristic: avoid flipping holidays_enabled=true -> false on startup if nothing holiday-related changed. + try { + const bHE = typeof base.holidays_enabled === 'boolean' ? base.holidays_enabled : undefined; + const iHE = typeof incoming.holidays_enabled === 'boolean' ? incoming.holidays_enabled : undefined; + // If background push (no user toggle) tries to change the value, ignore and keep backend + if (this._holidaysToggleTouched !== true && bHE !== undefined && iHE !== undefined && bHE !== iHE) { + merged.holidays_enabled = bHE; + } else if (bHE === true && iHE === false) { + const normArr = (a)=>{ try { return Array.from(new Set((a||[]).map(String))).sort(); } catch { return []; } }; + const eqDates = JSON.stringify(normArr(incoming.holidays_dates)) === JSON.stringify(normArr(base.holidays_dates)); + const eqSource = String(incoming.holidays_source||'') === String(base.holidays_source||''); + const eqEntity = String(incoming.holidays_entity||'') === String(base.holidays_entity||''); + const eqGroups = JSON.stringify(incoming.holidays_groups||[]) === JSON.stringify(base.holidays_groups||[]); + if (eqDates && eqSource && eqEntity && eqGroups) { + merged.holidays_enabled = true; + } + } + } catch {} + // Preserve backend boiler_enabled unless user explicitly toggled this session + try { + if (!this._boilerToggleTouched && typeof base.boiler_enabled === 'boolean') { + merged.boiler_enabled = base.boiler_enabled; + } + if (!this._awayTouched) { + const localAway = (this._config?.away && typeof this._config.away === 'object') ? this._config.away : null; + if (localAway) merged.away = JSON.parse(JSON.stringify(localAway)); + if (typeof this._config?.presence_live_header === 'boolean') { + merged.presence_live_header = this._config.presence_live_header; + } + const ldEn = this._config?.away?.delay_enabled; + const ldVal = this._config?.away?.delay_value; + const ldUnit = this._config?.away?.delay_unit; + if (typeof ldEn === 'boolean') merged.away_delay_enabled = ldEn; + if (Number.isFinite(Number(ldVal))) merged.away_delay_value = Number(ldVal); + if (typeof ldUnit === 'string') merged.away_delay_unit = (String(ldUnit || 'minutes') === 'seconds') ? 'seconds' : 'minutes'; + } + } catch {} + const keepIfEmpty = (key)=>{ + try { + const inc = incoming[key]; + const hasInc = inc && typeof inc === 'object' && Object.keys(inc).length > 0; + const baseVal = base[key]; + const hasBase = baseVal && typeof baseVal === 'object' && Object.keys(baseVal).length > 0; + if (!hasInc && hasBase) merged[key] = baseVal; // preserve backend value when our copy is empty + } catch {} + }; + // If labels were explicitly edited in this session, allow an empty object to clear labels + if (!this._labelsTouched) keepIfEmpty('labels'); + keepIfEmpty('merges'); + keepIfEmpty('temp_sensors'); + payload = { ...payload, settings: merged }; + } catch {} + // Push all data to the integration via set_store service (writes to file) + if (this._config?.instance_enabled) { + payload = { ...payload, instance_id: this._instanceId(), activate: true }; + } + await this._hass.callService("thermostat_timeline", "set_store", payload); + try { this._broadcastStoreUpdated(this._localStoreKey()); } catch {} + } catch (e) {} + finally { + setTimeout(() => { + this._saving = false; + try { this._holidaysToggleTouched = false; } catch {} + try { this._holidaysSourceTouched = false; } catch {} + try { this._holidaysEntityTouched = false; } catch {} + try { this._holidaysDatesTouched = false; } catch {} + try { this._holidaysGroupsTouched = false; } catch {} + try { this._boilerToggleTouched = false; } catch {} + try { this._awayTouched = false; } catch {} + try { this._labelsTouched = false; } catch {} + // Show "done" for a short moment and ensure UI keeps ticking + this._syncJustUntil = Date.now() + 2500; + // Clear previous due so countdown disappears until next change + this._storeDelayDue = 0; + this._startSyncTimer(); + this._render(); + }, 600); + } + }; + + // Decide sync policy + const mode = this._config?.storage_sync_mode || 'instant'; + const secsCfg = Number(this._config?.storage_sync_sec || 0); + const secs = Math.max(5, Math.min(86400, Number.isFinite(secsCfg) ? secsCfg : Math.max(0, Number(this._config?.storage_sync_min || 0))*60)); + if (this._config?.storage_enabled && mode === 'delay' && secs > 0 && !force) { + // Trailing save after N minutes; coalesce multiple changes + const delayMs = secs * 1000; + if (this._storeDelayTimer) try { clearTimeout(this._storeDelayTimer); } catch {} + this._storeDelayDue = Date.now() + delayMs; + try { localStorage.setItem(this._syncDueKey(), String(this._storeDelayDue)); } catch {} + // Save a draft of current schedules so they survive a hard refresh until delayed sync writes + try { localStorage.setItem(this._draftKey(), JSON.stringify({ schedules: this._schedules })); } catch {} + // Persist due time across reloads + // If we just finished a sync, drop the "done" flag so countdown shows immediately + this._syncJustUntil = 0; + this._storeDelayTimer = setTimeout(() => { this._storeDelayTimer = null; this._writeStoreNow(); }, delayMs); + try { this._startSyncTimer(); this._render(); } catch {} + } else { + // Instant (existing behavior with short debounce handled by callers) + await this._writeStoreNow(); + } + } + + _debouncedSaveStore() { + // Keep short debounce for bursts; final saving respects sync policy inside _saveStore + if (this._saveTimer) clearTimeout(this._saveTimer); + // Persist a local draft immediately so edits survive hard refresh before save runs + try { this._persistDraftNow(); } catch {} + this._saveTimer = setTimeout(() => this._saveStore(), this._saveDebounceMs); + } + + // ---------- Helpers ---------- + _sortBlocks(entity) { const r = this._schedules[entity]; r.blocks.sort((a,b)=>a.startMin - b.startMin || a.endMin - b.endMin); } + _neighbors(entity, id) { const r = this._schedules[entity]; const i = r.blocks.findIndex(b=>b.id===id); if (i === -1) return {left:null, right:null, index:-1}; return { left: r.blocks[i-1] || null, right: r.blocks[i+1] || null, index:i }; } + _applyNoOverlapResize(entity, b, edge, proposed) { this._sortBlocks(entity); const {left, right} = this._neighbors(entity, b.id); if (edge === "left") { let ns = this._clamp(Math.floor(proposed), 0, b.endMin - 5); if (left) ns = Math.max(ns, left.endMin); b.startMin = ns; } else if (edge === "right") { let ne = this._clamp(Math.ceil(proposed), b.startMin + 5, 1440); if (right) ne = Math.min(ne, right.startMin); b.endMin = ne; } this._sortBlocks(entity); } + _label(min) { + if (!Number.isFinite(min)) min = 0; + const m = ((Math.floor(min) % 1440) + 1440) % 1440; + const hh = Math.floor(m/60), mm = Math.round(m%60); + if (this._config?.time_12h) { + const am = hh < 12; + let h = hh % 12; if (h === 0) h = 12; + return `${String(h).padStart(2,'0')}:${String(mm).padStart(2,'0')} ${am ? 'AM' : 'PM'}`; + } + return `${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')}`; + } + _timeParts(min){ + // Returns { main: 'hh:mm', mer: 'AM'|'PM'|null } according to current format + if (!Number.isFinite(min)) min = 0; + const m = ((Math.floor(min) % 1440) + 1440) % 1440; + const hh = Math.floor(m/60), mm = Math.round(m%60); + if (this._config?.time_12h) { + const am = hh < 12; + let h = hh % 12; if (h === 0) h = 12; + return { main: `${String(h).padStart(2,'0')}:${String(mm).padStart(2,'0')}`, mer: am ? 'AM' : 'PM' }; + } + return { main: `${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')}`, mer: null }; + } + _hourLabel(h){ const min = (h%24)*60; return this._label(min); } + _useHaTime(){ try { return String(this._config?.time_source||'browser') === 'ha'; } catch { return false; } } + _getNowMin(){ + try { + if (this._useHaTime() && this._hass?.config?.time_zone) { + const tz = this._hass.config.time_zone; + const fmt = new Intl.DateTimeFormat('en-US', { timeZone: tz, hour: '2-digit', minute: '2-digit', hour12: false }); + const parts = fmt.formatToParts(new Date()); + const hh = Number(parts.find(p=>p.type==='hour')?.value||'0'); + const mm = Number(parts.find(p=>p.type==='minute')?.value||'0'); + return hh*60 + mm; + } + } catch {} + const d=new Date(); return d.getHours()*60 + d.getMinutes(); + } + _clamp(v,a,b){ if (isNaN(v)) return a; return Math.min(Math.max(v,a),b); } + _prettyName(eid){ const st=this._hass?.states?.[eid]; if (st?.attributes?.friendly_name) return st.attributes.friendly_name; const base=(eid||"").split(".")[1]||eid||""; return base.replace(/_/g," ").replace(/\b\w/g,(m)=>m.toUpperCase()); } + + _isCompactScale(){ + // Compact = show every other hour on small or touch devices (incl. mobile portrait) + try { + const isCoarse = window.matchMedia && window.matchMedia('(pointer:coarse)').matches; + // Prefer the card width when available, fallback to window width + const hostW = (this.shadowRoot && this.shadowRoot.host && this.shadowRoot.host.clientWidth) || 0; + const w = hostW || window.innerWidth || 0; + // Consider compact on touch devices (phones/tablets) or narrow widths + return isCoarse || w <= 720; // ~mobile/tablet portrait breakpoint + } catch (e) { return false; } + } + _detectPrefer12h(){ + try { + // Prefer Home Assistant locale if available + const lf = this._hass?.locale?.time_format || this._hass?.locale?.time_format_preference || ''; + if (typeof lf === 'string') { + const s = lf.toLowerCase(); + if (s.includes('12')) return true; + if (s.includes('24')) return false; + } + } catch {} + try { + // Fallback: browser preference + const fmt = new Intl.DateTimeFormat(navigator.language || undefined, { hour: 'numeric' }); + const ro = fmt.resolvedOptions?.() || {}; + if (typeof ro.hour12 === 'boolean') return ro.hour12; + } catch {} + // Final fallback: 24h + return false; + } + _detectPreferF(){ + try { + const u = this._hass?.config?.unit_system?.temperature || this._hass?.locale?.temperature || ''; + if (typeof u === 'string' && u.toUpperCase().includes('F')) return true; + } catch {} + try { + const f = new Intl.NumberFormat(navigator.language || undefined, { style:'unit', unit:'fahrenheit' }); + // Heuristic: if formatter exists, still default to C; most regions outside US use C, but HA already covers primary preference + } catch {} + return false; + } + _getLocalProfilesEnabled(){ + try { + const raw = localStorage.getItem(this._localStoreKey()) || ''; + if (!raw) return false; + const parsed = JSON.parse(raw); + const s = parsed?.settings || {}; + return !!s.profiles_enabled; + } catch { return false; } + } + _isF(){ return (this._config?.temp_unit||'C')==='F'; } + _unitSymbol(){ return this._isF()? '°F' : '°C'; } + _toDisplayTemp(c){ return this._isF()? Math.round((c*9/5+32)*10)/10 : c; } + _fromDisplayTemp(v){ return this._isF()? ( (v-32)*5/9 ) : v; } + // --- Temperature conversion helpers (internal storage is °C) --- + _cToF(c){ try { return (Number(c)*9/5)+32; } catch { return c; } } + _fToC(f){ try { return (Number(f)-32)*5/9; } catch { return f; } } + // Service calls must follow Home Assistant's unit system (not the card display unit) + _serviceTempFromC(c){ return this._haIsF() ? this._cToF(c) : c; } + _haIsF(){ try { const u = String(this._hass?.config?.unit_system?.temperature||''); return u.toUpperCase().includes('F'); } catch { return false; } } + + _readSensorTemperatureC(sensorEid){ + try { + const st = this._hass?.states?.[sensorEid]; + const raw = Number(String(st?.state ?? '').trim().replace(',', '.')); + if (!Number.isFinite(raw)) return NaN; + const u = String(st?.attributes?.unit_of_measurement || this._hass?.config?.unit_system?.temperature || '').toUpperCase(); + const isF = u.includes('F'); + return isF ? this._fToC(raw) : raw; + } catch { return NaN; } + } + + _readClimateCurrentTemperatureC(climateEid){ + try { + const st = this._hass?.states?.[climateEid]; + let raw = Number(String(st?.attributes?.current_temperature ?? st?.attributes?.current_temp ?? st?.attributes?.temperature ?? '').trim().replace(',', '.')); + if (!Number.isFinite(raw)) { + // Some custom climate entities expose the temperature as a numeric state. + raw = Number(String(st?.state ?? '').trim().replace(',', '.')); + } + if (!Number.isFinite(raw)) return NaN; + return this._haIsF() ? this._fToC(raw) : raw; + } catch { return NaN; } + } + + _roomCurrentTemperatureC(primaryEid){ + try { + if (!this._hass || !this._config) return NaN; + const ents = Array.isArray(this._config.entities) ? this._config.entities.map(String) : []; + const idx = ents.indexOf(String(primaryEid)); + const flags = Array.isArray(this._config.room_use_temp_sensor) ? this._config.room_use_temp_sensor : []; + const enabled = (idx >= 0) ? !!flags[idx] : false; + + if (enabled) { + const sensorEid = String(this._config?.temp_sensors?.[primaryEid] || '').trim(); + if (sensorEid) { + const v = this._readSensorTemperatureC(sensorEid); + if (Number.isFinite(v)) return v; + } + } + + // Fallback: climate current_temperature (average across merged group) + const merged = this._config?.merges?.[primaryEid]; + const extra = Array.isArray(merged) ? merged : []; + const group = [primaryEid, ...extra]; + const vals = []; + for (const g of group) { + const v = this._readClimateCurrentTemperatureC(g); + if (Number.isFinite(v)) vals.push(v); + } + if (!vals.length) return NaN; + return vals.reduce((a,b)=>a+b,0) / vals.length; + } catch { return NaN; } + } + async _applySetpointForEntity(eid, desiredC){ + try { + const dom = String(eid||'').split('.')[0] || ''; + if (dom === 'input_number') { + // Mirror climate behavior: send in configured display/service unit (°C or °F) + const val = this._serviceTempFromC(desiredC); + await this._hass.callService('input_number','set_value', { entity_id: eid, value: val }); + return; + } + if (dom !== 'climate') return; + const st = this._hass?.states?.[eid]; if (!st) return; + const attrs = st.attributes || {}; + let hvacMode = String(attrs.hvac_mode ?? st.state ?? '').toLowerCase(); + const hvacModes = Array.isArray(attrs.hvac_modes) ? attrs.hvac_modes.map(x=>String(x).toLowerCase()) : []; + const presetMode = String(attrs.preset_mode || '').toLowerCase(); + const presetModes = Array.isArray(attrs.preset_modes) ? attrs.preset_modes.map(x=>String(x).toLowerCase()) : []; + + // Optional: enable manual/hold preset to avoid schedule overrides + try { + const want = presetModes.includes('manual') ? 'manual' : (presetModes.includes('hold') ? 'hold' : null); + if (want && presetMode !== want){ + await this._hass.callService('climate','set_preset_mode',{ entity_id: eid, preset_mode: want }); + } + } catch {} + + // If mode ignores setpoint, try switching to heat/cool + try { + if (['off','dry','fan_only'].includes(hvacMode)){ + let newMode = null; + if (hvacModes.includes('heat')) newMode = 'heat'; else if (hvacModes.includes('cool')) newMode = 'cool'; + if (newMode){ await this._hass.callService('climate','set_hvac_mode',{ entity_id: eid, hvac_mode: newMode }); hvacMode = newMode; } + } + } catch {} + + // Decide between single setpoint vs range + const hasLow = Object.prototype.hasOwnProperty.call(attrs, 'target_temp_low'); + const hasHigh = Object.prototype.hasOwnProperty.call(attrs, 'target_temp_high'); + const modeSuggestsRange = (hvacMode === 'heat_cool') || (hvacMode === 'auto' && hasLow && hasHigh); + if (modeSuggestsRange && (hasLow || hasHigh)){ + let band = 1.0; // °C total band + try { const b = Number(this._config?.range_band_c); if (Number.isFinite(b) && b > 0.2 && b <= 10) band = b; } catch {} + const half = Math.max(0.2, band/2); + const lowC = desiredC - half; + const highC = desiredC + half; + const data = { entity_id: eid, target_temp_low: this._serviceTempFromC(lowC), target_temp_high: this._serviceTempFromC(highC) }; + await this._hass.callService('climate','set_temperature', data); + return; + } + await this._hass.callService('climate','set_temperature', { entity_id: eid, temperature: this._serviceTempFromC(desiredC) }); + } catch {} + } + // Convert a schedules object between units (returns deep clone) + _convertSchedulesTemps(schedules, toUnit){ + try { + const clone = JSON.parse(JSON.stringify(schedules||{})); + const toF = String(toUnit).toUpperCase()==='F'; + const conv = (x)=>{ const n = Number(x); if (!Number.isFinite(n)) return x; return toF ? this._cToF(n) : this._fToC(n); }; + const convertBlocks = (arr)=>{ if (!Array.isArray(arr)) return; for (const b of arr){ if (b && typeof b==='object' && 'temp' in b){ b.temp = conv(b.temp); } } }; + for (const eid of Object.keys(clone||{})){ + const row = clone[eid]; if (!row || typeof row!=='object') continue; + if ('defaultTemp' in row) row.defaultTemp = conv(row.defaultTemp); + convertBlocks(row.blocks); + // Weekly + try { const days = row?.weekly?.days||{}; for (const k of Object.keys(days)) convertBlocks(days[k]); } catch {} + // Profiles + try { const prof = row?.profiles||{}; for (const p of Object.keys(prof)){ convertBlocks(prof[p]?.blocks); } } catch {} + // Holiday + try { convertBlocks(row?.holiday?.blocks); } catch {} + // Presence + try { const pres = row?.presence||{}; for (const key of Object.keys(pres)){ convertBlocks(pres[key]?.blocks); } } catch {} + // Seasonal (summer/winter) + try { + const seasons = row?.seasons || {}; + for (const sk of Object.keys(seasons||{})) { + const srow = seasons[sk]; if (!srow || typeof srow !== 'object') continue; + if ('defaultTemp' in srow) srow.defaultTemp = conv(srow.defaultTemp); + convertBlocks(srow.blocks); + try { const days = srow?.weekly?.days||{}; for (const k of Object.keys(days)) convertBlocks(days[k]); } catch {} + try { const wm = srow?.weekly_modes||{}; for (const mk of Object.keys(wm||{})) { const d2 = wm[mk]?.days||{}; for (const dk of Object.keys(d2)) convertBlocks(d2[dk]); } } catch {} + try { const pres = srow?.presence||{}; for (const pk of Object.keys(pres||{})) convertBlocks(pres[pk]?.blocks); } catch {} + } + } catch {} + } + return clone; + } catch { return schedules; } + } + + // Heuristic for legacy stores: detect if schedule temps look like °F (typical °C schedules are < ~45) + _maxTempInSchedules(schedules){ + let maxV = -Infinity; + const scanBlocks = (arr)=>{ + if (!Array.isArray(arr)) return; + for (const b of arr){ + try { + if (!b || typeof b !== 'object') continue; + const v = Number(b.temp); + if (Number.isFinite(v)) maxV = Math.max(maxV, v); + } catch {} + } + }; + try { + const s = schedules || {}; + for (const eid of Object.keys(s)){ + const row = s[eid]; + if (!row || typeof row !== 'object') continue; + const d = Number(row.defaultTemp); + if (Number.isFinite(d)) maxV = Math.max(maxV, d); + scanBlocks(row.blocks); + try { const days = row?.weekly?.days||{}; for (const k of Object.keys(days)) scanBlocks(days[k]); } catch {} + try { const prof = row?.profiles||{}; for (const p of Object.keys(prof)) scanBlocks(prof[p]?.blocks); } catch {} + try { scanBlocks(row?.holiday?.blocks); } catch {} + try { const pres = row?.presence||{}; for (const key of Object.keys(pres)) scanBlocks(pres[key]?.blocks); } catch {} + try { + const seasons = row?.seasons || {}; + for (const sk of Object.keys(seasons||{})) { + const srow = seasons[sk]; if (!srow || typeof srow !== 'object') continue; + const d = Number(srow.defaultTemp); + if (Number.isFinite(d)) maxV = Math.max(maxV, d); + scanBlocks(srow.blocks); + try { const days = srow?.weekly?.days||{}; for (const k of Object.keys(days)) scanBlocks(days[k]); } catch {} + try { const wm = srow?.weekly_modes||{}; for (const mk of Object.keys(wm||{})) { const d2 = wm[mk]?.days||{}; for (const dk of Object.keys(d2)) scanBlocks(d2[dk]); } } catch {} + } + } catch {} + } + } catch {} + return Number.isFinite(maxV) ? maxV : -Infinity; + } + + // Build payload for shared storage respecting selected unit for numbers + _makeStoragePayload(includeSchedules=true){ + try { + try { this._syncSeasonalRowsToBucket(); } catch {} + const base = (this._config && typeof this._config === 'object') ? this._config : {}; + // Canonical shared storage is always °C to avoid drift when multiple clients use different display units. + // (Display unit is controlled by settings.temp_unit and is independent of stored numeric values.) + const wantF = false; + let schedules = undefined; + let weekdays = {}; + let profiles = {}; + const useSeparateProfiles = this._config?.storage_enabled && !!this._storageEntity('profiles'); + if (includeSchedules) { + const cloned = JSON.parse(JSON.stringify(this._schedules||{})); + // Split weekday data into separate payload + try { + for (const eid of Object.keys(cloned||{})) { + const row = cloned[eid]; + if (!row || typeof row !== 'object') continue; + const wk = {}; + if (row.weekly) { wk.weekly = row.weekly; delete row.weekly; } + if (row.weekly_modes) { wk.weekly_modes = row.weekly_modes; delete row.weekly_modes; } + if (Object.keys(wk).length) weekdays[eid] = wk; + const prof = {}; + if (row.profiles && typeof row.profiles === 'object') prof.profiles = row.profiles; + if (Object.prototype.hasOwnProperty.call(row, 'activeProfile')) prof.activeProfile = row.activeProfile; + if (Object.keys(prof).length) { + // Always keep profiles inside the schedules payload as a fallback, even when a dedicated profiles sensor is configured. + // This prevents losing data if the profiles sensor is missing or fails to update. + profiles[eid] = prof; + } + } + } catch {} + schedules = cloned; + } + // Convert numeric settings if storing as °F + const toStore = (n)=>{ const v = Number(n); if (!Number.isFinite(v)) return n; return v; }; + const awayCfg = this._config?.away || {}; + const awayOut = { ...awayCfg }; + // Guard: Away/Presence should not be active until at least one person is selected. + // Keep enabled toggle; only disable advanced features when no persons are set. + try { + awayOut.persons = Array.isArray(awayOut.persons) ? awayOut.persons.filter(Boolean).map(String) : []; + if (!awayOut.persons.length) { awayOut.advanced_enabled = false; } + } catch {} + if (typeof awayOut.target_c !== 'undefined') awayOut.target_c = toStore(awayOut.target_c); + const settings = { + // Rooms (used by Settings popup) + entities: Array.isArray(this._config.entities) ? this._config.entities.filter(Boolean).map(String) : [], + room_use_input_number: (()=>{ + try { + const ents = Array.isArray(this._config.entities) ? this._config.entities.filter(Boolean).map(String) : []; + const raw = Array.isArray(this._config.room_use_input_number) ? this._config.room_use_input_number.slice() : []; + while (raw.length < ents.length) raw.push(false); + return ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return true; + if (String(eid).startsWith('climate.')) return false; + return !!raw[idx]; + }); + } catch { return []; } + })(), + + room_use_temp_sensor: (()=>{ + try { + const ents = Array.isArray(this._config.entities) ? this._config.entities.filter(Boolean).map(String) : []; + const raw = Array.isArray(this._config.room_use_temp_sensor) ? this._config.room_use_temp_sensor.slice() : []; + while (raw.length < ents.length) raw.push(false); + return ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return false; + if (!String(eid).startsWith('climate.')) return false; + return !!raw[idx]; + }); + } catch { return []; } + })(), + + time_12h: this._config.time_12h, + temp_unit: this._config.temp_unit, + storage_temp_unit: 'C', + time_source: this._config.time_source, + row_height: Number(this._config.row_height ?? 64), + default_temp: toStore(Number(this._config.default_temp||20)), + min_temp: toStore(Number(this._config.min_temp ?? 5)), + max_temp: toStore(Number(this._config.max_temp ?? 25)), + weekdays_enabled: !!this._config.weekdays_enabled, + seasonal_enabled: !!this._config.seasonal_enabled, + seasonal_mode: (this._config.seasonal_mode === 'summer') ? 'summer' : 'winter', + weekdays_mode: this._config.weekdays_mode || 'weekday_weekend', + weekdays_view: this._config.weekdays_view || 'all_rooms_one_day', + weekdays_view_switch_in_timeline: !!(this._config.weekdays_view_switch_in_timeline ?? false), + weekdays_selected_room: this._config.weekdays_selected_room || '', + per_room_defaults: !!this._config.per_room_defaults, + away: awayOut, + away_bypass: !!this._awayBypass, + merges: this._config.merges, + labels: this._config.labels, + temp_sensors: this._config.temp_sensors, + turn_on: this._config.turn_on, + + presence_sensor_enabled: !!(this._config.presence_sensor_enabled ?? false), + presence_live_header: !!(this._config.presence_live_header ?? true), + presence_sensors: (this._config.presence_sensors && typeof this._config.presence_sensors === 'object') ? this._config.presence_sensors : {}, + presence_sensor_temps: (this._config.presence_sensor_temps && typeof this._config.presence_sensor_temps === 'object') ? this._config.presence_sensor_temps : {}, + presence_sensor_delays: (this._config.presence_sensor_delays && typeof this._config.presence_sensor_delays === 'object') ? this._config.presence_sensor_delays : {}, + presence_sensor_delay_units: (this._config.presence_sensor_delay_units && typeof this._config.presence_sensor_delay_units === 'object') ? this._config.presence_sensor_delay_units : {}, + + show_pause_button: !!(this._config.show_pause_button ?? true), + pause_sensor_enabled: !!(this._config.pause_sensor_enabled ?? false), + pause_sensor_entity: String(this._config.pause_sensor_entity || ''), + pause_indef: !!this._pauseIndef, + pause_until_ms: Number(this._pauseUntilMs||0), + + boiler_enabled: !!this._config.boiler_enabled, + boiler_switch: String(this._config.boiler_switch || ''), + boiler_switch_domain: String((this._config.boiler_switch_domain === 'input_boolean') ? 'input_boolean' : 'switch'), + boiler_rooms: Array.isArray(this._config.boiler_rooms) ? this._config.boiler_rooms.filter(Boolean).map(String) : null, + boiler_on_offset: (this._config.boiler_on_offset === null || this._config.boiler_on_offset === undefined) ? null : Number(this._config.boiler_on_offset), + boiler_off_offset: (this._config.boiler_off_offset === null || this._config.boiler_off_offset === undefined) ? null : Number(this._config.boiler_off_offset), + boiler_temp_sensor: String(this._config.boiler_temp_sensor || ''), + boiler_min_temp: (this._config.boiler_min_temp === null || this._config.boiler_min_temp === undefined) ? null : Number(this._config.boiler_min_temp), + boiler_max_temp: (this._config.boiler_max_temp === null || this._config.boiler_max_temp === undefined) ? null : Number(this._config.boiler_max_temp), + boiler_multi_enabled: !!this._config.boiler_multi_enabled, + boiler_room_settings: (this._config.boiler_room_settings && typeof this._config.boiler_room_settings === 'object') ? this._config.boiler_room_settings : {}, + + weekdays_enabled: !!this._config.weekdays_enabled, + weekdays_mode: this._config.weekdays_mode || base.weekdays_mode || 'weekday_weekend', + weekdays_view: this._config.weekdays_view || base.weekdays_view || 'all_rooms_one_day', + weekdays_view_switch_in_timeline: !!(this._config.weekdays_view_switch_in_timeline ?? base.weekdays_view_switch_in_timeline ?? false), + weekdays_selected_room: this._config.weekdays_selected_room || base.weekdays_selected_room || '', + seasonal_enabled: !!this._config.seasonal_enabled, + seasonal_mode: (this._config.seasonal_mode === 'summer') ? 'summer' : 'winter', + presence_sensors: this._config.presence_sensors, + presence_sensor_temps: (()=>{ + try { + const src = (this._config.presence_sensor_temps && typeof this._config.presence_sensor_temps === 'object') ? this._config.presence_sensor_temps : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const v = Number(src[k]); + if (Number.isFinite(v)) out[String(k)] = toStore(v); + } + return out; + } catch { return {}; } + })(), + presence_sensor_delays: (()=>{ + try { + const src = (this._config.presence_sensor_delays && typeof this._config.presence_sensor_delays === 'object') ? this._config.presence_sensor_delays : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const v = src?.[k]; + if (!v || typeof v !== 'object') continue; + const onS = Number(v.on_s); + const offS = Number(v.off_s); + const obj = {}; + if (Number.isFinite(onS) && onS >= 0) obj.on_s = onS; + if (Number.isFinite(offS) && offS >= 0) obj.off_s = offS; + if (Object.keys(obj).length) out[String(k)] = obj; + } + return out; + } catch { return {}; } + })(), + presence_sensor_delay_units: (()=>{ + try { + const src = (this._config.presence_sensor_delay_units && typeof this._config.presence_sensor_delay_units === 'object') ? this._config.presence_sensor_delay_units : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const u = String(src[k]||'').toLowerCase().trim(); + if (u === 'seconds' || u === 'minutes') out[String(k)] = u; + } + return out; + } catch { return {}; } + })(), + open_window: this._config.open_window, + boiler_enabled: !!this._config.boiler_enabled, + boiler_switch: String(this._config.boiler_switch || ''), + boiler_switch_domain: (()=>{ + try { + const sw = String(this._config.boiler_switch || ''); + const dom = sw.split('.')[0]; + if (dom === 'input_boolean' || dom === 'switch') return dom; + const raw = String(this._config.boiler_switch_domain || '').toLowerCase().trim(); + return (raw === 'input_boolean') ? 'input_boolean' : 'switch'; + } catch { return 'switch'; } + })(), + boiler_rooms: Array.isArray(this._config.boiler_rooms) ? this._config.boiler_rooms.filter(Boolean).map(String) : null, + boiler_on_offset: (this._config.boiler_on_offset === null || this._config.boiler_on_offset === undefined) ? null : toStore(this._config.boiler_on_offset), + boiler_off_offset: (this._config.boiler_off_offset === null || this._config.boiler_off_offset === undefined) ? null : toStore(this._config.boiler_off_offset), + boiler_temp_sensor: String(this._config.boiler_temp_sensor || ''), + boiler_min_temp: (this._config.boiler_min_temp === null || this._config.boiler_min_temp === undefined) ? null : toStore(this._config.boiler_min_temp), + boiler_max_temp: (this._config.boiler_max_temp === null || this._config.boiler_max_temp === undefined) ? null : toStore(this._config.boiler_max_temp), + boiler_multi_enabled: !!this._config.boiler_multi_enabled, + boiler_room_settings: (()=>{ + try { + const src = (this._config.boiler_room_settings && typeof this._config.boiler_room_settings === 'object') ? this._config.boiler_room_settings : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const r = src[k]; if (!r || typeof r !== 'object') continue; + const enabled = !!r.enabled; + const sw = String(r.switch || ''); + const dom = String(r.switch_domain || '').toLowerCase().trim(); + const onOff = Number(r.on_offset); + const offOff = Number(r.off_offset); + const obj = { enabled, switch: sw, switch_domain: (dom === 'input_boolean') ? 'input_boolean' : 'switch' }; + if (Number.isFinite(onOff)) obj.on_offset = toStore(onOff); + if (Number.isFinite(offOff)) obj.off_offset = toStore(offOff); + if (enabled || sw || Number.isFinite(onOff) || Number.isFinite(offOff) || dom) out[String(k)] = obj; + } + return out; + } catch { return {}; } + })(), + show_pause_button: !!(this._config.show_pause_button ?? true), + show_room_temp: !!(this._config.show_room_temp ?? true), + pause_sensor_enabled: !!(this._config.pause_sensor_enabled ?? false), + pause_sensor_entity: String(this._config.pause_sensor_entity || ''), + presence_sensor_enabled: !!(this._config.presence_sensor_enabled ?? false), + presence_live_header: !!(this._config.presence_live_header ?? true), + auto_apply_enabled: !!this._config.auto_apply, + apply_on_edit: !!this._config.apply_on_edit, + apply_on_default_change: !!this._config.apply_on_default_change, + backup_auto_enabled: !!this._config.backup_auto_enabled, + backup_interval_days: Number(this._config.backup_interval_days||1), + profiles_enabled: !!this._config.profiles_enabled, + global_profile: this._globalProfile || null, + pause_indef: !!this._pauseIndef, + pause_until_ms: Number(this._pauseUntilMs||0), + holidays_enabled: !!this._config.holidays_enabled, + holidays_source: this._config.holidays_source || 'calendar', + holidays_entity: this._config.holidays_entity || '', + holidays_dates: Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : [], + holidays_groups: Array.isArray(this._config.holidays_groups) ? this._config.holidays_groups : [], + sync_mode: (this._config.storage_sync_mode||'instant'), + sync_delay_min: Number(Math.max(0, Math.round((this._config.storage_sync_sec||0)/60))), + sync_delay_sec: Number(this._config.storage_sync_sec||0) + }; + // Do not include holidays_enabled in generic payload unless user toggled it in this session + try { if (!this._holidaysToggleTouched) delete settings.holidays_enabled; } catch {} + // Do not include holidays source/entity/dates/groups unless explicitly edited in this session + try { + if (!this._holidaysSourceTouched) delete settings.holidays_source; + if (!this._holidaysEntityTouched) delete settings.holidays_entity; + if (!this._holidaysDatesTouched) delete settings.holidays_dates; + if (!this._holidaysGroupsTouched) delete settings.holidays_groups; + } catch {} + // Do not include boiler_enabled unless user toggled it in this session + try { if (!this._boilerToggleTouched) delete settings.boiler_enabled; } catch {} + // Always include away settings to avoid losing away.enabled due touch-flag races. + const colors = { color_ranges: this._config.color_ranges, color_global: !!this._config.color_global }; + // When a dedicated colors sensor is configured, avoid duplicating colors inside settings payload + if (this._config?.storage_enabled && this._storageEntity('colors')) { + delete settings.color_ranges; + delete settings.color_global; + } else { + settings.color_ranges = colors.color_ranges; + settings.color_global = colors.color_global; + } + const out = { settings, colors }; + if (includeSchedules) { out.schedules = schedules; out.weekdays = weekdays; out.profiles = profiles; } + return out; + } catch { return { schedules: this._schedules, settings: { temp_unit: this._config.temp_unit } }; } + } + _maxDisplay(){ const c = Number(this._config?.max_temp ?? 25); return this._toDisplayTemp(c); } + _minDisplay(){ const c = Number(this._config?.min_temp ?? 5); return this._toDisplayTemp(c); } + _rowDefaultTemp(row){ + try { + if (this._config?.per_room_defaults) { + const v = Number(row?.defaultTemp); + return Number.isFinite(v) ? v : Number(this._config?.default_temp ?? 20); + } + return Number(this._config?.default_temp ?? 20); + } catch { return Number(this._config?.default_temp ?? 20); } + } + _todayFullName(){ + try { + const lang = ttGetLangFromHass(this._hass || this._lang || 'en'); + const opts = { weekday: 'long' }; + if (this._useHaTime() && this._hass?.config?.time_zone) Object.assign(opts, { timeZone: this._hass.config.time_zone }); + const s = new Intl.DateTimeFormat(lang, opts).format(new Date()); + return s.charAt(0).toUpperCase()+s.slice(1); + } catch { + const names = { mon:'Monday', tue:'Tuesday', wed:'Wednesday', thu:'Thursday', fri:'Friday', sat:'Saturday', sun:'Sunday' }; + return names[this._todayKey()] || 'Today'; + } + } + + // --- Seasonal schedule helpers --- + _seasonEnabled(){ try { return !!this._config?.seasonal_enabled; } catch { return false; } } + _seasonKey(){ try { return (String(this._config?.seasonal_mode || 'winter') === 'summer') ? 'summer' : 'winter'; } catch { return 'winter'; } } + _cloneSeasonDataFromRow(row){ + try { + return { + blocks: Array.isArray(row?.blocks) ? JSON.parse(JSON.stringify(row.blocks)) : [], + weekly: row?.weekly ? JSON.parse(JSON.stringify(row.weekly)) : undefined, + weekly_modes: row?.weekly_modes ? JSON.parse(JSON.stringify(row.weekly_modes)) : undefined, + presence: row?.presence ? JSON.parse(JSON.stringify(row.presence)) : undefined, + }; + } catch { return { blocks: [] }; } + } + _ensureSeasonStruct(row){ + try { + if (!row) return row; + if (!row.seasons || typeof row.seasons !== 'object') row.seasons = {}; + const ensureBucket = (key)=>{ + if (!row.seasons[key] || typeof row.seasons[key] !== 'object') row.seasons[key] = {}; + if (!Array.isArray(row.seasons[key].blocks)) row.seasons[key].blocks = []; + }; + ensureBucket('winter'); + ensureBucket('summer'); + return row; + } catch { return row; } + } + _syncSeasonDataFromRow(row, seasonKey){ + try { + if (!row) return; + this._ensureSeasonStruct(row); + const key = (seasonKey === 'summer') ? 'summer' : 'winter'; + row.seasons[key] = this._cloneSeasonDataFromRow(row); + } catch {} + } + _loadSeasonDataIntoRow(row, seasonKey){ + try { + if (!row) return; + this._ensureSeasonStruct(row); + const key = (seasonKey === 'summer') ? 'summer' : 'winter'; + const src = row.seasons?.[key] || {}; + row.blocks = Array.isArray(src.blocks) ? JSON.parse(JSON.stringify(src.blocks)) : []; + if (src.weekly) row.weekly = JSON.parse(JSON.stringify(src.weekly)); else delete row.weekly; + if (src.weekly_modes) row.weekly_modes = JSON.parse(JSON.stringify(src.weekly_modes)); else delete row.weekly_modes; + if (src.presence) row.presence = JSON.parse(JSON.stringify(src.presence)); else delete row.presence; + } catch {} + } + _prepareSeasonalRowsOnEnable(){ + try { + if (!this._seasonEnabled()) return; + for (const eid of (this._config?.entities || [])) { + const row = this._schedules?.[eid]; + if (!row) continue; + this._ensureSeasonStruct(row); + const hasData = (bucket)=>{ + try { + if (!bucket || typeof bucket !== 'object') return false; + if (Array.isArray(bucket.blocks) && bucket.blocks.length) return true; + if (bucket.weekly && typeof bucket.weekly === 'object') return true; + if (bucket.weekly_modes && typeof bucket.weekly_modes === 'object') return true; + } catch {} + return false; + }; + const curData = this._cloneSeasonDataFromRow(row); + if (!hasData(row.seasons?.winter)) row.seasons.winter = JSON.parse(JSON.stringify(curData)); + if (!hasData(row.seasons?.summer)) row.seasons.summer = JSON.parse(JSON.stringify(curData)); + } + } catch {} + } + _applySeasonalModeToRows(force=false, prevSeasonOverride=null){ + try { + if (!this._seasonEnabled()) { this._seasonAppliedKey = null; return; } + const next = this._seasonKey(); + if (!force && this._seasonAppliedKey === next) return; + const prev = prevSeasonOverride || this._seasonAppliedKey || next; + for (const eid of (this._config?.entities || [])) { + const row = this._schedules?.[eid]; + if (!row) continue; + this._ensureSeasonStruct(row); + // Store current working data into previous season bucket + if (prev) this._syncSeasonDataFromRow(row, prev); + // Load target season into working fields + this._loadSeasonDataIntoRow(row, next); + } + this._seasonAppliedKey = next; + } catch {} + } + _setSeasonMode(next){ + try { + if (!this._seasonEnabled()) return; + const prev = this._seasonKey(); + const target = (String(next) === 'summer') ? 'summer' : 'winter'; + if (target === prev) return; + this._config.seasonal_mode = target; + try { if (this._settingsDraft) this._settingsDraft.seasonal_mode = target; } catch {} + this._applySeasonalModeToRows(true, prev); + try { this._render(); } catch {} + try { this._scheduleNextApply(); } catch {} + try { this._debouncedSaveStore(); } catch {} + } catch {} + } + _toggleSeasonMode(){ try { const next = (this._seasonKey() === 'summer') ? 'winter' : 'summer'; this._setSeasonMode(next); } catch {} } + _syncSeasonalRowsToBucket(){ + try { + if (!this._seasonEnabled()) return; + const key = this._seasonKey(); + for (const eid of (this._config?.entities || [])) { + const row = this._schedules?.[eid]; + if (!row) continue; + this._syncSeasonDataFromRow(row, key); + } + } catch {} + } + + _weekdayFullName(dayKey){ + try { + const k = String(dayKey || '').toLowerCase(); + const map = { + mon: 'week.days_full.mon', + tue: 'week.days_full.tue', + wed: 'week.days_full.wed', + thu: 'week.days_full.thu', + fri: 'week.days_full.fri', + sat: 'week.days_full.sat', + sun: 'week.days_full.sun' + }; + const t = map[k] ? this._t(map[k]) : ''; + if (t) return String(t); + } catch {} + try { + const names = { mon:'Monday', tue:'Tuesday', wed:'Wednesday', thu:'Thursday', fri:'Friday', sat:'Saturday', sun:'Sunday' }; + return names[String(dayKey||'').toLowerCase()] || this._todayFullName(); + } catch { return this._todayFullName(); } + } + + // Format ISO timestamp in a readable, localized way + _fmtDateTimeIso(iso){ + try { + if (!iso) return ''; + const d = new Date(iso); + const lang = ttGetLangFromHass(this._hass || this._lang || 'en'); + const opts = { dateStyle: 'medium', timeStyle: 'short' }; + if (this._useHaTime() && this._hass?.config?.time_zone) opts.timeZone = this._hass.config.time_zone; + return new Intl.DateTimeFormat(lang, opts).format(d); + } catch { return String(iso||''); } + } + + // --- Weekday helpers --- + _weekdayNames(){ return ['mon','tue','wed','thu','fri','sat','sun']; } + + _hasAnyMainBlocks(){ + try { + for (const eid of (this._config?.entities || [])) { + const row = this._schedules?.[eid]; + if (row && Array.isArray(row.blocks) && row.blocks.length) return true; + } + } catch {} + return false; + } + + _promptWeekdayCopyMode(){ + try { + const m1 = this._t('week.mode.weekday_weekend') || 'Weekdays + Weekend'; + const m2 = this._t('week.mode.weekday_sat_sun') || 'Weekdays + Saturday + Sunday'; + const m3 = this._t('week.mode.all_7') || 'All days'; + const msgT = this._t('week.copy_from_main.mode_prompt') || 'Choose weekday mode:\n1) {m1}\n2) {m2}\n3) {m3}\nEnter 1-3:'; + const msg = msgT.replace('{m1}', m1).replace('{m2}', m2).replace('{m3}', m3); + const defMode = String(this._config?.weekdays_mode || 'weekday_weekend'); + const def = defMode === 'weekday_sat_sun' ? '2' : (defMode === 'all_7' ? '3' : '1'); + const ans = prompt(msg, def); + if (ans == null) return null; + const a = String(ans).trim().toLowerCase(); + if (a === '2' || a === 'weekday_sat_sun') return 'weekday_sat_sun'; + if (a === '3' || a === 'all_7' || a === 'all7') return 'all_7'; + if (a === '1' || a === 'weekday_weekend') return 'weekday_weekend'; + return 'weekday_weekend'; + } catch { return null; } + } + + _copyMainToWeekdays(mode, target){ + try { + const m = (mode === 'weekday_sat_sun' || mode === 'all_7') ? mode : 'weekday_weekend'; + const makeBlocks = (arr)=> (Array.isArray(arr) ? arr.map(b=>({ id: Math.random().toString(36).slice(2,9), startMin: b.startMin, endMin: b.endMin, temp: b.temp })) : []); + const names = this._weekdayNames(); + const t = String(target || '').toLowerCase().trim(); + for (const eid of (this._config?.entities || [])) { + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureWeeklyStruct(row); + const days = {}; + for (const k of names) days[k] = []; + const src = row.blocks || []; + if (m === 'weekday_weekend') { + const tgt = (t === 'weekend') ? ['sat','sun'] : ['mon','tue','wed','thu','fri']; + for (const k of tgt) days[k] = makeBlocks(src); + } else if (m === 'weekday_sat_sun') { + if (t === 'sat' || t === 'sun') { + days[t] = makeBlocks(src); + } else { + for (const k of ['mon','tue','wed','thu','fri']) days[k] = makeBlocks(src); + } + } else { + const dk = names.includes(t) ? t : 'mon'; + days[dk] = makeBlocks(src); + } + row.weekly_modes[m] = { days: JSON.parse(JSON.stringify(days)) }; + row.weekly = { mode: m, days: JSON.parse(JSON.stringify(days)) }; + this._schedules[eid] = row; + } + this._config.weekdays_mode = m; + try { this._syncSeasonalRowsToBucket(); } catch {} + } catch {} + } + + _timelineDisplayDayKey(){ + try { + const enabled = !!this._config?.weekdays_enabled; + const view = String(this._config?.weekdays_view || 'all_rooms_one_day'); + if (enabled && view === 'all_rooms_one_day') { + const names = this._weekdayNames(); + const cur = String(this._weekdaysViewDayKey || '').toLowerCase(); + if (names.includes(cur)) return cur; + const today = this._todayKey(); + this._weekdaysViewDayKey = today; + return today; + } + } catch {} + return this._todayKey(); + } + + _setTimelineDisplayDayKey(dayKey){ + try { + const k = String(dayKey || '').toLowerCase(); + if (this._weekdayNames().includes(k)) this._weekdaysViewDayKey = k; + } catch {} + } + + _weekdaysStorageDayKeyForMode(displayDayKey, mode){ + const d = String(displayDayKey || 'mon').toLowerCase(); + const isWk = ['mon','tue','wed','thu','fri'].includes(d); + const m = String(mode || 'weekday_weekend'); + if (m === 'weekday_weekend') return isWk ? 'mon' : 'sat'; + if (m === 'weekday_sat_sun') return isWk ? 'mon' : d; // sat/sun + return d; // all_7 + } + _todayKey(){ + try { + if (this._useHaTime() && this._hass?.config?.time_zone) { + const tz = this._hass.config.time_zone; + const parts = new Intl.DateTimeFormat('en-US', { timeZone: tz, weekday: 'short' }).format(new Date()); + const map = { Mon:'mon', Tue:'tue', Wed:'wed', Thu:'thu', Fri:'fri', Sat:'sat', Sun:'sun' }; + return map[parts] || 'mon'; + } + } catch {} + const d=new Date(); const idx=((d.getDay()+6)%7); return this._weekdayNames()[idx]; + } + _ensureWeeklyStruct(row){ + if (!row.weekly) { row.weekly = { mode: this._config?.weekdays_mode || 'weekday_weekend', days: {} }; } + const names=this._weekdayNames(); + for (const k of names){ if (!Array.isArray(row.weekly.days[k])) row.weekly.days[k] = []; } + // Maintain separate saved schedules per mode + if (!row.weekly_modes) { + row.weekly_modes = { + weekday_weekend: { days: {} }, + weekday_sat_sun: { days: {} }, + all_7: { days: {} } + }; + // Seed current into active mode bucket + try { + const curMode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + row.weekly_modes[curMode] = { days: JSON.parse(JSON.stringify(row.weekly?.days||{})) }; + } catch {} + } + try { + const modes = ['weekday_weekend','weekday_sat_sun','all_7']; + for (const m of modes){ + const bucket = row.weekly_modes[m] = row.weekly_modes[m] || { days: {} }; + for (const k of names){ if (!Array.isArray(bucket.days[k])) bucket.days[k] = []; } + } + } catch {} + return row; + } + _getBlocksForDay(row, dayKey){ if (!this._config?.weekdays_enabled || !row?.weekly) return row?.blocks || []; const days = row.weekly.days || {}; return Array.isArray(days[dayKey]) ? days[dayKey] : (row.blocks||[]); } + _weeklyEditableKeys(mode){ const all = this._weekdayNames(); if (!mode) mode = 'weekday_weekend'; if (mode==='weekday_weekend') return ['weekdays','weekend']; if (mode==='weekday_sat_sun') return ['weekdays','sat','sun']; if (mode==='all_7') return all; return ['weekdays','weekend']; } + _effectiveDayKey(sel){ if (sel==='weekdays') return 'mon'; if (sel==='weekend') return 'sat'; return sel || 'mon'; } + + // --- Profiles (named daily schedules) helpers --- + _ensureProfilesStruct(row){ + try { + if (!row) return row; + if (!row.profiles || typeof row.profiles !== 'object') row.profiles = {}; + if (!('activeProfile' in row)) row.activeProfile = null; + } catch {} + return row; + } + + // --- Holidays helpers --- + _ensureHolidayStruct(row){ + try { if (!row) return row; if (!row.holiday || !Array.isArray(row.holiday.blocks)) row.holiday = { blocks: [] }; } catch {} return row; + } + + // --- Presence (advanced away) helpers --- + _ensurePresenceStruct(row){ try { if (!row) return row; if (!row.presence || typeof row.presence !== 'object') row.presence = {}; } catch {} return row; } + _presencePersons(){ try { return Array.isArray(this._config?.away?.persons) ? [...this._config.away.persons] : []; } catch { return []; } } + _presenceComboKey(homeList, awayList){ + try { + const h = [...(homeList||[])].map(String).sort(); + const a = [...(awayList||[])].map(String).sort(); + return `H:${h.join(',')}|A:${a.join(',')}`; + } catch { return 'H:|A:'; } + } + _presenceAllCombos(){ + try { + const persons = this._presencePersons().map(String); + const n = persons.length; const out = []; + for (let mask=0; mask < (1<>i)&1) ? home.push(persons[i]) : away.push(persons[i]); } + // Skip the "all home" combination — when everyone is home, the main/weekdays + // schedule should be used and advanced presence should not override. + if (home.length === persons.length) continue; + const key = this._presenceComboKey(home, away); + const homeNames = home.map(e=> this._hass?.states?.[e]?.attributes?.friendly_name || (e.split('.')[1]||e)).join(', '); + const awayNames = away.map(e=> this._hass?.states?.[e]?.attributes?.friendly_name || (e.split('.')[1]||e)).join(', '); + let label; + if (home.length === 0) label = this._t('presence.none_home'); + else { + const h = (this._t('presence.home') || 'Home: {names}').replace('{names}', homeNames); + const a = (this._t('presence.away') || 'Away: {names}').replace('{names}', awayNames); + label = `${h} • ${a}`; + } + out.push({ key, home:[...home], away:[...away], label }); + } + // Sort with "none home" first, then by length of home desc (all‑home is omitted) + out.sort((x,y)=>{ + if (x.home.length===0 && y.home.length!==0) return -1; + if (y.home.length===0 && x.home.length!==0) return 1; + return y.home.length - x.home.length; + }); + return out; + } catch { return []; } + } + _activePresenceComboKey(){ + try { + if (!this._config?.away?.advanced_enabled) return null; + const persons = this._presencePersons(); if (!persons.length) return null; + const home=[], away=[]; + for (const p of persons){ const st = this._hass?.states?.[p]; const s = String(st?.state||'').toLowerCase(); if (s==='home') home.push(p); else away.push(p); } + // Apply optional delay before switching into (or out of) presence mode + try { + const delayMs = this._awayDelayThresholdMs(); + if (delayMs > 0){ + const anyAway = (home.length !== persons.length); + const now = Date.now(); + if (this._awayWant !== anyAway) { this._awayWant = anyAway; this._awayWantSinceMs = now; } + const stable = (now - (this._awayWantSinceMs||0)) >= delayMs; + if (!stable) return this._lastPresenceKey || null; + } + } catch {} + // When everyone is home, do not use advanced presence at all — fall back to + // the main/weekdays schedule. No need for an "all home" presence plan. + if (home.length === persons.length) return null; + const key = this._presenceComboKey(home, away); + const combos = (this._config?.away?.combos) || {}; + const meta = combos[key]; + if (meta && meta.enabled) { this._lastPresenceKey = key; return key; } + return null; + } catch { return null; } + } + _todayISO(){ + try { + if (this._useHaTime() && this._hass?.config?.time_zone) { + const tz = this._hass.config.time_zone; + const parts = new Intl.DateTimeFormat('en-CA', { timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit' }).format(new Date()); + // en-CA gives YYYY-MM-DD + return String(parts); + } + } catch {} + try { const d=new Date(); const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`; } catch { return ''; } + } + _isHolidayActive(){ + try { + if (!this._config?.holidays_enabled) return false; + const src = this._config.holidays_source || 'calendar'; + if (src === 'manual') { + const list = Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : []; + const today = this._todayISO(); + return !!list.find(d=>String(d)===today); + } + // calendar/binary_sensor entity + const eid = this._config.holidays_entity || ''; + if (!eid) return false; + const st = this._hass?.states?.[eid]; + const s = String(st?.state||'').toLowerCase(); + return s === 'on'; + } catch { return false; } + } + + // --- Merge helpers --- + _groupPrimaryOf(eid){ + const prims = this._config?.entities || []; + if (prims.includes(eid)) return eid; + const merges = this._config?.merges || {}; + for (const p of prims){ const arr = merges[p]||[]; if (arr.includes(eid)) return p; } + return eid; + } + _allTargetEntities(){ + const out = new Set(); + const merges = this._config?.merges || {}; + for (const p of (this._config?.entities||[])){ + out.add(p); + for (const l of (merges[p]||[])) out.add(l); + } + return Array.from(out); + } + + _desiredTempFor(eid, nowMin){ const primary = this._groupPrimaryOf(eid); const row = this._schedules[primary]; if (!row) return null; this._ensureHolidayStruct(row); this._ensurePresenceStruct(row); let blocks = row.blocks||[]; let usedPresence = false; let usedHoliday = false; let presenceKeyActive = null; let presenceHasBlocksForRoom = false; + // Holidays override take highest precedence when enabled + try { + if (this._isHolidayActive() && row.holiday && Array.isArray(row.holiday.blocks) && row.holiday.blocks.length) { + blocks = row.holiday.blocks || []; + usedHoliday = true; + } else { + // fall through + } + } catch {} + // Advanced presence overrides (if enabled and a combo is active) — below holidays, above profiles/weekdays + try { + presenceKeyActive = this._activePresenceComboKey && this._activePresenceComboKey(); + if (presenceKeyActive) { + const roomBlocks = row.presence && row.presence[presenceKeyActive] && Array.isArray(row.presence[presenceKeyActive].blocks) + ? row.presence[presenceKeyActive].blocks : []; + if (roomBlocks.length) { + blocks = roomBlocks; + usedPresence = true; + presenceHasBlocksForRoom = true; + } else { + // Presence combo is active but this room has no presence schedule -> use a virtual all‑day Away block + const awayC = Number(this._config?.away?.target_c ?? 17); + const fallback = Number.isFinite(awayC) ? awayC : this._rowDefaultTemp(row); + blocks = [{ id: '__presence_away__', startMin: 0, endMin: 1440, temp: fallback }]; + usedPresence = true; // treat as presence so simple away clamp won't run later + presenceHasBlocksForRoom = false; + } + } + } catch {} + // Profiles/Weekdays fallback if presence not used + if (!usedPresence) { + try { + if (this._config?.profiles_enabled && !(this._isHolidayActive() && row?.holiday?.blocks?.length)) { + this._ensureProfilesStruct(row); + const ap = row.activeProfile; + if (ap && row.profiles && Array.isArray(row.profiles[ap]?.blocks) && (row.profiles[ap].blocks||[]).length) { + blocks = row.profiles[ap].blocks || []; + } else if (this._config?.weekdays_enabled && row.weekly) { + const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey); + } + } else if (this._config?.weekdays_enabled && row.weekly) { + const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey); + } + } catch { /* fall back to default blocks */ } + } + const hit = (blocks||[]).find(b => nowMin >= b.startMin && nowMin < b.endMin); + let want = Number(hit ? hit.temp : this._rowDefaultTemp(row)); + // Apply away override if active + // Rules: + // - If advanced presence is enabled, the simple Away clamp must NEVER apply + // (advanced replaces basic away entirely), regardless of whether a presence + // schedule is currently used. + // - If advanced presence is OFF, apply simple Away clamp only when we did not + // use a presence schedule for this room (usedPresence=false). + try { + const advOn = !!(this._config?.away?.advanced_enabled); + if (!advOn && !usedPresence && this._isAwayActive() && !this._awayBypass) { + const targetC = Number(this._config?.away?.target_c ?? 17); + if (Number.isFinite(targetC)) want = Math.min(want, targetC); + } + } catch {} + + // Per-room presence sensor override with optional on/off delays + try { + if (this._config?.presence_sensor_enabled) { + const sEid = this._presenceSensorEntityFor ? this._presenceSensorEntityFor(primary) : ''; + if (sEid) { + const active = this._presenceSensorActiveFor ? !!this._presenceSensorActiveFor(primary) : false; + if (active) { + const tmap = (this._config?.presence_sensor_temps && typeof this._config.presence_sensor_temps === 'object') ? this._config.presence_sensor_temps : {}; + const ov = Number(tmap?.[primary]); + if (Number.isFinite(ov)) want = ov; + } + } + } + } catch {} + return want; } + + // ---- Presence sensor (per-room) helpers ---- + _presenceSensorEntityFor(primary){ + try { + if (!this._config?.presence_sensor_enabled) return ''; + const map = (this._config?.presence_sensors && typeof this._config.presence_sensors === 'object') ? this._config.presence_sensors : {}; + const sEid = String(map?.[primary] || '').trim(); + if (sEid && sEid.startsWith('binary_sensor.')) return sEid; + } catch {} + return ''; + } + + _presenceSensorDelayMsFor(primary){ + try { + const defaultS = 180; // 3 minutes + const dmap = (this._config?.presence_sensor_delays && typeof this._config.presence_sensor_delays === 'object') ? this._config.presence_sensor_delays : {}; + const o = (dmap?.[primary] && typeof dmap?.[primary] === 'object') ? dmap[primary] : {}; + const onS0 = Number(o?.on_s); + const offS0 = Number(o?.off_s); + const onS = Number.isFinite(onS0) ? Math.max(0, onS0) : defaultS; + const offS = Number.isFinite(offS0) ? Math.max(0, offS0) : defaultS; + const onMs = onS * 1000; + const offMs = offS * 1000; + return { onMs, offMs }; + } catch {} + return { onMs: 0, offMs: 0 }; + } + + _presenceSensorActiveFor(primary){ + try { + if (!this._config?.presence_sensor_enabled) return false; + const sEid = this._presenceSensorEntityFor(primary); + if (!sEid) return false; + + const st = this._hass?.states?.[sEid]; + const s = String(st?.state || '').toLowerCase().trim(); + const now = Date.now(); + const changedMs = (()=>{ + try { + const raw = st?.last_changed || st?.last_updated || null; + const ms = raw ? Date.parse(String(raw)) : NaN; + return Number.isFinite(ms) ? ms : now; + } catch { return now; } + })(); + + const { onMs, offMs } = this._presenceSensorDelayMsFor(primary); + + try { if (!this._presenceSensorLatch) this._presenceSensorLatch = {}; } catch {} + const latch0 = this._presenceSensorLatch?.[primary]; + const latch = (latch0 && typeof latch0 === 'object') ? latch0 : { last: null, sinceMs: changedMs, active: false }; + + // Sync our "since" to HA's last_changed for accuracy + if (latch.last !== s || Number(latch.sinceMs||0) !== Number(changedMs||0)) { + latch.last = s; + latch.sinceMs = changedMs; + } + const dur = now - Number(latch.sinceMs||now); + + if (s === 'on') { + if (!latch.active) { + latch.active = (dur >= onMs); + } + } else if (s === 'off') { + if (latch.active) { + latch.active = !(dur >= offMs); + } + } else { + // Unknown/unavailable: fall back to schedule (disable override) + latch.active = false; + } + + this._presenceSensorLatch[primary] = latch; + return !!latch.active; + } catch {} + return false; + } + + _presenceSensorActiveSnapshot(){ + const out = {}; + try { + if (!this._config?.presence_sensor_enabled) return out; + const ents = Array.isArray(this._config?.entities) ? this._config.entities.filter(Boolean).map(String) : []; + for (const primary of ents) { + const sEid = this._presenceSensorEntityFor(primary); + if (!sEid) continue; + out[primary] = !!this._presenceSensorActiveFor(primary); + } + } catch {} + return out; + } + + _presenceSensorSnapChanged(a, b){ + try { + const aa = (a && typeof a === 'object') ? a : {}; + const bb = (b && typeof b === 'object') ? b : {}; + const keys = new Set([...(Object.keys(aa)), ...(Object.keys(bb))]); + for (const k of keys) { + if (!!aa[k] !== !!bb[k]) return true; + } + } catch {} + return false; + } + + _schedulePresenceDelayTimer(){ + try { if (this._presenceDelayTimer) { clearTimeout(this._presenceDelayTimer); this._presenceDelayTimer = null; } } catch {} + try { + if (!this._config?.auto_apply) return; + if (!this._config?.presence_sensor_enabled) return; + if (!this._hass) return; + if (!this._hasConfig) return; + if (this._isPaused && this._isPaused()) return; + + const ents = Array.isArray(this._config?.entities) ? this._config.entities.filter(Boolean).map(String) : []; + const now = Date.now(); + let bestDue = null; + + for (const primary of ents) { + const sEid = this._presenceSensorEntityFor(primary); + if (!sEid) continue; + const st = this._hass?.states?.[sEid]; + const s = String(st?.state || '').toLowerCase().trim(); + if (s !== 'on' && s !== 'off') continue; + + // Ensure latch is updated before computing due + const active = !!this._presenceSensorActiveFor(primary); + const { onMs, offMs } = this._presenceSensorDelayMsFor(primary); + + const changedMs = (()=>{ + try { + const raw = st?.last_changed || st?.last_updated || null; + const ms = raw ? Date.parse(String(raw)) : NaN; + return Number.isFinite(ms) ? ms : now; + } catch { return now; } + })(); + + let due = null; + if (s === 'on' && !active && onMs > 0) due = changedMs + onMs; + else if (s === 'off' && active && offMs > 0) due = changedMs + offMs; + + if (due != null && due > now) { + if (bestDue == null || due < bestDue) bestDue = due; + } + } + + if (bestDue == null) return; + const delay = Math.max(500, bestDue - Date.now()); + this._presenceDelayTimer = setTimeout(async ()=>{ + try { + if (!this._config?.auto_apply) return; + if (this._isPaused && this._isPaused()) return; + if (this._config?.storage_enabled) { + await this._nudgeBackgroundApplyNow(); + } else { + await this._applyCurrentSetpoints(true, true); + } + this._scheduleNextApply(); + } catch {} + try { this._schedulePresenceDelayTimer(); } catch {} + }, delay); + } catch {} + } + + _awayDelayThresholdMs(){ try { const a=this._config?.away||{}; if (!a.delay_enabled) return 0; const v = Number(a.delay_value||0); const u = String(a.delay_unit||'minutes'); const ms = Math.max(0, Math.round(v)) * (u==='seconds'?1000:60000); return ms; } catch { return 0; } } + _isAwayActive(){ + try { + const a = this._config?.away; if (!a || !a.enabled) return false; + const persons = Array.isArray(a.persons) ? a.persons : []; + if (!persons.length) return false; + let want = true; // assume away until we find someone home + for (const p of persons){ const st = this._hass?.states?.[p]; const s = (st?.state||'').toLowerCase(); if (s === 'home') { want = false; break; } } + const delayMs = this._awayDelayThresholdMs(); + if (!delayMs) { this._lastAwayActive = want; return want; } + const now = Date.now(); + if (this._awayWant !== want) { this._awayWant = want; this._awayWantSinceMs = now; } + const stable = (now - (this._awayWantSinceMs||0)) >= delayMs; + const prev = !!this._lastAwayActive; + const out = stable ? want : prev; + this._lastAwayActive = out; + return out; + } catch { return false; } + } + + // ---- Pause helpers ---- + _isPausedBySensor(){ + try { + if (!this._config?.pause_sensor_enabled) return false; + const eid = String(this._config?.pause_sensor_entity || '').trim(); + if (!eid || !eid.startsWith('binary_sensor.')) return false; + const st = this._hass?.states?.[eid]; + const s = String(st?.state || '').toLowerCase(); + return s === 'on'; + } catch { return false; } + } + _isPaused(){ + try { + if (this._isPausedBySensor()) return true; + if (this._pauseIndef) return true; + const until = Number(this._pauseUntilMs||0); + if (Number.isFinite(until) && until > Date.now()) return true; + } catch {} + return false; + } + _resumeAtMs(){ const u = Number(this._pauseUntilMs||0); return (Number.isFinite(u) && u>0) ? u : null; } + _scheduleResumeTimer(){ + try { + if (this._resumeTimer) clearTimeout(this._resumeTimer); + if (!this._isPaused()) return; + if (this._pauseIndef || this._isPausedBySensor()) return; // manual or sensor resume only + const when = this._resumeAtMs(); + if (!when) return; + const delay = Math.max(500, when - Date.now()); + this._resumeTimer = setTimeout(async ()=>{ + // Resume reached: re-render UI and apply immediately once + try { this._render(); } catch {} + await this._applyCurrentSetpoints(true, true); + this._scheduleNextApply(); + }, delay); + } catch {} + } + + _formatPauseCountdown(){ + try { + const until = Number(this._pauseUntilMs||0); + const now = Date.now(); + if (!until || until <= now) return ''; + let ms = until - now; + const totalSec = Math.floor(ms/1000); + const h = Math.floor(totalSec/3600); + const m = Math.floor((totalSec%3600)/60); + const s = totalSec%60; + const pad = (n)=>String(n).padStart(2,'0'); + if (h>0) return `${h}:${pad(m)}:${pad(s)}`; + return `${m}:${pad(s)}`; + } catch { return ''; } + } + _startCountdownTimer(){ + try { + if (this._countdownTimer) clearInterval(this._countdownTimer); + const wrap = this.shadowRoot && this.shadowRoot.querySelector('.pause-wrap'); + const etaEl = wrap && wrap.querySelector && wrap.querySelector('.pause-eta'); + if (!this._isPaused() || this._pauseIndef || this._isPausedBySensor() || !etaEl) return; + const tick = ()=>{ try { if (!this._isPaused() || this._pauseIndef) { clearInterval(this._countdownTimer); this._countdownTimer=null; etaEl.textContent=''; return; } etaEl.textContent = this._formatPauseCountdown(); } catch {} }; + tick(); + this._countdownTimer = setInterval(tick, 1000); + } catch {} + } + + _nextBoundaryDate(){ + // HA timezone mode: compute delay in HA tz and return a local Date for the timeout + if (this._useHaTime() && this._hass?.config?.time_zone) { + const nowMin = this._getNowMin(); let bestT = null; let bestDelta = Infinity; + for (const eid of (this._config.entities || [])){ + const row = this._schedules[eid]; if (!row) continue; const times = []; + // Prefer presence schedule when enabled and active; then profiles/weekdays + let blocks = row.blocks||[]; let usedPresence=false; + try { + const key = this._activePresenceComboKey && this._activePresenceComboKey(); + if (key && row.presence && Array.isArray(row.presence[key]?.blocks) && row.presence[key].blocks.length) { + blocks = row.presence[key].blocks || []; usedPresence=true; + } + } catch {} + if (!usedPresence) { + try { + if (this._config?.profiles_enabled) { + this._ensureProfilesStruct(row); + const ap = row.activeProfile; + if (ap && row.profiles && Array.isArray(row.profiles[ap]?.blocks) && (row.profiles[ap].blocks||[]).length) { + blocks = row.profiles[ap].blocks || []; + } else if (this._config?.weekdays_enabled && row.weekly) { + const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey) || []; + } + } else if (this._config?.weekdays_enabled && row.weekly) { + const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey) || []; + } + } catch {} + } + for (const b of blocks) { times.push(b.startMin, b.endMin); } + for (const t of times){ let delta = (t - nowMin + 1440) % 1440; if (delta === 0) delta = 1; if (delta < bestDelta){ bestDelta = delta; bestT = t; } } + } + // Always consider midnight rollover (00:00) + { + let deltaMid = (1440 - nowMin) % 1440; const tMid = 0; if (deltaMid === 0) deltaMid = 1; if (deltaMid < bestDelta) { bestDelta = deltaMid; bestT = tMid; } + } + // Use delta minutes (not the raw boundary minute) so a boundary that + // equals nowMin (e.g. 08:00) never schedules a rapid 500ms loop. + let delayMs; + if (!Number.isFinite(bestDelta) || bestDelta === Infinity) { + delayMs = Math.max(60000, ((1440 - nowMin) % 1440) * 60000); + } else { + delayMs = Math.max(60000, bestDelta * 60000); + } + return new Date(Date.now() + delayMs); + } + // Browser/local timezone mode (existing behavior) + const now = new Date(); const nowMin = this._getNowMin(); let bestT = null; let bestDelta = Infinity; for (const eid of (this._config.entities || [])){ + const row = this._schedules[eid]; if (!row) continue; const times = []; + // Prefer presence schedule when enabled and active; then profiles/weekdays + let blocks = row.blocks||[]; let usedPresence=false; + try { + const key = this._activePresenceComboKey && this._activePresenceComboKey(); + if (key && row.presence && Array.isArray(row.presence[key]?.blocks) && row.presence[key].blocks.length) { + blocks = row.presence[key].blocks || []; usedPresence=true; + } + } catch {} + if (!usedPresence) { + try { + if (this._config?.profiles_enabled) { + this._ensureProfilesStruct(row); + const ap = row.activeProfile; + if (ap && row.profiles && Array.isArray(row.profiles[ap]?.blocks) && (row.profiles[ap].blocks||[]).length) { + blocks = row.profiles[ap].blocks || []; + } else if (this._config?.weekdays_enabled && row.weekly) { + const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey) || []; + } + } else if (this._config?.weekdays_enabled && row.weekly) { + const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey) || []; + } + } catch {} + } + for (const b of blocks) { times.push(b.startMin, b.endMin); } + for (const t of times){ let delta = (t - nowMin + 1440) % 1440; if (delta === 0) delta = 1; if (delta < bestDelta){ bestDelta = delta; bestT = t; } } + } + // Always consider midnight rollover (00:00) as a boundary so we re-apply + // the next day's desired temperature even if today's schedule has no block + // ending/starting at 00:00. This makes day change bullet-proof. + { + let deltaMid = (1440 - nowMin) % 1440; // minutes until midnight + const tMid = 0; + if (deltaMid === 0) deltaMid = 1; // avoid scheduling "now" + if (deltaMid < bestDelta) { bestDelta = deltaMid; bestT = tMid; } + } + // Same safeguard as HA-time mode: schedule based on delta minutes. + if (!Number.isFinite(bestDelta) || bestDelta === Infinity) { + const ms = Math.max(60000, ((1440 - nowMin) % 1440) * 60000); + return new Date(Date.now() + ms); + } + return new Date(Date.now() + Math.max(60000, bestDelta * 60000)); } + + // (removed next-boundary-per-row helper) + + _scheduleNextApply(){ + if (this._transitionTimer) clearTimeout(this._transitionTimer); + // If auto-apply is off completely, do nothing + if (!this._config?.auto_apply) return; + // Don't schedule until config is available + if (!this._hasConfig) return; + // If paused by binary sensor, don't schedule boundary timer; resume when sensor changes + if (this._isPausedBySensor && this._isPausedBySensor()) { this._scheduleResumeTimer(); return; } + // If paused indefinitely, don't schedule boundary timer; only resume via user action + if (this._pauseIndef) { this._scheduleResumeTimer(); return; } + // Choose earlier of boundary and pause-resume moment if set + const boundary = this._nextBoundaryDate(); + let targetMs = boundary.getTime(); + const resumeMs = this._resumeAtMs(); + const isResume = !!(resumeMs && resumeMs < targetMs); + if (isResume) targetMs = resumeMs; + const delay = Math.max(500, targetMs - Date.now()); + this._transitionTimer = setTimeout(async () => { + await this._applyCurrentSetpoints(true, isResume); + this._scheduleNextApply(); + }, delay); + // Also make sure we have a standalone resume timer if paused + this._scheduleResumeTimer(); + } + + _setupBoilerControl(){ + try { + if (this._boilerTimer) { clearInterval(this._boilerTimer); this._boilerTimer = null; } + if (!this._hass || !this._config) return; + // Boiler control runs in frontend only when Sync is OFF. + if (this._config?.storage_enabled) return; + if (!this._config?.auto_apply) return; + if (!this._config?.boiler_enabled) return; + const multiOn = !!this._config?.boiler_multi_enabled; + if (!multiOn) { + const sw = String(this._config.boiler_switch || ''); + if (!sw) return; + const dom = String(sw).split('.')[0]; + if (!(dom === 'switch' || dom === 'input_boolean')) return; + } else { + const map = (this._config.boiler_room_settings && typeof this._config.boiler_room_settings === 'object') ? this._config.boiler_room_settings : {}; + const hasAny = Object.values(map||{}).some(r=>{ + try { + if (!r || !r.enabled) return false; + const sw = String(r.switch || '').trim(); + const dom = String(sw).split('.')[0]; + return !!sw && (dom === 'switch' || dom === 'input_boolean'); + } catch { return false; } + }); + if (!hasAny) return; + } + + // Run once immediately + then every minute. + try { this._boilerTick(); } catch {} + this._boilerTimer = setInterval(() => { try { this._boilerTick(); } catch {} }, 60000); + } catch {} + } + + _boilerTick(){ + try { + if (!this._hass || !this._config) return; + if (this._config?.storage_enabled) return; + if (!this._config?.auto_apply) return; + if (!this._config?.boiler_enabled) return; + if (this._isPaused && this._isPaused()) return; + + const nowMin = this._getNowMin(); + const allRooms = (Array.isArray(this._config.entities) ? this._config.entities : []) + .map(e=>String(e||'')) + .filter(e=>e.startsWith('climate.')); + if (!allRooms.length) return; + + const multiOn = !!this._config?.boiler_multi_enabled; + const clampOffset = (v)=>{ const n = Number(v); return Number.isFinite(n) ? Math.max(-10, Math.min(10, n)) : null; }; + + // Multi-boiler mode: group rooms by assigned boiler switch + if (multiOn) { + const roomCfgs = (this._config.boiler_room_settings && typeof this._config.boiler_room_settings === 'object') ? this._config.boiler_room_settings : {}; + const groups = {}; + for (const eid of allRooms) { + const rc = roomCfgs?.[eid]; + if (!rc || typeof rc !== 'object') continue; + if (!rc.enabled) continue; + const sw = String(rc.switch || '').trim(); + if (!sw) continue; + const dom = String(sw).split('.')[0]; + if (!(dom === 'switch' || dom === 'input_boolean')) continue; + const onOff = clampOffset(rc.on_offset); + const offOff = clampOffset(rc.off_offset); + const entry = { eid, onOff: (onOff === null ? Number(this._config?.boiler_on_offset ?? 0) : onOff), offOff: (offOff === null ? Number(this._config?.boiler_off_offset ?? 0) : offOff) }; + if (!groups[sw]) groups[sw] = []; + groups[sw].push(entry); + } + const switches = Object.keys(groups); + if (!switches.length) return; + + for (const sw of switches) { + const rooms = groups[sw] || []; + if (!rooms.length) continue; + let anyNeedHeat = false; + let anyUnknown = false; + let anySeen = false; + let allOkForOff = true; + + for (const r of rooms) { + const desiredC = Number(this._desiredTempFor ? this._desiredTempFor(r.eid, nowMin) : null); + const curC = Number(this._roomCurrentTemperatureC ? this._roomCurrentTemperatureC(r.eid) : NaN); + if (!Number.isFinite(desiredC) || !Number.isFinite(curC)) { anyUnknown = true; continue; } + anySeen = true; + if (curC < (desiredC - r.onOff)) { anyNeedHeat = true; break; } + if (curC < (desiredC + r.offOff)) allOkForOff = false; + } + + if (!anySeen) continue; + let want = null; + if (anyNeedHeat) want = 'on'; + else if (!anyUnknown && allOkForOff) want = 'off'; + else continue; + + const stSw = this._hass.states?.[sw]; + const cur = String(stSw?.state || ''); + if (want === 'on' && cur === 'on') continue; + if (want === 'off' && cur === 'off') continue; + + const dom = String(sw).split('.')[0]; + if (!(dom === 'switch' || dom === 'input_boolean')) continue; + if (want === 'on') this._hass.callService(dom, 'turn_on', { entity_id: sw }); + else this._hass.callService(dom, 'turn_off', { entity_id: sw }); + } + return; + } + + // Single-boiler mode + const sw = String(this._config.boiler_switch || ''); + if (!sw) return; + const dom0 = String(sw).split('.')[0]; + if (!(dom0 === 'switch' || dom0 === 'input_boolean')) return; + + // New logic: compare each selected climate entity's current_temperature to + // its scheduled target temperature for "now". + // - If ANY room is below its scheduled target -> switch ON + // - If ALL rooms are at/above their scheduled target -> switch OFF + // If some rooms have missing data, we only turn OFF when we can confirm + // ALL rooms are at/above target. + const allSet = new Set(allRooms); + const cfgRooms = this._config?.boiler_rooms; + const roomEntities = Array.isArray(cfgRooms) + ? cfgRooms.map(e=>String(e||'')).filter(e=>allSet.has(e) && e.startsWith('climate.')) + : allRooms; + if (!roomEntities.length) return; + + const onOff = (()=>{ const v = Number(this._config?.boiler_on_offset ?? 0); return Number.isFinite(v) ? v : 0; })(); + const offOff = (()=>{ const v = Number(this._config?.boiler_off_offset ?? 0); return Number.isFinite(v) ? v : 0; })(); + let anyNeedHeat = false; + let anyUnknown = false; + let anySeen = false; + let allOkForOff = true; + + for (const eid of roomEntities) { + const desiredC = Number(this._desiredTempFor ? this._desiredTempFor(eid, nowMin) : null); + const curC = Number(this._roomCurrentTemperatureC ? this._roomCurrentTemperatureC(eid) : NaN); + + if (!Number.isFinite(desiredC) || !Number.isFinite(curC)) { + anyUnknown = true; + continue; + } + + anySeen = true; + if (curC < (desiredC - onOff)) { anyNeedHeat = true; break; } + if (curC < (desiredC + offOff)) allOkForOff = false; + } + + if (!anySeen) return; + + let want = null; + if (anyNeedHeat) want = 'on'; + else if (!anyUnknown && allOkForOff) want = 'off'; + else return; + + const stSw = this._hass.states?.[sw]; + const cur = String(stSw?.state || ''); + if (want === 'on' && cur === 'on') return; + if (want === 'off' && cur === 'off') return; + + const dom = String(sw).split('.')[0]; + if (!(dom === 'switch' || dom === 'input_boolean')) return; + if (want === 'on') this._hass.callService(dom, 'turn_on', { entity_id: sw }); + else this._hass.callService(dom, 'turn_off', { entity_id: sw }); + } catch {} + } + + async _applyCurrentSetpoints(force=false, reconcile=false){ if (!this._config?.auto_apply || !this._hass) return; if (!this._hasConfig) return; if (this._config?.storage_enabled) return; if (this._isPaused()) return; const nowMin = this._getNowMin(); const targets = this._allTargetEntities(); for (const eid of targets){ + // If this call is from boundary timer (force===true), only apply for entities that have boundary now. + // On resume from pause (reconcile=true), reconcile across all entities. + if (force && !reconcile) { + const primary = this._groupPrimaryOf(eid); + const row = this._schedules[primary]; + if (!row || !this._entityHasBoundaryNow(row, nowMin)) { continue; } + } + let desired = this._desiredTempFor(eid, nowMin); + const mx = this._config?.max_temp ?? 25; const mn = this._config?.min_temp ?? 5; if (Number.isFinite(mx)) desired = Math.min(desired, mx); if (Number.isFinite(mn)) desired = Math.max(desired, mn); + if (desired == null || Number.isNaN(desired)) continue; + const last = this._lastApplied[eid] || {}; + // Only send when desired setpoint changes for this entity. + // Prevents overriding manual changes in other rooms. + if (!reconcile && Number.isFinite(last.temp) && Math.abs(Number(last.temp) - desired) < 0.05) continue; + const st = this._hass.states?.[eid]; + const cur = Number(st?.attributes?.temperature ?? st?.attributes?.target_temperature ?? st?.attributes?.target_temp); + const willSendF = this._isF(); const haIsF = this._haIsF(); + let curU = cur; + if (Number.isFinite(curU)) { + if (willSendF && !haIsF) curU = this._cToF(curU); + else if (!willSendF && haIsF) curU = this._fToC(curU); + } + const sendTemp = this._serviceTempFromC(desired); + if (Number.isFinite(curU) && Math.abs(curU - sendTemp) < 0.05){ this._lastApplied[eid] = { min: nowMin, temp: desired }; continue; } + try { await this._applySetpointForEntity(eid, desired); this._lastApplied[eid] = { min: nowMin, temp: desired }; } + catch (e) {} + } } + + // ---------- "Apply on change" helper ---------- + _desiredNowSnapshot(){ const nowMin = this._getNowMin(); const snap = {}; for (const eid of this._allTargetEntities()) snap[eid] = this._desiredTempFor(eid, nowMin); return snap; } + async _applyIfDesiredChanged(beforeSnap){ if (!this._config?.auto_apply) return; if (this._isPaused()) return; const after = this._desiredNowSnapshot(); const nowMin = this._getNowMin(); + if (this._config?.storage_enabled) { return; } + for (const eid of Object.keys(after)){ let a = after[eid]; const b = beforeSnap[eid]; if (a == null) continue; const mx=this._config?.max_temp ?? 25; const mn=this._config?.min_temp ?? 5; if (Number.isFinite(mx)) a = Math.min(a, mx); if (Number.isFinite(mn)) a = Math.max(a, mn); if (b == null || Math.abs(a - b) > 0.049){ try { await this._applySetpointForEntity(eid, a); this._lastApplied[eid] = { min: nowMin, temp: a }; } catch(e){} } } + } + + // Ping backend to apply now without waiting for delayed sync + async _nudgeBackgroundApplyNow(){ + try { + if (!this._hass || !this._config?.storage_enabled) return; + const _awayCfg = this._config?.away || {}; + const _awayOut = { ..._awayCfg }; + const payload = this._makeStoragePayload(false); + // Always include away settings to avoid losing away.enabled due touch-flag races. + // set_store replaces the full settings object, so merge with current backend + // settings first to avoid dropping unrelated keys (e.g. holidays_*). + let mergedSettings = { ...payload.settings, away: _awayOut }; + try { + const state = await this._apiFetchState(); + const base = (state && typeof state === 'object' && state.settings && typeof state.settings === 'object') ? state.settings : {}; + mergedSettings = { ...base, ...mergedSettings }; + } catch {} + payload.settings = mergedSettings; + const p = { settings: payload.settings, force: true }; + if (this._config?.instance_enabled) { p.instance_id = this._instanceId(); p.activate = true; } + await this._hass.callService('thermostat_timeline','set_store', p); + } catch {} + } + + // Apply a setpoint immediately for the given entity (and its merged entities) + // using the provided temperature (in °C). Honours away override and min/max. + async _applyImmediateForEntity(entity, tempC, nowMinOverride=null){ + try { + if (!this._config?.auto_apply || !this._config?.apply_on_edit || !this._hass) return; + // When shared storage is enabled, integration handles the apply; card should not send + if (this._config?.storage_enabled) return; + if (this._isPaused()) return; + let desired = Number(tempC); + // Away override (disabled when advanced presence is enabled) + try { const advOn = !!(this._config?.away?.advanced_enabled); if (!advOn && this._isAwayActive()) { const a = Number(this._config?.away?.target_c ?? 17); if (Number.isFinite(a)) desired = Math.min(desired, a); } } catch {} + // Clamp to min/max + const mx = this._config?.max_temp ?? 25; const mn = this._config?.min_temp ?? 5; + if (Number.isFinite(mx)) desired = Math.min(desired, mx); + if (Number.isFinite(mn)) desired = Math.max(desired, mn); + const nowMin = Number.isFinite(nowMinOverride) ? nowMinOverride : this._getNowMin(); + const dom = String(entity||'').split('.')[0] || ''; + const targets = (dom === 'climate') ? [entity, ...(this._config?.merges?.[entity] || [])] : [entity]; + for (const eid of targets){ + try { + const __eid = eid; + if (!(typeof __eid==='string' && __eid.includes('.') && this._hass?.states?.[__eid])) continue; + const dom2 = __eid.split('.')[0]; + // Avoid redundant command if already at desired + let curU = NaN; + try { + const st = this._hass.states[__eid]; + if (dom2 === 'climate') { + const cur = Number(st?.attributes?.temperature ?? st?.attributes?.target_temperature ?? st?.attributes?.target_temp); + const willSendF = this._isF(); const haIsF = this._haIsF(); + curU = cur; + if (Number.isFinite(curU)) { + if (willSendF && !haIsF) curU = this._cToF(curU); + else if (!willSendF && haIsF) curU = this._fToC(curU); + } + } else if (dom2 === 'input_number') { + const raw = Number(st?.state); + curU = raw; + } + } catch {} + const sendTemp = this._serviceTempFromC(desired); + if (Number.isFinite(curU) && Math.abs(curU - sendTemp) < 0.049) { this._lastApplied[__eid] = { min: nowMin, temp: desired }; continue; } + await this._applySetpointForEntity(__eid, desired); + this._lastApplied[__eid] = { min: nowMin, temp: desired }; + } catch {} + } + } catch {} + } + + _entityHasBoundaryNow(row, nowMin){ + try { + let blocks = row.blocks||[]; + // Prefer presence schedule when enabled and active + try { + const key = this._activePresenceComboKey && this._activePresenceComboKey(); + if (key && row.presence && Array.isArray(row.presence[key]?.blocks) && row.presence[key].blocks.length) { + blocks = row.presence[key].blocks || []; + } else if (this._config?.weekdays_enabled && row.weekly) { + const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey) || []; + } + } catch { if (this._config?.weekdays_enabled && row.weekly){ const dayKey = this._todayKey(); blocks = this._getBlocksForDay(row, dayKey) || []; } } + // Allow a 1-minute grace window because we intentionally schedule the + // timer a tiny bit after the exact boundary to avoid races. Otherwise we + // might miss the equality check and skip applying at the boundary. + const prev = ((nowMin - 1) % 1440 + 1440) % 1440; + for (const b of (blocks||[])){ + const s = Number(b.startMin), e = Number(b.endMin); + if (Number.isFinite(s) && (s === nowMin || s === prev)) return true; + if (Number.isFinite(e) && (e === nowMin || e === prev)) return true; + } + } catch {} + return false; + } + + // ---------- UI ---------- + _init() { + this.shadowRoot.innerHTML = ` + + + +
    +
    +
    + +
    + +
    +
    + +
    + + +
    + +
    + +
    + +
    + + +
    + +
    + + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    + +
    +
    `; this._applyCardI18n(); + + // Apply hide-popups class if requested + try { const card = this.shadowRoot.querySelector('ha-card'); if (this._disablePopups && card) card.classList.add('hide-popups'); } catch {} + + + this._ensureSchedules(); + this._startUiTimer(); + // Show onboarding for first-time users (disabled while popups are turned off) + try { if (!this._disablePopups) this._maybeOpenOnboard(); } catch {} + + // Wire pause overlay actions + try { + const ov = this.shadowRoot.querySelector('.overlay-pause'); + const b1 = this.shadowRoot.querySelector('.pause-1h'); + const b5 = this.shadowRoot.querySelector('.pause-5h'); + const bc = this.shadowRoot.querySelector('.pause-custom'); + const bi = this.shadowRoot.querySelector('.pause-indef'); + const br = this.shadowRoot.querySelector('.pause-resume'); + const bx = this.shadowRoot.querySelector('.pause-close'); + b1?.addEventListener('click', ()=>{ this._setPauseForMinutes(60); }); + b5?.addEventListener('click', ()=>{ this._setPauseForMinutes(300); }); + bc?.addEventListener('click', ()=>{ + try { + const txt = prompt(this._t('pause.prompt_minutes') || 'Enter duration (minutes):', '60'); + const m = Number(txt); + if (Number.isFinite(m) && m > 0) this._setPauseForMinutes(m); + } catch {} + }); + bi?.addEventListener('click', ()=>{ this._setPauseIndef(); }); + br?.addEventListener('click', ()=>{ this._resumeNow(); }); + bx?.addEventListener('click', ()=>{ try { ov?.classList.remove('open'); } catch {} }); + } catch {} + } + + _renderPauseStatus(){ + try { + const st = this.shadowRoot && this.shadowRoot.querySelector('.pause-status'); + if (!st) return; + if (!this._isPaused()) { st.textContent = ''; return; } + if (this._pauseIndef) { st.textContent = this._t('pause.active_indef'); return; } + const until = new Date(Number(this._pauseUntilMs||0)); + // Format time according to card preference + const hh = until.getHours(), mm = until.getMinutes(); + let label; + if (this._config?.time_12h) { + const am = hh < 12; let h = hh % 12; if (h===0) h=12; + label = `${String(h).padStart(2,'0')}:${String(mm).padStart(2,'0')} ${am?'AM':'PM'}`; + } else { + label = `${String(hh).padStart(2,'0')}:${String(mm).padStart(2,'0')}`; + } + const msgTmpl = this._t('pause.active_until'); + st.textContent = (msgTmpl || 'Paused until {time}').replace('{time}', label); + } catch {} + } + + // --- Sync countdown helpers (delayed sync UI) --- + _formatSyncCountdown(){ + try { + const leftMs = Math.max(0, Number(this._storeDelayDue||0) - Date.now()); + const s = Math.ceil(leftMs/1000); + const mm = Math.floor(s/60), ss = s%60; + return `${String(mm).padStart(2,'0')}:${String(ss).padStart(2,'0')}`; + } catch { return '00:00'; } + } + _startSyncTimer(){ + if (this._syncTimer) return; + try { this._syncTimer = setInterval(()=>{ try { if (!this._inlineEditing && !this._editing) this._render(); } catch {} }, 1000); } catch {} + } + _stopSyncTimer(){ try { if (this._syncTimer) clearInterval(this._syncTimer); } catch {} this._syncTimer = null; } + + // Persist/restore delayed sync due time across hard refresh + _syncDueKey(){ return `tt_sync_due:${this._instanceId()}`; } + _draftKey(){ return `tt_sync_draft:${this._instanceId()}`; } + + _localStoreKeyFor(iid){ + const id = this._normInstanceId(iid); + if (!id) return 'thermostat_timeline_store'; + return `thermostat_timeline_store:${id}`; + } + _syncDueKeyFor(iid){ return `tt_sync_due:${this._normInstanceId(iid)}`; } + _draftKeyFor(iid){ return `tt_sync_draft:${this._normInstanceId(iid)}`; } + + _copyInstanceLocalKeys(oldId, newId){ + try { + const oldI = this._normInstanceId(oldId); + const newI = this._normInstanceId(newId); + if (!oldI || !newI || oldI === newI) return false; + + const oldStoreKey = this._localStoreKeyFor(oldI); + const newStoreKey = this._localStoreKeyFor(newI); + const oldStore = localStorage.getItem(oldStoreKey) || ''; + const newStore = localStorage.getItem(newStoreKey) || ''; + if (oldStore && newStore && newStore.length > 2 && oldStore !== newStore) { + const ok = confirm('Det nye ID har allerede data. Overskriv?'); + if (!ok) return false; + } + if (oldStore) { + try { localStorage.setItem(newStoreKey, oldStore); } catch {} + } + + // Move pending sync/draft keys (best-effort) + try { + const okDue = this._syncDueKeyFor(oldI); + const nkDue = this._syncDueKeyFor(newI); + const due = localStorage.getItem(okDue); + if (due != null) { + try { localStorage.setItem(nkDue, String(due)); } catch {} + } + } catch {} + try { + const okDr = this._draftKeyFor(oldI); + const nkDr = this._draftKeyFor(newI); + const dr = localStorage.getItem(okDr); + if (dr != null) { + try { localStorage.setItem(nkDr, String(dr)); } catch {} + } + } catch {} + + return true; + } catch { return false; } + } + + _moveInstanceLocalKeys(oldId, newId){ + try { + const oldI = this._normInstanceId(oldId); + const newI = this._normInstanceId(newId); + if (!oldI || !newI || oldI === newI) return false; + + const ok = this._copyInstanceLocalKeys(oldI, newI); + if (!ok) return false; + + try { localStorage.removeItem(this._localStoreKeyFor(oldI)); } catch {} + try { localStorage.removeItem(this._syncDueKeyFor(oldI)); } catch {} + try { localStorage.removeItem(this._draftKeyFor(oldI)); } catch {} + return true; + } catch { return false; } + } + + _clearAllLocalScheduleData(){ + // Wipes ALL local/browser keys related to thermostat timeline across all instances. + try { + const toDel = new Set(); + try { toDel.add('thermostat_timeline_store'); } catch {} + try { toDel.add('thermostat_timeline_onboard_seen_v1'); } catch {} + try { toDel.add('tt_defer_holiday_open'); } catch {} + + const prefixes = [ + 'thermostat_timeline_store:', + 'tt_sync_due:', + 'tt_sync_draft:', + 'tt_instance_cfg:', + ]; + + // Broad safety net: these prefixes are reserved for this card/integration. + const broadPrefixes = [ + 'thermostat_timeline', + 'tt_', + ]; + + for (let i = 0; i < localStorage.length; i++) { + const k = localStorage.key(i); + if (!k) continue; + if (k === 'thermostat_timeline_store') { toDel.add(k); continue; } + for (const p of prefixes) { + if (k.startsWith(p)) { toDel.add(k); break; } + } + + // Also catch any remaining thermostat_timeline/tt_* keys (UI flags etc.) + for (const p of broadPrefixes) { + if (k.startsWith(p)) { toDel.add(k); break; } + } + } + + for (const k of toDel) { + try { localStorage.removeItem(k); } catch {} + } + } catch {} + } + + async _clearAllDataEverywhereConfirmed(){ + // Full wipe: local/browser + backend store (if available) + in-memory state. + // Confirmation must be handled by caller. + try { + // Stop any pending delayed-sync timers so cleared data can't be written back. + try { if (this._storeDelayTimer) clearTimeout(this._storeDelayTimer); } catch {} + this._storeDelayTimer = null; + try { this._storeDelayDue = 0; } catch {} + try { this._stopSyncTimer(); } catch {} + + // Wipe local browser copy (ALL instances + pending sync/draft) + try { this._clearAllLocalScheduleData(); } catch {} + + // Mark a hard reset so subsequent loads won't re-import legacy/local drafts. + try { localStorage.setItem(this._hardResetKey(), String(Date.now())); } catch {} + + // Also clear room list and related config (rooms are configuration, not schedules) + try { + this._config.entities = []; + this._config.room_use_input_number = []; + this._config.room_use_temp_sensor = []; + this._config.merges = {}; + this._config.labels = {}; + this._config.temp_sensors = {}; + + // Reset instance config so the card starts in non-instance mode. + this._config.instance_enabled = false; + this._config.instance_id = ''; + this._config.instance_uid = ''; + + // Reset title + this._config.title = ''; + } catch {} + + // Also wipe in-memory schedules so UI reflects deletion immediately + try { + this._schedules = {}; + this._config.color_ranges = {}; + this._profilesSelected = null; this._profilesDraft = null; this._profilesDirty = false; this._profilesEditingNew = false; + try { this._openRows = new Set(); } catch {} + } catch {} + + if (this._hass) { + // Hard reset (deletes .storage files and recreates them empty) + let ok = false; + try { await this._hass.callService('thermostat_timeline', 'factory_reset', {}); ok = true; } catch {} + + // Backwards compatible fallback + if (!ok) { + try { await this._hass.callService('thermostat_timeline', 'clear_store', {}); ok = true; } catch {} + if (!ok) { + try { await this._hass.callService('thermostat_timeline', 'clear', {}); } catch {} + } + try { await this._hass.callService('thermostat_timeline', 'delete_backup', {}); } catch {} + } + + if (!ok) { + try { alert('Kunne ikke rydde data i Home Assistant (service-kald fejlede). Tjek Home Assistant log.'); } catch {} + } + } + + // Repaint editor/live cards + try { this._emit(true); } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + + // If settings popup is open, refresh its draft so it doesn't show stale values + try { this._initSettingsPopupDraft(); } catch {} + try { this._renderSettingsPopupTabs(); } catch {} + try { this._renderSettingsPopupTab(); } catch {} + return true; + } catch { return false; } + } + + _clearAllLocalOnlyConfirmed(){ + // Wipes local/browser thermostat timeline data only. + try { + try { this._clearAllLocalScheduleData(); } catch {} + try { localStorage.setItem(this._hardResetKey(), String(Date.now())); } catch {} + try { this._schedules = {}; this._ensureSchedules(); } catch {} + try { this._emit(true); } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + try { this._initSettingsPopupDraft(); } catch {} + try { this._renderSettingsPopupTab(); } catch {} + return true; + } catch { return false; } + } + _persistDraftNow(){ try { this._syncSeasonalRowsToBucket(); } catch {} try { localStorage.setItem(this._draftKey(), JSON.stringify({ ts: Date.now(), schedules: this._schedules })); } catch {} } + _tryRestoreDraftRecent(maxAgeMs=15*60*1000){ try { const raw = localStorage.getItem(this._draftKey()) || ''; if (!raw) return false; const parsed = JSON.parse(raw); const ts = Number(parsed?.ts); if (!Number.isFinite(ts) || (Date.now()-ts) > maxAgeMs) return false; const sch = parsed?.schedules && parsed.schedules.schedules ? parsed.schedules.schedules : (parsed?.schedules || parsed || {}); if (sch && typeof sch === 'object') { this._schedules = sch; try { this._seasonAppliedKey = null; } catch {} this._ensureSchedules(); return true; } } catch {} return false; } + _restoreSyncDue(){ + try { + if (!this._config?.storage_enabled || this._config?.storage_sync_mode !== 'delay') return; + // Prefer value from storage (already read in _loadStore); fallback to localStorage + if (Number.isFinite(this._storeDelayDue) && this._storeDelayDue > Date.now()) { + // Restore any recent local draft so UI shows latest edits + try { this._tryRestoreDraftRecent(); } catch {} + const remaining = Math.max(500, this._storeDelayDue - Date.now()); + if (this._storeDelayTimer) { try { clearTimeout(this._storeDelayTimer); } catch {} } + this._storeDelayTimer = setTimeout(()=>{ this._storeDelayTimer = null; this._writeStoreNow(); }, remaining); + this._startSyncTimer(); + return; + } + const key = this._syncDueKey(); + const raw = localStorage.getItem(key) || ''; + const due = Number(raw); + if (!Number.isFinite(due)) return; + if (due <= Date.now()) { try { localStorage.removeItem(key); } catch {} return; } + // Restore recent draft too + try { this._tryRestoreDraftRecent(); } catch {} + this._storeDelayDue = due; + const remaining = Math.max(500, due - Date.now()); + if (this._storeDelayTimer) { try { clearTimeout(this._storeDelayTimer); } catch {} } + this._storeDelayTimer = setTimeout(()=>{ this._storeDelayTimer = null; this._writeStoreNow(); }, remaining); + this._startSyncTimer(); + } catch {} + } + + async _writeStoreNow(){ + if (!this._config?.storage_enabled) return; + this._saving = true; + const key = this._syncDueKey(); + try { + // Build away for backend using current persisted config values. + const _awayCfg = this._config?.away || {}; + const _awayOut = { ..._awayCfg }; + let payload = this._makeStoragePayload(true); + payload.settings = { ...payload.settings, away: _awayOut }; + // Merge with current backend settings to avoid clearing omitted keys (e.g., labels) on delayed write + try { + const state = await this._apiFetchState(); + const base = (state && typeof state === 'object' && state.settings && typeof state.settings === 'object') ? state.settings : {}; + const incoming = payload.settings || {}; + const merged = { ...base, ...incoming }; + const keepIfEmpty = (key)=>{ + try { + const inc = incoming[key]; + const hasInc = inc && typeof inc === 'object' && Object.keys(inc).length > 0; + const baseVal = base[key]; + const hasBase = baseVal && typeof baseVal === 'object' && Object.keys(baseVal).length > 0; + if (!hasInc && hasBase) merged[key] = baseVal; + } catch {} + }; + // Respect explicit label edits: allow an empty object to clear labels when touched + if (!this._labelsTouched) keepIfEmpty('labels'); + keepIfEmpty('merges'); + keepIfEmpty('temp_sensors'); + payload = { ...payload, settings: merged }; + } catch {} + // Push all data to the integration via set_store service (writes to file) + if (this._config?.instance_enabled) { + payload = { ...payload, instance_id: this._instanceId(), activate: true }; + } + await this._hass.callService("thermostat_timeline", "set_store", payload); + try { this._broadcastStoreUpdated(this._localStoreKey()); } catch {} + } + catch (e) {} + finally { + setTimeout(() => { + this._saving = false; + this._syncJustUntil = Date.now() + 2500; + this._storeDelayDue = 0; + try { localStorage.removeItem(key); } catch {} + this._startSyncTimer(); + this._render(); + try { this._labelsTouched = false; } catch {} + }, 600); + } + } + + _setPauseForMinutes(mins){ + try { + const ms = Math.max(1, Math.round(Number(mins)||0)) * 60000; + this._pauseIndef = false; + this._pauseUntilMs = Date.now() + ms; + // Persist to storage/local + this._debouncedSaveStore(); + this._render(); + this._renderPauseStatus(); + // Reschedule apply/resume + this._scheduleNextApply(); + // Notify backend immediately when shared storage is on + try { if (this._config?.storage_enabled) this._saveStore(); } catch {} + // Close overlay + try { this.shadowRoot.querySelector('.overlay-pause')?.classList.remove('open'); } catch {} + } catch {} + } + + _setPauseIndef(){ + try { + this._pauseIndef = true; this._pauseUntilMs = 0; + this._debouncedSaveStore(); + this._render(); + this._renderPauseStatus(); + this._scheduleNextApply(); + try { if (this._config?.storage_enabled) this._saveStore(); } catch {} + try { this.shadowRoot.querySelector('.overlay-pause')?.classList.remove('open'); } catch {} + } catch {} + } + + _resumeNow(){ + try { + this._pauseIndef = false; this._pauseUntilMs = 0; + this._debouncedSaveStore(); + this._render(); + this._scheduleNextApply(); + // Apply immediately to match plan "now" + this._applyCurrentSetpoints(true, true); + try { if (this._config?.storage_enabled) this._saveStore(); } catch {} + try { this.shadowRoot.querySelector('.overlay-pause')?.classList.remove('open'); } catch {} + } catch {} + } + + // If the editor asked to open the Holiday modal after the user pressed Save, + // read the flag from localStorage and open once, then clear the flag. + _maybeOpenDeferredHoliday(){ + try { + const flag = localStorage.getItem('tt_defer_holiday_open'); + if (flag === '1') { + localStorage.removeItem('tt_defer_holiday_open'); + const first = (this._config?.entities||[])[0] || null; + this._openHolidayEditor(first); + } + } catch {} + } + + _ensureSchedules() { + for (const eid of this._config.entities) { + if (!this._schedules[eid]) this._schedules[eid] = { defaultTemp: this._config.default_temp, blocks: [] }; + // Always maintain structures so state is preserved if toggles are flipped later + this._ensureProfilesStruct(this._schedules[eid]); + this._ensureHolidayStruct(this._schedules[eid]); + this._ensurePresenceStruct(this._schedules[eid]); + if (this._config?.seasonal_enabled) this._ensureSeasonStruct(this._schedules[eid]); + } + // Apply seasonal mode and ensure weekly structure for the active season + try { this._applySeasonalModeToRows(); } catch {} + if (this._config?.weekdays_enabled) { + for (const eid of this._config.entities) { + this._ensureWeeklyStruct(this._schedules[eid]); + } + } + } + + _startUiTimer() { + if (this._uiTimer) clearInterval(this._uiTimer); + this._uiTimer = setInterval(() => { if (!this._inlineEditing && !this._editing) this._render(); }, this._config.now_update_ms); + } + + _startStoreWatchTimer(){ + try { if (this._storeWatchTimer) clearInterval(this._storeWatchTimer); } catch {} + if (!this._config?.storage_enabled) { return; } + const ms = Math.max(800, Number(this._storeWatchMs||0) || 10000); + this._storeWatchTimer = setInterval(()=>{ + if (this._storeWatchBusy) return; + this._storeWatchBusy = true; + (async () => { + try { + let verSched = NaN, verSet = NaN, verCol = NaN, verWeek = NaN, verProf = NaN; + let haveVersions = false; + // Always use API for version checking - file-based storage only + if (this._apiSupported()) { + const ver = await this._apiFetchVersion(); + if (ver && typeof ver === 'object') { + verSched = Number(ver.version || 0); + verSet = Number(ver.settings_version || verSched || 0); + verCol = Number(ver.colors_version || verSet || verSched || 0); + verWeek = Number(ver.weekday_version || verCol || verSet || verSched || 0); + verProf = Number(ver.profile_version || verWeek || verCol || verSet || verSched || 0); + // Check for global profile changes from API data + const s = ver.settings || {}; + const newGP = (typeof s.global_profile === 'string' && s.global_profile) ? String(s.global_profile) : null; + const newPE = typeof s.profiles_enabled === 'boolean' ? !!s.profiles_enabled : this._config?.profiles_enabled; + let changed = false; + if (newGP !== this._globalProfile) { this._globalProfile = newGP; changed = true; } + if (newPE !== this._config.profiles_enabled) { this._config.profiles_enabled = newPE; changed = true; } + if (changed) { + try { + for (const id of (this._config?.entities||[])){ + const r = this._schedules[id]; if (!r) continue; this._ensureProfilesStruct(r); + if (!this._globalProfile) { r.activeProfile = null; continue; } + r.activeProfile = (r.profiles && r.profiles[this._globalProfile]) ? this._globalProfile : null; + } + } catch {} + if (!this._inlineEditing && !this._editing) this._render(); + this._scheduleNextApply(); + } + haveVersions = true; + } + } + if (!haveVersions) return; + const schedChanged = this._config?.storage_enabled && !this._saving && !Number.isNaN(verSched) && verSched !== this._lastVersion; + const settingsChanged = this._config?.storage_enabled && !this._saving && !Number.isNaN(verSet) && verSet !== this._lastSettingsVersion; + const colorsChanged = this._config?.storage_enabled && !this._saving && !Number.isNaN(verCol) && verCol !== this._lastColorsVersion; + const weekdaysChanged = this._config?.storage_enabled && !this._saving && !Number.isNaN(verWeek) && verWeek !== this._lastWeekdayVersion; + const profilesChanged = this._config?.storage_enabled && !this._saving && !Number.isNaN(verProf) && verProf !== this._lastProfileVersion; + if (schedChanged || settingsChanged || colorsChanged || weekdaysChanged || profilesChanged){ + if (this._isSettingsPopupOpen && this._isSettingsPopupOpen()) return; + this._lastVersion = verSched; + this._lastSettingsVersion = verSet; + this._lastColorsVersion = verCol; + this._lastWeekdayVersion = verWeek; + this._lastProfileVersion = verProf; + this._loadStore(false).then(()=>{ this._ensureSchedules(); if (!this._inlineEditing && !this._editing) this._render(); this._scheduleNextApply(); }); + } + } catch {} finally { + this._storeWatchBusy = false; + } + })(); + }, ms); + } + + _render() { + // Do not re-render while the user is actively typing in inline inputs (to avoid caret/selection loss) + if (this._suspendRender) return; + const qs = (s) => this.shadowRoot.querySelector(s); + const titleEl = qs('.title'); + const dayFull = qs('.weekday-full'); + const rowsEl = qs('.rows'); + const scaleEl = qs('.scale-inner'); + const headerEl = qs('.header'); + if (!rowsEl || !scaleEl || !titleEl) return; + try { this._applySeasonalModeToRows(); } catch {} + + // Helper: build "All rooms" / "All days" view switch + const makeWeekViewSeg = (extraClass='')=>{ + try { + const isWeekEnabled = !!this._config?.weekdays_enabled; + const allowInlineViewSwitch = !!(this._config?.weekdays_view_switch_in_timeline); + const isHoliday = !!(this._isHolidayActive && this._isHolidayActive()); + if (!isWeekEnabled || !allowInlineViewSwitch || isHoliday) return null; + + const fullRooms = this._t('week.view.rooms_one_day') || 'All rooms • one day'; + const fullDays = this._t('week.view.days_one_room') || 'All days • one room'; + const short = (txt)=>{ + try { + const s = String(txt || '').split('•')[0].trim(); + return s || String(txt || ''); + } catch { return String(txt || ''); } + }; + + const setView = (mode)=>{ + try { + if (!this._config?.weekdays_enabled) return; + const next = (mode === 'one_room_all_days') ? 'one_room_all_days' : 'all_rooms_one_day'; + const cur = String(this._config?.weekdays_view || 'all_rooms_one_day'); + if (cur === next) return; + this._config.weekdays_view = next; + // Keep selected room consistent + try { + const ents = Array.isArray(this._config?.entities) ? this._config.entities.filter(Boolean).map(String) : []; + if (next === 'one_room_all_days') { + const want = String(this._config?.weekdays_selected_room || ''); + this._config.weekdays_selected_room = (want && ents.includes(want)) ? want : (ents[0] || ''); + } else { + this._config.weekdays_selected_room = ''; + try { this._setTimelineDisplayDayKey(this._todayKey()); } catch {} + } + } catch {} + this._render(); + try { this._debouncedSaveStore(); } catch {} + } catch {} + }; + + const seg = document.createElement('div'); + seg.className = 'sp-week-view-inline' + (extraClass ? (' ' + extraClass) : ''); + seg.setAttribute('role','group'); + seg.setAttribute('aria-label', this._t('week.title') || 'Weekdays'); + + const bRooms = document.createElement('button'); + bRooms.type = 'button'; + bRooms.textContent = short(fullRooms); + bRooms.title = String(fullRooms || ''); + bRooms.classList.toggle('active', String(this._config?.weekdays_view || 'all_rooms_one_day') === 'all_rooms_one_day'); + bRooms.addEventListener('click', ()=>setView('all_rooms_one_day')); + + const bDays = document.createElement('button'); + bDays.type = 'button'; + bDays.textContent = short(fullDays); + bDays.title = String(fullDays || ''); + bDays.classList.toggle('active', String(this._config?.weekdays_view || 'all_rooms_one_day') === 'one_room_all_days'); + bDays.addEventListener('click', ()=>setView('one_room_all_days')); + + seg.append(bRooms, bDays); + return seg; + } catch { return null; } + }; + + // Determine first active profile (if any) for header pill + let _activeProfileName = null; + try { + if (this._config?.profiles_enabled) { + for (const eid of (this._config?.entities || [])) { + const row = this._schedules?.[eid]; + if (!row) continue; + this._ensureProfilesStruct(row); + const ap = row.activeProfile || null; + if (ap) { _activeProfileName = String(ap); break; } + } + } + } catch {} + + { + const isDef = (txt)=>{ + try { + const s = String(txt || '').trim(); + for (const k of Object.keys(TT_I18N || {})){ + const v = TT_I18N[k] && TT_I18N[k]['card.title_default']; + if (v && String(v).trim() === s) return true; + } + } catch {} + return false; + }; + const shown = (!this._config.title || isDef(this._config.title)) ? this._t('card.title_default') : this._config.title; + titleEl.innerHTML = ''; + const tspan = document.createElement('span'); tspan.className = 'title-text'; tspan.textContent = shown; titleEl.append(tspan); + // Sync countdown/dots (left side, next to title) + try { + // Render only when shared storage enabled + const useDelay = !!(this._config?.storage_enabled) && (this._config?.storage_sync_mode === 'delay'); + const due = Number(this._storeDelayDue||0); + const justDone = Number(this._syncJustUntil||0) > Date.now(); + const running = !!this._saving; + if (useDelay && (running || justDone || (due > Date.now()))) { + const pill = document.createElement('span'); + pill.className = 'profile-pill sync-pill'; + const icon = document.createElement('ha-icon'); + const txt = document.createElement('span'); + txt.style.marginLeft = '6px'; + if (running) { + icon.setAttribute('icon','mdi:sync'); + pill.classList.add('primary'); + txt.textContent = this._t('sync.running'); + } else if (justDone) { + icon.setAttribute('icon','mdi:check-circle-outline'); + pill.classList.add('success'); + txt.textContent = this._t('sync.completed'); + } else { + icon.setAttribute('icon','mdi:timer-outline'); + pill.classList.add('ghost'); + txt.textContent = this._formatSyncCountdown(); + this._startSyncTimer(); + } + pill.append(icon, txt); + titleEl.append(pill); + if (!running && !justDone && !(due> Date.now())) this._stopSyncTimer(); + } else { + this._stopSyncTimer(); + } + } catch {} + if (_activeProfileName) { + const p = document.createElement('span'); p.className = 'profile-pill success'; const prefix = this._t('profiles.active_label') || 'Manual profile active:'; p.textContent = `${prefix} ${_activeProfileName}`; titleEl.append(p); + } + + } + // (Profile pill moved next to title; weekday center shows only day name now) + // Center header: show current weekday name when weekday mode is enabled + try { + if (dayFull) { + let label = ''; + // Prefer a Holiday label if holidays are active + try { if (this._isHolidayActive && this._isHolidayActive()) { label = this._t('holidays.button') || 'Holidays'; } } catch {} + if (!label && this._config?.weekdays_enabled) { + // Localized full weekday, e.g., "Mandag" (or browsed day in weekdays view) + const viewDayKey = this._timelineDisplayDayKey(); + const dayName = this._weekdayFullName(viewDayKey) || this._todayFullName(); + // If viewing all days for a single room, append room name for clarity + try { + const perRoomAllDays = String(this._config?.weekdays_view||'all_rooms_one_day') === 'one_room_all_days'; + if (perRoomAllDays) { + const ents = (this._config?.entities||[]); + const roomEid = (this._config?.weekdays_selected_room && ents.includes(this._config.weekdays_selected_room)) + ? this._config.weekdays_selected_room + : (ents[0]||''); + const roomLabel = this._config?.labels?.[roomEid] || this._prettyName(roomEid); + label = `${roomLabel}`.trim(); + } else { + label = dayName; + } + } catch { label = dayName; } + } + if (label) { + dayFull.innerHTML = ''; + + const ents = (this._config?.entities || []); + const isWeekEnabled = !!this._config?.weekdays_enabled; + const viewMode = String(this._config?.weekdays_view || 'all_rooms_one_day'); + const mkArrow = (dir)=>{ + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'wd-arrow'; + btn.textContent = dir < 0 ? '◀' : '▶'; + btn.style.cssText = 'background:none; border:none; color:var(--primary-text-color); cursor:pointer; padding:0 4px; font-size:inherit;'; + return btn; + }; + + let leftNavBtn = null; + let rightNavBtn = null; + + // In "Alle rum • én dag" view: arrows browse days (Mon..Sun) + if (isWeekEnabled && viewMode === 'all_rooms_one_day') { + const prevBtn = mkArrow(-1); + prevBtn.title = 'Previous day'; + prevBtn.addEventListener('click', () => { + const names = this._weekdayNames(); + const cur = this._timelineDisplayDayKey(); + const idx = Math.max(0, names.indexOf(cur)); + const newIdx = (idx - 1 + names.length) % names.length; + this._setTimelineDisplayDayKey(names[newIdx]); + this._render(); + }); + leftNavBtn = prevBtn; + + const nextBtn = mkArrow(1); + nextBtn.title = 'Next day'; + nextBtn.addEventListener('click', () => { + const names = this._weekdayNames(); + const cur = this._timelineDisplayDayKey(); + const idx = Math.max(0, names.indexOf(cur)); + const newIdx = (idx + 1) % names.length; + this._setTimelineDisplayDayKey(names[newIdx]); + this._render(); + }); + rightNavBtn = nextBtn; + } + // In "Alle dage • ét rum" view: keep room navigation arrows (only if multiple rooms) + else if (isWeekEnabled && viewMode === 'one_room_all_days' && ents.length > 1) { + const roomEid = (this._config?.weekdays_selected_room && ents.includes(this._config.weekdays_selected_room)) + ? this._config.weekdays_selected_room + : (ents[0]||''); + const roomIdx = ents.indexOf(roomEid); + + const prevBtn = mkArrow(-1); + prevBtn.title = 'Previous room'; + prevBtn.addEventListener('click', () => { + const newIdx = roomIdx <= 0 ? ents.length - 1 : roomIdx - 1; + this._config.weekdays_selected_room = ents[newIdx]; + this._render(); + }); + leftNavBtn = prevBtn; + + const nextBtn = mkArrow(1); + nextBtn.title = 'Next room'; + nextBtn.addEventListener('click', () => { + const newIdx = roomIdx >= ents.length - 1 ? 0 : roomIdx + 1; + this._config.weekdays_selected_room = ents[newIdx]; + this._render(); + }); + rightNavBtn = nextBtn; + } + + const leftWrap = document.createElement('div'); + leftWrap.className = 'wd-left'; + const centerWrap = document.createElement('div'); + centerWrap.className = 'wd-center'; + const rightWrap = document.createElement('div'); + rightWrap.className = 'wd-right'; + + // Mobile-only: show view switch on the weekday row (left-aligned) + try { + const segMobile = makeWeekViewSeg('sp-week-view-mobile'); + if (segMobile) leftWrap.appendChild(segMobile); + } catch {} + + const labelSpan = document.createElement('span'); + labelSpan.textContent = label; + // Keep arrows tight to the label (centered as a group) + if (leftNavBtn) centerWrap.appendChild(leftNavBtn); + centerWrap.appendChild(labelSpan); + if (rightNavBtn) centerWrap.appendChild(rightNavBtn); + + dayFull.append(leftWrap, centerWrap, rightWrap); + + dayFull.style.display = ''; + } else { + dayFull.textContent = ''; + dayFull.style.display = 'none'; + } + } + } catch {} + + // Scale ticks + scaleEl.innerHTML = ''; + const compact = this._isCompactScale(); + for (let i = 0; i <= 24; i++) { + if (compact && i % 2 !== 0) continue; + const t = document.createElement('div'); + t.className = 'tick ' + (i % 2 === 0 ? 'full' : 'short'); + t.style.left = `${(i / 24) * 100}%`; + if (i % 2 === 0 || !compact) { + const l = document.createElement('label'); + if (this._config?.time_12h){ + const p = this._timeParts(i===24?0:i*60); + l.innerHTML = `${p.main}${p.mer||''}`; + } else { + l.textContent = (i === 24) ? this._label(0) : this._label(i*60); + } + t.append(l); + } + scaleEl.append(t); + } + + // Rows + rowsEl.innerHTML = ''; + // Right header tools: global pause toggle (absolute top-right) + try { + if (headerEl) { + const spacer = headerEl.querySelector('.spacer') || headerEl; + let wrap = headerEl.querySelector('.pause-wrap'); + if (!wrap) { wrap = document.createElement('div'); wrap.className = 'pause-wrap'; spacer.append(wrap); } + else if (wrap.parentNode !== spacer) { try { spacer.append(wrap); } catch {} } + wrap.innerHTML = ''; + + // Desktop: show view switch in top row (hidden on mobile) + try { + const segDesktop = makeWeekViewSeg('sp-week-view-desktop'); + if (segDesktop) wrap.append(segDesktop); + } catch {} + + // Settings button (top-right) + try { + if (!this._disablePopups) { + const sbtn = document.createElement('button'); + sbtn.className = 'settings-btn'; + sbtn.type = 'button'; + try { sbtn.dataset.role = 'settingsbtn'; } catch {} + const lab = this._t('editor.tabs.settings') || 'Settings'; + sbtn.setAttribute('title', lab); + sbtn.setAttribute('aria-label', lab); + sbtn.innerHTML = ''; + sbtn.addEventListener('click', ()=>{ try { this._openSettingsPopup(); } catch {} }); + wrap.append(sbtn); + } + } catch {} + + // Seasonal switch (Summer/Winter) + try { + if (this._config?.seasonal_enabled) { + const cur = this._seasonKey(); + const label = this._t(cur === 'summer' ? 'season.mode.summer' : 'season.mode.winter') || (cur === 'summer' ? 'Summer' : 'Winter'); + const next = (cur === 'summer') ? 'winter' : 'summer'; + const nextLabel = this._t(next === 'summer' ? 'season.mode.summer' : 'season.mode.winter') || (next === 'summer' ? 'Summer' : 'Winter'); + const sb = document.createElement('button'); + sb.className = 'profiles-btn primary season-btn'; + sb.type = 'button'; + sb.textContent = label; + sb.setAttribute('title', `${label} → ${nextLabel}`); + sb.setAttribute('aria-label', `${label} → ${nextLabel}`); + sb.addEventListener('click', ()=>{ try { this._setSeasonMode(next); } catch {} }); + wrap.append(sb); + } + } catch {} + + if (this._config?.show_pause_button ?? true) { + const btn = document.createElement('button'); + btn.className = 'pause-btn' + (this._isPaused() ? ' paused' : ''); + btn.setAttribute('title', this._isPaused() ? this._t('pause.active_indef') : this._t('pause.menu_title')); + btn.innerHTML = ''; + btn.addEventListener('click', ()=>{ + try { + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-pause'); + if (ov) ov.classList.add('open'); + this._renderPauseStatus(); + } catch {} + }); + // Build optional bubble to the left + const paused = this._isPaused(); + const until = Number(this._pauseUntilMs||0); + const showCountdown = paused && !this._pauseIndef && until > Date.now(); + if (paused) { + const eta = document.createElement('span'); eta.className = 'pause-eta'; + eta.textContent = showCountdown ? this._formatPauseCountdown() : (this._t('pause.off_indef')); + wrap.append(eta); + wrap.append(btn); + if (showCountdown) this._startCountdownTimer(); + else if (this._countdownTimer) { try { clearInterval(this._countdownTimer); } catch {} this._countdownTimer = null; } + } else { + wrap.append(btn); + if (this._countdownTimer) { try { clearInterval(this._countdownTimer); } catch {} this._countdownTimer = null; } + } + } + // Global Away bypass button (visible only when Away is active: nobody home) + try { + const advOn = !!(this._config?.away?.advanced_enabled); + const awayActive = !!this._isAwayActive(); + if (this._config?.away?.enabled && !advOn && awayActive) { + const ab = document.createElement('button'); + ab.className = 'profiles-btn away-bypass-btn'; + // Make it red like a warning action + try { + ab.style.borderColor = 'var(--error-color)'; + ab.style.background = 'var(--error-color)'; + ab.style.color = 'var(--text-primary-color, #fff)'; + } catch {} + ab.textContent = this._awayBypass ? (this._t('away.resume_away') || 'Resume Away temp') : (this._t('away.resume_schedule') || 'Resume schedule'); + ab.addEventListener('click', async ()=>{ + try { + this._awayBypass = !this._awayBypass; + try { await this._saveStore(); } catch {} + try { await this._applyCurrentSetpoints(true); } catch {} + try { if (this._config?.storage_enabled) this._nudgeBackgroundApplyNow(); } catch {} + this._scheduleNextApply(); + ab.textContent = this._awayBypass ? (this._t('away.resume_away') || 'Resume Away temp') : (this._t('away.resume_schedule') || 'Resume schedule'); + } catch {} + }); + const pauseBtn = wrap.querySelector('.pause-btn'); + if (pauseBtn && pauseBtn.parentNode === wrap) { + wrap.insertBefore(ab, pauseBtn); + } else { + wrap.append(ab); + } + } else { + // Remove if not applicable + const old = wrap.querySelector('.away-bypass-btn'); if (old) old.remove(); + } + } catch {} + // Presence live status for Away persons + try { + const persons = (this._presencePersons && typeof this._presencePersons === 'function') ? this._presencePersons().filter(Boolean) : []; + const awayOn = !!(this._config?.away?.enabled); + const advOn = !!(this._config?.away?.advanced_enabled); + const wantLive = (awayOn || advOn) && !!(this._config?.presence_live_header ?? true); + if (persons.length && wantLive) { + let live = wrap.querySelector('.presence-live'); + if (!live) { live = document.createElement('div'); live.className = 'presence-live'; + const pauseBtn = wrap.querySelector('.pause-btn'); + if (pauseBtn && pauseBtn.parentNode === wrap) wrap.insertBefore(live, pauseBtn); else wrap.append(live); + } + live.innerHTML = ''; + for (const p of persons) { + const st = this._hass?.states?.[p]; + const state = String(st?.state || 'unknown'); + const friendly = st?.attributes?.friendly_name || (String(p||'').split('.')[1] || p); + const chip = document.createElement('span'); + chip.className = 'presence-chip ' + (state.toLowerCase() === 'home' ? 'home' : 'away'); + chip.setAttribute('title', `${friendly}: ${state}`); + const dot = document.createElement('span'); dot.className = 'presence-dot'; + const name = document.createElement('span'); name.className = 'presence-name'; name.textContent = friendly; + chip.append(dot, name); + live.append(chip); + } + } else { + // Remove if disabled + const live = wrap.querySelector('.presence-live'); if (live) live.remove(); + } + } catch {} + // Collapsible header tools (Presence / Profiles / Weekdays) + try { + let htools = wrap.querySelector('.head-tools'); + if (!htools) { htools = document.createElement('div'); htools.className = 'head-tools toolbar' + (this._headToolsOpen ? ' open' : ''); wrap.append(htools); } + htools.innerHTML = ''; + const toggleBtn = document.createElement('button'); toggleBtn.className='tools-toggle'; toggleBtn.type='button'; toggleBtn.setAttribute('aria-label','Toggle header tools'); toggleBtn.textContent = this._headToolsOpen ? '✕' : '❮'; + const toolsWrap = document.createElement('div'); toolsWrap.className = 'tools-wrap'; + const hasPending = !!this._pendingSaveAfterEdit; + const recentlySaved = (this._syncJustUntil || 0) > Date.now(); + const showButton = hasPending || this._saving || recentlySaved; + // Explicit Save button that writes all pending changes to storage; always visible (outside fold), only when pending changes exist + let saveBtn = null; + try { + saveBtn = document.createElement('button'); + saveBtn.className = 'profiles-btn success'; + saveBtn.type = 'button'; + saveBtn.style.marginRight = '6px'; + const idleLabel = this._t('ui.save') || 'Save'; + const savingLabel = this._t('sync.running') || 'Saving…'; + const doneLabel = this._t('sync.completed') || 'Saved'; + const setState = (state)=>{ + if (state==='saving') { saveBtn.disabled = true; saveBtn.textContent = savingLabel; } + else if (state==='done') { saveBtn.disabled = false; saveBtn.textContent = doneLabel; } + else { saveBtn.disabled = false; saveBtn.textContent = idleLabel; } + }; + setState(this._saving ? 'saving' : (recentlySaved ? 'done' : 'idle')); + if (!showButton) saveBtn.style.display = 'none'; + saveBtn.addEventListener('click', async ()=>{ + try { + setState('saving'); + const savePromise = this._saveStore(true); + const delayPromise = new Promise(r => setTimeout(r, 800)); + await Promise.all([savePromise, delayPromise]); + setState('done'); + setTimeout(()=>{ setState('idle'); this._render(); }, 1000); + } catch { + setState('idle'); + } + }); + } catch {} + // Presence button + if (this._config?.away?.advanced_enabled) { + const prb = document.createElement('button'); prb.className='profiles-btn primary'; prb.type='button'; prb.textContent=this._t('presence.button'); prb.addEventListener('click', ()=>{ const first=(this._config?.entities||[])[0]||null; if (first) this._openPresenceEditor(first); }); toolsWrap.append(prb); + } + // Profiles button + const wantProfilesBtn = !!(this._config?.profiles_enabled || this._getLocalProfilesEnabled()); + if ((this._config?.entities||[]).length && wantProfilesBtn) { + const pbtn = document.createElement('button'); pbtn.className='profiles-btn primary'; pbtn.type='button'; pbtn.textContent=this._t('profiles.button'); pbtn.addEventListener('click', ()=>{ const first=(this._config?.entities||[])[0]||null; if (first) this._openProfilesEditor(first); }); toolsWrap.append(pbtn); + } + // Weekdays button (header) + if (this._config?.weekdays_enabled && (this._config?.entities||[]).length) { + const wbtn = document.createElement('button'); wbtn.className='profiles-btn primary'; wbtn.type='button'; wbtn.textContent = this._t('week.button'); wbtn.addEventListener('click', ()=>{ const first=(this._config?.entities||[])[0]; if (first) this._openWeeklyEditor(first); }); toolsWrap.append(wbtn); + // When viewing all days for one room, show a small room selector + try { + const perDayView = String(this._config?.weekdays_view||'all_rooms_one_day')==='one_room_all_days'; + if (perDayView) { + const sel = document.createElement('select'); + sel.className = 'weekroom-select'; + sel.style.height = '32px'; sel.style.border = '1px solid var(--divider-color)'; sel.style.borderRadius = '8px'; sel.style.background = 'var(--card-background-color)'; sel.style.color = 'var(--primary-text-color)'; sel.style.marginLeft = '6px'; + const ents = (this._config?.entities || []); + for (const eid of ents){ const opt=document.createElement('option'); const label = this._config?.labels?.[eid] || (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1]||eid)); opt.value=eid; opt.textContent=label; sel.append(opt); } + const want = this._config?.weekdays_selected_room || ents[0] || ''; + sel.value = want; + sel.addEventListener('change', (e)=>{ const v=String(e.target.value||''); this._config.weekdays_selected_room = v; this._render(); }); + toolsWrap.append(sel); + } + } catch {} + } + // Toggle behavior + toggleBtn.addEventListener('click', ()=>{ + this._headToolsOpen = !this._headToolsOpen; if (this._headToolsOpen) { htools.classList.add('open'); toggleBtn.textContent='✕'; requestAnimationFrame(()=>{ try { toolsWrap.style.maxWidth=(toolsWrap.scrollWidth+2)+'px'; } catch {} }); } else { try { toolsWrap.style.maxWidth=(toolsWrap.scrollWidth+2)+'px'; } catch {}; htools.classList.remove('open'); toggleBtn.textContent='❮'; requestAnimationFrame(()=>{ toolsWrap.style.maxWidth='0px'; }); } + }); + // Place Save (if any) before collapsible tools, then toggle on the far right + if (saveBtn) htools.append(saveBtn); + htools.append(toolsWrap, toggleBtn); + // Hide toggle button if toolsWrap is empty + if (toolsWrap.children.length === 0) { + toggleBtn.style.display = 'none'; + } + // Ensure initial width reflects current open state + try { + if (this._headToolsOpen) { + requestAnimationFrame(()=>{ try { toolsWrap.style.maxWidth = (toolsWrap.scrollWidth + 2) + 'px'; } catch {} }); + } else { + toolsWrap.style.maxWidth = '0px'; + } + } catch {} + } catch {} + // Ensure pause button is the rightmost item in the header + try { + const pauseBtn = wrap.querySelector('.pause-btn'); + if (pauseBtn) wrap.appendChild(pauseBtn); + const settingsBtn = wrap.querySelector('.settings-btn'); + if (settingsBtn) wrap.appendChild(settingsBtn); + } catch {} + } + } catch {} + const nowMin = this._getNowMin(); + const nowPct = (nowMin / 1440) * 100; + const showTop = this._config.show_top_now; + const extendPx = this._config.now_extend_px; + + // Optional: render all selected weekdays for one room + let __weeklyBuilt = false; + try { + const perRoomAllDays = !!(this._config?.weekdays_enabled) && String(this._config?.weekdays_view||'all_rooms_one_day')==='one_room_all_days'; + if (perRoomAllDays) { + const ents = (this._config?.entities||[]); + const roomEid = (this._config?.weekdays_selected_room && ents.includes(this._config.weekdays_selected_room)) ? this._config.weekdays_selected_room : (ents[0]||null); + if (roomEid) { + const row = this._schedules[roomEid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureWeeklyStruct(row); + const mode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + const groupKeys = mode==='weekday_weekend' ? ['weekdays','weekend'] : (mode==='weekday_sat_sun' ? ['weekdays','sat','sun'] : ['mon','tue','wed','thu','fri','sat','sun']); + const labelFor = (k)=>{ + const map = { weekdays:'week.group.weekdays', weekend:'week.group.weekend', mon:'week.days_full.mon', tue:'week.days_full.tue', wed:'week.days_full.wed', thu:'week.days_full.thu', fri:'week.days_full.fri', sat:'week.days_full.sat', sun:'week.days_full.sun' }; + return this._t(map[k]) || k; + }; + const effKey = (k)=> (k==='weekdays') ? 'mon' : (k==='weekend' ? 'sat' : k); + const getBlocks = (dk)=>{ + try { const src = row.weekly_modes?.[mode]?.days || {}; const arr = src?.[dk]; if (Array.isArray(arr)) return arr; } catch {} + try { const arr2 = row.weekly?.days?.[dk]; if (Array.isArray(arr2)) return arr2; } catch {} + return []; + }; + for (const gk of groupKeys){ + const rowEl = document.createElement('div'); rowEl.className='row'; + const head = document.createElement('div'); head.className='row-head'; + const meta = document.createElement('div'); meta.className='meta'; + const name = document.createElement('span'); name.textContent = labelFor(gk); meta.append(name); head.append(meta); + + // Add toolbar with Add and Copy buttons for weekday view + const toolbar = document.createElement('div'); + toolbar.className = 'toolbar' + (this._toolsOpen[roomEid + ':' + effKey(gk)] ? ' open' : ''); + toolbar.style.minHeight = '32px'; + const toolsWrap = document.createElement('div'); + toolsWrap.className = 'tools-wrap'; + + // Toggle button for collapsible toolbar + const toggleBtn = document.createElement('button'); + toggleBtn.className = 'tools-toggle'; + toggleBtn.type = 'button'; + toggleBtn.setAttribute('aria-label', 'Toggle actions'); + toggleBtn.textContent = this._toolsOpen[roomEid + ':' + effKey(gk)] ? '✕' : '❮'; + toggleBtn.addEventListener('click', () => { + const toolKey = roomEid + ':' + effKey(gk); + const willOpen = !this._toolsOpen[toolKey]; + this._toolsOpen[toolKey] = willOpen; + if (willOpen) { + try { toolsWrap.style.maxWidth = '0px'; } catch {} + toolbar.classList.add('open'); + toggleBtn.textContent = '✕'; + requestAnimationFrame(() => { + try { const w = toolsWrap.scrollWidth || 0; toolsWrap.style.maxWidth = (w + 2) + 'px'; } catch {} + }); + } else { + try { const w = toolsWrap.scrollWidth || 0; toolsWrap.style.maxWidth = (w + 2) + 'px'; } catch {} + toolbar.classList.remove('open'); + toggleBtn.textContent = '❮'; + requestAnimationFrame(() => { try { toolsWrap.style.maxWidth = '0px'; } catch {} }); + } + }); + + // Add block button + const addBtn = document.createElement('button'); + addBtn.className = 'btn primary'; + addBtn.textContent = '+ ' + this._t('ui.add_block'); + addBtn.addEventListener('click', () => { + this._weeklyEntity = roomEid; + // Keep group/day key so editor respects current weekday mode. + // E.g. weekday_weekend: editing on Sunday should edit the "weekend" group. + this._ensureWeeklyStruct(row); + const mode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + this._weeklyDayKey = gk; + // Build draft from active mode bucket (weekly_modes is the primary source). + try { + const src = row.weekly_modes?.[mode]?.days || row.weekly?.days || {}; + const names = this._weekdayNames(); + const draftDays = JSON.parse(JSON.stringify(src)); + for (const k of names){ if (!Array.isArray(draftDays[k])) draftDays[k] = []; } + this._weeklyDraft = { mode, days: draftDays }; + // Include modes so edit/delete can fully reconcile sources. + try { this._weeklyDraft._weeklyModes = JSON.parse(JSON.stringify(row.weekly_modes || {})); } catch {} + } catch { + this._weeklyDraft = JSON.parse(JSON.stringify(row.weekly)); + try { this._weeklyDraft.mode = this._weeklyDraft.mode || mode; } catch {} + try { this._weeklyDraft._weeklyModes = JSON.parse(JSON.stringify(row.weekly_modes || {})); } catch {} + } + this._autoSaveWeeklyFromTimeline = true; + this._openWeeklyBlockEditor(null, 0); + }); + toolsWrap.append(addBtn); + + // Copy/Paste button + const copyBtn = document.createElement('button'); + copyBtn.className = 'btn ghost'; + const updateCopyBtn = ()=>{ + if (!this._copyFrom) { copyBtn.textContent = this._t('ui.copy_plan'); copyBtn.classList.remove('success', 'danger'); copyBtn.classList.add('ghost'); } + else if (this._copyFrom === roomEid + ':' + effKey(gk)) { copyBtn.textContent = this._t('ui.cancel'); copyBtn.classList.remove('success', 'ghost'); copyBtn.classList.add('danger'); } + else { copyBtn.textContent = this._t('ui.paste_plan'); copyBtn.classList.remove('ghost', 'danger'); copyBtn.classList.add('success'); } + }; + updateCopyBtn(); + copyBtn.addEventListener('click', async () => { + const dayKey = effKey(gk); + const eid_day = roomEid + ':' + dayKey; + + if (this._copyFrom && this._copyFrom === eid_day) { + this._copyFrom = null; this._copyPayload = null; this._render(); return; + } + + if (this._copyFrom && this._copyPayload) { + try { + const src = this._copyPayload; + this._ensureWeeklyStruct(row); + // Generate new IDs for copied blocks and paste them + const pastedBlocks = src.map(b=>({ + ...b, + id: Math.random().toString(36).slice(2,9) + })); + + // Paste to the same location where blocks come from + // Check if blocks come from weekly_modes (first priority in getBlocks) + const srcModes = row.weekly_modes?.[mode]?.days || {}; + if (srcModes?.[dayKey]) { + // Blocks are in weekly_modes, paste there + if (!row.weekly_modes[mode]) row.weekly_modes[mode] = { days: {} }; + row.weekly_modes[mode].days[dayKey] = pastedBlocks; + } else { + // Blocks are in weekly or new, paste to weekly + row.weekly.days[dayKey] = pastedBlocks; + } + + await this._saveStore(); + this._render(); + } catch(e){} + return; + } + + try { + const dayBlocks = getBlocks(dayKey); + const payload = dayBlocks.map(b=>({ startMin:b.startMin, endMin:b.endMin, temp:b.temp })); + this._copyFrom = eid_day; + this._copyPayload = payload; + this._render(); + } catch(e){} + }); + toolsWrap.append(copyBtn); + + toolbar.append(toggleBtn); + toolbar.append(toolsWrap); + head.append(toolbar); + + rowEl.append(head); + const track = document.createElement('div'); track.className='track'; track.style.setProperty('--row-height', `${this._config.row_height}px`); + const dkey = effKey(gk); + const blocks = getBlocks(dkey); + for (const b of blocks){ + const pctStart = (b.startMin / 1440) * 100; + const pctWidth = ((b.endMin - b.startMin) / 1440) * 100; + const bl = document.createElement('div'); + bl.className='block'; + bl.style.left=`${pctStart}%`; + bl.style.width=`${pctWidth}%`; + try { + const clr = this._colorFor(roomEid, b.temp); + if (clr){ + bl.style.background = clr; + bl.style.borderColor = clr; + const txt=this._contrastTextColor(clr); + if (txt) bl.style.color = txt; + } + } catch {} + UiHelper.genBlockPill( + this, + bl, + `${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}` + ); + + // Add double-click to edit block + bl.addEventListener('dblclick', (e) => { + try { e.preventDefault(); e.stopPropagation(); } catch {} + this._weeklyEntity = roomEid; + // Keep group key (gk) so the editor knows whether it's weekdays/weekend vs specific day. + this._ensureWeeklyStruct(row); + const mode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + this._weeklyDayKey = gk; + try { + const src = row.weekly_modes?.[mode]?.days || row.weekly?.days || {}; + const names = this._weekdayNames(); + const draftDays = JSON.parse(JSON.stringify(src)); + for (const k of names){ if (!Array.isArray(draftDays[k])) draftDays[k] = []; } + this._weeklyDraft = { mode, days: draftDays }; + try { this._weeklyDraft._weeklyModes = JSON.parse(JSON.stringify(row.weekly_modes || {})); } catch {} + } catch { + this._weeklyDraft = JSON.parse(JSON.stringify(row.weekly)); + try { this._weeklyDraft.mode = this._weeklyDraft.mode || mode; } catch {} + try { this._weeklyDraft._weeklyModes = JSON.parse(JSON.stringify(row.weekly_modes || {})); } catch {} + } + this._autoSaveWeeklyFromTimeline = true; + this._openWeeklyBlockEditor(b.id); + }); + + // Touch/iPad: detect double-tap + bl.addEventListener('touchend', (e) => { + const now = Date.now(); + const last = this._lastWeeklyTap || { t: 0, target: null }; + if (last.target === bl && (now - last.t) < 350) { + try { e.preventDefault(); e.stopPropagation(); } catch {} + this._lastWeeklyTap = { t: 0, target: null }; + this._weeklyEntity = roomEid; + this._ensureWeeklyStruct(row); + const mode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + this._weeklyDayKey = gk; + try { + const src = row.weekly_modes?.[mode]?.days || row.weekly?.days || {}; + const names = this._weekdayNames(); + const draftDays = JSON.parse(JSON.stringify(src)); + for (const k of names){ if (!Array.isArray(draftDays[k])) draftDays[k] = []; } + this._weeklyDraft = { mode, days: draftDays }; + try { this._weeklyDraft._weeklyModes = JSON.parse(JSON.stringify(row.weekly_modes || {})); } catch {} + } catch { + this._weeklyDraft = JSON.parse(JSON.stringify(row.weekly)); + try { this._weeklyDraft.mode = this._weeklyDraft.mode || mode; } catch {} + try { this._weeklyDraft._weeklyModes = JSON.parse(JSON.stringify(row.weekly_modes || {})); } catch {} + } + this._autoSaveWeeklyFromTimeline = true; + this._openWeeklyBlockEditor(b.id); + } else { + this._lastWeeklyTap = { t: now, target: bl }; + setTimeout(() => { if (this._lastWeeklyTap?.target === bl) this._lastWeeklyTap = { t: 0, target: null }; }, 350); + } + }); + + track.append(bl); + } + rowEl.append(track); rowsEl.append(rowEl); + } + __weeklyBuilt = true; + } + } + } catch {} + + // Default: render one row per room + if (!__weeklyBuilt) { for (const eid of this._config.entities) { + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + const rowEl = document.createElement('div'); + rowEl.className = 'row'; + const head = document.createElement('div'); + head.className = 'row-head'; + const meta = document.createElement('div'); + meta.className = 'meta'; + const name = document.createElement('span'); + // Brug brugerdefineret label hvis sat i config.labels, ellers fallback til friendly_name/_prettyName + const customLabel = (this._config.labels && this._config.labels[eid]) ? this._config.labels[eid] : null; + name.textContent = customLabel || this._prettyName(eid); + meta.append(name); + // Per-room Default °C input (optional) + try { + if (this._config?.per_room_defaults) { + const inp = document.createElement('input'); + inp.type = 'number'; + inp.step = '0.5'; + try { inp.min = String(this._minDisplay()); inp.max = String(this._maxDisplay()); } catch {} + const base = Number(row.defaultTemp ?? this._config.default_temp ?? 20); + inp.value = String(this._toDisplayTemp(base)); + inp.title = this._t('editor.default_c').replace('°C', this._unitSymbol()); + inp.addEventListener('change', async () => { + const before = this._config.apply_on_default_change ? this._desiredNowSnapshot() : null; + let raw = String(inp.value||'').replace(',','.'); + let vDisp = Number(raw); + let vC = Number.isFinite(vDisp) ? this._fromDisplayTemp(vDisp) : (row.defaultTemp ?? this._config.default_temp ?? 20); + const mx = Number(this._config?.max_temp ?? 25); + const mn = Number(this._config?.min_temp ?? 5); + vC = Math.max(mn, Math.min(mx, vC)); + row.defaultTemp = vC; + try { inp.value = String(this._toDisplayTemp(vC)); } catch {} + this._debouncedSaveStore(); + if (before) { try { await this._applyIfDesiredChanged(before); } catch {} } + this._scheduleNextApply(); + }); + meta.append(inp); + } + } catch {} + // inline default temperature input removed from timeline to keep UI clean + // Current room temperature bubble: show climate.current_temperature + // If room is merged with additional thermostats, show the average temperature + try { + if (this._config?.show_room_temp ?? true) { + const curC = this._roomCurrentTemperatureC ? this._roomCurrentTemperatureC(eid) : NaN; + if (Number.isFinite(curC)) { + const vDisp = this._toDisplayTemp(curC); + const sb = document.createElement('span'); + sb.className = 'sensor-bubble'; + sb.textContent = `${this._t('ui.current_temp')} ${String(Math.round(Number(vDisp) * 10) / 10)} ${this._unitSymbol()}`; + meta.append(sb); + } + } + } catch {} + // (Global Away bypass button moved to header; no per-room button) + // (manual indicator removed) + head.append(meta); + // Collapsible toolbar: tools slide out from a small arrow button + const toolbar = document.createElement('div'); + toolbar.className = 'toolbar' + (this._toolsOpen[eid] ? ' open' : ''); + // Keep header height stable regardless of open/closed + toolbar.style.minHeight = '32px'; + const toolsWrap = document.createElement('div'); + toolsWrap.className = 'tools-wrap'; + const toggleBtn = document.createElement('button'); + toggleBtn.className = 'tools-toggle'; + toggleBtn.type = 'button'; + toggleBtn.setAttribute('aria-label', 'Toggle actions'); + toggleBtn.textContent = this._toolsOpen[eid] ? '✕' : '❮'; + toggleBtn.addEventListener('click', () => { + const bar = toggleBtn.parentElement; + const wrap = toolsWrap; + const willOpen = !this._toolsOpen[eid]; + this._toolsOpen[eid] = willOpen; + if (willOpen) { + // prepare then expand + try { wrap.style.maxWidth = '0px'; } catch {} + bar && bar.classList.add('open'); + toggleBtn.textContent = '✕'; + requestAnimationFrame(() => { + try { const w = wrap.scrollWidth || 0; wrap.style.maxWidth = (w + 2) + 'px'; } catch {} + }); + } else { + // freeze width then collapse + try { const w = wrap.scrollWidth || 0; wrap.style.maxWidth = (w + 2) + 'px'; } catch {} + bar && bar.classList.remove('open'); + toggleBtn.textContent = '❮'; + requestAnimationFrame(() => { try { wrap.style.maxWidth = '0px'; } catch {} }); + } + }); + // (resume schedule button removed) + const addBtn = document.createElement('button'); + addBtn.className = 'btn primary'; + addBtn.textContent = '+ ' + this._t('ui.add_block'); + try { addBtn.dataset.role = 'addbtn'; if (this._disablePopups) addBtn.style.display = 'none'; } catch {} + addBtn.addEventListener('click', () => { + // If weekdays are enabled, open the block editor directly for today's weekly plan + try { + const row = this._schedules[eid]; + if (this._config?.weekdays_enabled && row) { + // Ensure weekly structures exist + this._ensureWeeklyStruct(row); + const mode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + const today = this._timelineDisplayDayKey(); + const isWeekday = ['mon','tue','wed','thu','fri'].includes(today); + let sel = today; + if (mode === 'weekday_weekend') sel = isWeekday ? 'weekdays' : 'weekend'; + else if (mode === 'weekday_sat_sun') sel = isWeekday ? 'weekdays' : today; // sat/sun + // Prepare an in-memory weekly draft without opening the weekdays popup + try { + const src = row.weekly_modes?.[mode]?.days || {}; + const names = this._weekdayNames(); + const draftDays = JSON.parse(JSON.stringify(src)); + for (const k of names){ if (!Array.isArray(draftDays[k])) draftDays[k] = []; } + this._weeklyDraft = { mode, days: draftDays }; + } catch { + this._weeklyDraft = JSON.parse(JSON.stringify(row.weekly)); + } + this._weeklyEntity = eid; + this._weeklyDayKey = sel; + this._autoSaveWeeklyFromTimeline = true; + const now = this._getNowMin(); + this._openWeeklyBlockEditor(null, now); + return; + } + } catch {} + // Fallback: non-weekly add + this._openNewEditor(eid); + }); + toolsWrap.append(addBtn); + // Copy/Paste schedule button + const copyBtn = document.createElement('button'); + copyBtn.className = 'btn ghost'; + const applyCopyStyle = (mode)=>{ + // mode: 'copy' | 'paste' | 'cancel' + copyBtn.className = 'btn'; + if (mode === 'copy') copyBtn.classList.add('ghost'); + if (mode === 'paste') copyBtn.classList.add('success'); + if (mode === 'cancel') copyBtn.classList.add('danger'); + }; + const updateCopyBtn = ()=>{ + if (!this._copyFrom) { copyBtn.textContent = this._t('ui.copy_plan'); applyCopyStyle('copy'); } + else if (this._copyFrom === eid) { copyBtn.textContent = this._t('ui.cancel'); applyCopyStyle('cancel'); } + else { copyBtn.textContent = this._t('ui.paste_plan'); applyCopyStyle('paste'); } + }; + updateCopyBtn(); + copyBtn.addEventListener('click', async () => { + // Cancel on source row + if (this._copyFrom && this._copyFrom === eid) { + this._copyFrom = null; this._copyPayload = null; this._render(); return; + } + // Paste into this row when copy active + if (this._copyFrom && this._copyPayload) { + const before = this._desiredNowSnapshot(); + const src = this._copyPayload; + try { + const tgt = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + // Deep copy + const clone = JSON.parse(JSON.stringify(src)); + tgt.defaultTemp = Number.isFinite(clone.defaultTemp) ? clone.defaultTemp : (tgt.defaultTemp || this._config.default_temp); + tgt.blocks = Array.isArray(clone.blocks) ? clone.blocks.map(b=>({ ...b, id: Math.random().toString(36).slice(2,9) })) : []; + // Weekday structures (keep modes if present) + if (clone.weekly) tgt.weekly = JSON.parse(JSON.stringify(clone.weekly)); + if (clone.weekly_modes) tgt.weekly_modes = JSON.parse(JSON.stringify(clone.weekly_modes)); + this._schedules[eid] = tgt; + await this._saveStore(); + this._render(); + if (this._config.apply_on_edit) await this._applyIfDesiredChanged(before); + this._scheduleNextApply(); + } catch(e){} + return; + } + // Start copy from this row + try { + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + const payload = { defaultTemp: row.defaultTemp, blocks: (row.blocks||[]).map(b=>({ startMin:b.startMin, endMin:b.endMin, temp:b.temp })), weekly: row.weekly ? JSON.parse(JSON.stringify(row.weekly)) : undefined, weekly_modes: row.weekly_modes ? JSON.parse(JSON.stringify(row.weekly_modes)) : undefined }; + this._copyFrom = eid; this._copyPayload = payload; + this._render(); + } catch(e){} + }); + toolsWrap.append(copyBtn); + // Weekdays button moved to header tools; hidden on rows to avoid duplication + // Row-level Manual schedule button removed – profiles button now lives in the header only + try { /* intentionally no-op */ } catch {} + // (Profiles button moved to header) + toolbar.append(toolsWrap, toggleBtn); + // Hide toggle button if toolsWrap is empty + if (toolsWrap.children.length === 0) { + toggleBtn.style.display = 'none'; + } + // Ensure correct initial width for open state without rerendering + try { + if (this._toolsOpen[eid]) { + requestAnimationFrame(() => { try { toolsWrap.style.maxWidth = (toolsWrap.scrollWidth + 2) + 'px'; } catch {} }); + } else { + toolsWrap.style.maxWidth = '0px'; + } + } catch {} + head.append(toolbar); + rowEl.append(head); + const track = document.createElement('div'); + track.className = 'track'; + track.style.setProperty('--row-height', `${this._config.row_height}px`); + // Use browsed day in weekdays view (all rooms • one day) + const displayDayKey = this._timelineDisplayDayKey(); + let dayKey = displayDayKey; + try { + if (this._config?.weekdays_enabled && row?.weekly) { + const mode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + dayKey = this._weekdaysStorageDayKeyForMode(displayDayKey, mode); + } + } catch { dayKey = displayDayKey; } + this._ensureHolidayStruct(row); + this._ensurePresenceStruct(row); + let showBlocks = (row.blocks || []); + try { + if (this._isHolidayActive() && row?.holiday?.blocks?.length) { + showBlocks = row.holiday.blocks || []; + } else { + // Advanced Presence overrides visual track as well (when a combo is active) + try { + const key = this._activePresenceComboKey && this._activePresenceComboKey(); + if (key) { + const roomBlocks = (row.presence && Array.isArray(row.presence[key]?.blocks)) ? (row.presence[key].blocks || []) : []; + if (roomBlocks.length) { + showBlocks = roomBlocks; + } else { + // Presence combo active but no schedule for this room -> show a virtual all-day Away block + const awayC = Number(this._config?.away?.target_c ?? 17); + const fallback = Number.isFinite(awayC) ? awayC : this._rowDefaultTemp(row); + showBlocks = [{ id: '__presence_away__', startMin: 0, endMin: 1440, temp: fallback }]; + } + } else if (this._config?.profiles_enabled) { + this._ensureProfilesStruct(row); + const ap = row.activeProfile; + if (ap && row.profiles && Array.isArray(row.profiles[ap]?.blocks) && (row.profiles[ap].blocks||[]).length) { + showBlocks = row.profiles[ap].blocks || []; + } else if (this._config?.weekdays_enabled) { + showBlocks = this._getBlocksForDay(row, dayKey) || []; + } + } else if (this._config?.weekdays_enabled) { + showBlocks = this._getBlocksForDay(row, dayKey) || []; + } + } catch { /* fallback below */ } + } + } catch {} + for (const b of showBlocks) { + const pctStart = (b.startMin / 1440) * 100; + const pctWidth = ((b.endMin - b.startMin) / 1440) * 100; + const bl = document.createElement('div'); + bl.className = 'block'; + bl.style.left = `${pctStart}%`; + bl.style.width = `${pctWidth}%`; + try { + const clr = this._colorFor(eid, b.temp); + if (clr) { + bl.style.background = clr; + bl.style.borderColor = clr; + const txt = this._contrastTextColor(clr); + if (txt) bl.style.color = txt; + } + } catch {} + if (this._active?.entity === eid && this._active?.id === b.id) bl.classList.add('active'); + // Time will be shown in a hover tooltip (like the weekdays popup). Do not render time pill here. + UiHelper.genBlockPill( + this, + bl, + `${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}` + ); + track.append(bl); + + // Ensure a tooltip element exists for main timeline (reuse weekly style) + try { + let tooltip = this.shadowRoot.querySelector('.wk-tooltip.main'); + const rowsEl = this.shadowRoot.querySelector('.rows'); + const modalHost = rowsEl || this.shadowRoot.querySelector('ha-card'); + if (!tooltip) { + tooltip = document.createElement('div'); + tooltip.className = 'wk-tooltip main'; + tooltip.style.display = 'none'; + // append to rows so positioning is relative to timeline + if (modalHost) modalHost.append(tooltip); + } + const showTip = () => { + const txt = `${this._label(b.startMin)} - ${this._label(b.endMin)} • ${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}`; + if (!tooltip) return; + // Cancel any pending hide + try { if (tooltip._hideTimer) { clearTimeout(tooltip._hideTimer); tooltip._hideTimer = null; } } catch {} + // Do not show while editor popup is open + try { + const ov = this.shadowRoot.querySelector('.overlay'); + if (this._editing || (ov && ov.classList.contains('open'))) return; + } catch {} + tooltip.textContent = txt; + const box = bl.getBoundingClientRect(); + const cont = (modalHost && modalHost.getBoundingClientRect) ? modalHost.getBoundingClientRect() : { left: 0, top: 0 }; + const left = box.left + box.width / 2 - (cont.left || 0); + const top = (box.top - (cont.top || 0)) - 8; + tooltip.style.left = left + 'px'; + tooltip.style.top = top + 'px'; + tooltip.style.transform = 'translate(-50%,-100%)'; + tooltip.style.display = ''; + }; + const hideTip = () => { + if (!tooltip) return; + // On coarse pointers (tablet) keep tooltip a bit longer + const delay = (window.matchMedia && window.matchMedia('(pointer:coarse)').matches) ? 3000 : 120; + try { if (tooltip._hideTimer) clearTimeout(tooltip._hideTimer); } catch {} + tooltip._hideTimer = setTimeout(() => { try { tooltip.style.display = 'none'; tooltip._hideTimer = null; } catch {} }, delay); + }; + bl.addEventListener('mouseenter', showTip); + bl.addEventListener('mouseleave', hideTip); + } catch (e) { /* ignore tooltip errors */ } + + // Open editor only on double interaction: + // - Desktop: double-click + // - Touch/iPad: double-tap within 350ms on the same block + const openEditor = () => { + // If a manual profile is active, edit that profile directly instead of weekdays + try { + if (this._config?.profiles_enabled) { + this._ensureProfilesStruct(row); + const ap = row?.activeProfile; + if (ap && row?.profiles && Array.isArray(row.profiles[ap]?.blocks) && (row.profiles[ap].blocks||[]).length) { + // Configure profile context and open profile block editor + this._profilesEntity = eid; + this._profilesSelected = ap; + this._profilesRoom = eid; + this._autoSaveProfileFromTimeline = true; + this._openProfileBlockEditor(b.id); + return; + } + } + } catch {} + + // Else if weekdays are enabled, edit inline (no weekday popup); auto‑save back to weekdays + try { + const rowHasWeekly = !!(this._config?.weekdays_enabled && row?.weekly); + if (rowHasWeekly) { + // Prepare an in‑memory weekly draft for the current mode + this._ensureWeeklyStruct(row); + const mode = row.weekly?.mode || this._config?.weekdays_mode || 'weekday_weekend'; + const today = displayDayKey; // computed earlier + const isWeekday = ['mon','tue','wed','thu','fri'].includes(today); + let sel = today; + if (mode === 'weekday_weekend') sel = isWeekday ? 'weekdays' : 'weekend'; + else if (mode === 'weekday_sat_sun') sel = isWeekday ? 'weekdays' : today; // sat/sun + // Build draft from the selected mode bucket + try { + const src = row.weekly_modes?.[mode]?.days || {}; + const names = this._weekdayNames(); + const draftDays = JSON.parse(JSON.stringify(src)); + for (const k of names){ if (!Array.isArray(draftDays[k])) draftDays[k] = []; } + this._weeklyDraft = { mode, days: draftDays }; + } catch { this._weeklyDraft = JSON.parse(JSON.stringify(row.weekly)); } + this._weeklyEntity = eid; + this._weeklyDayKey = sel; + this._autoSaveWeeklyFromTimeline = true; + // Locate corresponding block in effective day using midpoint + const eff = this._effectiveDayKey(sel); + const mid = Math.floor((b.startMin + b.endMin) / 2); + const effArr = (this._weeklyDraft?.days?.[eff] || []); + let targetId = b.id; + if (eff !== today) { + const match = effArr.find(x => mid >= x.startMin && mid < x.endMin); + targetId = match ? match.id : null; + } + if (targetId) this._openWeeklyBlockEditor(targetId); + else this._openWeeklyBlockEditor(null, mid); + return; + } + } catch {} + // Fallback to normal (non-weekly/non-profile) editor + this._openEditor(eid, b.id); + }; + + // Desktop/mouse + bl.addEventListener('dblclick', (e) => { + try { e.preventDefault(); e.stopPropagation(); } catch {} + openEditor(); + }); + + // Touch/iPad: detect double-tap + bl.addEventListener('touchend', (e) => { + const now = Date.now(); + const last = this._lastTap || { t: 0, target: null }; + if (last.target === bl && (now - last.t) < 350) { + try { e.preventDefault(); e.stopPropagation(); } catch {} + this._lastTap = { t: 0, target: null }; + openEditor(); + } else { + this._lastTap = { t: now, target: bl }; + // Clear after timeout to avoid stale reference + setTimeout(() => { + if (this._lastTap && this._lastTap.target === bl && (Date.now() - this._lastTap.t) > 350) { + this._lastTap = { t: 0, target: null }; + } + }, 360); + } + }, { passive: false }); + // Note: single click/tap no longer opens the editor + } + // Single-line NOW: rely on .now with extend; remove extra top pin + /* now-top removed */ + rowEl.append(track); + rowsEl.append(rowEl); + } + } + // Single global NOW line across all rows + { + const nowEl = document.createElement('div'); + nowEl.className = 'now'; + // left will be set in pixels to account for row padding + rowsEl.append(nowEl); + + const updateNowGeom = () => { + try { + const tracks = Array.from(this.shadowRoot.querySelectorAll('.track')); + const rowsBox = rowsEl.getBoundingClientRect(); + // Vertical placement/height + if (tracks.length) { + const overhang = 12; + const firstBox = tracks[0].getBoundingClientRect(); + const lastBox = tracks[tracks.length - 1].getBoundingClientRect(); + const top = Math.round(firstBox.top - rowsBox.top - overhang); + const bottom = Math.round(lastBox.bottom - rowsBox.top); + nowEl.style.top = top + 'px'; + nowEl.style.height = (bottom - top) + 'px'; + } + // Horizontal left: respect rows padding so it matches .track coordinate system + const cs = getComputedStyle(rowsEl); + const pl = parseFloat(cs.paddingLeft || '0') || 0; + const pr = parseFloat(cs.paddingRight || '0') || 0; + const contentW = rowsEl.clientWidth - pl - pr; + const leftPx = pl + (nowMin / 1440) * contentW; + nowEl.style.left = leftPx + 'px'; + // Hide NOW line in "All days • one room" view (multi-day timeline) + try { + const perRoomAllDays = !!(this._config?.weekdays_enabled) && String(this._config?.weekdays_view||'all_rooms_one_day') === 'one_room_all_days'; + if (perRoomAllDays) { nowEl.style.display = 'none'; return; } + nowEl.style.display = ''; + } catch {} + // Set visibility of this NowGeo elt depending on the selected day key + let selectedDayKey = ''; + try { + // In normal timeline, the current display day is derived from the timeline browsing state. + // In weekdays view, _weekdaysViewDayKey may be used. + selectedDayKey = String((this._timelineDisplayDayKey && this._timelineDisplayDayKey()) || this._weekdaysViewDayKey || '').toLowerCase(); + } catch { selectedDayKey = String(this._weekdaysViewDayKey || '').toLowerCase(); } + const todayKey = String(this._todayKey && this._todayKey() || '').toLowerCase(); + // If we don't have a selected day (common in normal view), assume today. + if (!selectedDayKey) selectedDayKey = todayKey; + nowEl.style.visibility = (selectedDayKey === todayKey) ? "visible" : "hidden"; + } catch {} + }; + + updateNowGeom(); + + const ro = new ResizeObserver(() => { + updateNowGeom(); + }); + try { ro.observe(rowsEl); } catch {} + } + + // Update footer labels to reflect time format + try { + const s = this.shadowRoot.querySelector('.lbl-start'); + const e = this.shadowRoot.querySelector('.lbl-end'); + if (s) s.textContent = this._label(0); + if (e) e.textContent = this._label(0); + } catch {} + + // Editor modal events + const overlay = qs('.overlay'); + const edSave = qs('.ed-save'); + const edCancel = qs('.ed-cancel'); + const edDelete = qs('.ed-delete'); + if (edSave) edSave.addEventListener('click', () => this._saveEditor()); + if (edCancel) edCancel.addEventListener('click', () => this._closeEditor()); + if (edDelete) edDelete.addEventListener('click', () => this._deleteFromEditor()); + // Do not close editor when clicking outside; require explicit buttons + overlay.addEventListener('click', (e) => { if (e.target === overlay) { e.preventDefault(); e.stopPropagation(); } }); + // Week overlay events + const wov = qs('.overlay-week'); + const wsave = qs('.wk-save'); + const wcancel = qs('.wk-cancel'); + if (wsave) wsave.addEventListener('click', () => this._weeklySaveAll()); + if (wcancel) wcancel.addEventListener('click', () => this._closeWeeklyEditor()); + // Do not close weekly editor on outside click either + if (wov) wov.addEventListener('click', (e) => { if (e.target === wov) { e.preventDefault(); e.stopPropagation(); } }); + + // Profiles overlay events + const pov = qs('.overlay-profiles'); + const pclose = qs('.prof-close'); + const psave = qs('.prof-save'); + if (psave) psave.addEventListener('click', () => { this._profilesSave(); }); + const _maybeCloseProfiles = () => { + try { if (this._profilesDirty) { this._openConfirmCloseProfiles(); return false; } } catch {} + this._closeProfilesEditor(); return true; + }; + if (pclose) pclose.addEventListener('click', _maybeCloseProfiles); + if (pov) pov.addEventListener('click', (e) => { + // Close profiles overlay when clicking outside the modal (with guard) + if (e.target === pov) { e.preventDefault(); _maybeCloseProfiles(); } + }); + + // Holidays overlay events + const hov = qs('.overlay-holiday'); + const hsave = qs('.holiday-save'); + const hclose = qs('.holiday-close'); + if (hsave) hsave.addEventListener('click', () => this._holidaySave()); + if (hclose) hclose.addEventListener('click', () => { this._closeHolidayEditor(); this._render(); }); + if (hov) hov.addEventListener('click', (e)=>{ if (e.target === hov) { e.preventDefault(); this._closeHolidayEditor(); } }); + + // Holiday copy popup overlay events + const hcov = qs('.overlay-hcopy'); + if (hcov) hcov.addEventListener('click', (e)=>{ if (e.target === hcov) { e.preventDefault(); this._closeHolidayCopy(); } }); + + // Onboarding controls + try { + const next = qs('.onb-next'); + const prev = qs('.onb-prev'); + const skip = qs('.onb-skip'); + const done = qs('.onb-done'); + const ov = qs('.overlay-onboard'); + next && (next.onclick = ()=>{ this._onboardStep = Math.min(this._onboardSlides.length-1, this._onboardStep+1); this._renderOnboard(); }); + prev && (prev.onclick = ()=>{ this._onboardStep = Math.max(0, this._onboardStep-1); this._renderOnboard(); }); + skip && (skip.onclick = ()=>{ this._closeOnboard(true); }); + done && (done.onclick = ()=>{ this._closeOnboard(true); }); + ov && ov.addEventListener('click', (e)=>{ if (e.target === ov) { e.preventDefault(); e.stopPropagation(); } }); + } catch {} + + // Test popup events + try { + const tov = qs('.overlay-test'); + const tclose = qs('.test-close'); + const texport = qs('.test-export'); + if (tclose) tclose.onclick = ()=>{ try { tov?.classList.remove('open'); } catch {} }; + if (texport) texport.onclick = ()=>{ try { this._exportTestCommands(); } catch {} }; + if (tov) tov.onclick = (e)=>{ if (e.target === tov) { e.preventDefault(); e.stopPropagation(); } }; + } catch {} + + // Settings popup events + try { + const sov = qs('.overlay-settings'); + const sclose = qs('.settings-close'); + const ssave = qs('.settings-save'); + const stabs = qs('.settings-tabs'); + if (sclose) sclose.onclick = ()=>{ try { this._closeSettingsPopup(false); } catch {} }; + if (ssave) ssave.onclick = ()=>{ try { this._saveSettingsPopup(); } catch {} }; + if (stabs) stabs.onclick = (ev)=>{ + try { + // In Shadow DOM, the click target can be a Text node inside the button. + // Use composedPath() to reliably find the originating button. + const path = typeof ev.composedPath === 'function' ? ev.composedPath() : []; + let btn = null; + for (const n of path) { + if (n && n.classList && n.classList.contains('settings-tab-btn')) { btn = n; break; } + } + if (!btn) btn = (ev.target && ev.target.closest) ? ev.target.closest('.settings-tab-btn') : null; + if (!btn) return; + const tab = btn.getAttribute('data-tab') || 'settings'; + this._setSettingsPopupTab(tab); + } catch {} + }; + if (sov) sov.onclick = (e)=>{ if (e.target === sov) { e.preventDefault(); e.stopPropagation(); } }; + + // Prevent scroll wheel from accidentally changing number inputs (which triggers "unsaved" state) + try { + const scroller = qs('.overlay-settings .settings-body') || sov; + const onWheel = (ev)=>{ + try { + const t = ev?.target; + if (!t || t.tagName !== 'INPUT' || t.type !== 'number') return; + // Stop number increment/decrement and treat it as scroll + try { t.blur?.(); } catch {} + ev.preventDefault(); + const s = t.closest('.settings-body') || scroller; + if (s && typeof s.scrollTop === 'number') s.scrollTop += (Number(ev.deltaY) || 0); + } catch {} + }; + try { + if (this._settingsPopupWheelHandler?.el && this._settingsPopupWheelHandler?.fn) { + this._settingsPopupWheelHandler.el.removeEventListener('wheel', this._settingsPopupWheelHandler.fn); + } + } catch {} + if (scroller) { + scroller.addEventListener('wheel', onWheel, { passive: false }); + this._settingsPopupWheelHandler = { el: scroller, fn: onWheel }; + } + } catch {} + + // Settings tab controls (draft-only) + const onNum = (key) => (e)=>{ + try { + if (!this._settingsDraft) return; + const v = e && e.target ? e.target.value : ''; + if (v === '') return; + const n = Number(v); + if (!Number.isFinite(n)) return; + this._settingsDraft[key] = n; + } catch {} + }; + const onTempNum = (key) => (e)=>{ + try { + if (!this._settingsDraft) return; + const v = e && e.target ? e.target.value : ''; + if (v === '') return; + const raw = String(v).trim().replace(',', '.'); + const nDisp = Number(raw); + if (!Number.isFinite(nDisp)) return; + // Convert based on draft unit (config unit may not match while popup is open) + const wantF = String(this._settingsDraft?.temp_unit || 'C').toUpperCase() === 'F'; + const nC = wantF ? ((nDisp - 32) * 5/9) : nDisp; + if (!Number.isFinite(nC)) return; + this._settingsDraft[key] = nC; + } catch {} + }; + const onSw = (key) => (e)=>{ + try { + if (!this._settingsDraft) return; + const checked = !!(e && e.target && e.target.checked); + this._settingsDraft[key] = checked; + } catch {} + }; + + const spDef = qs('.overlay-settings .sp-def'); + const spRowH = qs('.overlay-settings .sp-rowh'); + const spMinC = qs('.overlay-settings .sp-minc'); + const spMaxC = qs('.overlay-settings .sp-maxc'); + if (spDef) spDef.oninput = onTempNum('default_temp'); + if (spRowH) spRowH.oninput = onNum('row_height'); + if (spMinC) spMinC.oninput = onTempNum('min_temp'); + if (spMaxC) spMaxC.oninput = onTempNum('max_temp'); + + const swAuto = qs('.overlay-settings .sp-auto'); + const swApplyEdit = qs('.overlay-settings .sp-applyedit'); + const swApplyDef = qs('.overlay-settings .sp-applydef'); + const swPerRoom = qs('.overlay-settings .sp-perroom'); + const swShowRoomTemp = qs('.overlay-settings .sp-showroomtemp'); + if (swAuto) swAuto.onchange = onSw('auto_apply'); + if (swApplyEdit) swApplyEdit.onchange = onSw('apply_on_edit'); + if (swApplyDef) swApplyDef.onchange = onSw('apply_on_default_change'); + if (swPerRoom) swPerRoom.onchange = onSw('per_room_defaults'); + if (swShowRoomTemp) swShowRoomTemp.onchange = onSw('show_room_temp'); + + // Segmented controls + profiles enable + const fmt24 = qs('.overlay-settings .sp-fmt24'); + const fmt12 = qs('.overlay-settings .sp-fmt12'); + const srcB = qs('.overlay-settings .sp-srcBrowser'); + const srcH = qs('.overlay-settings .sp-srcHa'); + const unitC = qs('.overlay-settings .sp-unitC'); + const unitF = qs('.overlay-settings .sp-unitF'); + const swProfiles = qs('.overlay-settings .sp-profiles'); + const swSeasonal = qs('.overlay-settings .sp-seasonal'); + const swInstance = qs('.overlay-settings .sp-instance-enable'); + const btnRegenerateInstance = qs('.overlay-settings .sp-instance-regenerate-btn'); + if (fmt24) fmt24.onclick = ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.time_12h = false; this._renderSettingsPopupSettingsTab(); } catch {} }; + if (fmt12) fmt12.onclick = ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.time_12h = true; this._renderSettingsPopupSettingsTab(); } catch {} }; + if (srcB) srcB.onclick = ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.time_source = 'browser'; this._renderSettingsPopupSettingsTab(); } catch {} }; + if (srcH) srcH.onclick = ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.time_source = 'ha'; this._renderSettingsPopupSettingsTab(); } catch {} }; + if (unitC) unitC.onclick = ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.temp_unit = 'C'; this._renderSettingsPopupTab(); } catch {} }; + if (unitF) unitF.onclick = ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.temp_unit = 'F'; this._renderSettingsPopupTab(); } catch {} }; + if (swProfiles) swProfiles.onchange = onSw('profiles_enabled'); + if (swSeasonal) swSeasonal.onchange = onSw('seasonal_enabled'); + + // Configuration ID (instance) toggle (persists outside of the popup draft) + if (swInstance) { + swInstance.onchange = async (e)=>{ + try { + const on = !!e?.target?.checked; + if (!on) { + const oldId = this._normInstanceId(String(this._config?.instance_id || '').trim() || 'default'); + const oldKey = (this._config?.instance_enabled) ? `thermostat_timeline_store:${oldId}` : 'thermostat_timeline_store'; + // Best-effort migration: copy current instance back into default + activate it + try { + if (this._config?.storage_enabled && this._hass?.callApi) { + const api = await this._hass.callApi('GET', `thermostat_timeline/state?instance_id=${encodeURIComponent(oldId)}`); + const payload = { force: true, instance_id: 'default', activate: true }; + if (api?.schedules) payload.schedules = api.schedules; + if (api?.settings) payload.settings = api.settings; + if (api?.colors) payload.colors = api.colors; + if (api?.weekdays) payload.weekdays = api.weekdays; + if (api?.profiles) payload.profiles = api.profiles; + await this._hass.callService('thermostat_timeline','set_store', payload); + } else { + const dstKey = 'thermostat_timeline_store'; + const src = localStorage.getItem(oldKey) || ''; + const dst = localStorage.getItem(dstKey) || ''; + if (src && (!dst || dst.length < 3)) { + try { localStorage.setItem(dstKey, src); } catch {} + } + } + } catch {} + try { this._config.instance_enabled = false; this._config.instance_id = ''; } catch {} + try { this._persistInstanceCfg(false, String(this._config?.instance_id||'')); } catch {} + try { this._renderSettingsPopupSettingsTab(); } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + return; + } + let cur = String(this._config?.instance_id || '').trim(); + if (!cur) cur = this._genInstanceId(); + cur = this._normInstanceId(cur); + try { this._config.instance_enabled = true; this._config.instance_id = cur; } catch {} + try { this._persistInstanceCfg(true, cur); } catch {} + + // Best-effort migration: copy legacy store into this instance + try { + if (this._config?.storage_enabled && this._hass?.callApi) { + const api = await this._hass.callApi('GET', 'thermostat_timeline/state'); + const payload = { force: true, instance_id: cur, activate: false }; + if (api?.schedules) payload.schedules = api.schedules; + if (api?.settings) payload.settings = api.settings; + if (api?.colors) payload.colors = api.colors; + if (api?.weekdays) payload.weekdays = api.weekdays; + if (api?.profiles) payload.profiles = api.profiles; + await this._hass.callService('thermostat_timeline','set_store', payload); + } else { + const srcKey = 'thermostat_timeline_store'; + const dstKey = this._localStoreKey(); + const src = localStorage.getItem(srcKey) || ''; + const dst = localStorage.getItem(dstKey) || ''; + if (src && (!dst || dst.length < 3)) { + try { localStorage.setItem(dstKey, src); } catch {} + } + } + } catch {} + + try { this._renderSettingsPopupSettingsTab(); } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + } catch {} + }; + } + + // Regenerate Configuration ID (instance_id) - renames/moves data to a new id (no duplicate instance) + if (btnRegenerateInstance) { + btnRegenerateInstance.onclick = async ()=>{ + try { + if (!this._config?.instance_enabled) return; + const oldId = this._normInstanceId(this._config?.instance_id || ''); + if (!oldId) return; + const newId = this._genInstanceId(); + if (!newId || newId === oldId) return; + + // Rename/move old -> new (avoids creating an extra instance/section) + const renamed = await this._renameInstanceData(oldId, newId); + if (!renamed) return; + + // Switch this card to the new id + persist outside YAML + try { this._config.instance_id = newId; } catch {} + try { this._persistInstanceCfg(true, newId); } catch {} + + try { this._renderSettingsPopupSettingsTab(); } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + } catch {} + }; + } + } catch {} + + // Weekdays copy modal events + try { + const wov = qs('.overlay-weekcopy'); + const applyBtn = qs('.weekcopy-apply'); + const skipBtn = qs('.weekcopy-skip'); + if (applyBtn) applyBtn.onclick = ()=>{ try { this._applyWeekCopyModal(true); } catch {} }; + if (skipBtn) skipBtn.onclick = ()=>{ try { this._applyWeekCopyModal(false); } catch {} }; + if (wov) wov.onclick = (e)=>{ if (e.target === wov) { e.preventDefault(); this._applyWeekCopyModal(false); } }; + } catch {} + } + + // --- Settings popup (in-card, not HA editor) --- + _openSettingsPopup(){ + try { + if (this._disablePopups) return; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings'); + if (!ov) return; + ov.classList.add('open'); + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + try { if (!this._settingsPopupTab) this._settingsPopupTab = 'settings'; } catch {} + try { this._initSettingsPopupDraft(); } catch {} + try { this._applyCardI18n(); } catch {} + try { this._renderSettingsPopupTabs(); } catch {} + try { this._renderSettingsPopupTab(); } catch {} + // Post-open refresh for Configuration ID toggle from local cache + try { + const refreshInstance = ()=>{ try { this._hydrateInstanceCfgFromLocal(); this._renderSettingsPopupSettingsTab(); } catch {} }; + setTimeout(refreshInstance, 0); + setTimeout(refreshInstance, 300); + } catch {} + try { + const onKey = (ev) => { if (ev.key === 'Escape') { this._closeSettingsPopup(false); } }; + try { if (this._settingsPopupKeyHandler) window.removeEventListener('keydown', this._settingsPopupKeyHandler); } catch {} + this._settingsPopupKeyHandler = onKey; + window.addEventListener('keydown', onKey); + } catch {} + } catch {} + } + + _setSettingsPopupTab(tab){ + try { + const allowed = new Set(['settings','pause','weekdays','rooms','boiler','colors','presence_sensor','owd','holidays','away','reset']); + const next = allowed.has(String(tab)) ? String(tab) : 'settings'; + this._settingsPopupTab = next; + this._renderSettingsPopupTabs(); + this._renderSettingsPopupTab(); + } catch {} + } + + _renderSettingsPopupTabs(){ + try { + const active = String(this._settingsPopupTab || 'settings'); + const btns = this.shadowRoot ? Array.from(this.shadowRoot.querySelectorAll('.overlay-settings .settings-tab-btn')) : []; + for (const b of btns){ + const isActive = (b.getAttribute('data-tab') === active); + try { b.classList.toggle('active', isActive); } catch {} + try { b.setAttribute('aria-selected', isActive ? 'true' : 'false'); } catch {} + } + } catch {} + } + + _openWeekCopyModal(){ + try { + if (this._disablePopups) return; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-weekcopy'); + if (!ov) return; + ov.classList.add('open'); + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + try { this._renderWeekCopyModal(); } catch {} + } catch {} + } + + _closeWeekCopyModal(){ + try { + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-weekcopy'); + if (ov) ov.classList.remove('open'); + try { + const settingsOpen = !!this.shadowRoot?.querySelector('.overlay-settings')?.classList.contains('open'); + if (!settingsOpen) this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); + } catch {} + } catch {} + } + + _renderWeekCopyModal(){ + try { + const title = this.shadowRoot && this.shadowRoot.querySelector('.weekcopy-title'); + const desc = this.shadowRoot && this.shadowRoot.querySelector('.weekcopy-desc'); + const modeLabel = this.shadowRoot && this.shadowRoot.querySelector('.weekcopy-mode-label'); + const sel = this.shadowRoot && this.shadowRoot.querySelector('.weekcopy-mode'); + const targetLabel = this.shadowRoot && this.shadowRoot.querySelector('.weekcopy-target-label'); + const targetSel = this.shadowRoot && this.shadowRoot.querySelector('.weekcopy-target'); + const applyBtn = this.shadowRoot && this.shadowRoot.querySelector('.weekcopy-apply'); + const skipBtn = this.shadowRoot && this.shadowRoot.querySelector('.weekcopy-skip'); + + if (title) title.textContent = this._t('week.copy_from_main.title') || 'Copy main schedule'; + if (desc) desc.textContent = this._t('week.copy_from_main.desc') || ''; + if (modeLabel) modeLabel.textContent = this._t('week.copy_from_main.mode_label') || (this._t('week.mode') || 'Weekday mode'); + if (targetLabel) targetLabel.textContent = this._t('week.copy_from_main.target_label') || 'Copy to'; + if (applyBtn) applyBtn.textContent = this._t('week.copy_from_main.copy_btn') || (this._t('ui.copy_plan') || 'Copy'); + if (skipBtn) skipBtn.textContent = this._t('week.copy_from_main.skip_btn') || (this._t('ui.cancel') || 'Skip'); + + const renderTargets = (mode, preferred='')=>{ + if (!targetSel) return; + const m = String(mode || 'weekday_weekend'); + const opts = []; + if (m === 'weekday_weekend') { + opts.push({ v: 'weekdays', l: this._t('week.group.weekdays') || 'Weekdays' }); + opts.push({ v: 'weekend', l: this._t('week.group.weekend') || 'Weekend' }); + } else if (m === 'weekday_sat_sun') { + opts.push({ v: 'weekdays', l: this._t('week.group.weekdays') || 'Weekdays' }); + opts.push({ v: 'sat', l: this._t('week.days_full.sat') || 'Saturday' }); + opts.push({ v: 'sun', l: this._t('week.days_full.sun') || 'Sunday' }); + } else { + opts.push({ v: 'mon', l: this._t('week.days_full.mon') || 'Monday' }); + opts.push({ v: 'tue', l: this._t('week.days_full.tue') || 'Tuesday' }); + opts.push({ v: 'wed', l: this._t('week.days_full.wed') || 'Wednesday' }); + opts.push({ v: 'thu', l: this._t('week.days_full.thu') || 'Thursday' }); + opts.push({ v: 'fri', l: this._t('week.days_full.fri') || 'Friday' }); + opts.push({ v: 'sat', l: this._t('week.days_full.sat') || 'Saturday' }); + opts.push({ v: 'sun', l: this._t('week.days_full.sun') || 'Sunday' }); + } + targetSel.innerHTML = ''; + for (const o of opts) { + const opt = document.createElement('option'); + opt.value = o.v; opt.textContent = o.l; targetSel.append(opt); + } + const allowed = new Set(opts.map(o=>o.v)); + let pick = String(preferred || '').toLowerCase(); + if (!allowed.has(pick)) { + if (allowed.has('weekdays')) pick = 'weekdays'; + else pick = opts[0]?.v || ''; + } + if (pick) targetSel.value = pick; + }; + + if (sel) { + const opt1 = sel.querySelector('.opt-weekday-weekend'); + const opt2 = sel.querySelector('.opt-weekday-sat-sun'); + const opt3 = sel.querySelector('.opt-all-7'); + if (opt1) opt1.textContent = this._t('week.mode.weekday_weekend') || 'Weekdays + Weekend'; + if (opt2) opt2.textContent = this._t('week.mode.weekday_sat_sun') || 'Weekdays + Saturday + Sunday'; + if (opt3) opt3.textContent = this._t('week.mode.all_7') || 'All days'; + const defMode = String(this._config?.weekdays_mode || 'weekday_weekend'); + sel.value = (defMode === 'weekday_sat_sun' || defMode === 'all_7') ? defMode : 'weekday_weekend'; + try { renderTargets(sel.value, this._settingsDraft?.weekdays_copy_from_main_target || ''); } catch {} + sel.onchange = ()=>{ try { renderTargets(sel.value, targetSel?.value || ''); } catch {} }; + } else { + try { renderTargets('weekday_weekend', this._settingsDraft?.weekdays_copy_from_main_target || ''); } catch {} + } + } catch {} + } + + _applyWeekCopyModal(doCopy){ + try { + if (this._settingsDraft) { + if (doCopy) { + const sel = this.shadowRoot && this.shadowRoot.querySelector('.weekcopy-mode'); + const tsel = this.shadowRoot && this.shadowRoot.querySelector('.weekcopy-target'); + const v = String(sel?.value || 'weekday_weekend'); + this._settingsDraft.weekdays_copy_from_main_mode = (v === 'weekday_sat_sun' || v === 'all_7') ? v : 'weekday_weekend'; + this._settingsDraft.weekdays_copy_from_main_target = String(tsel?.value || '').toLowerCase(); + } else { + this._settingsDraft.weekdays_copy_from_main_mode = ''; + this._settingsDraft.weekdays_copy_from_main_target = ''; + } + } + this._closeWeekCopyModal(); + try { this._renderSettingsPopupWeekdaysTab(); } catch {} + } catch { try { this._closeWeekCopyModal(); } catch {} } + } + + _renderSettingsPopupTab(){ + try { + const active = String(this._settingsPopupTab || 'settings'); + const pages = this.shadowRoot ? Array.from(this.shadowRoot.querySelectorAll('.overlay-settings .settings-page')) : []; + for (const p of pages){ + const isActive = (p.getAttribute('data-page') === active); + try { p.classList.toggle('active', isActive); } catch {} + // Some tab renderers apply inline display styles (e.g. grid). Clear those on inactive tabs + // so CSS `.settings-page{display:none}` can hide them properly. + if (!isActive) { + try { p.style.display = ''; } catch {} + } + } + if (active === 'settings') { + this._renderSettingsPopupSettingsTab(); + } else if (active === 'pause') { + this._renderSettingsPopupPauseTab(); + } else if (active === 'weekdays') { + this._renderSettingsPopupWeekdaysTab(); + } else if (active === 'rooms') { + this._renderSettingsPopupRoomsTab(); + } else if (active === 'boiler') { + this._renderSettingsPopupBoilerTab(); + } else if (active === 'colors') { + this._renderSettingsPopupColorsTab(); + } else if (active === 'presence_sensor') { + this._renderSettingsPopupPresenceSensorTab(); + } else if (active === 'owd') { + this._renderSettingsPopupOwdTab(); + } else if (active === 'holidays') { + this._renderSettingsPopupHolidaysTab(); + } else if (active === 'away') { + this._renderSettingsPopupAwayTab(); + } else if (active === 'reset') { + this._renderSettingsPopupResetTab(); + } + // Some pages (like Weekdays) are built dynamically; re-apply i18n after rendering. + try { this._applyCardI18n(); } catch {} + } catch {} + } + + _initSettingsPopupDraft(){ + try { + try { this._hydrateInstanceCfgFromLocal(); } catch {} + // Start with current config + const cfg = { ...(this._config || {}) }; + // On initial open after a hard refresh, backend settings may not be loaded yet. + // Prefer last localStorage copy for boiler settings so the toggle reflects saved state. + try { + const raw = localStorage.getItem(this._localStoreKey()) || ''; + if (raw) { + const parsed = JSON.parse(raw); + const ls = parsed?.settings || {}; + const has = (obj,k)=> Object.prototype.hasOwnProperty.call(obj||{}, k); + if (!this._yamlProvided?.presence_live_header && has(ls, 'presence_live_header')) cfg.presence_live_header = !!ls.presence_live_header; + if (!this._yamlProvided?.boiler_enabled && has(ls, 'boiler_enabled')) cfg.boiler_enabled = !!ls.boiler_enabled; + if (!this._yamlProvided?.boiler_switch && has(ls, 'boiler_switch')) cfg.boiler_switch = String(ls.boiler_switch || ''); + if (!this._yamlProvided?.boiler_rooms && has(ls, 'boiler_rooms')) cfg.boiler_rooms = Array.isArray(ls.boiler_rooms) ? ls.boiler_rooms.filter(Boolean).map(String) : (ls.boiler_rooms === null ? null : cfg.boiler_rooms); + if (!this._yamlProvided?.boiler_on_offset && has(ls, 'boiler_on_offset')) { const v = (ls.boiler_on_offset === null) ? 0 : Number(ls.boiler_on_offset); if (Number.isFinite(v)) cfg.boiler_on_offset = Math.max(-10, Math.min(10, v)); } + if (!this._yamlProvided?.boiler_off_offset && has(ls, 'boiler_off_offset')) { const v = (ls.boiler_off_offset === null) ? 0 : Number(ls.boiler_off_offset); if (Number.isFinite(v)) cfg.boiler_off_offset = Math.max(-10, Math.min(10, v)); } + if (!this._yamlProvided?.boiler_temp_sensor && has(ls, 'boiler_temp_sensor')) cfg.boiler_temp_sensor = String(ls.boiler_temp_sensor || ''); + if (!this._yamlProvided?.boiler_min_temp && has(ls, 'boiler_min_temp')) { const v = (ls.boiler_min_temp === null) ? 20 : Number(ls.boiler_min_temp); if (Number.isFinite(v)) cfg.boiler_min_temp = v; } + if (!this._yamlProvided?.boiler_max_temp && has(ls, 'boiler_max_temp')) { const v = (ls.boiler_max_temp === null) ? 25 : Number(ls.boiler_max_temp); if (Number.isFinite(v)) cfg.boiler_max_temp = v; } + } + } catch {} + const ow = (cfg.open_window && typeof cfg.open_window === 'object') ? cfg.open_window : {}; + const owEnabled = (typeof ow.enabled === 'boolean') ? ow.enabled : false; + const owSensors = (ow.sensors && typeof ow.sensors === 'object') ? ow.sensors : {}; + const owOpenDelay = Number.isFinite(Number(ow.open_delay_min)) ? Math.max(0, Math.min(1440, Math.round(Number(ow.open_delay_min)))) : 2; + const owCloseDelay = Number.isFinite(Number(ow.close_delay_min)) ? Math.max(0, Math.min(1440, Math.round(Number(ow.close_delay_min)))) : 5; + let owSensorsClone = {}; + try { + for (const k of Object.keys(owSensors||{})) { + const arr = Array.isArray(owSensors[k]) ? owSensors[k].filter(Boolean).map(String) : []; + if (arr.length) owSensorsClone[String(k)] = Array.from(new Set(arr)); + } + } catch {} + + // Holidays + let holDates = []; + try { + holDates = Array.isArray(cfg.holidays_dates) ? cfg.holidays_dates.filter(Boolean).map(String) : []; + holDates = Array.from(new Set(holDates)).sort(); + } catch {} + + // Away + const awayCfg = (cfg.away && typeof cfg.away === 'object') ? cfg.away : {}; + const awayPersons = Array.isArray(awayCfg.persons) ? awayCfg.persons.filter(Boolean).map(String) : []; + const awayCombos = (awayCfg.combos && typeof awayCfg.combos === 'object') ? awayCfg.combos : {}; + const awayCombosClone = (()=>{ + try { + const out = {}; + for (const k of Object.keys(awayCombos||{})) { + const v = awayCombos[k]; + if (v && typeof v === 'object' && v.enabled) out[String(k)] = { enabled: true }; + } + return out; + } catch { return {}; } + })(); + + this._settingsDraft = { + // Rooms (edited in popup, persisted via store) + entities: (()=>{ + try { return Array.isArray(cfg.entities) ? cfg.entities.map(String) : []; } catch { return []; } + })(), + room_use_input_number: (()=>{ + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.map(String) : []; + const modes = Array.isArray(cfg.room_use_input_number) ? cfg.room_use_input_number.slice() : []; + while (modes.length < ents.length) modes.push(false); + return modes.map(v => !!v).slice(0, ents.length); + } catch { return []; } + })(), + room_use_temp_sensor: (()=>{ + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.map(String) : []; + const modes = Array.isArray(cfg.room_use_temp_sensor) ? cfg.room_use_temp_sensor.slice() : []; + while (modes.length < ents.length) modes.push(false); + return modes.map(v => !!v).slice(0, ents.length); + } catch { return []; } + })(), + labels: (()=>{ try { return (cfg.labels && typeof cfg.labels === 'object') ? JSON.parse(JSON.stringify(cfg.labels)) : {}; } catch { return {}; } })(), + merges: (()=>{ + try { + const src = (cfg.merges && typeof cfg.merges === 'object') ? cfg.merges : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const arr = Array.isArray(src[k]) ? src[k].filter(Boolean).map(String) : []; + if (arr.length) out[String(k)] = Array.from(new Set(arr)); + } + return out; + } catch { return {}; } + })(), + temp_sensors: (()=>{ try { return (cfg.temp_sensors && typeof cfg.temp_sensors === 'object') ? JSON.parse(JSON.stringify(cfg.temp_sensors)) : {}; } catch { return {}; } })(), + turn_on: (()=>{ try { return (cfg.turn_on && typeof cfg.turn_on === 'object') ? JSON.parse(JSON.stringify(cfg.turn_on)) : {}; } catch { return {}; } })(), + + default_temp: this._ttCoerceNum(cfg.default_temp, 20), + row_height: this._ttCoerceNum(cfg.row_height, 64), + min_temp: this._ttCoerceNum(cfg.min_temp, 5), + max_temp: this._ttCoerceNum(cfg.max_temp, 25), + auto_apply: !!cfg.auto_apply, + apply_on_edit: !!cfg.apply_on_edit, + apply_on_default_change: !!cfg.apply_on_default_change, + per_room_defaults: !!cfg.per_room_defaults, + show_pause_button: cfg.show_pause_button !== false, + show_room_temp: cfg.show_room_temp !== false, + pause_sensor_enabled: !!cfg.pause_sensor_enabled, + pause_sensor_entity: String(cfg.pause_sensor_entity || ''), + time_12h: !!cfg.time_12h, + time_source: (cfg.time_source === 'ha') ? 'ha' : 'browser', + temp_unit: (String(cfg.temp_unit || 'C').toUpperCase() === 'F') ? 'F' : 'C', + profiles_enabled: !!cfg.profiles_enabled, + + presence_sensor_enabled: !!cfg.presence_sensor_enabled, + + presence_sensors: (()=>{ try { return (cfg.presence_sensors && typeof cfg.presence_sensors === 'object') ? JSON.parse(JSON.stringify(cfg.presence_sensors)) : {}; } catch { return {}; } })(), + + presence_sensor_temps: (()=>{ try { return (cfg.presence_sensor_temps && typeof cfg.presence_sensor_temps === 'object') ? JSON.parse(JSON.stringify(cfg.presence_sensor_temps)) : {}; } catch { return {}; } })(), + + presence_sensor_delays: (()=>{ try { return (cfg.presence_sensor_delays && typeof cfg.presence_sensor_delays === 'object') ? JSON.parse(JSON.stringify(cfg.presence_sensor_delays)) : {}; } catch { return {}; } })(), + + presence_sensor_delay_units: (()=>{ try { return (cfg.presence_sensor_delay_units && typeof cfg.presence_sensor_delay_units === 'object') ? JSON.parse(JSON.stringify(cfg.presence_sensor_delay_units)) : {}; } catch { return {}; } })(), + + weekdays_enabled: !!cfg.weekdays_enabled, + weekdays_copy_from_main_mode: '', + weekdays_copy_from_main_target: '', + seasonal_enabled: !!cfg.seasonal_enabled, + seasonal_mode: (String(cfg.seasonal_mode || 'winter') === 'summer') ? 'summer' : 'winter', + weekdays_view: String(cfg.weekdays_view || 'all_rooms_one_day'), + weekdays_view_switch_in_timeline: !!(cfg.weekdays_view_switch_in_timeline ?? false), + weekdays_selected_room: String(cfg.weekdays_selected_room || ''), + + boiler_enabled: !!cfg.boiler_enabled, + boiler_switch: String(cfg.boiler_switch || ''), + boiler_switch_domain: (()=>{ + try { + const sw = String(cfg.boiler_switch || ''); + const fromEid = (()=>{ + const dom = String(sw||'').split('.')[0]; + return (dom === 'input_boolean') ? 'input_boolean' : 'switch'; + })(); + const raw = String((cfg.boiler_switch_domain ?? fromEid) || '').toLowerCase().trim(); + return (raw === 'input_boolean') ? 'input_boolean' : 'switch'; + } catch { return 'switch'; } + })(), + boiler_rooms: (()=>{ + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.map(String) : []; + const climateRooms = ents.filter(e=>String(e).startsWith('climate.')); + const raw = cfg.boiler_rooms; + if (Array.isArray(raw)) { + const want = new Set(raw.filter(Boolean).map(String)); + return climateRooms.filter(eid => want.has(eid)); + } + return climateRooms; + } catch { return []; } + })(), + boiler_on_offset: (()=>{ try { const v = Number(cfg.boiler_on_offset); return Number.isFinite(v) ? v : 0; } catch { return 0; } })(), + boiler_off_offset: (()=>{ try { const v = Number(cfg.boiler_off_offset); return Number.isFinite(v) ? v : 0; } catch { return 0; } })(), + boiler_temp_sensor: String(cfg.boiler_temp_sensor || ''), + boiler_min_temp: this._ttCoerceNum(cfg.boiler_min_temp, 20), + boiler_max_temp: this._ttCoerceNum(cfg.boiler_max_temp, 25), + boiler_multi_enabled: !!cfg.boiler_multi_enabled, + boiler_room_settings: (()=>{ try { return (cfg.boiler_room_settings && typeof cfg.boiler_room_settings === 'object') ? JSON.parse(JSON.stringify(cfg.boiler_room_settings)) : {}; } catch { return {}; } })(), + + color_ranges: (()=>{ + try { + const src = (cfg.color_ranges && typeof cfg.color_ranges === 'object') ? cfg.color_ranges : {}; + const out = {}; + for (const k of Object.keys(src||{})) { + const arr = Array.isArray(src[k]) ? src[k] : []; + out[String(k)] = arr.map(r => ({ + from: Number.isFinite(Number(r?.from)) ? Number(r.from) : (this._ttCoerceNum(cfg.default_temp, 20)), + to: Number.isFinite(Number(r?.to)) ? Number(r.to) : (this._ttCoerceNum(cfg.default_temp, 20)), + color: String(r?.color || '#ffb347') + })); + } + return out; + } catch { return {}; } + })(), + color_global: !!cfg.color_global, + + open_window: { + enabled: !!owEnabled, + sensors: owSensorsClone, + open_delay_min: owOpenDelay, + close_delay_min: owCloseDelay, + }, + + holidays_enabled: !!cfg.holidays_enabled, + holidays_source: (cfg.holidays_source === 'manual') ? 'manual' : 'calendar', + holidays_entity: String(cfg.holidays_entity || ''), + holidays_dates: holDates, + holidays_groups: (()=>{ try { return Array.isArray(cfg.holidays_groups) ? JSON.parse(JSON.stringify(cfg.holidays_groups)) : []; } catch { return []; } })(), + + away: { + enabled: !!awayCfg.enabled, + target_c: this._ttCoerceNum(awayCfg.target_c, 17), + persons: Array.from(new Set(awayPersons)), + advanced_enabled: !!awayCfg.advanced_enabled, + combos: awayCombosClone, + }, + presence_live_header: !!(cfg.presence_live_header ?? true), + // Away delay (defaults) + away_delay_enabled: !!(awayCfg.delay_enabled ?? false), + away_delay_value: Number.isFinite(Number(awayCfg.delay_value)) ? Number(awayCfg.delay_value) : 0, + away_delay_unit: String(awayCfg.delay_unit || 'minutes'), + }; + } catch { this._settingsDraft = null; } + } + + _applySettingsPopupDraftToConfig(){ + try { + const d = this._settingsDraft; + if (!d) return; + const cfg = { ...(this._config || {}) }; + const prevSeasonalEnabled = !!this._config?.seasonal_enabled; + const prevSeasonalMode = (String(this._config?.seasonal_mode || 'winter') === 'summer') ? 'summer' : 'winter'; + const prevWeekEnabled = !!this._config?.weekdays_enabled; + + // Rooms settings (entities + per-room mode + merges/labels) + try { + const rawEnts = Array.isArray(d.entities) ? d.entities.map(x => String(x||'').trim()) : []; + const ents = rawEnts.filter(Boolean); + const rawModes = Array.isArray(d.room_use_input_number) ? d.room_use_input_number.slice() : []; + while (rawModes.length < rawEnts.length) rawModes.push(false); + const modes = ents.map((eid, idx) => { + // If the entity domain is explicit, prefer it to avoid inconsistent mode state + if (String(eid).startsWith('input_number.')) return true; + if (String(eid).startsWith('climate.')) return false; + // Otherwise, take draft mode + return !!rawModes[idx]; + }); + cfg.entities = ents; + cfg.room_use_input_number = modes; + + // Per-room: use temperature sensor for room temp + try { + const rawTemp = Array.isArray(d.room_use_temp_sensor) ? d.room_use_temp_sensor.slice() : []; + while (rawTemp.length < rawEnts.length) rawTemp.push(false); + cfg.room_use_temp_sensor = ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return false; + if (!String(eid).startsWith('climate.')) return false; + return !!rawTemp[idx]; + }); + } catch { cfg.room_use_temp_sensor = []; } + + // Labels: keep only for active rooms and only non-empty strings + try { + const src = (d.labels && typeof d.labels === 'object') ? d.labels : {}; + const out = {}; + for (const eid of ents) { + const v = (src && Object.prototype.hasOwnProperty.call(src, eid)) ? String(src[eid]||'').trim() : ''; + if (v) out[eid] = v; + } + cfg.labels = out; + } catch {} + + // Merges: only keep mappings for active primaries, only climate rooms, and only linked climate.* entities + try { + const src = (d.merges && typeof d.merges === 'object') ? d.merges : {}; + const out = {}; + const primSet = new Set(ents); + for (let i=0;i String(x).startsWith('climate.') && !primSet.has(String(x))))); + if (cleaned.length) out[primary] = cleaned; + } + cfg.merges = out; + } catch {} + + // Temp sensors: keep only for active rooms + try { + const src = (d.temp_sensors && typeof d.temp_sensors === 'object') ? d.temp_sensors : {}; + const out = {}; + for (const eid of ents) { + const v = (src && Object.prototype.hasOwnProperty.call(src, eid)) ? String(src[eid]||'').trim() : ''; + if (v) out[eid] = v; + } + cfg.temp_sensors = out; + } catch {} + + // Per-room turn_on behavior: keep only for active climate rooms + try { + const src = (d.turn_on && typeof d.turn_on === 'object') ? d.turn_on : {}; + const out = {}; + for (let i=0;i cfg.max_temp) { + const tmp = cfg.min_temp; + cfg.min_temp = cfg.max_temp; + cfg.max_temp = tmp; + } + + cfg.auto_apply = !!d.auto_apply; + cfg.apply_on_edit = !!d.apply_on_edit; + cfg.apply_on_default_change = !!d.apply_on_default_change; + cfg.per_room_defaults = !!d.per_room_defaults; + cfg.show_pause_button = !!d.show_pause_button; + cfg.show_room_temp = !!d.show_room_temp; + cfg.pause_sensor_enabled = !!d.pause_sensor_enabled; + cfg.pause_sensor_entity = String(d.pause_sensor_entity || ''); + + cfg.time_12h = !!d.time_12h; + cfg.time_source = (d.time_source === 'ha') ? 'ha' : 'browser'; + cfg.temp_unit = (String(d.temp_unit || 'C').toUpperCase() === 'F') ? 'F' : 'C'; + cfg.profiles_enabled = !!d.profiles_enabled; + cfg.seasonal_enabled = !!d.seasonal_enabled; + cfg.seasonal_mode = (String(d.seasonal_mode || prevSeasonalMode || 'winter') === 'summer') ? 'summer' : 'winter'; + + cfg.presence_sensor_enabled = !!d.presence_sensor_enabled; + + // Presence sensors: keep only for active rooms + only binary_sensor.* + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.filter(Boolean).map(String) : []; + const src = (d.presence_sensors && typeof d.presence_sensors === 'object') ? d.presence_sensors : {}; + const out = {}; + for (const eid of ents) { + const v = (src && Object.prototype.hasOwnProperty.call(src, eid)) ? String(src[eid]||'').trim() : ''; + if (v && String(v).startsWith('binary_sensor.')) out[eid] = v; + } + cfg.presence_sensors = out; + } catch {} + + // Presence sensor temps (°C): keep only for active rooms + only numeric + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.filter(Boolean).map(String) : []; + const src = (d.presence_sensor_temps && typeof d.presence_sensor_temps === 'object') ? d.presence_sensor_temps : {}; + const out = {}; + const minC = Number(cfg.min_temp); + const maxC = Number(cfg.max_temp); + for (const eid of ents) { + const v0 = (src && Object.prototype.hasOwnProperty.call(src, eid)) ? Number(src[eid]) : NaN; + if (!Number.isFinite(v0)) continue; + let v = v0; + if (Number.isFinite(minC)) v = Math.max(minC, v); + if (Number.isFinite(maxC)) v = Math.min(maxC, v); + out[eid] = v; + } + cfg.presence_sensor_temps = out; + } catch {} + + // Presence sensor delays (seconds): keep only for active rooms + only numeric >= 0 + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.filter(Boolean).map(String) : []; + const src = (d.presence_sensor_delays && typeof d.presence_sensor_delays === 'object') ? d.presence_sensor_delays : {}; + const out = {}; + for (const eid of ents) { + const v = src?.[eid]; + if (!v || typeof v !== 'object') continue; + const onS = Number(v.on_s); + const offS = Number(v.off_s); + const obj = {}; + if (Number.isFinite(onS) && onS >= 0) obj.on_s = onS; + if (Number.isFinite(offS) && offS >= 0) obj.off_s = offS; + if (Object.keys(obj).length) out[eid] = obj; + } + cfg.presence_sensor_delays = out; + } catch {} + + // Presence sensor delay units: keep only for active rooms + only 'minutes'|'seconds' + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.filter(Boolean).map(String) : []; + const src = (d.presence_sensor_delay_units && typeof d.presence_sensor_delay_units === 'object') ? d.presence_sensor_delay_units : {}; + const out = {}; + for (const eid of ents) { + const u = (src && Object.prototype.hasOwnProperty.call(src, eid)) ? String(src[eid]||'').toLowerCase().trim() : ''; + out[eid] = (u === 'seconds') ? 'seconds' : 'minutes'; + } + cfg.presence_sensor_delay_units = out; + } catch {} + + cfg.weekdays_enabled = !!d.weekdays_enabled; + cfg.weekdays_view = (String(d.weekdays_view || 'all_rooms_one_day') === 'one_room_all_days') ? 'one_room_all_days' : 'all_rooms_one_day'; + cfg.weekdays_view_switch_in_timeline = !!d.weekdays_view_switch_in_timeline; + // If per-room view, ensure we have a selected room + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.filter(Boolean) : []; + const want = String(d.weekdays_selected_room || ''); + if (cfg.weekdays_view === 'one_room_all_days') { + cfg.weekdays_selected_room = (want && ents.includes(want)) ? want : (ents[0] || ''); + } else { + cfg.weekdays_selected_room = ''; + } + } catch {} + + // Optional: copy main schedule into weekdays when enabling + try { + const copyMode = String(d.weekdays_copy_from_main_mode || '').trim(); + const copyTarget = String(d.weekdays_copy_from_main_target || '').trim(); + if (!prevWeekEnabled && cfg.weekdays_enabled && copyMode) { + const m = (copyMode === 'weekday_sat_sun' || copyMode === 'all_7') ? copyMode : 'weekday_weekend'; + cfg.weekdays_mode = m; + this._copyMainToWeekdays(m, copyTarget); + } + } catch {} + + // Boiler settings + cfg.boiler_enabled = !!d.boiler_enabled; + cfg.boiler_switch = String(d.boiler_switch || ''); + try { + const raw = String(d.boiler_switch_domain || '').toLowerCase().trim(); + const rawDom = (raw === 'input_boolean') ? 'input_boolean' : 'switch'; + const swDom = String(cfg.boiler_switch || '').split('.')[0]; + if (cfg.boiler_switch && (swDom === 'switch' || swDom === 'input_boolean')) { + cfg.boiler_switch_domain = swDom; + } else { + cfg.boiler_switch_domain = rawDom; + } + } catch { cfg.boiler_switch_domain = 'switch'; } + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.filter(Boolean).map(String) : []; + const climateRooms = ents.filter(e=>String(e).startsWith('climate.')); + const wantRaw = Array.isArray(d.boiler_rooms) ? d.boiler_rooms.filter(Boolean).map(String) : climateRooms; + const wantSet = new Set(wantRaw); + cfg.boiler_rooms = climateRooms.filter(eid => wantSet.has(eid)); + } catch { cfg.boiler_rooms = []; } + const clampOffset = (n, fallback)=>{ + const v = Number(n); + if (!Number.isFinite(v)) return fallback; + return Math.max(-10, Math.min(10, v)); + }; + cfg.boiler_on_offset = clampOffset(d.boiler_on_offset, (cfg.boiler_on_offset ?? 0)); + cfg.boiler_off_offset = clampOffset(d.boiler_off_offset, (cfg.boiler_off_offset ?? 0)); + cfg.boiler_temp_sensor = String(d.boiler_temp_sensor || ''); + const clampBoiler = (n, fallback)=>{ + const v = Number(n); + if (!Number.isFinite(v)) return fallback; + return Math.max(0, Math.min(120, v)); + }; + cfg.boiler_min_temp = clampBoiler(d.boiler_min_temp, cfg.boiler_min_temp); + cfg.boiler_max_temp = clampBoiler(d.boiler_max_temp, cfg.boiler_max_temp); + // Normalize min/max if user swapped them + if (Number.isFinite(Number(cfg.boiler_min_temp)) && Number.isFinite(Number(cfg.boiler_max_temp)) && Number(cfg.boiler_min_temp) > Number(cfg.boiler_max_temp)) { + const tmpB = cfg.boiler_min_temp; + cfg.boiler_min_temp = cfg.boiler_max_temp; + cfg.boiler_max_temp = tmpB; + } + + // Multi-boiler per-room settings + cfg.boiler_multi_enabled = !!d.boiler_multi_enabled; + try { + const ents = Array.isArray(cfg.entities) ? cfg.entities.filter(Boolean).map(String) : []; + const climateRooms = ents.filter(e=>String(e).startsWith('climate.')); + const src = (d.boiler_room_settings && typeof d.boiler_room_settings === 'object') ? d.boiler_room_settings : {}; + const out = {}; + const clampOffsetLocal = (n, fallback)=>{ + const v = Number(n); + if (!Number.isFinite(v)) return fallback; + return Math.max(-10, Math.min(10, v)); + }; + for (const eid of climateRooms) { + const raw = src?.[eid]; + if (!raw || typeof raw !== 'object') continue; + const enabled = !!raw.enabled; + const sw = String(raw.switch || '').trim(); + const domFromEid = (sw ? String(sw).split('.')[0] : ''); + const rawDom = String(raw.switch_domain || '').toLowerCase().trim(); + const dom = (domFromEid === 'input_boolean' || domFromEid === 'switch') + ? domFromEid + : ((rawDom === 'input_boolean') ? 'input_boolean' : 'switch'); + const onOff = clampOffsetLocal(raw.on_offset, this._ttCoerceNum(cfg.boiler_on_offset, 0)); + const offOff = clampOffsetLocal(raw.off_offset, this._ttCoerceNum(cfg.boiler_off_offset, 0)); + const hasAny = enabled || sw || Number.isFinite(Number(raw.on_offset)) || Number.isFinite(Number(raw.off_offset)) || !!raw.switch_domain; + if (hasAny) out[eid] = { enabled, switch: sw, switch_domain: dom, on_offset: onOff, off_offset: offOff }; + } + cfg.boiler_room_settings = out; + } catch { cfg.boiler_room_settings = {}; } + + // Colors settings + try { + cfg.color_global = !!d.color_global; + const src = (d.color_ranges && typeof d.color_ranges === 'object') ? d.color_ranges : {}; + const out = {}; + const minC = Number.isFinite(Number(cfg.min_temp)) ? Number(cfg.min_temp) : 5; + const maxC = Number.isFinite(Number(cfg.max_temp)) ? Number(cfg.max_temp) : 25; + const clampC = (n, fallback)=>{ + const v = Number(n); + if (!Number.isFinite(v)) return fallback; + return Math.max(minC, Math.min(maxC, v)); + }; + for (const k of Object.keys(src||{})) { + const arr = Array.isArray(src[k]) ? src[k] : []; + const cleaned = []; + for (const r of arr) { + const def = Number.isFinite(Number(cfg.default_temp)) ? Number(cfg.default_temp) : 20; + const from = clampC(r?.from, def); + const to = clampC(r?.to, def); + const color = String(r?.color || '#ffb347'); + cleaned.push({ from, to, color }); + } + out[String(k)] = cleaned; + } + cfg.color_ranges = out; + } catch {} + + // Open Window Detection settings + try { + const ow = (d.open_window && typeof d.open_window === 'object') ? d.open_window : {}; + const out = {}; + out.enabled = !!ow.enabled; + out.open_delay_min = Number.isFinite(Number(ow.open_delay_min)) ? Math.max(0, Math.min(1440, Math.round(Number(ow.open_delay_min)))) : 2; + out.close_delay_min = Number.isFinite(Number(ow.close_delay_min)) ? Math.max(0, Math.min(1440, Math.round(Number(ow.close_delay_min)))) : 5; + out.sensors = {}; + const sMap = (ow.sensors && typeof ow.sensors === 'object') ? ow.sensors : {}; + for (const k of Object.keys(sMap||{})) { + const arr = Array.isArray(sMap[k]) ? sMap[k].filter(Boolean).map(String) : []; + if (arr.length) out.sensors[String(k)] = Array.from(new Set(arr)); + } + cfg.open_window = out; + } catch {} + + // Holidays settings + try { + cfg.holidays_enabled = !!d.holidays_enabled; + cfg.holidays_source = (d.holidays_source === 'manual') ? 'manual' : 'calendar'; + cfg.holidays_entity = String(d.holidays_entity || ''); + let dates = []; + try { + dates = Array.isArray(d.holidays_dates) ? d.holidays_dates.filter(Boolean).map(String) : []; + // Keep only YYYY-MM-DD (simple/strict) + dates = dates.filter(x => /^\d{4}-\d{2}-\d{2}$/.test(String(x))); + dates = Array.from(new Set(dates)).sort(); + } catch {} + cfg.holidays_dates = dates; + try { + const groups = Array.isArray(d.holidays_groups) ? d.holidays_groups : []; + const cleaned = groups.map(g=>({ + id: String(g?.id || ('hg_'+Date.now())), + from: String(g?.from||''), + to: String(g?.to||''), + dates: Array.isArray(g?.dates) ? Array.from(new Set(g.dates.filter(Boolean).map(String))).sort() : [] + })); + cfg.holidays_groups = cleaned; + } catch {} + } catch {} + + // Away settings + try { + const __prevAway = JSON.stringify(cfg.away || {}); + const __prevLive = !!(cfg.presence_live_header ?? true); + const src = (d.away && typeof d.away === 'object') ? d.away : {}; + const cur = { ...(cfg.away || {}) }; + cur.enabled = !!src.enabled; + + // Clamp target C to min/max from draft (stored in C) + const mn = Number.isFinite(Number(d.min_temp)) ? Number(d.min_temp) : (Number.isFinite(Number(cfg.min_temp)) ? Number(cfg.min_temp) : 5); + const mx = Number.isFinite(Number(d.max_temp)) ? Number(d.max_temp) : (Number.isFinite(Number(cfg.max_temp)) ? Number(cfg.max_temp) : 25); + const tC = Number(src.target_c); + const safeTC = Number.isFinite(tC) ? Math.max(mn, Math.min(mx, tC)) : (Number.isFinite(Number(cur.target_c)) ? Number(cur.target_c) : 17); + cur.target_c = safeTC; + + const persons = Array.isArray(src.persons) ? src.persons.filter(Boolean).map(String) : []; + cur.persons = Array.from(new Set(persons)); + cur.advanced_enabled = !!src.advanced_enabled; + // Delay fields from draft + try { + cur.delay_enabled = !!(d.away_delay_enabled ?? cur.delay_enabled ?? false); + const v = Number(d.away_delay_value); + cur.delay_value = Number.isFinite(v) ? Math.max(0, Math.round(v)) : (Number.isFinite(cur.delay_value) ? cur.delay_value : 0); + const u = String(d.away_delay_unit || cur.delay_unit || 'minutes'); + cur.delay_unit = (u === 'seconds') ? 'seconds' : 'minutes'; + } catch {} + + // Normalize combos (only keep enabled entries) + const combosSrc = (src.combos && typeof src.combos === 'object') ? src.combos : {}; + const combosOut = {}; + for (const k of Object.keys(combosSrc||{})) { + const v = combosSrc[k]; + if (v && typeof v === 'object' && v.enabled) combosOut[String(k)] = { enabled: true }; + } + cur.combos = combosOut; + + // If Away is turned OFF, disable advanced mode only. + // Keep live-header preference persisted; visibility is gated at runtime. + if (!cur.enabled) { + cur.advanced_enabled = false; + } + const live = !!(d.presence_live_header ?? true); + cfg.presence_live_header = live; + + cfg.away = cur; + try { + const __nextAway = JSON.stringify(cfg.away || {}); + const __nextLive = !!(cfg.presence_live_header ?? true); + if (__prevAway !== __nextAway || __prevLive !== __nextLive) this._awayTouched = true; + } catch {} + } catch {} + + this._config = cfg; + try { + if (cfg.seasonal_enabled) { + if (!prevSeasonalEnabled) { + this._prepareSeasonalRowsOnEnable(); + } + this._applySeasonalModeToRows(true, prevSeasonalMode); + } else if (prevSeasonalEnabled) { + this._seasonAppliedKey = null; + } + } catch {} + } catch {} + } + + _renderSettingsPopupRoomsTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="rooms"]'); + if (!page) return; + + // Build base markup once + if (!page.dataset.built) { + page.dataset.built = '1'; + page.innerHTML = ` +
    +
    +
    +
    +
    +
    + +
    +
    +
    + `; + } + + // Ensure draft shape + if (!Array.isArray(d.entities)) d.entities = []; + if (!Array.isArray(d.room_use_input_number)) d.room_use_input_number = []; + while (d.room_use_input_number.length < d.entities.length) d.room_use_input_number.push(false); + if (!Array.isArray(d.room_use_temp_sensor)) d.room_use_temp_sensor = []; + while (d.room_use_temp_sensor.length < d.entities.length) d.room_use_temp_sensor.push(false); + if (!d.labels || typeof d.labels !== 'object') d.labels = {}; + if (!d.merges || typeof d.merges !== 'object') d.merges = {}; + if (!d.temp_sensors || typeof d.temp_sensors !== 'object') d.temp_sensors = {}; + try { if (!this._openPopupRooms) this._openPopupRooms = new Set(); } catch {} + + const q = (sel)=> this.shadowRoot.querySelector(sel); + const list = q('.overlay-settings .sp-rooms-list'); + if (!list) return; + list.innerHTML = ''; + + const ents = d.entities.map(x => String(x||'')); + const usedPrimary = new Set(ents.filter(Boolean)); + const usedLinked = new Set(Object.values(d.merges||{}).flat().filter(Boolean).map(String)); + + const titleEl = q('.overlay-settings .sp-rooms-title'); + if (titleEl) titleEl.textContent = this._t('editor.tabs.rooms') || 'Rooms'; + const descEl = q('.overlay-settings .sp-rooms-desc'); + if (descEl) descEl.textContent = this._t('editor.select_entities') || 'Select rooms'; + const addBtn = q('.overlay-settings .sp-rooms-add'); + if (addBtn) addBtn.textContent = this._t('editor.add_entity') || 'Add room'; + + const getRowKey = (idx)=>{ + const eid = ents[idx] || ''; + return eid ? String(eid) : `#idx:${idx}`; + }; + + const pretty = (eid)=>{ + try { + const lbl = d.labels?.[eid]; + if (lbl) return String(lbl); + } catch {} + return this._prettyName(eid); + }; + + const mkFoldout = (idx)=>{ + const line = document.createElement('div'); + line.className = 'inline'; + + const summary = document.createElement('div'); + summary.className = 'summary'; + const sumLeft = document.createElement('div'); + sumLeft.className = 'summary-left'; + const exp = document.createElement('button'); + exp.type = 'button'; + exp.className = 'expander'; + exp.innerHTML = ''; + + const txtWrap = document.createElement('div'); + txtWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); + titleSpan.className = 'summary-title'; + const subSpan = document.createElement('span'); + subSpan.className = 'summary-sub'; + + const actions = document.createElement('div'); + actions.className = 'actions'; + const handle = document.createElement('button'); + handle.type = 'button'; + handle.className = 'drag-handle'; + try { handle.setAttribute('title', this._t('editor.drag_reorder') || 'Drag to reorder'); } catch {} + handle.innerHTML = ''; + const del = document.createElement('button'); + del.type = 'button'; + del.className = 'remove-btn'; + del.innerHTML = '' + (this._t('editor.remove') || 'Remove') + ''; + actions.append(handle, del); + + txtWrap.append(titleSpan, subSpan); + sumLeft.append(exp, txtWrap); + summary.append(sumLeft, actions); + line.append(summary); + + const details = document.createElement('div'); + details.className = 'details'; + line.append(details); + + // Inline styles (keep consistent with other popup foldouts) + try { + summary.style.display = 'flex'; + summary.style.alignItems = 'center'; + summary.style.justifyContent = 'space-between'; + summary.style.padding = '6px'; + summary.style.border = '1px dashed var(--divider-color)'; + summary.style.borderRadius = '8px'; + summary.style.background = 'var(--secondary-background-color, transparent)'; + sumLeft.style.display = 'flex'; + sumLeft.style.alignItems = 'center'; + sumLeft.style.gap = '8px'; + sumLeft.style.minWidth = '0'; + txtWrap.style.display = 'grid'; + txtWrap.style.gap = '2px'; + txtWrap.style.minWidth = '0'; + titleSpan.style.fontWeight = '600'; + titleSpan.style.overflow = 'hidden'; + titleSpan.style.textOverflow = 'ellipsis'; + titleSpan.style.whiteSpace = 'nowrap'; + titleSpan.style.maxWidth = '320px'; + subSpan.style.fontSize = '.85rem'; + subSpan.style.color = 'var(--secondary-text-color)'; + subSpan.style.overflow = 'hidden'; + subSpan.style.textOverflow = 'ellipsis'; + subSpan.style.whiteSpace = 'nowrap'; + subSpan.style.maxWidth = '320px'; + + // Match the Colors foldout expander styling + exp.style.width = '28px'; + exp.style.height = '28px'; + exp.style.borderRadius = '8px'; + exp.style.border = '1px solid var(--divider-color)'; + exp.style.background = 'var(--card-background-color)'; + exp.style.display = 'inline-flex'; + exp.style.alignItems = 'center'; + exp.style.justifyContent = 'center'; + exp.style.cursor = 'pointer'; + } catch {} + try { + details.style.display = 'none'; + details.style.gridTemplateColumns = 'minmax(250px, 520px)'; + details.style.gap = '8px'; + details.style.padding = '8px'; + details.style.border = '1px dashed var(--divider-color)'; + details.style.borderRadius = '8px'; + details.style.background = 'var(--secondary-background-color, transparent)'; + } catch {} + + const rowKey = getRowKey(idx); + const applyOpen = (isOpen)=>{ + try { + if (isOpen) { + line.classList.add('open'); + details.style.display = 'grid'; + exp.style.transform = 'rotate(180deg)'; + } else { + line.classList.remove('open'); + details.style.display = 'none'; + exp.style.transform = ''; + } + } catch {} + }; + const toggle = (ev)=>{ + try { ev?.stopPropagation?.(); } catch {} + const nextOpen = !line.classList.contains('open'); + applyOpen(nextOpen); + try { + if (!this._openPopupRooms) this._openPopupRooms = new Set(); + if (nextOpen) this._openPopupRooms.add(String(rowKey)); + else this._openPopupRooms.delete(String(rowKey)); + } catch {} + }; + exp.addEventListener('click', toggle); + summary.style.cursor = 'pointer'; + summary.addEventListener('click', (ev)=>{ + try { if (ev?.target?.closest?.('button, a, input, select, textarea, tt-entity-picker, ha-entity-picker, ha-switch')) return; } catch {} + toggle(ev); + }); + try { applyOpen(!!this._openPopupRooms?.has(String(rowKey))); } catch {} + + // Build details content + const left = document.createElement('div'); + left.style.display = 'grid'; + left.style.gap = '6px'; + + const useInput = (()=>{ + try { + const eid = String(ents[idx]||''); + if (eid.startsWith('input_number.')) return true; + if (eid.startsWith('climate.')) return false; + return !!d.room_use_input_number?.[idx]; + } catch { return false; } + })(); + + const modeRow = document.createElement('div'); + modeRow.style.display = 'flex'; + modeRow.style.alignItems = 'center'; + modeRow.style.justifyContent = 'space-between'; + modeRow.style.gap = '10px'; + const modeText = document.createElement('div'); + modeText.style.display = 'grid'; + modeText.style.gap = '2px'; + const modeTitle = document.createElement('div'); + modeTitle.style.fontWeight = '600'; + modeTitle.textContent = this._t('editor.room_mode.title') || 'Use input_number instead of climate'; + const modeDesc = document.createElement('div'); + modeDesc.style.fontSize = '.85rem'; + modeDesc.style.color = 'var(--secondary-text-color)'; + modeDesc.textContent = this._t('editor.room_mode.desc') || ''; + modeText.append(modeTitle, modeDesc); + const modeSwitch = document.createElement('ha-switch'); + modeSwitch.className = 'sp-room-mode-switch'; + modeSwitch.checked = !!useInput; + modeSwitch.addEventListener('change', (e)=>{ + try { + const on = !!e.target.checked; + if (!Array.isArray(this._settingsDraft?.room_use_input_number)) this._settingsDraft.room_use_input_number = []; + while (this._settingsDraft.room_use_input_number.length < this._settingsDraft.entities.length) this._settingsDraft.room_use_input_number.push(false); + this._settingsDraft.room_use_input_number[idx] = on; + + // Switching to input_number mode means room temp sensor override is not applicable + try { + if (!Array.isArray(this._settingsDraft?.room_use_temp_sensor)) this._settingsDraft.room_use_temp_sensor = []; + while (this._settingsDraft.room_use_temp_sensor.length < this._settingsDraft.entities.length) this._settingsDraft.room_use_temp_sensor.push(false); + if (on) this._settingsDraft.room_use_temp_sensor[idx] = false; + } catch {} + + const oldEid = String(this._settingsDraft.entities[idx]||'').trim(); + const mismatch = !!oldEid && ((on && !oldEid.startsWith('input_number.')) || (!on && !oldEid.startsWith('climate.'))); + if (mismatch) { + // Clean mappings tied to old primary + try { if (this._settingsDraft.labels && this._settingsDraft.labels[oldEid]) { const next = { ...(this._settingsDraft.labels||{}) }; delete next[oldEid]; this._settingsDraft.labels = next; this._labelsTouched = true; } } catch {} + try { + const merges = { ...(this._settingsDraft.merges||{}) }; + if (merges[oldEid]) delete merges[oldEid]; + for (const k of Object.keys(merges)) { + merges[k] = (merges[k] || []).filter(x => String(x) !== String(oldEid)); + if (!merges[k].length) delete merges[k]; + } + this._settingsDraft.merges = merges; + } catch {} + try { if (this._settingsDraft.temp_sensors && this._settingsDraft.temp_sensors[oldEid]) { const next = { ...(this._settingsDraft.temp_sensors||{}) }; delete next[oldEid]; this._settingsDraft.temp_sensors = next; } } catch {} + try { if (this._settingsDraft.turn_on && this._settingsDraft.turn_on[oldEid]) { const next = { ...(this._settingsDraft.turn_on||{}) }; delete next[oldEid]; this._settingsDraft.turn_on = next; } } catch {} + try { if (this._settingsDraft.presence_sensors && this._settingsDraft.presence_sensors[oldEid]) { const next = { ...(this._settingsDraft.presence_sensors||{}) }; delete next[oldEid]; this._settingsDraft.presence_sensors = next; } } catch {} + this._settingsDraft.entities[idx] = ''; + } + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + modeRow.append(modeText, modeSwitch); + + const pick = document.createElement('tt-entity-picker'); + try { pick.style.display = 'block'; pick.style.minHeight = '48px'; pick.style.maxWidth = '440px'; } catch {} + try { pick.hass = this._hass; } catch {} + pick.setAttribute('include-domains', useInput ? '["input_number"]' : '["climate"]'); + pick.setAttribute('label', this._t('editor.entity_placeholder') || 'Entity'); + try { pick.value = String(ents[idx] || ''); } catch {} + pick.entityFilter = (st) => { + try { + const id = st?.entity_id || st?.entityId || st?.entityID || ''; + if (!id) return false; + const current = String(ents[idx] || ''); + const blocked = new Set([...usedPrimary, ...usedLinked]); + return !blocked.has(id) || id === current; + } catch { return true; } + }; + + const nameLabel = document.createElement('div'); + nameLabel.className = 'label'; + nameLabel.textContent = this._t('editor.display_name_optional') || 'Room name (optional)'; + + const nameInp = document.createElement('input'); + nameInp.type = 'text'; + nameInp.className = 'label-input tt-input tt-input-lg'; + try { nameInp.style.width = '100%'; nameInp.style.maxWidth = '440px'; } catch {} + nameInp.setAttribute('placeholder', this._t('editor.display_name_optional') || 'Room name (optional)'); + try { + const curEid = String(ents[idx] || ''); + nameInp.value = (d.labels && curEid && d.labels[curEid]) ? String(d.labels[curEid]) : ''; + } catch {} + const setDraftLabel = (eid, value)=>{ + try { + if (!eid) return; + const labels = { ...(this._settingsDraft.labels || {}) }; + if (value) labels[eid] = value; + else delete labels[eid]; + this._settingsDraft.labels = labels; + // Mark labels as edited so an empty labels object should clear stored labels + this._labelsTouched = true; + } catch {} + }; + // Important: don't re-render the whole Rooms tab on every keystroke, + // otherwise the input element gets recreated and loses focus. + nameInp.addEventListener('input', (e)=>{ + try { + const eid = String(this._settingsDraft?.entities?.[idx] || '').trim(); + if (!eid) return; + const raw = String(e?.target?.value ?? ''); + setDraftLabel(eid, raw ? raw : ''); + } catch {} + }); + nameInp.addEventListener('change', (e)=>{ + try { + const eid = String(this._settingsDraft?.entities?.[idx] || '').trim(); + if (!eid) return; + const val = String(e?.target?.value ?? '').trim(); + setDraftLabel(eid, val); + try { e.target.value = val; } catch {} + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + + // Per-room: optional turn_on behavior + const turnOnWrap = document.createElement('div'); + turnOnWrap.style.display = 'grid'; + turnOnWrap.style.gap = '6px'; + const turnOnRow = document.createElement('div'); + turnOnRow.style.display = 'flex'; + turnOnRow.style.alignItems = 'center'; + turnOnRow.style.justifyContent = 'space-between'; + turnOnRow.style.gap = '10px'; + const turnOnText = document.createElement('div'); + turnOnText.style.display = 'grid'; + turnOnText.style.gap = '2px'; + const turnOnTitle = document.createElement('div'); + turnOnTitle.style.fontWeight = '600'; + turnOnTitle.textContent = this._t('editor.turn_on.title') || 'Send turn_on command'; + const turnOnDesc = document.createElement('div'); + turnOnDesc.style.fontSize = '.85rem'; + turnOnDesc.style.color = 'var(--secondary-text-color)'; + turnOnDesc.textContent = this._t('editor.turn_on.desc') || ''; + turnOnText.append(turnOnTitle, turnOnDesc); + const turnOnSwitch = document.createElement('ha-switch'); + turnOnSwitch.className = 'turn-on-switch'; + + // Dropdown (only visible when enabled) + const turnOnOrderRow = document.createElement('div'); + turnOnOrderRow.style.display = 'grid'; + turnOnOrderRow.style.gap = '2px'; + const orderLbl = document.createElement('div'); + orderLbl.className = 'slabel'; + orderLbl.textContent = this._t('editor.turn_on.order') || 'Order'; + const orderSel = document.createElement('select'); + orderSel.className = 'turn-on-order'; + try { + orderSel.style.height = '32px'; + orderSel.style.border = '1px solid var(--divider-color)'; + orderSel.style.borderRadius = '8px'; + orderSel.style.background = 'var(--card-background-color)'; + orderSel.style.color = 'var(--primary-text-color)'; + orderSel.style.padding = '0 10px'; + orderSel.style.width = '100%'; + orderSel.style.maxWidth = '440px'; + } catch {} + const optBefore = document.createElement('option'); + optBefore.value = 'before'; + optBefore.textContent = this._t('editor.turn_on.order.before') || 'Before set_temperature'; + const optAfter = document.createElement('option'); + optAfter.value = 'after'; + optAfter.textContent = this._t('editor.turn_on.order.after') || 'After set_temperature'; + orderSel.append(optBefore, optAfter); + + const readTurnOnCfg = ()=>{ + try { + const eid = String(this._settingsDraft?.entities?.[idx] || '').trim(); + const map = (this._settingsDraft?.turn_on && typeof this._settingsDraft.turn_on === 'object') ? this._settingsDraft.turn_on : {}; + const cfg0 = (eid && map[eid] && typeof map[eid] === 'object') ? map[eid] : {}; + const enabled = !!cfg0.enabled; + const order = String(cfg0.order || 'before'); + return { eid, enabled, order: (order === 'after' ? 'after' : 'before') }; + } catch { return { eid: String(this._settingsDraft?.entities?.[idx] || '').trim(), enabled: false, order: 'before' }; } + }; + const writeTurnOnCfg = (eid, patch)=>{ + try { + if (!eid) return; + const prev = (this._settingsDraft.turn_on && typeof this._settingsDraft.turn_on === 'object') ? this._settingsDraft.turn_on : {}; + const cur = (prev[eid] && typeof prev[eid] === 'object') ? prev[eid] : {}; + this._settingsDraft.turn_on = { ...prev, [eid]: { ...cur, ...patch } }; + } catch {} + }; + const applyTurnOnUi = ()=>{ + const { enabled, order } = readTurnOnCfg(); + try { turnOnSwitch.checked = !!enabled; } catch {} + try { orderSel.value = (order === 'after' ? 'after' : 'before'); } catch {} + try { turnOnOrderRow.style.display = enabled ? '' : 'none'; } catch {} + }; + turnOnSwitch.addEventListener('change', (e)=>{ + try { + const { eid, order } = readTurnOnCfg(); + const enabled = !!e.target.checked; + writeTurnOnCfg(eid, { enabled, order }); + applyTurnOnUi(); + } catch {} + }); + orderSel.addEventListener('change', ()=>{ + try { + const { eid, enabled } = readTurnOnCfg(); + const order = String(orderSel.value || 'before'); + writeTurnOnCfg(eid, { enabled: !!enabled, order: (order === 'after' ? 'after' : 'before') }); + applyTurnOnUi(); + } catch {} + }); + applyTurnOnUi(); + turnOnRow.append(turnOnText, turnOnSwitch); + turnOnOrderRow.append(orderLbl, orderSel); + turnOnWrap.append(turnOnRow, turnOnOrderRow); + try { if (useInput) turnOnWrap.style.display = 'none'; } catch {} + + // Per-room: temperature sensor override (for display + boiler) + const tempWrap = document.createElement('div'); + tempWrap.style.display = 'grid'; + tempWrap.style.gap = '6px'; + + const tempRow = document.createElement('div'); + tempRow.style.display = 'flex'; + tempRow.style.alignItems = 'center'; + tempRow.style.justifyContent = 'space-between'; + tempRow.style.gap = '10px'; + + const tempText = document.createElement('div'); + tempText.style.display = 'grid'; + tempText.style.gap = '2px'; + const tempTitle = document.createElement('div'); + tempTitle.style.fontWeight = '600'; + tempTitle.textContent = this._t('editor.room_temp_sensor_override.title') || 'Use room temperature sensor'; + const tempDesc = document.createElement('div'); + tempDesc.style.fontSize = '.85rem'; + tempDesc.style.color = 'var(--secondary-text-color)'; + tempDesc.textContent = this._t('editor.room_temp_sensor_override.desc') || ''; + tempText.append(tempTitle, tempDesc); + + const tempSwitch = document.createElement('ha-switch'); + tempSwitch.className = 'room-temp-sensor-switch'; + + const tempPicker = document.createElement('tt-entity-picker'); + try { tempPicker.style.display = 'block'; tempPicker.style.minHeight = '48px'; tempPicker.style.maxWidth = '440px'; } catch {} + tempPicker.setAttribute('include-domains', '["sensor"]'); + tempPicker.setAttribute('label', this._t('editor.room_temp_sensor_override.entity') || (this._t('editor.temp_sensor') || 'Temperature sensor')); + try { tempPicker.hass = this._hass; } catch {} + + const applyTempUi = ()=>{ + try { + const primary = String(this._settingsDraft?.entities?.[idx] || '').trim(); + const can = !useInput && primary.startsWith('climate.'); + const enabled = can && !!this._settingsDraft?.room_use_temp_sensor?.[idx]; + tempRow.style.display = can ? '' : 'none'; + tempPicker.style.display = (can && enabled) ? '' : 'none'; + tempSwitch.checked = !!enabled; + tempSwitch.disabled = !can; + if (can) { + const cur = String(this._settingsDraft?.temp_sensors?.[primary] || '').trim(); + tempPicker.value = cur; + } else { + tempPicker.value = ''; + } + } catch {} + }; + + tempSwitch.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + if (!Array.isArray(this._settingsDraft.room_use_temp_sensor)) this._settingsDraft.room_use_temp_sensor = []; + while (this._settingsDraft.room_use_temp_sensor.length < this._settingsDraft.entities.length) this._settingsDraft.room_use_temp_sensor.push(false); + this._settingsDraft.room_use_temp_sensor[idx] = !!e.target.checked; + applyTempUi(); + } catch {} + }); + + tempPicker.addEventListener('value-changed', (e)=>{ + try { + if (!this._settingsDraft) return; + const primary = String(this._settingsDraft?.entities?.[idx] || '').trim(); + if (!primary) return; + const v = String(e?.detail?.value || '').trim(); + const next = { ...(this._settingsDraft.temp_sensors || {}) }; + if (v) next[primary] = v; else delete next[primary]; + this._settingsDraft.temp_sensors = next; + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + + tempRow.append(tempText, tempSwitch); + tempWrap.append(tempRow, tempPicker); + applyTempUi(); + + // Merge UI + const linkWrap = document.createElement('div'); + linkWrap.style.display = 'grid'; + linkWrap.style.gap = '6px'; + const linkLabel = document.createElement('div'); + linkLabel.className = 'label'; + linkLabel.textContent = this._t('editor.merge_label') || 'Merge with'; + const linkPicker = document.createElement('tt-entity-picker'); + try { linkPicker.style.display = 'block'; linkPicker.style.minHeight = '48px'; linkPicker.style.width = '100%'; linkPicker.style.maxWidth = '440px'; } catch {} + linkPicker.className = 'merge-picker'; + linkPicker.setAttribute('include-domains', '["climate"]'); + linkPicker.setAttribute('label', this._t('editor.merge_label') || 'Merge with'); + try { linkPicker.hass = this._hass; } catch {} + linkPicker.entityFilter = (st) => { + try { + const id = st?.entity_id || st?.entityId || st?.entityID || ''; + if (!id) return false; + const prims = (this._settingsDraft?.entities || []).filter(Boolean).map(String); + const allLinked = Object.values(this._settingsDraft?.merges || {}).flat().filter(Boolean).map(String); + const used = new Set([...prims, ...allLinked]); + const cur = String(this._settingsDraft?.entities?.[idx] || ''); + return !used.has(id) && id !== cur; + } catch { return true; } + }; + + const chips = document.createElement('div'); + chips.className = 'linked-chips'; + + const refreshSubtitle = ()=>{ + try { + const eid = String(this._settingsDraft?.entities?.[idx] || ''); + const links = (this._settingsDraft?.merges?.[eid] || []).map(String); + const linkedNames = links.map(id => this._prettyName(id)); + const parts = []; + if (eid) parts.push(eid); + if (linkedNames.length) parts.push(`${this._t('editor.merged_with') || 'Merged with'}: ${linkedNames.join(', ')}`); + subSpan.textContent = parts.join(' • '); + } catch { subSpan.textContent = ''; } + }; + + const refreshChips = ()=>{ + try { + chips.innerHTML = ''; + const primary = String(this._settingsDraft?.entities?.[idx] || ''); + const links = (this._settingsDraft?.merges?.[primary] || []).map(String); + for (const l of links) { + const chip = document.createElement('span'); + chip.className = 'pill-chip'; + const txt = document.createElement('span'); + txt.textContent = this._prettyName(l); + const rm = document.createElement('button'); + rm.type = 'button'; + rm.className = 'rm'; + rm.textContent = '×'; + try { rm.setAttribute('aria-label', this._t('editor.remove') || 'Remove'); } catch {} + rm.addEventListener('click', ()=>{ + try { + const merges = { ...(this._settingsDraft.merges || {}) }; + merges[primary] = (merges[primary] || []).filter(x => String(x) !== String(l)); + if (!merges[primary]?.length) delete merges[primary]; + this._settingsDraft.merges = merges; + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + chip.append(txt, rm); + chips.append(chip); + } + refreshSubtitle(); + } catch {} + }; + + linkPicker.addEventListener('value-changed', (e)=>{ + try { + const toAdd = String(e?.detail?.value || ''); + try { linkPicker.value = ''; } catch {} + const primary = String(this._settingsDraft?.entities?.[idx] || ''); + if (!primary || !toAdd) return; + const merges = { ...(this._settingsDraft.merges || {}) }; + const arr = Array.from(new Set([...(merges[primary] || []).map(String), toAdd])); + merges[primary] = arr; + this._settingsDraft.merges = merges; + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + + left.append(modeRow, pick, nameLabel, nameInp, tempWrap, turnOnWrap, linkWrap, chips); + try { linkWrap.append(linkLabel, linkPicker); } catch {} + details.append(left); + + // Summary title + try { + const eid = String(ents[idx] || '').trim(); + titleSpan.textContent = eid ? pretty(eid) : (this._t('editor.entity_placeholder') || 'Select entity'); + } catch { titleSpan.textContent = this._t('editor.entity_placeholder') || 'Select entity'; } + refreshSubtitle(); + refreshChips(); + + // Remove room + del.addEventListener('click', (ev)=>{ + try { ev.stopPropagation?.(); } catch {} + try { + const arr = [...(this._settingsDraft.entities || [])]; + const modes = Array.isArray(this._settingsDraft.room_use_input_number) ? [...this._settingsDraft.room_use_input_number] : []; + const tmodes = Array.isArray(this._settingsDraft.room_use_temp_sensor) ? [...this._settingsDraft.room_use_temp_sensor] : []; + const primary = String(arr[idx] || ''); + arr.splice(idx, 1); + try { if (modes.length) modes.splice(idx, 1); } catch {} + try { if (tmodes.length) tmodes.splice(idx, 1); } catch {} + + // Clean labels + try { + const labels = { ...(this._settingsDraft.labels || {}) }; + if (primary && labels[primary]) delete labels[primary]; + this._settingsDraft.labels = labels; + } catch {} + // Clean merges + try { + const merges = { ...(this._settingsDraft.merges || {}) }; + if (primary && merges[primary]) delete merges[primary]; + for (const k of Object.keys(merges)) { + merges[k] = (merges[k] || []).filter(e => String(e) !== String(primary)); + if (!merges[k].length) delete merges[k]; + } + this._settingsDraft.merges = merges; + } catch {} + // Clean temp sensors + try { + const sensors = { ...(this._settingsDraft.temp_sensors || {}) }; + if (primary && sensors[primary]) delete sensors[primary]; + this._settingsDraft.temp_sensors = sensors; + } catch {} + + // Clean presence sensors + try { + const ps = { ...(this._settingsDraft.presence_sensors || {}) }; + if (primary && ps[primary]) delete ps[primary]; + this._settingsDraft.presence_sensors = ps; + } catch {} + + // Clean turn_on + try { + const t = { ...(this._settingsDraft.turn_on || {}) }; + if (primary && t[primary]) delete t[primary]; + this._settingsDraft.turn_on = t; + } catch {} + + this._settingsDraft.entities = arr; + this._settingsDraft.room_use_input_number = modes; + this._settingsDraft.room_use_temp_sensor = tmodes; + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + + // Primary picker change + pick.addEventListener('value-changed', (e)=>{ + try { + const newPrimary = String(e?.detail?.value || '').trim(); + const oldPrimary = String(this._settingsDraft?.entities?.[idx] || '').trim(); + + // Auto-sync mode based on chosen domain + try { + while (this._settingsDraft.room_use_input_number.length < this._settingsDraft.entities.length) this._settingsDraft.room_use_input_number.push(false); + if (newPrimary.startsWith('input_number.')) this._settingsDraft.room_use_input_number[idx] = true; + else if (newPrimary.startsWith('climate.')) this._settingsDraft.room_use_input_number[idx] = false; + } catch {} + + // If switching away from climate, disable temp sensor override for this row + try { + if (!Array.isArray(this._settingsDraft.room_use_temp_sensor)) this._settingsDraft.room_use_temp_sensor = []; + while (this._settingsDraft.room_use_temp_sensor.length < this._settingsDraft.entities.length) this._settingsDraft.room_use_temp_sensor.push(false); + if (!newPrimary.startsWith('climate.')) this._settingsDraft.room_use_temp_sensor[idx] = false; + } catch {} + + // Migrate merges/temp sensors when primary changes + if (oldPrimary && newPrimary && oldPrimary !== newPrimary) { + try { + const merges = { ...(this._settingsDraft.merges || {}) }; + if (merges[oldPrimary] && !merges[newPrimary]) { merges[newPrimary] = merges[oldPrimary]; delete merges[oldPrimary]; } + else if (merges[oldPrimary] && merges[newPrimary]) { + merges[newPrimary] = Array.from(new Set([...(merges[newPrimary]||[]), ...(merges[oldPrimary]||[])])); + delete merges[oldPrimary]; + } + this._settingsDraft.merges = merges; + } catch {} + try { + const sensors = { ...(this._settingsDraft.temp_sensors || {}) }; + if (sensors[oldPrimary] && !sensors[newPrimary]) sensors[newPrimary] = sensors[oldPrimary]; + if (sensors[oldPrimary]) delete sensors[oldPrimary]; + this._settingsDraft.temp_sensors = sensors; + } catch {} + try { + const labels = { ...(this._settingsDraft.labels || {}) }; + if (labels[oldPrimary] && !labels[newPrimary]) labels[newPrimary] = labels[oldPrimary]; + if (labels[oldPrimary]) delete labels[oldPrimary]; + this._settingsDraft.labels = labels; + } catch {} + + try { + const t = { ...(this._settingsDraft.turn_on || {}) }; + if (t[oldPrimary] && !t[newPrimary]) t[newPrimary] = t[oldPrimary]; + if (t[oldPrimary]) delete t[oldPrimary]; + this._settingsDraft.turn_on = t; + } catch {} + + try { + const ps = { ...(this._settingsDraft.presence_sensors || {}) }; + if (ps[oldPrimary] && !ps[newPrimary]) ps[newPrimary] = ps[oldPrimary]; + if (ps[oldPrimary]) delete ps[oldPrimary]; + this._settingsDraft.presence_sensors = ps; + } catch {} + // Move open-state key old -> new + try { + if (this._openPopupRooms && this._openPopupRooms.has(oldPrimary)) { this._openPopupRooms.delete(oldPrimary); this._openPopupRooms.add(newPrimary); } + } catch {} + } + + const next = [...(this._settingsDraft.entities || [])]; + next[idx] = newPrimary; + this._settingsDraft.entities = next; + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + + // Drag reorder + handle.addEventListener('mousedown', (ev)=>{ + try { ev.preventDefault(); } catch {} + try { + const wrap = line.parentElement; + const curIdx = wrap ? Array.prototype.indexOf.call(wrap.children, line) : idx; + this._startSettingsPopupRoomsPointerDnd(line, curIdx, ev); + } catch { this._startSettingsPopupRoomsPointerDnd(line, idx, ev); } + }); + handle.addEventListener('touchstart', (ev)=>{ + try { ev.preventDefault(); } catch {} + try { + const wrap = line.parentElement; + const curIdx = wrap ? Array.prototype.indexOf.call(wrap.children, line) : idx; + this._startSettingsPopupRoomsPointerDnd(line, curIdx, ev); + } catch { this._startSettingsPopupRoomsPointerDnd(line, idx, ev); } + }, { passive: false }); + + return line; + }; + + // Add rows + for (let i=0;i{ + try { + if (!this._settingsDraft) return; + this._settingsDraft.entities = [...(this._settingsDraft.entities || []), '']; + const modes = Array.isArray(this._settingsDraft.room_use_input_number) ? [...this._settingsDraft.room_use_input_number] : []; + modes.push(false); + this._settingsDraft.room_use_input_number = modes; + const tmodes = Array.isArray(this._settingsDraft.room_use_temp_sensor) ? [...this._settingsDraft.room_use_temp_sensor] : []; + tmodes.push(false); + this._settingsDraft.room_use_temp_sensor = tmodes; + this._renderSettingsPopupRoomsTab(); + } catch {} + }); + } catch {} + } + } catch {} + } + + _startSettingsPopupRoomsPointerDnd(line, idx, startEv){ + try { + const wrap = line.parentElement; + if (!wrap) return; + if (this._settingsPopupPointerDndActive) return; + this._settingsPopupPointerDndActive = true; + + const rect = line.getBoundingClientRect(); + const ghost = line.cloneNode(true); + ghost.classList.add('drag-ghost'); + ghost.style.width = rect.width + 'px'; + ghost.style.height = rect.height + 'px'; + ghost.style.top = rect.top + 'px'; + ghost.style.left = rect.left + 'px'; + + const placeholder = document.createElement('div'); + placeholder.className = 'placeholder'; + placeholder.style.height = rect.height + 'px'; + + // place placeholder where line was, and hide line + wrap.insertBefore(placeholder, line); + line.style.display = 'none'; + document.body.appendChild(ghost); + + const readClient = (e)=>{ + if (e?.touches && e.touches[0]) return { x: e.touches[0].clientX, y: e.touches[0].clientY }; + return { x: e?.clientX, y: e?.clientY }; + }; + + const onMove = (ev)=>{ + try { ev.preventDefault(); } catch {} + const { y, x } = readClient(ev); + if (!Number.isFinite(y) || !Number.isFinite(x)) return; + ghost.style.top = (y - rect.height/2) + 'px'; + + // find element under pointer within this shadowRoot + let el = this.shadowRoot && this.shadowRoot.elementFromPoint(x, y); + // climb to row in the same wrap + while (el && el !== wrap && !el.classList?.contains('inline')) el = el.parentElement; + if (!el || el===placeholder || el===wrap) return; + const targetRect = el.getBoundingClientRect(); + const before = (y - targetRect.top) < targetRect.height/2; + if (before) wrap.insertBefore(placeholder, el); + else wrap.insertBefore(placeholder, el.nextSibling); + }; + + const endDrag = ()=>{ + window.removeEventListener('mousemove', onMove, true); + window.removeEventListener('mouseup', onUp, true); + window.removeEventListener('touchmove', onMove, true); + window.removeEventListener('touchend', onUp, true); + try { ghost.remove(); } catch {} + + try { + const from = idx; + const to = Array.prototype.indexOf.call(wrap.children, placeholder); + const arr = [...(this._settingsDraft?.entities || [])]; + const modes = Array.isArray(this._settingsDraft?.room_use_input_number) ? [...this._settingsDraft.room_use_input_number] : []; + const tmodes = Array.isArray(this._settingsDraft?.room_use_temp_sensor) ? [...this._settingsDraft.room_use_temp_sensor] : []; + while (modes.length < arr.length) modes.push(false); + while (tmodes.length < arr.length) tmodes.push(false); + const [item] = arr.splice(from,1); + const [mItem] = modes.splice(from,1); + const [tItem] = tmodes.splice(from,1); + const correctedTo = Math.max(0, Math.min(to, arr.length)); + arr.splice(correctedTo, 0, item); + modes.splice(correctedTo, 0, mItem); + tmodes.splice(correctedTo, 0, tItem); + this._settingsDraft.entities = arr; + this._settingsDraft.room_use_input_number = modes; + this._settingsDraft.room_use_temp_sensor = tmodes; + } catch {} + + try { placeholder.remove(); } catch {} + try { line.style.display = ''; } catch {} + this._settingsPopupPointerDndActive = false; + try { this._renderSettingsPopupRoomsTab(); } catch {} + }; + + const onUp = (ev)=>{ try { ev.preventDefault?.(); } catch {} endDrag(); }; + + window.addEventListener('mousemove', onMove, true); + window.addEventListener('mouseup', onUp, true); + window.addEventListener('touchmove', onMove, true); + window.addEventListener('touchend', onUp, true); + + // Initial nudge so the ghost doesn't feel stuck under finger + try { + const { x, y } = readClient(startEv || {}); + if (Number.isFinite(y) && Number.isFinite(x)) ghost.style.top = (y - rect.height/2) + 'px'; + } catch {} + } catch { this._settingsPopupPointerDndActive = false; } + } + + _renderSettingsPopupAwayTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="away"]'); + if (!page) return; + + // Build markup once + if (!page.dataset.built) { + page.dataset.built = '1'; + page.innerHTML = ` +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    + + + + +
    +
    +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    + `; + } + + // Ensure draft shape + if (!d.away || typeof d.away !== 'object') d.away = { enabled:false, target_c:17, persons:[], advanced_enabled:false, combos:{} }; + if (!Array.isArray(d.away.persons)) d.away.persons = []; + if (!d.away.combos || typeof d.away.combos !== 'object') d.away.combos = {}; + + const q = (sel)=> this.shadowRoot.querySelector(sel); + const awayOn = !!d.away.enabled; + const advOn = !!d.away.advanced_enabled; + + // Show/hide dependent controls based on master toggle + try { + const tempRow = q('.overlay-settings .sp-away-temp-row'); + const personsRow = q('.overlay-settings .sp-away-persons-row'); + const advRow = q('.overlay-settings .sp-away-adv-row'); + const delayRow = q('.overlay-settings .sp-away-delay-row'); + const liveRow = q('.overlay-settings .sp-presence-live-row'); + const combosRow = q('.overlay-settings .sp-away-combos-row'); + [tempRow, personsRow, delayRow, advRow, liveRow, combosRow].forEach(r=>{ if (r) r.style.display = awayOn ? '' : 'none'; }); + } catch {} + + // Toggle states + try { + const sw = q('.overlay-settings .sp-away-enable'); + if (sw) sw.checked = awayOn; + } catch {} + try { + const adv = q('.overlay-settings .sp-away-adv-enable'); + if (adv) adv.checked = advOn; + } catch {} + try { + const pl = q('.overlay-settings .sp-presence-live-enable'); + if (pl) { + pl.checked = !!(d.presence_live_header ?? true); + // Enabled whenever Away mode is enabled + pl.disabled = !awayOn; + } + } catch {} + + // Temperature field formatting based on draft unit + try { + const inp = q('.overlay-settings .sp-away-temp'); + if (inp) { + const wantF = String(d.temp_unit || 'C').toUpperCase() === 'F'; + const toDisp = (c)=> wantF ? ((Number(c) * 9/5) + 32) : Number(c); + const mnC = Number.isFinite(Number(d.min_temp)) ? Number(d.min_temp) : (Number.isFinite(Number(this._config?.min_temp)) ? Number(this._config.min_temp) : 5); + const mxC = Number.isFinite(Number(d.max_temp)) ? Number(d.max_temp) : (Number.isFinite(Number(this._config?.max_temp)) ? Number(this._config.max_temp) : 25); + inp.setAttribute('min', String(toDisp(mnC))); + inp.setAttribute('max', String(toDisp(mxC))); + if (inp !== this.shadowRoot.activeElement) { + const v = Number.isFinite(Number(d.away.target_c)) ? Number(d.away.target_c) : 17; + inp.value = String(Math.round(toDisp(v) * 2) / 2); + } + } + } catch {} + + // Delay controls + try { + const en = q('.overlay-settings .sp-away-delay-enable'); if (en) en.checked = !!d.away_delay_enabled; + const val = q('.overlay-settings .sp-away-delay-value'); if (val && val !== this.shadowRoot.activeElement) val.value = String(Number.isFinite(Number(d.away_delay_value)) ? Number(d.away_delay_value) : 0); + const unit = q('.overlay-settings .sp-away-delay-unit'); if (unit) unit.value = (String(d.away_delay_unit || 'minutes')); + } catch {} + + // Entity picker + try { + const picker = q('.overlay-settings .sp-away-person-picker'); + if (picker) { + picker.hass = this._hass; + picker.value = ''; + try { picker.setAttribute('label', this._t('away.add_person') || 'Add person'); } catch {} + } + } catch {} + + // Chips + try { + const chips = q('.overlay-settings .sp-away-chips'); + if (chips) { + chips.innerHTML = ''; + const arr = Array.isArray(d.away.persons) ? d.away.persons.filter(Boolean).map(String) : []; + for (const p of arr) { + const chip = document.createElement('span'); chip.className = 'pill-chip'; + const txt = document.createElement('span'); txt.textContent = this._hass?.states?.[p]?.attributes?.friendly_name || (p.split('.')[1]||p); + const rm = document.createElement('button'); rm.type='button'; rm.className='rm'; rm.textContent='×'; + try { rm.setAttribute('aria-label', this._t('editor.remove') || 'Remove'); } catch {} + rm.addEventListener('click', ()=>{ + try { + if (!this._settingsDraft?.away) return; + this._settingsDraft.away.persons = (this._settingsDraft.away.persons||[]).filter(x=>String(x)!==String(p)); + this._awayTouched = true; + this._renderSettingsPopupAwayTab(); + } catch {} + }); + chip.append(txt, rm); + chips.append(chip); + } + } + } catch {} + + // Presence combinations checklist + try { + const comboWrap = q('.overlay-settings .sp-presence-combo-list'); + if (comboWrap) { + comboWrap.innerHTML = ''; + const persons = (d.away.persons || []).map(String); + const enabled = !!d.away.advanced_enabled; + const conf = (d.away.combos && typeof d.away.combos === 'object') ? d.away.combos : {}; + const n = persons.length; + const labelOf = (arr)=> arr.map(e => (this._hass?.states?.[e]?.attributes?.friendly_name || (e.split('.')[1]||e))).join(', '); + for (let mask=0; mask < (1<>i)&1) ? home.push(persons[i]) : away.push(persons[i]); } + // Skip "All home" entry; fallback to main/weekdays when all are home + if (home.length===n) continue; + const key = `H:${home.slice().sort().join(',')}|A:${away.slice().sort().join(',')}`; + const row = document.createElement('label'); + row.style.display='flex'; + row.style.alignItems='center'; + row.style.gap='8px'; + row.style.opacity = enabled ? '' : '.6'; + const cb = document.createElement('input'); + cb.type='checkbox'; + cb.disabled = !enabled || !persons.length; + cb.checked = !!conf[key]?.enabled; + cb.addEventListener('change', ()=>{ + try { + if (!this._settingsDraft?.away) return; + const map = { ...(this._settingsDraft.away.combos || {}) }; + if (cb.checked) map[key] = { ...(map[key]||{}), enabled:true }; + else delete map[key]; + this._settingsDraft.away.combos = map; + this._awayTouched = true; + } catch {} + }); + const sp = document.createElement('span'); + let lab; + if (home.length===0) lab = this._t('presence.none_home'); + else { + const h = (this._t('presence.home') || 'Home: {names}').replace('{names}', labelOf(home)); + const a = (this._t('presence.away') || 'Away: {names}').replace('{names}', labelOf(away)); + lab = `${h} • ${a}`; + } + sp.textContent = lab; + row.append(cb, sp); + comboWrap.append(row); + } + } + } catch {} + + // Bind events once + if (!page.dataset.bound) { + page.dataset.bound = '1'; + + const _ttReadSwitchValue = (evt, el, fallback = false)=>{ + try { + const d = evt?.detail; + if (d && typeof d === 'object' && typeof d.value === 'boolean') return !!d.value; + } catch {} + try { + const ct = evt?.currentTarget; + if (ct && typeof ct.checked === 'boolean') return !!ct.checked; + } catch {} + try { + if (el && typeof el.checked === 'boolean') return !!el.checked; + } catch {} + try { + const t = evt?.target; + if (t && typeof t.checked === 'boolean') return !!t.checked; + } catch {} + return !!fallback; + }; + const _ttBindHaSwitch = (el, handler)=>{ + try { if (!el || typeof handler !== 'function') return; } catch { return; } + try { el.addEventListener('change', handler); } catch {} + try { el.addEventListener('checked-changed', handler); } catch {} + }; + + try { + const sw = q('.overlay-settings .sp-away-enable'); + if (sw) _ttBindHaSwitch(sw, (e)=>{ + try { + if (!this._settingsDraft?.away) return; + const enabled = _ttReadSwitchValue(e, sw, !!this._settingsDraft.away.enabled); + this._settingsDraft.away.enabled = enabled; + // If Away is turned OFF, also disable Advanced Away and live presence chips + if (!enabled) { + this._settingsDraft.away.advanced_enabled = false; + } + this._awayTouched = true; + this._renderSettingsPopupAwayTab(); + } catch {} + }); + } catch {} + + try { + const inp = q('.overlay-settings .sp-away-temp'); + if (inp) inp.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft?.away) return; + let raw = String(e.target.value||'').trim().replace(',', '.'); + const vDisp = Number(raw); + if (!Number.isFinite(vDisp)) return; + const wantF = String(this._settingsDraft.temp_unit || 'C').toUpperCase() === 'F'; + const fromDisp = (x)=> wantF ? ((Number(x) - 32) * 5/9) : Number(x); + let vC = fromDisp(vDisp); + const mn = Number.isFinite(Number(this._settingsDraft.min_temp)) ? Number(this._settingsDraft.min_temp) : 5; + const mx = Number.isFinite(Number(this._settingsDraft.max_temp)) ? Number(this._settingsDraft.max_temp) : 25; + vC = Math.max(mn, Math.min(mx, vC)); + this._settingsDraft.away.target_c = vC; + this._awayTouched = true; + } catch {} + }); + } catch {} + + try { + const picker = q('.overlay-settings .sp-away-person-picker'); + if (picker) picker.addEventListener('value-changed', (e)=>{ + try { + const val = e?.detail?.value; + if (!val || !this._settingsDraft?.away) return; + picker.value = ''; + const set = new Set((this._settingsDraft.away.persons || []).map(String)); + set.add(String(val)); + this._settingsDraft.away.persons = Array.from(set); + this._awayTouched = true; + this._renderSettingsPopupAwayTab(); + } catch {} + }); + } catch {} + + try { + const adv = q('.overlay-settings .sp-away-adv-enable'); + if (adv) _ttBindHaSwitch(adv, (e)=>{ + try { + if (!this._settingsDraft?.away) return; + this._settingsDraft.away.advanced_enabled = _ttReadSwitchValue(e, adv, !!this._settingsDraft.away.advanced_enabled); + // Do not force presence chips off when advanced is disabled; + // availability is controlled by Away mode. + this._awayTouched = true; + this._renderSettingsPopupAwayTab(); + } catch {} + }); + } catch {} + + try { + const pl = q('.overlay-settings .sp-presence-live-enable'); + if (pl) _ttBindHaSwitch(pl, (e)=>{ + try { + if (!this._settingsDraft?.away?.enabled) { + e.preventDefault?.(); + pl.checked = !!(this._settingsDraft?.presence_live_header ?? true); + return; + } + this._settingsDraft.presence_live_header = _ttReadSwitchValue(e, pl, !!(this._settingsDraft.presence_live_header ?? true)); + this._awayTouched = true; + } catch {} + }); + } catch {} + + // Delay bindings + try { + const en = q('.overlay-settings .sp-away-delay-enable'); + if (en) _ttBindHaSwitch(en, (e)=>{ try { this._settingsDraft.away_delay_enabled = _ttReadSwitchValue(e, en, !!(this._settingsDraft.away_delay_enabled ?? false)); this._awayTouched = true; } catch {} }); + } catch {} + try { + const unit = q('.overlay-settings .sp-away-delay-unit'); + if (unit) unit.addEventListener('change', (e)=>{ try { this._settingsDraft.away_delay_unit = String(e.target.value||'minutes'); this._awayTouched = true; } catch {} }); + } catch {} + try { + const val = q('.overlay-settings .sp-away-delay-value'); + if (val) val.addEventListener('change', (e)=>{ try { const n=Number(e.target.value); this._settingsDraft.away_delay_value = Number.isFinite(n)? Math.max(0,Math.round(n)) : 0; this._awayTouched = true; } catch {} }); + } catch {} + } + } catch {} + } + + _renderSettingsPopupResetTab(){ + try { + if (!this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="reset"]'); + if (!page) return; + + if (!page.dataset.built) { + page.dataset.built = '1'; + page.innerHTML = ` +
    +
    +
    +
    +
    +
    + + +
    +
    + `; + } + + const q = (sel)=> this.shadowRoot.querySelector(sel); + const ttl = q('.overlay-settings .sp-reset-title'); + if (ttl) ttl.textContent = this._t('editor.tabs.reset') || 'Reset'; + const desc = q('.overlay-settings .sp-reset-desc'); + if (desc) { + let msg = String(this._t('editor.reset.desc') || '').trim(); + if (!msg) { + let short = String(this._t('editor.clear_all_confirm') || '').trim(); + short = short.replace(/\?\s*$/, ''); + msg = short || 'Clear storage + local/browser data.'; + } + desc.textContent = msg; + } + + const bAll = q('.overlay-settings .sp-reset-clear-all'); + if (bAll) { + const s = bAll.querySelector('span') || bAll; + if (s) s.textContent = this._t('editor.clear_all') || 'Clear all data'; + } + const bLocal = q('.overlay-settings .sp-reset-clear-local'); + if (bLocal) { + const s = bLocal.querySelector('span') || bLocal; + if (s) s.textContent = this._t('editor.clear_local_only') || 'Clear ALL local data (local only)'; + } + + if (!page.dataset.bound) { + page.dataset.bound = '1'; + try { + const btn = page.querySelector('.sp-reset-clear-all'); + if (btn) btn.addEventListener('click', async ()=>{ + const msg = this._t('editor.clear_all_confirm'); + if (!confirm(msg)) return; + try { await this._clearAllDataEverywhereConfirmed(); } catch {} + }); + } catch {} + try { + const btn = page.querySelector('.sp-reset-clear-local'); + if (btn) btn.addEventListener('click', ()=>{ + const msg = this._t('editor.clear_local_only_confirm'); + if (!confirm(msg)) return; + try { this._clearAllLocalOnlyConfirmed(); } catch {} + }); + } catch {} + } + } catch {} + } + + _renderSettingsPopupOwdTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="owd"]'); + if (!page) return; + page.innerHTML = ''; + try { page.style.display = ''; page.style.gap = ''; } catch {} + + // Ensure draft shape + const cur = (d.open_window && typeof d.open_window === 'object') ? d.open_window : (d.open_window = { enabled:false, sensors:{}, open_delay_min:2, close_delay_min:5 }); + if (typeof cur.enabled !== 'boolean') cur.enabled = false; + if (!cur.sensors || typeof cur.sensors !== 'object') cur.sensors = {}; + if (!Number.isFinite(Number(cur.open_delay_min))) cur.open_delay_min = 2; + if (!Number.isFinite(Number(cur.close_delay_min))) cur.close_delay_min = 5; + cur.open_delay_min = Math.max(0, Math.min(1440, Math.round(Number(cur.open_delay_min)))); + cur.close_delay_min = Math.max(0, Math.min(1440, Math.round(Number(cur.close_delay_min)))); + + // Card with all OWD content + const enableCard = document.createElement('div'); enableCard.className = 'settings-card'; + const enRow = document.createElement('div'); enRow.className = 'setting'; + const enText = document.createElement('div'); enText.className = 'text'; + const enTitle = document.createElement('div'); enTitle.className = 'title sp-owd-enable-title'; + const enDesc = document.createElement('div'); enDesc.className = 'desc sp-owd-enable-desc'; + enText.append(enTitle, enDesc); + const enSwitch = document.createElement('ha-switch'); enSwitch.className = 'sp-owd-enable'; enSwitch.checked = !!cur.enabled; + enSwitch.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft?.open_window) return; + this._settingsDraft.open_window.enabled = !!e.target.checked; + try { contentWrap.style.display = this._settingsDraft.open_window.enabled ? 'grid' : 'none'; } catch {} + } catch {} + }); + enRow.append(enText, enSwitch); + enableCard.append(enRow); + + // Remaining content shown only when enabled (inside the same card, like other tabs) + const contentWrap = document.createElement('div'); contentWrap.className = 'owd-content'; + try { contentWrap.style.display = cur.enabled ? 'grid' : 'none'; contentWrap.style.gap = '16px'; contentWrap.style.marginTop = '6px'; } catch {} + enableCard.append(contentWrap); + page.append(enableCard); + + // Rooms foldouts, wrapped in a setting row (like Boiler) + const ents = Array.isArray(this._config?.entities) ? this._config.entities : []; + const sensorsRow = document.createElement('div'); sensorsRow.className = 'setting'; sensorsRow.style.gridTemplateColumns = '1fr'; + const sensorsText = document.createElement('div'); sensorsText.className = 'text'; + const sensorsTitle = document.createElement('div'); sensorsTitle.className = 'title sp-owd-sensors-label'; + sensorsText.append(sensorsTitle); + sensorsRow.append(sensorsText); + + const roomsWrap = document.createElement('div'); + try { roomsWrap.style.display = 'grid'; roomsWrap.style.gap = '10px'; } catch {} + sensorsRow.append(roomsWrap); + contentWrap.append(sensorsRow); + + if (!ents.length) { + const box = document.createElement('div'); box.className = 'empty-box'; + const p = document.createElement('div'); p.textContent = this._t('editor.select_entities') || 'Rooms'; + const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'add-entity-btn'; + btn.innerHTML = '' + (this._t('editor.add_entity') || 'Add room') + ''; + btn.onclick = ()=>{ this._setSettingsPopupTab('rooms'); }; + box.append(p, btn); + roomsWrap.append(box); + } + + // Remember open/closed state while popup is open + try { if (!this._openOwdRooms) this._openOwdRooms = new Set(); } catch {} + + for (const eid of ents) { + if (!eid) continue; + // Foldout (same UI pattern as HA editor sections) + const line = document.createElement('div'); line.className = 'inline'; + const summary = document.createElement('div'); summary.className = 'summary'; + const sumLeft = document.createElement('div'); sumLeft.className = 'summary-left'; + const exp = document.createElement('button'); exp.type = 'button'; exp.className = 'expander'; + exp.innerHTML = ''; + const txtWrap = document.createElement('div'); txtWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); titleSpan.className = 'summary-title'; + + // Inline styles to match the editor foldout appearance (robust across contexts) + try { + summary.style.display = 'flex'; + summary.style.alignItems = 'center'; + summary.style.justifyContent = 'space-between'; + summary.style.padding = '6px'; + summary.style.border = '1px dashed var(--divider-color)'; + summary.style.borderRadius = '8px'; + summary.style.background = 'var(--secondary-background-color, transparent)'; + sumLeft.style.display = 'flex'; + sumLeft.style.alignItems = 'center'; + sumLeft.style.gap = '8px'; + sumLeft.style.minWidth = '0'; + txtWrap.style.display = 'grid'; + txtWrap.style.gap = '2px'; + txtWrap.style.minWidth = '0'; + titleSpan.style.fontWeight = '600'; + titleSpan.style.overflow = 'hidden'; + titleSpan.style.textOverflow = 'ellipsis'; + titleSpan.style.whiteSpace = 'nowrap'; + titleSpan.style.maxWidth = '320px'; + exp.style.width = '28px'; + exp.style.height = '28px'; + exp.style.borderRadius = '8px'; + exp.style.border = '1px solid var(--divider-color)'; + exp.style.background = 'var(--card-background-color)'; + exp.style.display = 'inline-flex'; + exp.style.alignItems = 'center'; + exp.style.justifyContent = 'center'; + exp.style.cursor = 'pointer'; + } catch {} + try { + const labels = this._config.labels || {}; + const display = labels[eid] || this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid); + titleSpan.textContent = display; + } catch { titleSpan.textContent = this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid); } + txtWrap.append(titleSpan); + sumLeft.append(exp, txtWrap); + summary.append(sumLeft); + line.append(summary); + + const details = document.createElement('div'); details.className = 'details'; + line.append(details); + + try { + details.style.display = 'none'; + details.style.gridTemplateColumns = 'minmax(250px, 440px)'; + details.style.gap = '6px'; + details.style.padding = '6px'; + details.style.border = '1px dashed var(--divider-color)'; + details.style.borderRadius = '8px'; + details.style.background = 'var(--secondary-background-color, transparent)'; + } catch {} + + const applyOpen = (isOpen)=>{ + try { + if (isOpen) { + line.classList.add('open'); + details.style.display = 'grid'; + exp.style.transform = 'rotate(180deg)'; + } else { + line.classList.remove('open'); + details.style.display = 'none'; + exp.style.transform = ''; + } + } catch {} + }; + + const toggle = (ev)=>{ + try { ev?.stopPropagation?.(); } catch {} + const nextOpen = !line.classList.contains('open'); + applyOpen(nextOpen); + try { + if (!this._openOwdRooms) this._openOwdRooms = new Set(); + if (nextOpen) this._openOwdRooms.add(String(eid)); + else this._openOwdRooms.delete(String(eid)); + } catch {} + }; + exp.addEventListener('click', toggle); + summary.style.cursor = 'pointer'; + summary.addEventListener('click', (ev)=>{ + // Allow clicking the row header to toggle as well + try { if (ev?.target?.closest?.('button, a, input, select, textarea, tt-entity-picker, ha-entity-picker')) return; } catch {} + toggle(ev); + }); + try { applyOpen(!!this._openOwdRooms?.has(String(eid))); } catch {} + + const wrap = document.createElement('div'); + wrap.style.display = 'grid'; + wrap.style.gap = '8px'; + const picker = document.createElement('tt-entity-picker'); + picker.className = 'sp-owd-sensor-picker'; + picker.setAttribute('include-domains', '["binary_sensor"]'); + try { picker.hass = this._hass; } catch {} + try { picker.value = ''; } catch {} + const chips = document.createElement('div'); chips.className = 'linked-chips sp-owd-chips'; + + const renderChips = ()=>{ + chips.innerHTML = ''; + const map = (this._settingsDraft?.open_window?.sensors && typeof this._settingsDraft.open_window.sensors === 'object') ? this._settingsDraft.open_window.sensors : {}; + const arr = Array.isArray(map[eid]) ? map[eid] : []; + for (const sEid of arr) { + const chip = document.createElement('span'); chip.className = 'pill-chip'; + const txt = document.createElement('span'); txt.textContent = this._hass?.states?.[sEid]?.attributes?.friendly_name || (sEid.split('.')[1]||sEid); + const rm = document.createElement('button'); rm.type='button'; rm.className='rm'; rm.textContent='×'; rm.setAttribute('aria-label', this._t('editor.remove')); + rm.addEventListener('click', ()=>{ + try { + const ow = this._settingsDraft?.open_window; + if (!ow || !ow.sensors) return; + const next = { ...(ow.sensors || {}) }; + const curArr = Array.isArray(next[eid]) ? next[eid] : []; + const out = curArr.filter(x => x !== sEid); + if (out.length) next[eid] = out; else delete next[eid]; + ow.sensors = next; + renderChips(); + } catch {} + }); + chip.append(txt, rm); + chips.append(chip); + } + }; + renderChips(); + + picker.addEventListener('value-changed', (e)=>{ + const val = e.detail?.value || ''; + if (!val) return; + try { picker.value = ''; } catch {} + try { + const ow = this._settingsDraft?.open_window; + if (!ow) return; + const next = { ...(ow.sensors || {}) }; + const set = new Set(Array.isArray(next[eid]) ? next[eid] : []); + set.add(String(val)); + next[eid] = Array.from(set); + ow.sensors = next; + renderChips(); + } catch {} + }); + + wrap.append(picker, chips); + details.append(wrap); + roomsWrap.append(line); + } + + // Global delay settings + // Delay row inside same card + const row = document.createElement('div'); row.className = 'setting'; row.style.gridTemplateColumns = '1fr'; + const txt = document.createElement('div'); txt.className = 'text'; + const title = document.createElement('div'); title.className = 'title sp-owd-delays-title'; + const desc = document.createElement('div'); desc.className = 'desc sp-owd-delays-desc'; + txt.append(title, desc); + row.append(txt); + contentWrap.append(row); + const grid = document.createElement('div'); + grid.style.display = 'flex'; + grid.style.gap = '10px'; + grid.style.flexWrap = 'wrap'; + grid.style.alignItems = 'flex-end'; + + const mkField = (cls, labelKey, value, onChange)=>{ + const f = document.createElement('div'); f.className = 'sfield'; + const l = document.createElement('div'); l.className = 'slabel ' + cls + '-label'; l.textContent = (this._t(labelKey) || labelKey); + const inp = document.createElement('input'); inp.className = cls + ' settings-input'; inp.type = 'number'; inp.min='0'; inp.max='1440'; inp.step='1'; + if (inp !== this.shadowRoot.activeElement) inp.value = String(value); + inp.addEventListener('change', (e)=>{ + let v = Math.round(Number(String(e.target.value||'').replace(',','.'))); + if (!Number.isFinite(v)) v = 0; + v = Math.max(0, Math.min(1440, v)); + e.target.value = String(v); + onChange(v); + }); + f.append(l, inp); + return f; + }; + + const openField = mkField('sp-owd-open-delay', 'owd.open_delay', Number(cur.open_delay_min||0), (v)=>{ try { if (!this._settingsDraft?.open_window) return; this._settingsDraft.open_window.open_delay_min = v; } catch {} }); + const closeField = mkField('sp-owd-close-delay', 'owd.close_delay', Number(cur.close_delay_min||0), (v)=>{ try { if (!this._settingsDraft?.open_window) return; this._settingsDraft.open_window.close_delay_min = v; } catch {} }); + grid.append(openField, closeField); + contentWrap.append(grid); + } catch {} + } + + _renderSettingsPopupHolidaysTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="holidays"]'); + if (!page) return; + + // Build markup once + if (!page.dataset.built) { + page.dataset.built = '1'; + page.innerHTML = ` +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    +
    +
    + + + +
    +
    +
    +
    + +
    +
    +
    +
    + +
    +
    + `; + } + + const q = (sel)=> this.shadowRoot.querySelector(sel); + + // Ensure draft shape + if (!Array.isArray(d.holidays_dates)) d.holidays_dates = []; + if (d.holidays_source !== 'manual') d.holidays_source = 'calendar'; + + // Visibility of dependent controls + const holOn = !!d.holidays_enabled; + const src = (d.holidays_source === 'manual') ? 'manual' : 'calendar'; + try { + const editRow = q('.overlay-settings .sp-holidays-edit-setting'); + const srcRow = q('.overlay-settings .sp-holidays-source-setting'); + const entRow = q('.overlay-settings .sp-holidays-entity-setting'); + const datesRow = q('.overlay-settings .sp-holidays-dates-setting'); + if (editRow) editRow.style.display = holOn ? '' : 'none'; + if (srcRow) srcRow.style.display = holOn ? '' : 'none'; + if (entRow) entRow.style.display = (holOn && src === 'calendar') ? '' : 'none'; + if (datesRow) datesRow.style.display = (holOn && src === 'manual') ? '' : 'none'; + } catch {} + + // Segmented UI state + try { + const bCal = q('.overlay-settings .sp-holidays-source-cal'); + const bMan = q('.overlay-settings .sp-holidays-source-man'); + if (bCal) bCal.classList.toggle('active', src === 'calendar'); + if (bMan) bMan.classList.toggle('active', src === 'manual'); + } catch {} + + // Master toggle state + try { + const sw = q('.overlay-settings .sp-holidays-enable'); + if (sw) sw.checked = !!d.holidays_enabled; + } catch {} + + // Entity picker + try { + const p = q('.overlay-settings .sp-holidays-entity-picker'); + if (p) { + p.hass = this._hass; + p.value = String(d.holidays_entity || ''); + try { p.setAttribute('label', this._t('holidays.entity') || 'Holiday entity'); } catch {} + } + } catch {} + + // Dates chips + const chips = q('.overlay-settings .sp-holidays-date-chips'); + const renderChips = ()=>{ + try { + if (!chips) return; + chips.innerHTML = ''; + const arr = Array.isArray(this._settingsDraft?.holidays_dates) ? this._settingsDraft.holidays_dates.filter(Boolean).map(String) : []; + const groups = Array.isArray(this._settingsDraft?.holidays_groups) ? this._settingsDraft.holidays_groups : []; + const groupDates = new Set([].concat(...groups.map(g=>g.dates||[])).map(String)); + const sorted = Array.from(new Set(arr.filter(x => /^\d{4}-\d{2}-\d{2}$/.test(String(x)) && !groupDates.has(String(x))))).sort(); + for (const dt of sorted) { + const chip = document.createElement('span'); chip.className = 'pill-chip'; + const txt = document.createElement('span'); txt.textContent = dt; + const rm = document.createElement('button'); rm.type = 'button'; rm.className = 'rm'; rm.textContent = '×'; + try { rm.setAttribute('aria-label', this._t('editor.remove') || 'Remove'); } catch {} + rm.addEventListener('click', ()=>{ + try { + if (!this._settingsDraft) return; + const cur = Array.isArray(this._settingsDraft.holidays_dates) ? this._settingsDraft.holidays_dates : []; + this._settingsDraft.holidays_dates = cur.filter(x => String(x) !== dt); + this._holidaysDatesTouched = true; + renderChips(); + } catch {} + }); + chip.append(txt, rm); + chips.append(chip); + } + } catch {} + }; + renderChips(); + + // Range group chips + const grpChips = q('.overlay-settings .sp-holidays-range-chips'); + const renderGrpChips = ()=>{ + try { + if (!grpChips) return; + grpChips.innerHTML = ''; + const groups = Array.isArray(this._settingsDraft?.holidays_groups) ? this._settingsDraft.holidays_groups : []; + for (const g of groups) { + const chip = document.createElement('span'); chip.className='pill-chip'; + const txt = document.createElement('span'); txt.textContent = `${g.from} – ${g.to}`; + const rm = document.createElement('button'); rm.type='button'; rm.className='rm'; rm.textContent='×'; + rm.addEventListener('click', ()=>{ + try { + if (!this._settingsDraft) return; + const all = Array.isArray(this._settingsDraft.holidays_groups) ? this._settingsDraft.holidays_groups : []; + const others = all.filter(x=>x!==g); + const othersDates = new Set([].concat(...others.map(o=>o.dates||[])).map(String)); + const cur = new Set((this._settingsDraft.holidays_dates||[]).map(String)); + for (const d of (g.dates||[])) { if (!othersDates.has(String(d))) cur.delete(String(d)); } + this._settingsDraft.holidays_groups = others; + this._settingsDraft.holidays_dates = Array.from(cur).sort(); + this._holidaysGroupsTouched = true; + this._holidaysDatesTouched = true; + renderChips(); renderGrpChips(); + } catch {} + }); + chip.append(txt, rm); grpChips.append(chip); + } + } catch {} + }; + renderGrpChips(); + + // Bind events once + if (!page.dataset.bound) { + page.dataset.bound = '1'; + + try { + const sw = q('.overlay-settings .sp-holidays-enable'); + if (sw) sw.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + this._settingsDraft.holidays_enabled = !!e.target.checked; + this._holidaysToggleTouched = true; + this._renderSettingsPopupHolidaysTab(); + } catch {} + }); + } catch {} + + try { + const btn = q('.overlay-settings .sp-holidays-edit-btn'); + if (btn) btn.addEventListener('click', ()=>{ + try { + const eid = (this._config?.entities || [])[0] || null; + if (!eid) return; + this._resumeSettingsAfterHoliday = true; + this._suspendSettingsPopup(); + this._openHolidayEditor(eid); + } catch {} + }); + } catch {} + + try { + const bCal = q('.overlay-settings .sp-holidays-source-cal'); + const bMan = q('.overlay-settings .sp-holidays-source-man'); + if (bCal) bCal.addEventListener('click', ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.holidays_source = 'calendar'; this._holidaysSourceTouched = true; this._renderSettingsPopupHolidaysTab(); } catch {} }); + if (bMan) bMan.addEventListener('click', ()=>{ try { if (!this._settingsDraft) return; this._settingsDraft.holidays_source = 'manual'; this._holidaysSourceTouched = true; this._renderSettingsPopupHolidaysTab(); } catch {} }); + } catch {} + + try { + const p = q('.overlay-settings .sp-holidays-entity-picker'); + if (p) p.addEventListener('value-changed', (e)=>{ try { if (!this._settingsDraft) return; this._settingsDraft.holidays_entity = e.detail?.value || ''; this._holidaysEntityTouched = true; } catch {} }); + } catch {} + + try { + const inp = q('.overlay-settings .sp-holidays-date-input'); + const add = q('.overlay-settings .sp-holidays-add-date'); + const msg = q('.overlay-settings .sp-holidays-add-date-msg'); + const onAdd = ()=>{ + try { + if (!this._settingsDraft || !inp) return; + const v = String(inp.value || '').trim(); + if (!v) return; + if (!/^\d{4}-\d{2}-\d{2}$/.test(v)) return; + // Show info if v is inside any existing range; do not add + try { + const groups = Array.isArray(this._settingsDraft.holidays_groups) ? this._settingsDraft.holidays_groups : []; + const covered = groups.some(g => Array.isArray(g?.dates) && g.dates.map(String).includes(String(v))); + if (covered) { if (msg) msg.textContent = this._t('holidays.date_in_range') || 'Date already covered by a range'; return; } + } catch {} + const cur = Array.isArray(this._settingsDraft.holidays_dates) ? this._settingsDraft.holidays_dates.filter(Boolean).map(String) : []; + const next = Array.from(new Set(cur.concat([v]))).sort(); + this._settingsDraft.holidays_dates = next; + this._holidaysDatesTouched = true; + try { inp.value = ''; if (msg) msg.textContent = ''; } catch {} + renderChips(); + } catch {} + }; + if (add) add.addEventListener('click', onAdd); + if (inp) inp.addEventListener('keydown', (e)=>{ if (e.key === 'Enter') { try { e.preventDefault(); } catch {} onAdd(); } }); + } catch {} + + // Range add in settings overlay + try { + const inpf = q('.overlay-settings .sp-holidays-range-from'); + const inpt = q('.overlay-settings .sp-holidays-range-to'); + const addr = q('.overlay-settings .sp-holidays-add-range'); + const onAddRange = ()=>{ + try { + if (!this._settingsDraft || !inpf || !inpt) return; + const f = String(inpf.value||'').trim(); + const t = String(inpt.value||'').trim(); + if (!f || !t) return; + if (!/^\d{4}-\d{2}-\d{2}$/.test(f) || !/^\d{4}-\d{2}-\d{2}$/.test(t)) return; + let d1 = new Date(f+'T00:00:00'); + let d2 = new Date(t+'T00:00:00'); + if (isNaN(d1.getTime()) || isNaN(d2.getTime())) return; + if (d1.getTime() > d2.getTime()) { const tmp=d1; d1=d2; d2=tmp; } + const fmt = (d)=>{ const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`; }; + const curArr = Array.isArray(this._settingsDraft.holidays_dates) ? this._settingsDraft.holidays_dates.filter(Boolean).map(String) : []; + const cur = new Set(curArr); + const grpDates = []; + for (let d=new Date(d1); d.getTime()<=d2.getTime(); d.setDate(d.getDate()+1)) { const s=fmt(d); cur.add(s); grpDates.push(s); } + this._settingsDraft.holidays_dates = Array.from(cur).sort(); + // Add a group tag for UI + const gid = 'hg_'+Date.now()+'_'+Math.floor(Math.random()*1e6); + const groups = Array.isArray(this._settingsDraft.holidays_groups) ? [...this._settingsDraft.holidays_groups] : []; + groups.push({ id: gid, from: fmt(d1), to: fmt(d2), dates: grpDates }); + this._settingsDraft.holidays_groups = groups; + this._holidaysGroupsTouched = true; + this._holidaysDatesTouched = true; + // Mirror into live config so the main card view updates as well + try { + this._config.holidays_dates = Array.from(cur).sort(); + const liveGroups = Array.isArray(this._config.holidays_groups) ? [...this._config.holidays_groups] : []; + liveGroups.push({ id: gid, from: fmt(d1), to: fmt(d2), dates: grpDates.slice() }); + this._config.holidays_groups = liveGroups; + } catch {} + try { inpf.value=''; inpt.value=''; } catch {} + renderChips(); + try { renderGrpChips(); } catch {} + } catch {} + }; + if (addr) addr.addEventListener('click', onAddRange); + if (inpf) inpf.addEventListener('keydown', (e)=>{ if (e.key==='Enter') { try { e.preventDefault(); } catch {} onAddRange(); } }); + if (inpt) inpt.addEventListener('keydown', (e)=>{ if (e.key==='Enter') { try { e.preventDefault(); } catch {} onAddRange(); } }); + } catch {} + } + } catch {} + } + + _renderSettingsPopupColorsTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="colors"]'); + if (!page) return; + page.innerHTML = ''; + + // Wrap the entire Colors tab in one settings-card (border around everything) + const outerCard = document.createElement('div'); + outerCard.className = 'settings-card'; + try { + outerCard.style.padding = '10px'; + outerCard.style.display = 'grid'; + outerCard.style.gap = '10px'; + } catch {} + page.append(outerCard); + + // Ensure draft shape + try { + if (typeof d.color_global !== 'boolean') d.color_global = !!(this._config?.color_global); + if (!d.color_ranges || typeof d.color_ranges !== 'object') d.color_ranges = {}; + } catch {} + + const wantF = String(d.temp_unit || 'C').toUpperCase() === 'F'; + const toDisp = (c)=>{ const v = Number(c); if (!Number.isFinite(v)) return 0; return wantF ? Math.round((v*9/5+32)*10)/10 : v; }; + const fromDisp = (v)=>{ const n = Number(v); if (!Number.isFinite(n)) return NaN; return wantF ? ((n-32)*5/9) : n; }; + const minC = Number.isFinite(Number(d.min_temp)) ? Number(d.min_temp) : (Number.isFinite(Number(this._config?.min_temp)) ? Number(this._config.min_temp) : 5); + const maxC = Number.isFinite(Number(d.max_temp)) ? Number(d.max_temp) : (Number.isFinite(Number(this._config?.max_temp)) ? Number(this._config.max_temp) : 25); + const minDisp = toDisp(minC); + const maxDisp = toDisp(maxC); + const defC = Number.isFinite(Number(d.default_temp)) ? Number(d.default_temp) : (Number.isFinite(Number(this._config?.default_temp)) ? Number(this._config.default_temp) : 20); + + // Mode toggle (Per room | All rooms) + const modeWrap = document.createElement('div'); + modeWrap.style.display = 'flex'; + modeWrap.style.alignItems = 'center'; + modeWrap.style.justifyContent = 'space-between'; + modeWrap.style.gap = '10px'; + const modeText = document.createElement('div'); + modeText.style.display = 'grid'; + modeText.style.gap = '2px'; + const modeTitle = document.createElement('div'); + modeTitle.className = 'colors-mode-title'; + modeTitle.style.fontWeight = '600'; + modeTitle.textContent = this._t('editor.colors.mode') || 'Color mode'; + const modeDesc = document.createElement('div'); + modeDesc.className = 'colors-mode-desc'; + modeDesc.style.fontSize = '.85rem'; + modeDesc.style.color = 'var(--secondary-text-color)'; + modeDesc.textContent = this._t('editor.colors.mode.desc') || 'Choose per-room colors or one set for all rooms.'; + modeText.append(modeTitle, modeDesc); + const seg = document.createElement('div'); seg.className = 'seg colors-mode'; + const bRoom = document.createElement('button'); bRoom.type = 'button'; bRoom.className = 'mode-room'; + bRoom.textContent = this._t('editor.colors.mode_per_room') || 'Per room'; + const bGlob = document.createElement('button'); bGlob.type = 'button'; bGlob.className = 'mode-global'; + bGlob.textContent = this._t('editor.colors.mode_global') || 'All rooms'; + const applySegUi = ()=>{ + const on = !!d.color_global; + bGlob.classList.toggle('active', on); + bRoom.classList.toggle('active', !on); + }; + bRoom.onclick = ()=>{ try { d.color_global = false; applySegUi(); this._renderSettingsPopupColorsTab(); } catch {} }; + bGlob.onclick = ()=>{ try { d.color_global = true; applySegUi(); this._renderSettingsPopupColorsTab(); } catch {} }; + seg.append(bRoom, bGlob); + applySegUi(); + modeWrap.append(modeText, seg); + outerCard.append(modeWrap); + + const isGlobal = !!d.color_global; + const ents = Array.isArray(this._config?.entities) ? this._config.entities.filter(Boolean) : []; + + // If per-room mode and there are no rooms yet, show helper box + if (!isGlobal && ents.length === 0) { + const box = document.createElement('div'); box.className = 'empty-box'; + const p = document.createElement('div'); p.textContent = this._t('editor.colors.empty') || 'No rooms added yet.'; + const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'add-entity-btn'; + btn.innerHTML = '' + (this._t('editor.colors.go_add') || 'Add room') + ''; + btn.onclick = ()=>{ this._setSettingsPopupTab('rooms'); }; + box.append(p, btn); + outerCard.append(box); + } + + // Remember open/closed state while popup is open + try { if (!this._openPopupColorRows) this._openPopupColorRows = new Set(); } catch {} + + const roomsWrap = document.createElement('div'); + try { roomsWrap.style.display = 'grid'; roomsWrap.style.gap = '10px'; } catch {} + outerCard.append(roomsWrap); + + const buildFoldout = (key, titleText)=>{ + const line = document.createElement('div'); line.className = 'inline'; + const summary = document.createElement('div'); summary.className = 'summary'; + const sumLeft = document.createElement('div'); sumLeft.className = 'summary-left'; + const exp = document.createElement('button'); exp.type = 'button'; exp.className = 'expander'; + exp.innerHTML = ''; + const txtWrap = document.createElement('div'); txtWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); titleSpan.className = 'summary-title'; + titleSpan.textContent = titleText; + + // Inline styles to match the editor foldout appearance (robust across contexts) + try { + summary.style.display = 'flex'; + summary.style.alignItems = 'center'; + summary.style.justifyContent = 'space-between'; + summary.style.padding = '6px'; + summary.style.border = '1px dashed var(--divider-color)'; + summary.style.borderRadius = '8px'; + summary.style.background = 'var(--secondary-background-color, transparent)'; + sumLeft.style.display = 'flex'; + sumLeft.style.alignItems = 'center'; + sumLeft.style.gap = '8px'; + sumLeft.style.minWidth = '0'; + txtWrap.style.display = 'grid'; + txtWrap.style.gap = '2px'; + txtWrap.style.minWidth = '0'; + titleSpan.style.fontWeight = '600'; + titleSpan.style.overflow = 'hidden'; + titleSpan.style.textOverflow = 'ellipsis'; + titleSpan.style.whiteSpace = 'nowrap'; + titleSpan.style.maxWidth = '320px'; + exp.style.width = '28px'; + exp.style.height = '28px'; + exp.style.borderRadius = '8px'; + exp.style.border = '1px solid var(--divider-color)'; + exp.style.background = 'var(--card-background-color)'; + exp.style.display = 'inline-flex'; + exp.style.alignItems = 'center'; + exp.style.justifyContent = 'center'; + exp.style.cursor = 'pointer'; + } catch {} + + txtWrap.append(titleSpan); + sumLeft.append(exp, txtWrap); + summary.append(sumLeft); + line.append(summary); + + const details = document.createElement('div'); details.className = 'details'; + line.append(details); + + try { + details.style.display = 'none'; + details.style.gridTemplateColumns = 'minmax(250px, 440px)'; + details.style.gap = '6px'; + details.style.padding = '6px'; + details.style.border = '1px dashed var(--divider-color)'; + details.style.borderRadius = '8px'; + details.style.background = 'var(--secondary-background-color, transparent)'; + } catch {} + + const applyOpen = (isOpen)=>{ + try { + if (isOpen) { + line.classList.add('open'); + details.style.display = 'grid'; + exp.style.transform = 'rotate(180deg)'; + } else { + line.classList.remove('open'); + details.style.display = 'none'; + exp.style.transform = ''; + } + } catch {} + }; + const toggle = (ev)=>{ + try { ev?.stopPropagation?.(); } catch {} + const nextOpen = !line.classList.contains('open'); + applyOpen(nextOpen); + try { + if (!this._openPopupColorRows) this._openPopupColorRows = new Set(); + if (nextOpen) this._openPopupColorRows.add(String(key)); + else this._openPopupColorRows.delete(String(key)); + } catch {} + }; + exp.addEventListener('click', toggle); + summary.style.cursor = 'pointer'; + summary.addEventListener('click', (ev)=>{ + try { if (ev?.target?.closest?.('button, a, input, select, textarea')) return; } catch {} + toggle(ev); + }); + try { applyOpen(!!this._openPopupColorRows?.has(String(key))); } catch {} + + return { line, details }; + }; + + const renderRangesForKey = (key, details)=>{ + details.innerHTML = ''; + const card = document.createElement('div'); + card.className = 'settings-card'; + try { card.style.padding = '10px'; card.style.gap = '8px'; } catch {} + details.append(card); + + const head = document.createElement('div'); + head.className = 'colors-head'; + try { + head.style.display = 'grid'; + head.style.gridTemplateColumns = '88px 88px 70px 1fr'; + head.style.gap = '8px'; + head.style.alignItems = 'end'; + } catch {} + const mkHead = (k)=>{ + const el = document.createElement('div'); + el.className = 'slabel'; + el.textContent = this._t(k) || k; + return el; + }; + head.append( + mkHead('editor.colors.col_from'), + mkHead('editor.colors.col_to'), + mkHead('editor.colors.col_color'), + document.createElement('div') + ); + + const getArr = ()=>{ + const map = (d.color_ranges && typeof d.color_ranges === 'object') ? d.color_ranges : {}; + return Array.isArray(map[key]) ? map[key] : []; + }; + const setArr = (arr)=>{ + try { + const map = (d.color_ranges && typeof d.color_ranges === 'object') ? d.color_ranges : {}; + d.color_ranges = { ...map, [key]: arr }; + } catch {} + }; + + const ranges = getArr(); + if (ranges.length > 0) card.append(head); + + ranges.forEach((r, ri)=>{ + const outer = document.createElement('div'); outer.className = 'color-item'; + const row = document.createElement('div'); row.className = 'colors-row'; + try { + row.style.display = 'grid'; + row.style.gridTemplateColumns = '88px 88px 70px 1fr'; + row.style.gap = '8px'; + row.style.alignItems = 'center'; + } catch {} + const f = document.createElement('input'); + f.type = 'number'; f.step = '0.5'; + f.className = 'settings-input'; + f.value = String(toDisp(r?.from ?? defC)); + f.setAttribute('aria-label', this._t('editor.colors.col_from')); + f.title = this._t('editor.colors.col_from'); + const t = document.createElement('input'); + t.type = 'number'; t.step = '0.5'; + t.className = 'settings-input'; + t.value = String(toDisp(r?.to ?? defC)); + t.setAttribute('aria-label', this._t('editor.colors.col_to')); + t.title = this._t('editor.colors.col_to'); + const c = document.createElement('input'); + c.type = 'color'; c.value = String(r?.color || '#ffb347'); + c.setAttribute('aria-label', this._t('editor.colors.col_color')); + c.title = this._t('editor.colors.col_color'); + try { + c.style.height = '28px'; + c.style.width = '70px'; + c.style.padding = '0'; + c.style.borderRadius = '8px'; + c.style.border = '1px solid var(--divider-color)'; + c.style.background = 'var(--card-background-color)'; + c.style.boxSizing = 'border-box'; + } catch {} + + f.min = String(minDisp); t.min = String(minDisp); + f.max = String(maxDisp); t.max = String(maxDisp); + + const rm = document.createElement('button'); + rm.type = 'button'; + rm.className = 'btn ghost danger'; + rm.innerHTML = '' + (this._t('editor.remove') || 'Remove') + ''; + try { + rm.style.height = '28px'; + rm.style.padding = '2px 10px'; + rm.style.borderRadius = '8px'; + rm.style.justifySelf = 'end'; + rm.style.width = 'max-content'; + rm.style.display = 'inline-flex'; + rm.style.alignItems = 'center'; + rm.style.gap = '6px'; + rm.querySelector?.('ha-icon')?.style && (rm.querySelector('ha-icon').style.setProperty('--mdc-icon-size','16px')); + } catch {} + + const updateAt = (idx, patch)=>{ + const arr = getArr(); + const next = arr.map((x, i)=> (i===idx ? ({ ...x, ...patch }) : x)); + setArr(next); + }; + + f.onchange = ()=>{ + let v = parseFloat(String(f.value||'').replace(',','.')); + if (!Number.isFinite(v)) v = minDisp; + v = Math.max(minDisp, Math.min(maxDisp, v)); + f.value = String(v); + const vC = fromDisp(v); + const clampC = Math.max(minC, Math.min(maxC, Number.isFinite(vC) ? vC : defC)); + updateAt(ri, { from: clampC }); + }; + t.onchange = ()=>{ + let v = parseFloat(String(t.value||'').replace(',','.')); + if (!Number.isFinite(v)) v = minDisp; + v = Math.max(minDisp, Math.min(maxDisp, v)); + t.value = String(v); + const vC = fromDisp(v); + const clampC = Math.max(minC, Math.min(maxC, Number.isFinite(vC) ? vC : defC)); + updateAt(ri, { to: clampC }); + }; + c.onchange = ()=>{ updateAt(ri, { color: c.value }); }; + rm.onclick = ()=>{ + const arr = getArr(); + const next = [...arr]; + next.splice(ri, 1); + setArr(next); + renderRangesForKey(key, details); + }; + + row.append(f, t, c, rm); + outer.append(row); + card.append(outer); + }); + + const add = document.createElement('button'); + add.className = 'add-entity-btn'; + add.type = 'button'; + add.innerHTML = '' + (this._t('editor.heat_colors.add') || 'Add color range') + ''; + try { add.style.width = 'max-content'; add.style.justifySelf = 'start'; } catch {} + add.onclick = ()=>{ + const arr = getArr(); + const next = [...arr, { from: defC, to: defC, color: '#ffb347' }]; + setArr(next); + renderRangesForKey(key, details); + }; + card.append(add); + }; + + if (isGlobal) { + const key = '*'; + const f = buildFoldout(key, this._t('editor.colors.global_title') || 'All rooms'); + roomsWrap.append(f.line); + renderRangesForKey(key, f.details); + } else { + for (const eid of ents) { + const labels = this._config?.labels || {}; + const title = labels?.[eid] || this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid); + const f = buildFoldout(eid, title); + roomsWrap.append(f.line); + renderRangesForKey(eid, f.details); + } + } + + // Actions (Update schedule + Clear all) + const actions = document.createElement('div'); + actions.className = 'colors-actions'; + actions.style.display = 'flex'; + actions.style.gap = '8px'; + actions.style.alignItems = 'center'; + actions.style.flexWrap = 'wrap'; + + const clrAll = document.createElement('button'); + clrAll.className = 'remove-btn clr-colors-all-btn'; + clrAll.type = 'button'; + try { clrAll.style.width = 'max-content'; clrAll.style.justifySelf = 'start'; } catch {} + clrAll.innerHTML = '' + (this._t('editor.colors.clear_all') || 'Clear all color data') + ''; + clrAll.onclick = ()=>{ + const ok = confirm(this._t('editor.colors.clear_all_confirm') || 'This will delete all color ranges. Continue?'); + if (!ok) return; + try { d.color_ranges = {}; } catch {} + this._renderSettingsPopupColorsTab(); + }; + + actions.append(clrAll); + outerCard.append(actions); + } catch {} + } + + _renderSettingsPopupBoilerTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="boiler"]'); + if (!page) return; + + const wantF = String(d?.temp_unit || 'C').toUpperCase() === 'F'; + const toDispDelta = (nC)=>{ + const v = Number(nC); + if (!Number.isFinite(v)) return 0; + return wantF ? (v * 9/5) : v; + }; + const fromDispDelta = (nDisp)=>{ + const v = Number(nDisp); + if (!Number.isFinite(v)) return NaN; + return wantF ? (v * 5/9) : v; + }; + const round1 = (n)=>{ + const v = Number(n); + if (!Number.isFinite(v)) return 0; + return Math.round(v * 10) / 10; + }; + + if (!page.dataset.built) { + page.dataset.built = '1'; + page.innerHTML = ` +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    +
    + +
    +
    +
    + +
    +
    +
    +
    + `; + } + + const q = (sel)=> this.shadowRoot.querySelector(sel); + + // Visibility of dependent controls + const boilerOn = !!d.boiler_enabled; + const multiOn = !!d.boiler_multi_enabled; + try { + const switchRow = q('.overlay-settings .sp-boiler-switch-setting'); + const roomsRow = q('.overlay-settings .sp-boiler-rooms-setting'); + const offRow = q('.overlay-settings .sp-boiler-offsets-setting'); + const multiRow = q('.overlay-settings .sp-boiler-multi-setting'); + const multiRoomsRow = q('.overlay-settings .sp-boiler-multi-rooms-setting'); + if (switchRow) switchRow.style.display = (boilerOn && !multiOn) ? '' : 'none'; + if (roomsRow) roomsRow.style.display = (boilerOn && !multiOn) ? '' : 'none'; + if (offRow) offRow.style.display = (boilerOn && !multiOn) ? '' : 'none'; + if (multiRow) multiRow.style.display = boilerOn ? '' : 'none'; + if (multiRoomsRow) multiRoomsRow.style.display = (boilerOn && multiOn) ? '' : 'none'; + } catch {} + + // Master toggle + try { + const sw = q('.overlay-settings .sp-boiler-enable'); + if (sw) sw.checked = !!d.boiler_enabled; + } catch {} + + // Multi toggle + try { + const mw = q('.overlay-settings .sp-boiler-multi'); + if (mw) mw.checked = !!d.boiler_multi_enabled; + } catch {} + + // Pickers + try { + const domSel = q('.overlay-settings .sp-boiler-switch-domain'); + const p = q('.overlay-settings .sp-boiler-switch-picker'); + const swDom = String(d.boiler_switch || '').split('.')[0]; + const raw = String(d.boiler_switch_domain || '').toLowerCase().trim(); + const dom = (swDom === 'input_boolean' || swDom === 'switch') + ? swDom + : ((raw === 'input_boolean') ? 'input_boolean' : 'switch'); + try { d.boiler_switch_domain = dom; } catch {} + if (domSel) domSel.value = dom; + if (p) { + try { p.setAttribute('include-domains', JSON.stringify([dom])); } catch {} + p.hass = this._hass; + p.value = String(d.boiler_switch || ''); + } + } catch {} + + // Rooms list (checkboxes) + try { + const list = q('.overlay-settings .sp-boiler-rooms-list'); + if (list) { + list.innerHTML = ''; + const ents = Array.isArray(d.entities) ? d.entities.map(String) : []; + const rooms = ents.filter(e=>String(e).startsWith('climate.')); + const selected = Array.isArray(d.boiler_rooms) ? new Set(d.boiler_rooms.map(String)) : new Set(rooms); + const labels = (this._config && this._config.labels && typeof this._config.labels === 'object') ? this._config.labels : {}; + const byName = (eid)=>{ + try { + const lbl = labels && Object.prototype.hasOwnProperty.call(labels, eid) ? String(labels[eid]||'').trim() : ''; + if (lbl) return lbl; + return String(this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid)); + } catch { return eid; } + }; + for (const eid of rooms) { + const row = document.createElement('label'); + const cb = document.createElement('input'); + cb.type = 'checkbox'; + cb.checked = selected.has(eid); + cb.setAttribute('data-eid', eid); + cb.addEventListener('change', (ev)=>{ + try { + if (!this._settingsDraft) return; + const cur = Array.isArray(this._settingsDraft.boiler_rooms) ? this._settingsDraft.boiler_rooms.map(String) : rooms; + const curSet = new Set(cur); + const on = !!ev?.target?.checked; + if (on) curSet.add(eid); else curSet.delete(eid); + // Preserve order as in entities[] + this._settingsDraft.boiler_rooms = rooms.filter(x => curSet.has(x)); + } catch {} + }); + const sp = document.createElement('span'); + sp.textContent = byName(eid); + row.append(cb, sp); + list.append(row); + } + } + } catch {} + + // Multi-boiler rooms list (per-room foldouts) + try { + const list = q('.overlay-settings .sp-boiler-multi-rooms-list'); + if (list) { + list.innerHTML = ''; + const ents = Array.isArray(d.entities) ? d.entities.map(String) : []; + const rooms = ents.filter(e=>String(e).startsWith('climate.')); + const labels = (this._config && this._config.labels && typeof this._config.labels === 'object') ? this._config.labels : {}; + const byName = (eid)=>{ + try { + const lbl = labels && Object.prototype.hasOwnProperty.call(labels, eid) ? String(labels[eid]||'').trim() : ''; + if (lbl) return lbl; + return String(this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid)); + } catch { return eid; } + }; + if (!this._openBoilerMultiRooms) this._openBoilerMultiRooms = new Set(); + const ensureDraftMap = ()=>{ + if (!this._settingsDraft) return; + if (!this._settingsDraft.boiler_room_settings || typeof this._settingsDraft.boiler_room_settings !== 'object') this._settingsDraft.boiler_room_settings = {}; + }; + const getRoomCfg = (eid)=>{ + const src = (d.boiler_room_settings && typeof d.boiler_room_settings === 'object') ? d.boiler_room_settings : {}; + const raw = (src && Object.prototype.hasOwnProperty.call(src, eid)) ? src[eid] : null; + const sw = String(raw?.switch || ''); + const domFromEid = (sw ? String(sw).split('.')[0] : ''); + const rawDom = String(raw?.switch_domain || '').toLowerCase().trim(); + const dom = (domFromEid === 'input_boolean' || domFromEid === 'switch') + ? domFromEid + : ((rawDom === 'input_boolean') ? 'input_boolean' : 'switch'); + const onOff = Number.isFinite(Number(raw?.on_offset)) ? Number(raw.on_offset) : Number(d.boiler_on_offset ?? 0); + const offOff = Number.isFinite(Number(raw?.off_offset)) ? Number(raw.off_offset) : Number(d.boiler_off_offset ?? 0); + const enabled = !!raw?.enabled; + return { enabled, switch: sw, switch_domain: dom, on_offset: onOff, off_offset: offOff }; + }; + const updateRoomCfg = (eid, patch)=>{ + try { + if (!this._settingsDraft) return; + ensureDraftMap(); + const prev = (this._settingsDraft.boiler_room_settings && this._settingsDraft.boiler_room_settings[eid]) ? this._settingsDraft.boiler_room_settings[eid] : {}; + this._settingsDraft.boiler_room_settings[eid] = { ...prev, ...patch }; + } catch {} + }; + + for (const eid of rooms) { + const cfg = getRoomCfg(eid); + + const line = document.createElement('div'); + line.className = 'inline'; + + const summary = document.createElement('div'); + summary.className = 'summary'; + const sumLeft = document.createElement('div'); + sumLeft.className = 'summary-left'; + const exp = document.createElement('button'); + exp.type = 'button'; + exp.className = 'expander'; + exp.innerHTML = ''; + + const txtWrap = document.createElement('div'); + txtWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); + titleSpan.className = 'summary-title'; + titleSpan.textContent = byName(eid); + const subSpan = document.createElement('span'); + subSpan.className = 'summary-sub'; + const swLabel = (cfg.switch ? (this._hass?.states?.[cfg.switch]?.attributes?.friendly_name || cfg.switch) : ''); + subSpan.textContent = cfg.enabled ? (swLabel || '') : ''; + + txtWrap.append(titleSpan, subSpan); + sumLeft.append(exp, txtWrap); + summary.append(sumLeft); + line.append(summary); + + const details = document.createElement('div'); + details.className = 'details'; + line.append(details); + + // Styles (match other popup foldouts) + try { + summary.style.display = 'flex'; + summary.style.alignItems = 'center'; + summary.style.justifyContent = 'space-between'; + summary.style.padding = '6px'; + summary.style.border = '1px dashed var(--divider-color)'; + summary.style.borderRadius = '8px'; + summary.style.background = 'var(--secondary-background-color, transparent)'; + sumLeft.style.display = 'flex'; + sumLeft.style.alignItems = 'center'; + sumLeft.style.gap = '8px'; + sumLeft.style.minWidth = '0'; + txtWrap.style.display = 'grid'; + txtWrap.style.gap = '2px'; + txtWrap.style.minWidth = '0'; + titleSpan.style.fontWeight = '600'; + titleSpan.style.overflow = 'hidden'; + titleSpan.style.textOverflow = 'ellipsis'; + titleSpan.style.whiteSpace = 'nowrap'; + titleSpan.style.maxWidth = '320px'; + subSpan.style.fontSize = '.85rem'; + subSpan.style.color = 'var(--secondary-text-color)'; + subSpan.style.overflow = 'hidden'; + subSpan.style.textOverflow = 'ellipsis'; + subSpan.style.whiteSpace = 'nowrap'; + subSpan.style.maxWidth = '320px'; + exp.style.width = '28px'; + exp.style.height = '28px'; + exp.style.borderRadius = '8px'; + exp.style.border = '1px solid var(--divider-color)'; + exp.style.background = 'var(--card-background-color)'; + exp.style.display = 'inline-flex'; + exp.style.alignItems = 'center'; + exp.style.justifyContent = 'center'; + exp.style.cursor = 'pointer'; + details.style.display = 'none'; + details.style.gridTemplateColumns = 'minmax(250px, 520px)'; + details.style.gap = '8px'; + details.style.padding = '8px'; + details.style.border = '1px dashed var(--divider-color)'; + details.style.borderRadius = '8px'; + details.style.background = 'var(--secondary-background-color, transparent)'; + } catch {} + + const rowKey = String(eid); + const applyOpen = (isOpen)=>{ + try { + if (isOpen) { + line.classList.add('open'); + details.style.display = 'grid'; + exp.style.transform = 'rotate(180deg)'; + } else { + line.classList.remove('open'); + details.style.display = 'none'; + exp.style.transform = ''; + } + } catch {} + }; + const toggle = (ev)=>{ + try { ev?.stopPropagation?.(); } catch {} + const nextOpen = !line.classList.contains('open'); + applyOpen(nextOpen); + try { + if (!this._openBoilerMultiRooms) this._openBoilerMultiRooms = new Set(); + if (nextOpen) this._openBoilerMultiRooms.add(rowKey); + else this._openBoilerMultiRooms.delete(rowKey); + } catch {} + }; + exp.addEventListener('click', toggle); + summary.style.cursor = 'pointer'; + summary.addEventListener('click', (ev)=>{ + try { if (ev?.target?.closest?.('button, a, input, select, textarea, tt-entity-picker, ha-entity-picker, ha-switch')) return; } catch {} + toggle(ev); + }); + try { applyOpen(!!this._openBoilerMultiRooms?.has(rowKey)); } catch {} + + // Details content + const left = document.createElement('div'); + left.style.display = 'grid'; + left.style.gap = '6px'; + + const includeRow = document.createElement('div'); + includeRow.style.display = 'flex'; + includeRow.style.alignItems = 'center'; + includeRow.style.justifyContent = 'space-between'; + includeRow.style.gap = '10px'; + const incText = document.createElement('div'); + incText.style.display = 'grid'; + incText.style.gap = '2px'; + const incTitle = document.createElement('div'); + incTitle.style.fontWeight = '600'; + incTitle.textContent = this._t('boiler.room.enable') || 'Include room'; + const incDesc = document.createElement('div'); + incDesc.style.fontSize = '.85rem'; + incDesc.style.color = 'var(--secondary-text-color)'; + incDesc.textContent = this._t('boiler.room.enable.desc') || ''; + incText.append(incTitle, incDesc); + const incSwitch = document.createElement('ha-switch'); + incSwitch.checked = !!cfg.enabled; + incSwitch.addEventListener('change', (e)=>{ + updateRoomCfg(eid, { enabled: !!e.target.checked }); + this._renderSettingsPopupBoilerTab(); + }); + includeRow.append(incText, incSwitch); + + const domRow = document.createElement('div'); + domRow.className = 'sfield'; + const domLabel = document.createElement('div'); + domLabel.className = 'slabel'; + domLabel.textContent = this._t('boiler.switch_type') || 'Entity type'; + const domSel = document.createElement('select'); + domSel.className = 'settings-input'; + domSel.innerHTML = ''; + domSel.value = (cfg.switch_domain === 'input_boolean') ? 'input_boolean' : 'switch'; + domSel.addEventListener('change', (e)=>{ + const v = String(e?.target?.value || '').toLowerCase().trim(); + updateRoomCfg(eid, { switch_domain: (v === 'input_boolean') ? 'input_boolean' : 'switch', switch: '' }); + this._renderSettingsPopupBoilerTab(); + }); + domRow.append(domLabel, domSel); + + const pick = document.createElement('tt-entity-picker'); + try { pick.style.display = 'block'; pick.style.minHeight = '48px'; pick.style.maxWidth = '440px'; } catch {} + try { pick.hass = this._hass; } catch {} + pick.setAttribute('include-domains', JSON.stringify([(cfg.switch_domain === 'input_boolean') ? 'input_boolean' : 'switch'])); + pick.setAttribute('label', this._t('boiler.switch') || 'Boiler switch'); + try { pick.value = String(cfg.switch || ''); } catch {} + pick.addEventListener('value-changed', (e)=>{ + try { + const v = String(e.detail?.value || ''); + const dom = String(v||'').split('.')[0]; + const domVal = (dom === 'input_boolean') ? 'input_boolean' : 'switch'; + updateRoomCfg(eid, { switch: v, switch_domain: domVal }); + this._renderSettingsPopupBoilerTab(); + } catch {} + }); + + const offsetsTitle = document.createElement('div'); + offsetsTitle.style.fontWeight = '600'; + offsetsTitle.textContent = this._t('boiler.offsets') || 'Offsets'; + const offsetsDesc = document.createElement('div'); + offsetsDesc.style.fontSize = '.85rem'; + offsetsDesc.style.color = 'var(--secondary-text-color)'; + offsetsDesc.textContent = this._t('boiler.offsets.desc') || ''; + + const grid = document.createElement('div'); + grid.className = 'grid2'; + grid.style.marginTop = '6px'; + const onField = document.createElement('div'); + onField.className = 'sfield'; + const onLabel = document.createElement('div'); + onLabel.className = 'slabel'; + onLabel.textContent = String(this._t('boiler.on_offset') || '').replace('°C', this._unitSymbol()); + const onInput = document.createElement('input'); + onInput.className = 'settings-input'; + onInput.type = 'number'; + onInput.step = '0.1'; + onInput.min = String(wantF ? -18 : -10); + onInput.max = String(wantF ? 18 : 10); + onInput.value = String(round1(toDispDelta(cfg.on_offset ?? 0))); + onInput.addEventListener('change', (e)=>{ + try { + const raw = String(e?.target?.value ?? '').trim().replace(',', '.'); + if (raw === '') return; + const nDisp = Number(raw); + const nC = fromDispDelta(nDisp); + if (!Number.isFinite(nC)) return; + updateRoomCfg(eid, { on_offset: Math.max(-10, Math.min(10, nC)) }); + } catch {} + }); + onField.append(onLabel, onInput); + + const offField = document.createElement('div'); + offField.className = 'sfield'; + const offLabel = document.createElement('div'); + offLabel.className = 'slabel'; + offLabel.textContent = String(this._t('boiler.off_offset') || '').replace('°C', this._unitSymbol()); + const offInput = document.createElement('input'); + offInput.className = 'settings-input'; + offInput.type = 'number'; + offInput.step = '0.1'; + offInput.min = String(wantF ? -18 : -10); + offInput.max = String(wantF ? 18 : 10); + offInput.value = String(round1(toDispDelta(cfg.off_offset ?? 0))); + offInput.addEventListener('change', (e)=>{ + try { + const raw = String(e?.target?.value ?? '').trim().replace(',', '.'); + if (raw === '') return; + const nDisp = Number(raw); + const nC = fromDispDelta(nDisp); + if (!Number.isFinite(nC)) return; + updateRoomCfg(eid, { off_offset: Math.max(-10, Math.min(10, nC)) }); + } catch {} + }); + offField.append(offLabel, offInput); + grid.append(onField, offField); + + left.append(includeRow, domRow, pick, offsetsTitle, offsetsDesc, grid); + details.append(left); + + list.append(line); + } + } + } catch {} + + // Offsets + try { + const onIn = q('.overlay-settings .sp-boiler-on-offset'); + if (onIn) { + const lim = wantF ? 18 : 10; + onIn.min = String(-lim); + onIn.max = String(lim); + onIn.value = String(round1(toDispDelta(d.boiler_on_offset ?? 0))); + } + } catch {} + try { + const offIn = q('.overlay-settings .sp-boiler-off-offset'); + if (offIn) { + const lim = wantF ? 18 : 10; + offIn.min = String(-lim); + offIn.max = String(lim); + offIn.value = String(round1(toDispDelta(d.boiler_off_offset ?? 0))); + } + } catch {} + + // Bind events once + if (!page.dataset.bound) { + page.dataset.bound = '1'; + try { + const sw = q('.overlay-settings .sp-boiler-enable'); + if (sw) sw.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + this._settingsDraft.boiler_enabled = !!e.target.checked; + this._boilerToggleTouched = true; + this._renderSettingsPopupBoilerTab(); + } catch {} + }); + } catch {} + + try { + const mw = q('.overlay-settings .sp-boiler-multi'); + if (mw) mw.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + this._settingsDraft.boiler_multi_enabled = !!e.target.checked; + this._renderSettingsPopupBoilerTab(); + } catch {} + }); + } catch {} + + try { + const p = q('.overlay-settings .sp-boiler-switch-picker'); + if (p) p.addEventListener('value-changed', (e)=>{ + try { + if (!this._settingsDraft) return; + const v = String(e.detail?.value || ''); + this._settingsDraft.boiler_switch = v; + const dom = String(v||'').split('.')[0]; + if (dom === 'switch' || dom === 'input_boolean') { + this._settingsDraft.boiler_switch_domain = (dom === 'input_boolean') ? 'input_boolean' : 'switch'; + this._renderSettingsPopupBoilerTab(); + } + } catch {} + }); + } catch {} + + try { + const s = q('.overlay-settings .sp-boiler-switch-domain'); + if (s) s.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + const v = String(e?.target?.value || '').toLowerCase().trim(); + this._settingsDraft.boiler_switch_domain = (v === 'input_boolean') ? 'input_boolean' : 'switch'; + // Clear selected entity when changing type to avoid mismatch + this._settingsDraft.boiler_switch = ''; + this._renderSettingsPopupBoilerTab(); + } catch {} + }); + } catch {} + + try { + const onIn = q('.overlay-settings .sp-boiler-on-offset'); + if (onIn) onIn.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + const raw = String(e?.target?.value ?? '').trim().replace(',', '.'); + if (raw === '') return; + const nDisp = Number(raw); + const nC = fromDispDelta(nDisp); + if (!Number.isFinite(nC)) return; + this._settingsDraft.boiler_on_offset = Math.max(-10, Math.min(10, nC)); + } catch {} + }); + } catch {} + try { + const offIn = q('.overlay-settings .sp-boiler-off-offset'); + if (offIn) offIn.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + const raw = String(e?.target?.value ?? '').trim().replace(',', '.'); + if (raw === '') return; + const nDisp = Number(raw); + const nC = fromDispDelta(nDisp); + if (!Number.isFinite(nC)) return; + this._settingsDraft.boiler_off_offset = Math.max(-10, Math.min(10, nC)); + } catch {} + }); + } catch {} + } + } catch {} + } + + _renderSettingsPopupWeekdaysTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="weekdays"]'); + if (!page) return; + + // Build markup once + if (!page.dataset.built) { + page.dataset.built = '1'; + page.innerHTML = ` +
    +
    +
    +
    +
    +
    + +
    +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    +
    +
    +
    + +
    + +
    +
    +
    +
    +
    + +
    +
    + `; + } + + const q = (sel)=> this.shadowRoot.querySelector(sel); + const setChk = (sel, v)=>{ const el=q(sel); if (el) el.checked = !!v; }; + setChk('.overlay-settings .sp-week-enable', d.weekdays_enabled); + setChk('.overlay-settings .sp-week-view-switch', d.weekdays_view_switch_in_timeline); + + const view = (String(d.weekdays_view || 'all_rooms_one_day') === 'one_room_all_days') ? 'one_room_all_days' : 'all_rooms_one_day'; + try { + const bRooms = q('.overlay-settings .sp-week-view-rooms'); + const bDays = q('.overlay-settings .sp-week-view-days'); + bRooms && bRooms.classList.toggle('active', view === 'all_rooms_one_day'); + bDays && bDays.classList.toggle('active', view === 'one_room_all_days'); + // Disable when weekdays are off (match editor behavior) + const on = !!d.weekdays_enabled; + const row = q('.overlay-settings .sp-week-view-setting'); + if (row) row.style.opacity = on ? '' : '.5'; + if (bRooms) { bRooms.disabled = !on; bRooms.setAttribute('aria-disabled', (!on).toString()); } + if (bDays) { bDays.disabled = !on; bDays.setAttribute('aria-disabled', (!on).toString()); } + + const swRow = q('.overlay-settings .sp-week-view-switch-setting'); + if (swRow) swRow.style.opacity = on ? '' : '.5'; + const swInline = q('.overlay-settings .sp-week-view-switch'); + if (swInline) { swInline.disabled = !on; swInline.setAttribute('aria-disabled', (!on).toString()); } + + // Open weekdays editor button + const hasEnts = Array.isArray(this._config?.entities) && this._config.entities.filter(Boolean).length > 0; + const openRow = q('.overlay-settings .sp-week-open-setting'); + if (openRow) openRow.style.display = (on && hasEnts) ? '' : 'none'; + const openBtn = q('.overlay-settings .sp-week-open-btn'); + if (openBtn) { openBtn.disabled = !(on && hasEnts); openBtn.setAttribute('aria-disabled', (!(on && hasEnts)).toString()); } + } catch {} + + // Bind events once + if (!page.dataset.bound) { + page.dataset.bound = '1'; + try { + const sw = q('.overlay-settings .sp-week-enable'); + if (sw) sw.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + const wasOn = !!this._settingsDraft.weekdays_enabled; + const on = !!e.target.checked; + this._settingsDraft.weekdays_enabled = on; + if (!on) { + this._settingsDraft.weekdays_copy_from_main_mode = ''; + this._settingsDraft.weekdays_copy_from_main_target = ''; + } else if (!wasOn && this._hasAnyMainBlocks()) { + this._settingsDraft.weekdays_copy_from_main_mode = ''; + this._settingsDraft.weekdays_copy_from_main_target = ''; + this._openWeekCopyModal(); + } + // When enabling, default view to all rooms + if (on && (!this._settingsDraft.weekdays_view || this._settingsDraft.weekdays_view === 'undefined')) { + this._settingsDraft.weekdays_view = 'all_rooms_one_day'; + } + this._renderSettingsPopupWeekdaysTab(); + } catch {} + }); + } catch {} + + try { + const swInline = q('.overlay-settings .sp-week-view-switch'); + if (swInline) swInline.addEventListener('change', (e)=>{ + try { + if (!this._settingsDraft) return; + this._settingsDraft.weekdays_view_switch_in_timeline = !!e.target.checked; + this._renderSettingsPopupWeekdaysTab(); + } catch {} + }); + } catch {} + + const applyView = (mode)=>{ + try { + if (!this._settingsDraft) return; + if (!this._settingsDraft.weekdays_enabled) return; + this._settingsDraft.weekdays_view = (mode === 'one_room_all_days') ? 'one_room_all_days' : 'all_rooms_one_day'; + this._renderSettingsPopupWeekdaysTab(); + } catch {} + }; + try { + const bRooms = q('.overlay-settings .sp-week-view-rooms'); + const bDays = q('.overlay-settings .sp-week-view-days'); + if (bRooms) bRooms.addEventListener('click', ()=>applyView('all_rooms_one_day')); + if (bDays) bDays.addEventListener('click', ()=>applyView('one_room_all_days')); + } catch {} + try { + const seg = q('.overlay-settings .sp-week-view'); + if (seg) seg.addEventListener('click', (ev)=>{ + try { + const el = ev.target && ev.target.closest ? ev.target.closest('.sp-week-view-rooms, .sp-week-view-days') : null; + if (!el) return; + if (el.classList.contains('sp-week-view-rooms')) applyView('all_rooms_one_day'); + else if (el.classList.contains('sp-week-view-days')) applyView('one_room_all_days'); + } catch {} + }); + } catch {} + + try { + const btn = q('.overlay-settings .sp-week-open-btn'); + if (btn) btn.addEventListener('click', ()=>{ + try { + const ents = Array.isArray(this._config?.entities) ? this._config.entities.filter(Boolean).map(String) : []; + if (!ents.length) return; + const want = String(this._settingsDraft?.weekdays_selected_room || this._config?.weekdays_selected_room || ''); + const eid = (want && ents.includes(want)) ? want : (ents[0] || null); + if (!eid) return; + this._resumeSettingsAfterWeekdays = true; + this._suspendSettingsPopup(); + this._openWeeklyEditor(eid); + } catch {} + }); + } catch {} + } + } catch {} + } + + _renderSettingsPopupPresenceSensorTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const page = this.shadowRoot.querySelector('.overlay-settings .settings-page[data-page="presence_sensor"]'); + if (!page) return; + page.innerHTML = ''; + + const card = document.createElement('div'); + card.className = 'settings-card'; + page.append(card); + + // Toggle + const row = document.createElement('div'); + row.className = 'setting'; + const text = document.createElement('div'); + text.className = 'text'; + const title = document.createElement('div'); + title.className = 'title'; + title.textContent = this._t('editor.presence_sensor.enable.title') || 'Presence sensor'; + const desc = document.createElement('div'); + desc.className = 'desc'; + desc.textContent = this._t('editor.presence_sensor.enable.desc') || 'Enable per-room presence sensor configuration.'; + text.append(title, desc); + const sw = document.createElement('ha-switch'); + const on = !!d.presence_sensor_enabled; + try { sw.checked = on; sw.disabled = false; } catch {} + sw.addEventListener('change', ()=>{ + try { + d.presence_sensor_enabled = !!sw.checked; + this._renderSettingsPopupPresenceSensorTab(); + } catch {} + }); + row.append(text, sw); + card.append(row); + + // Rooms list + const ents = (()=>{ + try { + const arr = Array.isArray(d.entities) ? d.entities : (Array.isArray(this._config?.entities) ? this._config.entities : []); + return arr.filter(Boolean).map(String); + } catch { return []; } + })(); + + if (!ents.length) { + const box = document.createElement('div'); box.className = 'empty-box'; + const p = document.createElement('div'); p.textContent = this._t('editor.colors.empty') || 'No rooms added yet.'; + const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'add-entity-btn'; + btn.innerHTML = '' + (this._t('editor.colors.go_add') || 'Add room') + ''; + btn.onclick = ()=>{ this._setSettingsPopupTab('rooms'); }; + box.append(p, btn); + card.append(box); + return; + } + + try { if (!this._openPopupPresenceSensorRows) this._openPopupPresenceSensorRows = new Set(); } catch {} + const wrap = document.createElement('div'); + try { wrap.style.display = 'grid'; wrap.style.gap = '10px'; } catch {} + card.append(wrap); + + const buildFoldout = (key, titleText)=>{ + const line = document.createElement('div'); line.className = 'inline'; + const summary = document.createElement('div'); summary.className = 'summary'; + const sumLeft = document.createElement('div'); sumLeft.className = 'summary-left'; + const exp = document.createElement('button'); exp.type = 'button'; exp.className = 'expander'; + exp.innerHTML = ''; + const txtWrap = document.createElement('div'); txtWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); titleSpan.className = 'summary-title'; + titleSpan.textContent = titleText; + + try { + summary.style.display = 'flex'; + summary.style.alignItems = 'center'; + summary.style.justifyContent = 'space-between'; + summary.style.padding = '6px'; + summary.style.border = '1px dashed var(--divider-color)'; + summary.style.borderRadius = '8px'; + summary.style.background = 'var(--secondary-background-color, transparent)'; + sumLeft.style.display = 'flex'; + sumLeft.style.alignItems = 'center'; + sumLeft.style.gap = '8px'; + sumLeft.style.minWidth = '0'; + txtWrap.style.display = 'grid'; + txtWrap.style.gap = '2px'; + txtWrap.style.minWidth = '0'; + titleSpan.style.fontWeight = '600'; + titleSpan.style.overflow = 'hidden'; + titleSpan.style.textOverflow = 'ellipsis'; + titleSpan.style.whiteSpace = 'nowrap'; + titleSpan.style.maxWidth = '320px'; + exp.style.width = '28px'; + exp.style.height = '28px'; + exp.style.borderRadius = '8px'; + exp.style.border = '1px solid var(--divider-color)'; + exp.style.background = 'var(--card-background-color)'; + exp.style.display = 'inline-flex'; + exp.style.alignItems = 'center'; + exp.style.justifyContent = 'center'; + exp.style.cursor = 'pointer'; + } catch {} + + txtWrap.append(titleSpan); + sumLeft.append(exp, txtWrap); + summary.append(sumLeft); + line.append(summary); + + const details = document.createElement('div'); details.className = 'details'; + line.append(details); + + try { + details.style.display = 'none'; + details.style.gridTemplateColumns = 'minmax(250px, 440px)'; + details.style.gap = '6px'; + details.style.padding = '6px'; + details.style.border = '1px dashed var(--divider-color)'; + details.style.borderRadius = '8px'; + details.style.background = 'var(--secondary-background-color, transparent)'; + } catch {} + + const applyOpen = (isOpen)=>{ + try { + if (isOpen) { + line.classList.add('open'); + details.style.display = 'grid'; + exp.style.transform = 'rotate(180deg)'; + } else { + line.classList.remove('open'); + details.style.display = 'none'; + exp.style.transform = ''; + } + } catch {} + }; + const toggle = (ev)=>{ + try { ev?.stopPropagation?.(); } catch {} + const nextOpen = !line.classList.contains('open'); + applyOpen(nextOpen); + try { + if (!this._openPopupPresenceSensorRows) this._openPopupPresenceSensorRows = new Set(); + if (nextOpen) this._openPopupPresenceSensorRows.add(String(key)); + else this._openPopupPresenceSensorRows.delete(String(key)); + } catch {} + }; + exp.addEventListener('click', toggle); + summary.style.cursor = 'pointer'; + summary.addEventListener('click', (ev)=>{ + try { if (ev?.target?.closest?.('button, a, input, select, textarea')) return; } catch {} + toggle(ev); + }); + try { applyOpen(!!this._openPopupPresenceSensorRows?.has(String(key))); } catch {} + + return { line, details }; + }; + + try { if (!d.presence_sensors || typeof d.presence_sensors !== 'object') d.presence_sensors = {}; } catch {} + try { if (!d.presence_sensor_temps || typeof d.presence_sensor_temps !== 'object') d.presence_sensor_temps = {}; } catch {} + try { if (!d.presence_sensor_delays || typeof d.presence_sensor_delays !== 'object') d.presence_sensor_delays = {}; } catch {} + try { if (!d.presence_sensor_delay_units || typeof d.presence_sensor_delay_units !== 'object') d.presence_sensor_delay_units = {}; } catch {} + + for (const eid of ents) { + const labels = (d.labels && typeof d.labels === 'object') ? d.labels : (this._config?.labels || {}); + const friendly = this._hass?.states?.[eid]?.attributes?.friendly_name; + const titleText = labels?.[eid] || friendly || (eid.split('.')[1] || eid); + const { line, details } = buildFoldout(eid, titleText); + + try { + const fieldWrap = document.createElement('div'); + fieldWrap.style.display = 'grid'; + fieldWrap.style.gap = '2px'; + + const tTitle = document.createElement('div'); + tTitle.style.fontWeight = '600'; + tTitle.textContent = this._t('editor.presence_sensor.entity.title') || 'Presence entity'; + const tDesc = document.createElement('div'); + tDesc.style.fontSize = '.85rem'; + tDesc.style.color = 'var(--secondary-text-color)'; + tDesc.textContent = this._t('editor.presence_sensor.entity.desc') || 'Select the binary_sensor that indicates presence in this room.'; + fieldWrap.append(tTitle, tDesc); + + const picker = document.createElement('tt-entity-picker'); + try { picker.style.display = 'block'; picker.style.minHeight = '48px'; picker.style.maxWidth = '440px'; } catch {} + picker.setAttribute('include-domains', '["binary_sensor"]'); + picker.setAttribute('label', this._t('editor.presence_sensor.entity.title') || 'Presence entity'); + try { picker.hass = this._hass; } catch {} + try { picker.value = String(d.presence_sensors?.[eid] || ''); } catch {} + try { picker.disabled = !on; picker.setAttribute('aria-disabled', String(!on)); } catch {} + picker.addEventListener('value-changed', (e)=>{ + try { + const val = String(e?.detail?.value || '').trim(); + const next = { ...(d.presence_sensors || {}) }; + if (val && String(val).startsWith('binary_sensor.')) next[eid] = val; + else delete next[eid]; + d.presence_sensors = next; + } catch {} + }); + + details.append(fieldWrap, picker); + + // Temperature override when presence is ON + try { + const wantF = String(d.temp_unit || this._config?.temp_unit || 'C').toUpperCase() === 'F'; + const toDisp = (c)=>{ const v = Number(c); if (!Number.isFinite(v)) return ''; return wantF ? Math.round((v*9/5+32)*10)/10 : v; }; + const fromDisp = (x)=>{ const v = Number(x); if (!Number.isFinite(v)) return NaN; return wantF ? ((v-32)*5/9) : v; }; + const minC = Number.isFinite(Number(d.min_temp)) ? Number(d.min_temp) : Number(this._config?.min_temp ?? 5); + const maxC = Number.isFinite(Number(d.max_temp)) ? Number(d.max_temp) : Number(this._config?.max_temp ?? 25); + + const tWrap = document.createElement('div'); + tWrap.style.display = 'grid'; + tWrap.style.gap = '2px'; + + const tT = document.createElement('div'); + tT.style.fontWeight = '600'; + tT.textContent = this._t('editor.presence_sensor.on_temp.title') || 'Temperature when presence is ON'; + const tD = document.createElement('div'); + tD.style.fontSize = '.85rem'; + tD.style.color = 'var(--secondary-text-color)'; + tD.textContent = this._t('editor.presence_sensor.on_temp.desc') || 'If the sensor is ON, this room will use this temperature instead of the schedule.'; + tWrap.append(tT, tD); + + const inp = document.createElement('input'); + inp.type = 'number'; + inp.step = '0.5'; + inp.className = 'settings-input'; + try { + if (Number.isFinite(minC)) inp.min = String(toDisp(minC)); + if (Number.isFinite(maxC)) inp.max = String(toDisp(maxC)); + } catch {} + try { + const defaultC = 21; + let curC = Number(d.presence_sensor_temps?.[eid]); + if (!Number.isFinite(curC)) { + // Preset default (stored in °C) + let v = Number(defaultC); + if (Number.isFinite(minC)) v = Math.max(minC, v); + if (Number.isFinite(maxC)) v = Math.min(maxC, v); + const next = { ...(d.presence_sensor_temps || {}) }; + next[eid] = v; + d.presence_sensor_temps = next; + curC = v; + } + inp.value = String(toDisp(curC)); + } catch {} + try { inp.disabled = !on; } catch {} + + inp.addEventListener('change', (e)=>{ + try { + const raw = String(e?.target?.value ?? '').trim(); + const next = { ...(d.presence_sensor_temps || {}) }; + if (!raw) { + delete next[eid]; + d.presence_sensor_temps = next; + return; + } + const parsed = fromDisp(String(raw).replace(',', '.')); + if (!Number.isFinite(parsed)) { + e.target.value = ''; + delete next[eid]; + d.presence_sensor_temps = next; + return; + } + let v = parsed; + if (Number.isFinite(minC)) v = Math.max(minC, v); + if (Number.isFinite(maxC)) v = Math.min(maxC, v); + next[eid] = v; + d.presence_sensor_temps = next; + try { e.target.value = String(toDisp(v)); } catch {} + } catch {} + }); + + details.append(tWrap, inp); + } catch {} + + // Delay settings (stored as seconds) + try { + const defaultDelaySec = 180; // 3 minutes + const dm = (d.presence_sensor_delays && typeof d.presence_sensor_delays === 'object') ? d.presence_sensor_delays : {}; + const cur = (dm?.[eid] && typeof dm?.[eid] === 'object') ? dm[eid] : {}; + + const um = (d.presence_sensor_delay_units && typeof d.presence_sensor_delay_units === 'object') ? d.presence_sensor_delay_units : {}; + const unit = (String(um?.[eid] || 'minutes').toLowerCase().trim() === 'seconds') ? 'seconds' : 'minutes'; + const fmtVal = (sec)=>{ + const v = Number(sec); + if (!Number.isFinite(v)) return ''; + if (unit === 'seconds') return String(Math.round(v)); + return String(Math.round((v/60)*10)/10); + }; + const parseToSeconds = (raw)=>{ + const s = String(raw||'').trim(); + if (!s) return NaN; + const n = Number(s.replace(',', '.')); + if (!Number.isFinite(n)) return NaN; + if (unit === 'seconds') return Math.max(0, n); + return Math.max(0, n) * 60; + }; + + const setDelaySeconds = (which, seconds)=>{ + try { + const nextAll = { ...(d.presence_sensor_delays || {}) }; + const curObj = (nextAll[eid] && typeof nextAll[eid] === 'object') ? { ...nextAll[eid] } : {}; + if (!Number.isFinite(seconds)) { + delete curObj[which]; + } else { + curObj[which] = Math.max(0, Math.round(Number(seconds))); + } + const out = {}; + const onS = Number(curObj.on_s); const offS = Number(curObj.off_s); + if (Number.isFinite(onS) && onS >= 0) out.on_s = onS; + if (Number.isFinite(offS) && offS >= 0) out.off_s = offS; + if (Object.keys(out).length) nextAll[eid] = out; + else delete nextAll[eid]; + d.presence_sensor_delays = nextAll; + } catch {} + }; + + // Unit dropdown (per room) + try { + const uWrap = document.createElement('div'); + uWrap.style.display = 'grid'; + uWrap.style.gap = '2px'; + const uT = document.createElement('div'); + uT.style.fontWeight = '600'; + uT.textContent = this._t('away.delay_unit') || 'Unit'; + uWrap.append(uT); + + const sel = document.createElement('select'); + sel.className = 'settings-input'; + const optM = document.createElement('option'); + optM.value = 'minutes'; + optM.textContent = this._t('away.delay_unit.minutes') || 'Minutes'; + const optS = document.createElement('option'); + optS.value = 'seconds'; + optS.textContent = this._t('away.delay_unit.seconds') || 'Seconds'; + sel.append(optM, optS); + try { sel.value = unit; } catch {} + try { sel.disabled = !on; } catch {} + sel.addEventListener('change', ()=>{ + try { + const v = String(sel.value||'').toLowerCase().trim(); + const next = { ...(d.presence_sensor_delay_units || {}) }; + next[eid] = (v === 'seconds') ? 'seconds' : 'minutes'; + d.presence_sensor_delay_units = next; + this._renderSettingsPopupPresenceSensorTab(); + } catch {} + }); + + details.append(uWrap, sel); + } catch {} + + const mkField = (titleKey, descKey, which, currentSec)=>{ + const w = document.createElement('div'); + w.style.display = 'grid'; + w.style.gap = '2px'; + + const tt = document.createElement('div'); + tt.style.fontWeight = '600'; + tt.textContent = this._t(titleKey) || ''; + const dd = document.createElement('div'); + dd.style.fontSize = '.85rem'; + dd.style.color = 'var(--secondary-text-color)'; + dd.textContent = this._t(descKey) || ''; + w.append(tt, dd); + + const inp = document.createElement('input'); + inp.type = 'number'; + inp.min = '0'; + inp.step = (unit === 'seconds') ? '1' : '0.1'; + inp.className = 'settings-input'; + try { + const sec = Number.isFinite(Number(currentSec)) ? Number(currentSec) : defaultDelaySec; + inp.value = fmtVal(sec); + } catch {} + try { inp.disabled = !on; } catch {} + inp.addEventListener('change', (e)=>{ + try { + if (!String(e?.target?.value || '').trim()) { + // Treat empty as 0 (no delay) + setDelaySeconds(which, 0); + try { e.target.value = fmtVal(0); } catch {} + return; + } + const sec = parseToSeconds(e?.target?.value); + if (!Number.isFinite(sec)) { + e.target.value = ''; + setDelaySeconds(which, 0); + return; + } + setDelaySeconds(which, sec); + try { e.target.value = fmtVal(Math.round(sec)); } catch {} + } catch {} + }); + return { w, inp }; + }; + + const fOn = mkField('editor.presence_sensor.on_delay.title', 'editor.presence_sensor.on_delay.desc', 'on_s', cur?.on_s); + const fOff = mkField('editor.presence_sensor.off_delay.title', 'editor.presence_sensor.off_delay.desc', 'off_s', cur?.off_s); + details.append(fOn.w, fOn.inp, fOff.w, fOff.inp); + } catch {} + } catch {} + + wrap.append(line); + } + } catch {} + } + + _renderSettingsPopupSettingsTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const q = (sel)=> this.shadowRoot.querySelector(sel); + const setVal = (sel, v)=>{ const el=q(sel); if (el) el.value = (v ?? '') + ''; }; + const setChk = (sel, v)=>{ const el=q(sel); if (el) el.checked = !!v; }; + // Show temps in display unit based on draft.temp_unit, but keep draft values in °C + const wantF = String(d.temp_unit || 'C').toUpperCase() === 'F'; + const toDisp = (c)=>{ try { const v = Number(c); if (!Number.isFinite(v)) return c; return wantF ? this._toDisplayTemp(v) : v; } catch { return c; } }; + setVal('.overlay-settings .sp-def', toDisp(d.default_temp)); + setVal('.overlay-settings .sp-rowh', d.row_height); + setVal('.overlay-settings .sp-minc', toDisp(d.min_temp)); + setVal('.overlay-settings .sp-maxc', toDisp(d.max_temp)); + setChk('.overlay-settings .sp-auto', d.auto_apply); + setChk('.overlay-settings .sp-applyedit', d.apply_on_edit); + setChk('.overlay-settings .sp-applydef', d.apply_on_default_change); + setChk('.overlay-settings .sp-perroom', d.per_room_defaults); + setChk('.overlay-settings .sp-showroomtemp', d.show_room_temp); + + // Segmented controls: active state + try { + const b24 = q('.overlay-settings .sp-fmt24'); + const b12 = q('.overlay-settings .sp-fmt12'); + const is12 = !!d.time_12h; + b24 && b24.classList.toggle('active', !is12); + b12 && b12.classList.toggle('active', is12); + } catch {} + try { + const bB = q('.overlay-settings .sp-srcBrowser'); + const bH = q('.overlay-settings .sp-srcHa'); + const useHa = String(d.time_source||'browser')==='ha'; + bB && bB.classList.toggle('active', !useHa); + bH && bH.classList.toggle('active', useHa); + } catch {} + try { + const bC = q('.overlay-settings .sp-unitC'); + const bF = q('.overlay-settings .sp-unitF'); + bC && bC.classList.toggle('active', !wantF); + bF && bF.classList.toggle('active', wantF); + } catch {} + setChk('.overlay-settings .sp-profiles', d.profiles_enabled); + setChk('.overlay-settings .sp-seasonal', d.seasonal_enabled); + + // Configuration ID (instance) toggle + label (not part of the popup draft) + try { + this._hydrateInstanceCfgFromLocal(); + const sw = q('.overlay-settings .sp-instance-enable'); + if (sw) sw.checked = !!this._config?.instance_enabled; + const row = q('.overlay-settings .sp-instance-id-setting'); + if (row) row.style.display = this._config?.instance_enabled ? '' : 'none'; + const idEl = q('.overlay-settings .sp-instance-id'); + if (idEl) { + const id = String(this._config?.instance_id || ''); + idEl.textContent = id ? `ID: ${id}` : ''; + } + const b = q('.overlay-settings .sp-instance-regenerate-btn'); + if (b) b.style.display = this._config?.instance_enabled ? '' : 'none'; + } catch {} + + // Keep temp field labels in sync with draft unit + try { + const unitSym = wantF ? '°F' : '°C'; + const t = (k)=> (this._t ? this._t(k) : k); + const setLbl = (sel, key)=>{ const el=q(sel); if (!el) return; const s = String(t(key)||''); el.textContent = s.includes('°C') ? s.replace('°C', unitSym) : (s + (s.includes('(') ? '' : '')); }; + setLbl('.overlay-settings .sp-slabel-def', 'editor.default_c'); + setLbl('.overlay-settings .sp-slabel-minc', 'editor.min_c'); + setLbl('.overlay-settings .sp-slabel-maxc', 'editor.max_c'); + } catch {} + } catch {} + } + + _renderSettingsPopupPauseTab(){ + try { + const d = this._settingsDraft; + if (!d || !this.shadowRoot) return; + const q = (sel)=> this.shadowRoot.querySelector(sel); + const setChk = (sel, v)=>{ const el=q(sel); if (el) el.checked = !!v; }; + + setChk('.overlay-settings .sp-pausebtn', d.show_pause_button); + setChk('.overlay-settings .sp-pausesensor', d.pause_sensor_enabled); + + // Pause sensor entity row visibility + picker value + try { + const row = q('.overlay-settings .sp-pausesensor-entity-setting'); + if (row) row.style.display = d.pause_sensor_enabled ? '' : 'none'; + } catch {} + try { + const p = q('.overlay-settings .sp-pausesensor-entity'); + if (p) { + p.hass = this._hass; + p.value = String(d.pause_sensor_entity || ''); + try { p.setAttribute('label', this._t('editor.pause_sensor.entity_title') || 'Pause sensor'); } catch {} + } + } catch {} + + // Wire Pause tab controls + const onSw = (key) => (e)=>{ + try { + if (!this._settingsDraft) return; + const checked = !!(e && e.target && e.target.checked); + this._settingsDraft[key] = checked; + } catch {} + }; + + try { + const swPauseBtn = q('.overlay-settings .sp-pausebtn'); + if (swPauseBtn) swPauseBtn.onchange = onSw('show_pause_button'); + } catch {} + + try { + const swPauseSensor = q('.overlay-settings .sp-pausesensor'); + if (swPauseSensor) swPauseSensor.onchange = (e)=>{ try { if (!this._settingsDraft) return; this._settingsDraft.pause_sensor_enabled = !!e?.target?.checked; this._renderSettingsPopupPauseTab(); } catch {} }; + } catch {} + + // Pause sensor entity picker + try { + const p = q('.overlay-settings .sp-pausesensor-entity'); + if (p) { + if (!p._ttBoundPausePicker) { + p._ttBoundPausePicker = true; + p.addEventListener('value-changed', (ev)=>{ + try { + if (!this._settingsDraft) return; + const v = (ev?.detail && ev.detail.value != null) ? String(ev.detail.value) : ''; + this._settingsDraft.pause_sensor_entity = v; + } catch {} + }); + } + } + } catch {} + } catch {} + } + + _ttCoerceNum(v, fallback){ + const n = Number(v); + return Number.isFinite(n) ? n : fallback; + } + _ttClampNum(v, min, max, fallback){ + const n = Number(v); + if (!Number.isFinite(n)) return fallback; + return Math.max(min, Math.min(max, n)); + } + _settingsPopupHasUnsaved(){ + try { + const d = this._settingsDraft; + const c = this._config || {}; + if (!d) return false; + const eqNum = (a,b)=>{ + const na = Number(a); + const nb = Number(b); + if (!Number.isFinite(na) && !Number.isFinite(nb)) return true; + return Math.abs(na - nb) < 0.0001; + }; + // Normalize undefined config fields to the same defaults used in _initSettingsPopupDraft + const cDefaultTemp = this._ttCoerceNum(c.default_temp, 20); + const cRowHeight = this._ttCoerceNum(c.row_height, 64); + const cMinTemp = this._ttCoerceNum(c.min_temp, 5); + const cMaxTemp = this._ttCoerceNum(c.max_temp, 25); + + if (!eqNum(d.default_temp, cDefaultTemp)) return true; + if (!eqNum(d.row_height, cRowHeight)) return true; + if (!eqNum(d.min_temp, cMinTemp)) return true; + if (!eqNum(d.max_temp, cMaxTemp)) return true; + if (!!d.auto_apply !== !!c.auto_apply) return true; + if (!!d.apply_on_edit !== !!c.apply_on_edit) return true; + if (!!d.apply_on_default_change !== !!c.apply_on_default_change) return true; + if (!!d.per_room_defaults !== !!c.per_room_defaults) return true; + if (!!d.show_pause_button !== !!(c.show_pause_button !== false)) return true; + if (!!d.show_room_temp !== !!(c.show_room_temp !== false)) return true; + if (!!d.pause_sensor_enabled !== !!c.pause_sensor_enabled) return true; + if (String(d.pause_sensor_entity||'') !== String(c.pause_sensor_entity||'')) return true; + if (!!d.time_12h !== !!c.time_12h) return true; + if (String(d.time_source||'browser') !== String(c.time_source||'browser')) return true; + if (String(d.temp_unit||'C').toUpperCase() !== String(c.temp_unit||'C').toUpperCase()) return true; + if (!!d.profiles_enabled !== !!c.profiles_enabled) return true; + if (!!d.seasonal_enabled !== !!c.seasonal_enabled) return true; + if (String(d.seasonal_mode||'winter') !== String(c.seasonal_mode||'winter')) return true; + + if (!!d.presence_sensor_enabled !== !!c.presence_sensor_enabled) return true; + + if (!!d.weekdays_enabled !== !!c.weekdays_enabled) return true; + if (String(d.weekdays_view||'all_rooms_one_day') !== String(c.weekdays_view||'all_rooms_one_day')) return true; + if (String(d.weekdays_selected_room||'') !== String(c.weekdays_selected_room||'')) return true; + + if (!!d.boiler_enabled !== !!c.boiler_enabled) return true; + if (String(d.boiler_switch||'') !== String(c.boiler_switch||'')) return true; + + // boiler_switch_domain: normalize to switch/input_boolean + try { + const normDom = (eid, raw)=>{ + const fromEid = (()=>{ const dom = String(eid||'').split('.')[0]; return (dom === 'input_boolean') ? 'input_boolean' : 'switch'; })(); + const s = String((raw ?? fromEid) || '').toLowerCase().trim(); + return (s === 'input_boolean') ? 'input_boolean' : 'switch'; + }; + const dDom = normDom(d.boiler_switch, d.boiler_switch_domain); + const cDom = normDom(c.boiler_switch, c.boiler_switch_domain); + if (dDom !== cDom) return true; + } catch {} + + // boiler_rooms: compare sets; default is "all climate rooms" + try { + const cEnts = Array.isArray(c.entities) ? c.entities.filter(Boolean).map(String) : []; + const climateRooms = cEnts.filter(e=>String(e).startsWith('climate.')); + const norm = (arr)=>{ + const set = new Set((Array.isArray(arr) ? arr : climateRooms).filter(Boolean).map(String)); + return climateRooms.filter(eid => set.has(eid)); + }; + const dRooms = norm(d.boiler_rooms); + const cRooms = norm(c.boiler_rooms); + if (dRooms.length !== cRooms.length) return true; + for (let i=0;iString(e).startsWith('climate.')); + const norm = (src)=>{ + const map = (src && typeof src === 'object') ? src : {}; + const out = {}; + for (const eid of climateRooms) { + const raw = map?.[eid]; + if (!raw || typeof raw !== 'object') continue; + const enabled = !!raw.enabled; + const sw = String(raw.switch || '').trim(); + const domFromEid = (sw ? String(sw).split('.')[0] : ''); + const rawDom = String(raw.switch_domain || '').toLowerCase().trim(); + const dom = (domFromEid === 'input_boolean' || domFromEid === 'switch') + ? domFromEid + : ((rawDom === 'input_boolean') ? 'input_boolean' : 'switch'); + const onOff = Number.isFinite(Number(raw.on_offset)) ? Math.max(-10, Math.min(10, Number(raw.on_offset))) : this._ttCoerceNum(c.boiler_on_offset, 0); + const offOff = Number.isFinite(Number(raw.off_offset)) ? Math.max(-10, Math.min(10, Number(raw.off_offset))) : this._ttCoerceNum(c.boiler_off_offset, 0); + const hasAny = enabled || sw || raw.on_offset !== undefined || raw.off_offset !== undefined || raw.switch_domain; + if (hasAny) out[eid] = { enabled, switch: sw, switch_domain: dom, on_offset: onOff, off_offset: offOff }; + } + return out; + }; + const dMap = norm(d.boiler_room_settings); + const cMap = norm(c.boiler_room_settings); + if (JSON.stringify(dMap) !== JSON.stringify(cMap)) return true; + } catch {} + + // Colors + if (!!d.color_global !== !!c.color_global) return true; + try { + const dm = (d.color_ranges && typeof d.color_ranges === 'object') ? d.color_ranges : {}; + const cm = (c.color_ranges && typeof c.color_ranges === 'object') ? c.color_ranges : {}; + const keys = new Set([...Object.keys(dm||{}), ...Object.keys(cm||{})]); + for (const k of keys) { + const a = Array.isArray(dm[k]) ? dm[k] : []; + const b = Array.isArray(cm[k]) ? cm[k] : []; + if (a.length !== b.length) return true; + for (let i=0;i{ + const n = Number(v); + if (!Number.isFinite(n)) return fallback; + return Math.max(0, Math.min(1440, Math.round(n))); + }; + if (!eqNum(dn.open_delay_min, clampOw(cn.open_delay_min, 2))) return true; + if (!eqNum(dn.close_delay_min, clampOw(cn.close_delay_min, 5))) return true; + const ds = (dn.sensors && typeof dn.sensors === 'object') ? dn.sensors : {}; + const cs = (cn.sensors && typeof cn.sensors === 'object') ? cn.sensors : {}; + const keys = new Set([...Object.keys(ds||{}), ...Object.keys(cs||{})]); + for (const k of keys) { + const a = Array.isArray(ds[k]) ? ds[k].filter(Boolean).map(String) : []; + const b = Array.isArray(cs[k]) ? cs[k].filter(Boolean).map(String) : []; + const sa = Array.from(new Set(a)).sort(); + const sb = Array.from(new Set(b)).sort(); + if (sa.length !== sb.length) return true; + for (let i=0;i /^\d{4}-\d{2}-\d{2}$/.test(String(x))))).sort(); + const sc = Array.from(new Set(ca.filter(x => /^\d{4}-\d{2}-\d{2}$/.test(String(x))))).sort(); + if (sd.length !== sc.length) return true; + for (let i=0;i !!dcmb[k]?.enabled).map(String).sort(); + const ck = Object.keys(ccmb||{}).filter(k => !!ccmb[k]?.enabled).map(String).sort(); + if (dk.length !== ck.length) return true; + for (let i=0;iString(x||'').trim()).filter(Boolean) : []; + const ce = Array.isArray(c.entities) ? c.entities.map(x=>String(x||'').trim()).filter(Boolean) : []; + if (JSON.stringify(de) !== JSON.stringify(ce)) return true; + + const dm0 = Array.isArray(d.room_use_input_number) ? d.room_use_input_number.map(v=>!!v) : []; + const cm0 = Array.isArray(c.room_use_input_number) ? c.room_use_input_number.map(v=>!!v) : []; + const normModes = (ents, raw)=> ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return true; + if (String(eid).startsWith('climate.')) return false; + return !!raw[idx]; + }); + const dm = normModes(de, dm0); + const cm = normModes(ce, cm0); + if (JSON.stringify(dm) !== JSON.stringify(cm)) return true; + + const dl = (d.labels && typeof d.labels === 'object') ? d.labels : {}; + const cl = (c.labels && typeof c.labels === 'object') ? c.labels : {}; + const normMap = (src)=>{ + const out = {}; + for (const eid of de) { + const v = Object.prototype.hasOwnProperty.call(src, eid) ? String(src[eid]||'').trim() : ''; + if (v) out[eid] = v; + } + return out; + }; + if (JSON.stringify(normMap(dl)) !== JSON.stringify(normMap(cl))) return true; + + const dmrg = (d.merges && typeof d.merges === 'object') ? d.merges : {}; + const cmrg = (c.merges && typeof c.merges === 'object') ? c.merges : {}; + const primSet = new Set(de); + const normMerges = (src)=>{ + const out = {}; + for (let i=0;iString(x).startsWith('climate.') && !primSet.has(String(x))))).sort(); + if (cleaned.length) out[primary] = cleaned; + } + return out; + }; + if (JSON.stringify(normMerges(dmrg)) !== JSON.stringify(normMerges(cmrg))) return true; + + const dts = (d.temp_sensors && typeof d.temp_sensors === 'object') ? d.temp_sensors : {}; + const cts = (c.temp_sensors && typeof c.temp_sensors === 'object') ? c.temp_sensors : {}; + if (JSON.stringify(normMap(dts)) !== JSON.stringify(normMap(cts))) return true; + + // Presence sensors: one binary_sensor per active room + const dps = (d.presence_sensors && typeof d.presence_sensors === 'object') ? d.presence_sensors : {}; + const cps = (c.presence_sensors && typeof c.presence_sensors === 'object') ? c.presence_sensors : {}; + const normPresence = (src)=>{ + const out = {}; + for (const eid of de) { + const v = Object.prototype.hasOwnProperty.call(src, eid) ? String(src[eid]||'').trim() : ''; + if (v && String(v).startsWith('binary_sensor.')) out[eid] = v; + } + return out; + }; + if (JSON.stringify(normPresence(dps)) !== JSON.stringify(normPresence(cps))) return true; + + // Presence override temps (°C): compare numeric mappings for active rooms + try { + const dpt = (d.presence_sensor_temps && typeof d.presence_sensor_temps === 'object') ? d.presence_sensor_temps : {}; + const cpt = (c.presence_sensor_temps && typeof c.presence_sensor_temps === 'object') ? c.presence_sensor_temps : {}; + const normTemps = (src)=>{ + const out = {}; + for (const eid of de) { + const v0 = Object.prototype.hasOwnProperty.call(src, eid) ? Number(src[eid]) : NaN; + if (!Number.isFinite(v0)) continue; + out[eid] = v0; + } + return out; + }; + if (JSON.stringify(normTemps(dpt)) !== JSON.stringify(normTemps(cpt))) return true; + } catch {} + + // Presence delays (seconds): compare { on_s, off_s } per active room + try { + const dpd = (d.presence_sensor_delays && typeof d.presence_sensor_delays === 'object') ? d.presence_sensor_delays : {}; + const cpd = (c.presence_sensor_delays && typeof c.presence_sensor_delays === 'object') ? c.presence_sensor_delays : {}; + const normDelays = (src)=>{ + const out = {}; + for (const eid of de) { + const v = src?.[eid]; + if (!v || typeof v !== 'object') continue; + const onS = Number(v.on_s); + const offS = Number(v.off_s); + const obj = {}; + if (Number.isFinite(onS) && onS >= 0) obj.on_s = onS; + if (Number.isFinite(offS) && offS >= 0) obj.off_s = offS; + if (Object.keys(obj).length) out[eid] = obj; + } + return out; + }; + if (JSON.stringify(normDelays(dpd)) !== JSON.stringify(normDelays(cpd))) return true; + } catch {} + + // Delay unit per room: default 'minutes' + try { + const dpu = (d.presence_sensor_delay_units && typeof d.presence_sensor_delay_units === 'object') ? d.presence_sensor_delay_units : {}; + const cpu = (c.presence_sensor_delay_units && typeof c.presence_sensor_delay_units === 'object') ? c.presence_sensor_delay_units : {}; + const normUnits = (src)=>{ + const out = {}; + for (const eid of de) { + const u0 = Object.prototype.hasOwnProperty.call(src, eid) ? String(src[eid]||'').toLowerCase().trim() : ''; + out[eid] = (u0 === 'seconds') ? 'seconds' : 'minutes'; + } + return out; + }; + if (JSON.stringify(normUnits(dpu)) !== JSON.stringify(normUnits(cpu))) return true; + } catch {} + } catch {} + return false; + } catch { return false; } + } + + _closeSettingsPopup(force){ + try { + const mustConfirm = !force && this._settingsPopupHasUnsaved(); + if (mustConfirm) { + const msg = this._t('ui.unsaved_confirm') || 'You have unsaved changes. Close anyway?'; + const ok = window.confirm(msg); + if (!ok) return; + } + try { + if (this._settingsPopupKeyHandler) window.removeEventListener('keydown', this._settingsPopupKeyHandler); + } catch {} + this._settingsPopupKeyHandler = null; + try { + if (this._settingsPopupWheelHandler?.el && this._settingsPopupWheelHandler?.fn) { + this._settingsPopupWheelHandler.el.removeEventListener('wheel', this._settingsPopupWheelHandler.fn); + } + } catch {} + this._settingsPopupWheelHandler = null; + this._settingsPopupSuspended = false; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings'); + if (ov) ov.classList.remove('open'); + try { this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} + this._settingsDraft = null; + } catch {} + } + + _suspendSettingsPopup(){ + try { + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings'); + if (ov) ov.classList.remove('open'); + try { this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} + this._settingsPopupSuspended = true; + try { + if (this._settingsPopupKeyHandler) window.removeEventListener('keydown', this._settingsPopupKeyHandler); + } catch {} + } catch {} + } + + _resumeSettingsPopup(){ + try { + if (this._disablePopups) return; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings'); + if (!ov) return; + ov.classList.add('open'); + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + this._settingsPopupSuspended = false; + try { this._applyCardI18n(); } catch {} + try { this._renderSettingsPopupTabs(); } catch {} + try { this._renderSettingsPopupTab(); } catch {} + try { + const onKey = (ev) => { if (ev.key === 'Escape') { this._closeSettingsPopup(false); } }; + try { if (this._settingsPopupKeyHandler) window.removeEventListener('keydown', this._settingsPopupKeyHandler); } catch {} + this._settingsPopupKeyHandler = onKey; + window.addEventListener('keydown', onKey); + } catch {} + } catch {} + } + + async _saveSettingsPopup(){ + try { + // Robust fallback: sync Away controls directly from popup before applying draft + // in case component events were not emitted. + try { + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings'); + const d = this._settingsDraft; + const _ttReadSwitchNow = (el)=>{ + try { if (!el) return null; } catch { return null; } + try { if (typeof el.checked === 'boolean') return !!el.checked; } catch {} + try { + const inner = el.shadowRoot?.querySelector?.('input,button,[role="switch"]'); + if (inner && typeof inner.checked === 'boolean') return !!inner.checked; + } catch {} + try { + const aria = String(el.getAttribute?.('aria-checked') || '').toLowerCase(); + if (aria === 'true') return true; + if (aria === 'false') return false; + } catch {} + try { + const innerAria = String(el.shadowRoot?.querySelector?.('[aria-checked]')?.getAttribute?.('aria-checked') || '').toLowerCase(); + if (innerAria === 'true') return true; + if (innerAria === 'false') return false; + } catch {} + try { + if (el.hasAttribute?.('checked')) return true; + } catch {} + return null; + }; + if (ov && d && d.away && typeof d.away === 'object') { + const sw = ov.querySelector('.sp-away-enable'); + const swV = _ttReadSwitchNow(sw); + if (typeof swV === 'boolean') { + const v = swV; + if (!!d.away.enabled !== v) this._awayTouched = true; + d.away.enabled = v; + } + const adv = ov.querySelector('.sp-away-adv-enable'); + const advV = _ttReadSwitchNow(adv); + if (typeof advV === 'boolean') { + const v = advV; + if (!!d.away.advanced_enabled !== v) this._awayTouched = true; + d.away.advanced_enabled = v; + } + const pl = ov.querySelector('.sp-presence-live-enable'); + const plV = _ttReadSwitchNow(pl); + if (typeof plV === 'boolean') { + const v = plV; + if (!!(d.presence_live_header ?? true) !== v) this._awayTouched = true; + d.presence_live_header = v; + } + const den = ov.querySelector('.sp-away-delay-enable'); + const denV = _ttReadSwitchNow(den); + if (typeof denV === 'boolean') { + const v = denV; + if (!!(d.away_delay_enabled ?? false) !== v) this._awayTouched = true; + d.away_delay_enabled = v; + } + const dval = ov.querySelector('.sp-away-delay-value'); + if (dval) { + const n = Number(dval.value); + const v = Number.isFinite(n) ? Math.max(0, Math.round(n)) : 0; + if (Number(d.away_delay_value ?? 0) !== v) this._awayTouched = true; + d.away_delay_value = v; + } + const dunit = ov.querySelector('.sp-away-delay-unit'); + if (dunit) { + const v = (String(dunit.value || 'minutes') === 'seconds') ? 'seconds' : 'minutes'; + if (String(d.away_delay_unit || 'minutes') !== v) this._awayTouched = true; + d.away_delay_unit = v; + } + } + } catch {} + // IMPORTANT: do not persist on toggle/input; only commit+save here. + this._applySettingsPopupDraftToConfig(); + if (this._config?.storage_enabled && typeof this._saveStore === 'function') { + await this._saveStore(true); + // Pull fresh settings back from the integration to avoid any race where UI shows defaults after a hard refresh + try { + const api = await this._apiFetchState(); + const s = api?.settings || null; + if (s && typeof s === 'object') { + if (typeof s.holidays_enabled === 'boolean') this._config.holidays_enabled = !!s.holidays_enabled; + if (typeof s.holidays_source === 'string') this._config.holidays_source = (s.holidays_source === 'manual') ? 'manual' : 'calendar'; + if (typeof s.holidays_entity === 'string') this._config.holidays_entity = String(s.holidays_entity || ''); + if (Array.isArray(s.holidays_dates)) this._config.holidays_dates = [...s.holidays_dates]; + if (s.away && typeof s.away === 'object') { + try { this._config.away = JSON.parse(JSON.stringify(s.away)); } catch { this._config.away = { ...(s.away || {}) }; } + } + if (typeof s.presence_live_header === 'boolean') this._config.presence_live_header = !!s.presence_live_header; + if (typeof s.away_delay_enabled === 'boolean') this._config.away = { ...(this._config.away || {}), delay_enabled: !!s.away_delay_enabled }; + if (Number.isFinite(Number(s.away_delay_value))) this._config.away = { ...(this._config.away || {}), delay_value: Number(s.away_delay_value) }; + if (typeof s.away_delay_unit === 'string') this._config.away = { ...(this._config.away || {}), delay_unit: (String(s.away_delay_unit || 'minutes') === 'seconds') ? 'seconds' : 'minutes' }; + if (s.open_window && typeof s.open_window === 'object') this._config.open_window = { ...s.open_window }; + } + } catch {} + } else { + try { if (typeof this._debouncedSaveStore === 'function') this._debouncedSaveStore(); } catch {} + } + } catch {} + try { this._closeSettingsPopup(true); } catch {} + try { this._render(); } catch {} + // Ensure other card instances update immediately after Save (used for Colors "Update schedule") + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + try { document.querySelectorAll('thermostat-timeline-card').forEach(el=>el?.devRefreshFromEditor?.()); } catch {} + } + + // --- Test popup (entity picker + available services) --- + _openTestPopup(){ + try { + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-test'); + const picker = this.shadowRoot && this.shadowRoot.querySelector('.test-picker'); + const select = this.shadowRoot && this.shadowRoot.querySelector('.test-select'); + if (!ov || !picker) return; + ov.classList.add('open'); + // If HA's entity picker web component isn't loaded, show a simple to avoid menu overlays escaping the modal + const hasPicker = false && !!customElements.get('ha-entity-picker'); + if (hasPicker) { + try { picker.style.display = ''; } catch {} + try { if (select) select.style.display = 'none'; } catch {} + // Bind hass to entity picker and set current value + try { picker.hass = this._hass; } catch {} + try { if (this._testEntity) picker.value = this._testEntity; } catch {} + // Attach one-time listener (avoid duplicates) + try { + if (!picker._ttBound) { + picker._ttBound = true; + picker.addEventListener('value-changed', (e)=>{ try { this._testEntity = e.detail?.value || e.target?.value || ''; this._renderTestCommands(); } catch {} }); + picker.addEventListener('change', (e)=>{ try { this._testEntity = e.target?.value || ''; this._renderTestCommands(); } catch {} }); + } + } catch {} + } else { + // Fallback: populate select with all entity ids + try { picker.style.display = 'none'; } catch {} + if (select) { + try { select.style.display = ''; } catch {} + // Build options once per open + select.innerHTML = ''; + const states = this._hass?.states || {}; + const items = Object.keys(states).map(id=>{ + const fn = states[id]?.attributes?.friendly_name || id; + return { id, fn: String(fn) }; + }).sort((a,b)=> a.fn.localeCompare(b.fn)); + const ph = document.createElement('option'); ph.value=''; ph.textContent = this._t('editor.test_select_placeholder'); select.append(ph); + for (const it of items){ const o=document.createElement('option'); o.value=it.id; o.textContent=`${it.fn} (${it.id})`; if (this._testEntity===it.id) o.selected = true; select.append(o); } + if (!select._ttBound){ select._ttBound = true; select.addEventListener('change',(e)=>{ try { this._testEntity = e.target?.value || ''; this._renderTestCommands(); } catch {} }); } + } + } + this._renderTestCommands(); + this._updateTestExportEnabled(); + } catch {} + } + + _renderTestCommands(){ + try { + const list = this.shadowRoot && this.shadowRoot.querySelector('.test-list'); + if (!list) return; + list.innerHTML = ''; + const eid = String(this._testEntity || '').trim(); + if (!eid) { + const empty = document.createElement('div'); empty.className = 'test-empty'; empty.textContent = this._t('editor.test_empty'); list.append(empty); return; + } + const domain = eid.split('.')[0]; + const all = (this._hass && this._hass.services) ? this._hass.services : {}; + const svcObj = all[domain] || {}; + // If chosen entity is a climate, also show generic homeassistant "set_state" fields? Keep to domain services for now + const names = Object.keys(svcObj).sort(); + if (!names.length) { + const empty = document.createElement('div'); empty.className = 'test-empty'; empty.textContent = `Ingen services fundet for domænet "${domain}".`; list.append(empty); return; + } + for (const s of names){ + const def = svcObj[s] || {}; + const item = document.createElement('div'); item.className = 'test-item'; + const h = document.createElement('h4'); h.textContent = `${domain}.${s}`; item.append(h); + const ul = document.createElement('ul'); ul.className = 'test-fields'; + const fields = def.fields || {}; + const fNames = Object.keys(fields); + if (!fNames.length) { + const li = document.createElement('li'); li.textContent = this._t('editor.test_no_fields'); ul.append(li); + } else { + for (const fn of fNames){ + const fd = fields[fn] || {}; + const li = document.createElement('li'); + const req = fd.required ? ' (påkrævet)' : ''; + const desc = fd.description ? ` – ${fd.description}` : ''; + const ex = typeof fd.example !== 'undefined' ? ` Eksempel: ${JSON.stringify(fd.example)}` : ''; + li.textContent = `${fn}${req}${desc}${ex}`; + ul.append(li); + } + } + item.append(ul); + list.append(item); + } + this._updateTestExportEnabled(); + } catch {} + } + + _updateTestExportEnabled(){ + try { + const btn = this.shadowRoot && this.shadowRoot.querySelector('.test-export'); + if (!btn) return; + const eid = String(this._testEntity || '').trim(); + const domain = eid && eid.includes('.') ? eid.split('.')[0] : ''; + const has = domain && this._hass && this._hass.services && this._hass.services[domain] && Object.keys(this._hass.services[domain]).length; + btn.disabled = !(eid && has); + } catch {} + } + + _exportTestCommands(){ + try { + if (this._exporting) return; // throttle to avoid accidental multi-download + this._exporting = true; setTimeout(()=>{ this._exporting = false; }, 600); + const text = this._buildTestExportText(); + if (!text) return; + const eid = String(this._testEntity || 'entity').replace(/[^a-zA-Z0-9_.-]+/g,'_'); + const name = `${eid}-commands.md`; + const blob = new Blob([text], { type: 'text/markdown;charset=utf-8' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = name; document.body.appendChild(a); a.click(); + setTimeout(()=>{ try { document.body.removeChild(a); URL.revokeObjectURL(url); } catch {} }, 0); + } catch {} + } + + _buildTestExportText(){ + try { + const eid = String(this._testEntity || '').trim(); + if (!eid) return ''; + const st = this._hass?.states?.[eid]; + const friendly = st?.attributes?.friendly_name || ''; + const domain = eid.split('.')[0]; + const date = new Date(); + const lines = []; + lines.push(`Entity: ${eid}${friendly?` (${friendly})`:''}`); + lines.push(`Domain: ${domain}`); + lines.push(`Generated: ${date.toISOString()}`); + lines.push(''); + const svcObj = (this._hass?.services?.[domain]) || {}; + const names = Object.keys(svcObj).sort(); + if (!names.length) { lines.push('No services for this domain.'); return lines.join('\n'); } + for (const s of names){ + // Mark command in Markdown-style bold for easier reading + lines.push(`**${domain}.${s}**`); + const fields = (svcObj[s] && svcObj[s].fields) || {}; + const fNames = Object.keys(fields); + if (!fNames.length) { lines.push(' (no fields)'); continue; } + for (const fn of fNames){ + const fd = fields[fn] || {}; + const req = fd.required ? 'required' : 'optional'; + const desc = fd.description ? ` - ${fd.description}` : ''; + const ex = typeof fd.example !== 'undefined' ? ` Example: ${JSON.stringify(fd.example)}` : ''; + lines.push(` ${fn} (${req})${desc}${ex}`); + } + lines.push(''); + } + return lines.join('\n'); + } catch { return ''; } + } + + // Allow editor to nudge a repaint explicitly + devRefreshFromEditor(){ + try { this._ensureSchedules(); } catch {} + try { if (!this._inlineEditing && !this._editing) this._render(); } catch {} + } + + _openEditor(entity, blockId) { + this._ensureEditModeOn(); + if (!blockId) return this._openNewEditor(entity); + this._editing = { entity, blockId }; + // Hide any tooltips while editing + try { + const tips = this.shadowRoot.querySelectorAll('.wk-tooltip'); + tips.forEach(t => { try { if (t._hideTimer) clearTimeout(t._hideTimer); } catch {} t.style.display = 'none'; }); + } catch {} + const row = this._schedules[entity]; + if (!row) return; + const b = row.blocks.find(x => x.id === blockId); + if (!b) return; + const edTemp = this.shadowRoot.querySelector(".ed-temp"); + const edFrom = this.shadowRoot.querySelector(".ed-from"); + const edTo = this.shadowRoot.querySelector(".ed-to"); + const fromMer = this.shadowRoot.querySelector('.ed-from-mer'); + const toMer = this.shadowRoot.querySelector('.ed-to-mer'); + const overlay = this.shadowRoot.querySelector(".overlay"); + const title = this.shadowRoot.querySelector('.modal h3'); + const delBtn = this.shadowRoot.querySelector('.ed-delete'); + if (edTemp) { edTemp.value = String(this._toDisplayTemp(b.temp)); edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } + if (this._config?.time_12h) { + if (edFrom) edFrom.value = this._toTimeInput12h(b.startMin); + if (edTo) edTo.value = this._toTimeInput12h(b.endMin); + if (fromMer) { fromMer.style.display=''; fromMer.value = (b.startMin>=720)?'PM':'AM'; } + if (toMer) { toMer.style.display=''; toMer.value = (b.endMin>=720 && b.endMin<1440)?'PM':'AM'; } + } else { + if (edFrom) edFrom.value = this._toTimeInput(b.startMin); + if (edTo) edTo.value = this._toTimeInput(b.endMin); + if (fromMer) fromMer.style.display='none'; + if (toMer) toMer.style.display='none'; + } + if (title) title.textContent = this._t('ui.edit_block'); + if (delBtn) delBtn.style.display = ''; + const err = this.shadowRoot.querySelector(".ed-error"); + if (err) { err.style.display = "none"; err.textContent = ""; } + + try { const modalEl = this.shadowRoot.querySelector('.overlay .modal:not(.modal-week)'); if (modalEl) modalEl.classList.toggle('modal-12h', !!this._config?.time_12h); } catch {} + overlay.classList.add("open"); + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + try { + const onKey = (ev) => { if (ev.key === 'Escape') { this._closeEditor(); } }; + window.addEventListener('keydown', onKey, { once: true }); + } catch {} + // Improved keyboard navigation between time fields (two-step Tab in "Fra") + try { + const fromInput = this.shadowRoot.querySelector(".ed-from"); + const toInput = this.shadowRoot.querySelector(".ed-to"); + if (fromInput && toInput) { + // Focus "Fra" on open for fast editing + setTimeout(() => { try { fromInput.focus(); fromInput.select?.(); } catch {} }, 0); + // Two-step Tab inside "Fra": hours -> minutes -> move to "Til" + let fraStep = 0; + fromInput.addEventListener("keydown", (e) => { + if (e.key === "Tab" && !e.shiftKey) { + e.preventDefault(); + try { + const val = fromInput.value || ""; + if (fraStep === 0) { + if (fromInput.setSelectionRange) { + setTimeout(() => { try { fromInput.setSelectionRange(3, 5); } catch {} }, 0); + } + fraStep = 1; + } else { + fraStep = 0; + toInput.focus(); + toInput.select?.(); + } + } catch { + if (fraStep == 0) { fraStep = 1; } else { fraStep = 0; toInput.focus(); toInput.select?.(); } + } + } + }); + // Shift+Tab from Til -> Fra + toInput.addEventListener("keydown", (e) => { + if (e.key === "Tab" && e.shiftKey) { + e.preventDefault(); + fraStep = 1; + fromInput.focus(); + try { fromInput.setSelectionRange?.(3,5); } catch {} + } + }); + fromInput.addEventListener("input", () => { fraStep = 0; }); + } + } catch {} + + try { + const onKey = (ev) => { if (ev.key === 'Escape') { this._closeEditor(); } }; + window.addEventListener('keydown', onKey, { once: true }); + } catch {} + // Improved keyboard navigation between time fields + try { + const fromInput = this.shadowRoot.querySelector(".ed-from"); + const toInput = this.shadowRoot.querySelector(".ed-to"); + if (fromInput && toInput) { + // Focus "Fra" on open for fast editing + setTimeout(() => { try { fromInput.focus(); fromInput.select?.(); } catch {} }, 0); + // Tab from Fra -> Til (skip any intermediary focusable elements like clock icons) + fromInput.addEventListener("keydown", (e) => { + if (e.key === "Tab" && !e.shiftKey) { + e.preventDefault(); + toInput.focus(); + toInput.select?.(); + } + }); + // Shift+Tab from Til -> Fra + toInput.addEventListener("keydown", (e) => { + if (e.key === "Tab" && e.shiftKey) { + e.preventDefault(); + fromInput.focus(); + fromInput.select?.(); + } + }); + } + } catch {} + + try { + const onKey = (ev) => { if (ev.key === 'Escape') { this._closeEditor(); } }; + // Add once to avoid stacking listeners across multiple opens + window.addEventListener('keydown', onKey, { once: true }); + } catch {} + + // Also reflect the current option on the global select entity instantly + try { + const selOpt = (()=>{ + const all = this._hass?.states || {}; + for (const eid in all){ + if (!Object.prototype.hasOwnProperty.call(all, eid)) continue; + if (!eid.startsWith('select.')) continue; + const st = all[eid]; + const fn = String(st?.attributes?.friendly_name || '').toLowerCase(); + if (fn === 'thermostat profile' || fn.startsWith('thermostat profile')){ + return String(st?.state || ''); + } + } + return null; + })(); + if (selOpt !== null) { + const norm = (v)=>{ const s = String(v||'').trim(); return (s.toLowerCase()==='none' || s==='') ? null : s; }; + const want = norm(selOpt); + const changed = want !== this._globalProfile; + if (changed) { + this._globalProfile = want; + if (want) this._config.profiles_enabled = true; + // Update local schedules so UI reflects change instantly (no save) + try { + for (const eid of (this._config?.entities || [])){ + const r = this._schedules[eid]; if (!r) continue; this._ensureProfilesStruct(r); + if (!want) { r.activeProfile = null; continue; } + const has = !!(r.profiles && r.profiles[want]); + r.activeProfile = has ? want : null; + } + } catch {} + if (!this._inlineEditing && !this._editing) this._render(); + this._scheduleNextApply(); + // Persist back to integration so sensor version updates immediately + try { this._debouncedSaveStore(); } catch {} + } + } + } catch {} + + } + _closeEditor(){ + this._editing = null; + this.shadowRoot.querySelector(".overlay")?.classList.remove("open"); + try { this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} + // If we hid the weekday modal for editing, decide whether to restore it + // Only restore when the edit was initiated from inside the weekdays popup. + // If the edit was initiated from the main timeline (auto-save flow), do not reopen the weekdays popup. + if (this._weeklyOverlayHiddenForEditor && this._weeklyDraft) { + const cameFromTimeline = !!this._autoSaveWeeklyFromTimeline; + this._weeklyOverlayHiddenForEditor = false; + if (cameFromTimeline) { + // Do not bounce back into weekdays popup on cancel; reset the auto-save flag and close weekly context + this._autoSaveWeeklyFromTimeline = false; + this._closeWeeklyEditor(); + } else { + const wov = this.shadowRoot.querySelector('.overlay-week'); + if (wov) wov.classList.add('open'); + this._renderWeeklyModal(); + } + } + // If we hid the profiles modal for editing, restore it on close + if (this._profilesOverlayHiddenForEditor && this._profilesOpen) { + this._profilesOverlayHiddenForEditor = false; + const pov = this.shadowRoot.querySelector('.overlay-profiles'); + if (pov) pov.classList.add('open'); + this._renderProfilesModal(); + } + // If we hid the holiday modal for editing, restore it on close + if (this._holidayOverlayHiddenForEditor && this._holidayOpen) { + this._holidayOverlayHiddenForEditor = false; + const hov = this.shadowRoot.querySelector('.overlay-holiday'); + if (hov) hov.classList.add('open'); + this._renderHolidayModal(); + } + // Restore presence modal if it is open + try { + if (this._presenceOpen) { + const pov = this.shadowRoot.querySelector('.overlay-presence'); + if (pov) pov.classList.add('open'); + this._renderPresenceModal(); + } + } catch {} + } + + async _deleteFromEditor(){ if (!this._editing) return; const { entity, blockId, weeklyDay, profileName }=this._editing; if (!blockId) { this._closeEditor(); return; } + if (weeklyDay && this._weeklyDraft) { + // Delete from both _weeklyDraft.days AND _weeklyDraft._weeklyModes to ensure complete removal + + // First, try to delete from _weeklyDraft.days + let foundInDays = false; + if (this._weeklyDraft.days?.[weeklyDay]) { + const arr = this._weeklyDraft.days[weeklyDay]; + const idx = arr.findIndex(x=>x.id===blockId); + if (idx >= 0) { + arr.splice(idx, 1); + foundInDays = true; + } + } + + // Also delete from _weeklyDraft._weeklyModes if it exists (should delete from ALL modes to be safe) + let foundInModes = false; + if (this._weeklyDraft?._weeklyModes) { + for (const modeName in this._weeklyDraft._weeklyModes) { + const modeBlocks = this._weeklyDraft._weeklyModes[modeName]?.days?.[weeklyDay]; + if (Array.isArray(modeBlocks)) { + const idx = modeBlocks.findIndex(x=>x.id===blockId); + if (idx >= 0) { + modeBlocks.splice(idx, 1); + foundInModes = true; + } + } + } + } + + // If the weekly editor was opened implicitly from the main timeline, + // save directly without applying group sync (which would re-copy deleted blocks) + if (this._autoSaveWeeklyFromTimeline) { + this._autoSaveWeeklyFromTimeline = false; + // Save without applying group propagation + const entity = this._weeklyEntity; + const row = this._schedules[entity]; + if (row) { + const mode = this._weeklyDraft.mode || 'weekday_weekend'; + this._ensureWeeklyStruct(row); + // Save to both weekly and weekly_modes without syncing across group days. + // IMPORTANT: main timeline builds _weeklyDraft from weekly_modes, so if we only + // update row.weekly.days here, the next edit will reload stale blocks from + // row.weekly_modes and "revive" deleted blocks. + const daysCopy = JSON.parse(JSON.stringify(this._weeklyDraft.days || {})); + // Keep any existing modes, but ensure the active mode reflects this draft. + try { + if (this._weeklyDraft._weeklyModes) { + row.weekly_modes = JSON.parse(JSON.stringify(this._weeklyDraft._weeklyModes)); + } + } catch {} + try { + if (!row.weekly_modes || typeof row.weekly_modes !== 'object') row.weekly_modes = {}; + row.weekly_modes[mode] = { days: JSON.parse(JSON.stringify(daysCopy)) }; + } catch { + try { row.weekly_modes = row.weekly_modes || {}; row.weekly_modes[mode] = { days: daysCopy }; } catch {} + } + row.weekly = { mode, days: JSON.parse(JSON.stringify(daysCopy)) }; + this._schedules[entity] = row; + this._config.weekdays_mode = mode; + const before = this._desiredNowSnapshot(); + try { this._persistDraftNow(); } catch {} + this._markPendingSave(); + if (this._config.apply_on_edit) await this._applyIfDesiredChanged(before); + this._scheduleNextApply(); + } + this._closeEditor(); + // Close weekly overlay when editing from main timeline + const wov = this.shadowRoot.querySelector('.overlay-week'); + if (wov) wov.classList.remove('open'); + this._weeklyOpen = false; + this._render(); + } else { + // Regular flow when user is inside the weekdays popup: update preview and stay there + this._renderWeeklyModal(); + this._closeEditor(); + } + return; + } + if (this._editing.presence && this._presenceDraft && this._presenceDraft.rooms) { + try { + const eid = this._editing.entity; + const arr = (this._presenceDraft.rooms||{})[eid] || []; + const idx = arr.findIndex(x=>x.id===blockId); + if (idx>=0) arr.splice(idx,1); + (this._presenceDraft.rooms||{})[eid] = arr; + this._render(); this._closeEditor(); this._renderPresenceModal(); + } catch { this._closeEditor(); } + return; + } + if (this._editing.holiday && this._holidayDraft && this._holidayDraft.rooms) { + try { + const eid = this._editing.entity; + const arr = (this._holidayDraft.rooms||{})[eid] || []; + const idx = arr.findIndex(x=>x.id===blockId); + if (idx>=0) arr.splice(idx,1); + (this._holidayDraft.rooms||{})[eid] = arr; + this._render(); this._closeEditor(); this._renderHolidayModal(); + } catch { this._closeEditor(); } + return; + } + if (profileName) { + try { + // Ensure draft exists and operate on the current room's draft blocks map + const isNewProf = (profileName === '__new__'); + const curRoom = this._profilesRoom || this._profilesEntity || entity; + if (!this._profilesDraft || (!isNewProf && this._profilesDraft.name !== profileName)) { + const row = this._schedules[entity]; + const roomsMap = {}; + for (const e of (this._config?.entities||[])){ + const src = !isNewProf ? (this._schedules[e]?.profiles?.[profileName]?.blocks || []) : []; + roomsMap[e] = JSON.parse(JSON.stringify(src)); + } + this._profilesDraft = { name: (isNewProf? null : profileName), rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + } + const arr = (this._profilesDraft.rooms||{})[curRoom] || []; + const idx = arr.findIndex(x=>x.id===blockId); + if (idx>=0) arr.splice(idx,1); + (this._profilesDraft.rooms||{})[curRoom] = arr; + this._profilesDirty = true; + // If deletion was initiated from the main timeline, persist immediately + if (this._autoSaveProfileFromTimeline) { + try { this._autoSaveProfileFromTimeline = false; await this._profilesSaveDraft(); } catch {} + } + this._render(); this._closeEditor(); if (this._profilesOpen) this._renderProfilesModal(); + } catch { this._closeEditor(); } + return; + } + this._deleteBlock(entity, blockId); this._closeEditor(); } + + _openNewEditor(entity){ this._ensureEditModeOn(); this._editing = { entity, blockId: null }; const row = this._schedules[entity]; if (!row) return; const edTemp = this.shadowRoot.querySelector(".ed-temp"); const edFrom = this.shadowRoot.querySelector(".ed-from"); const edTo = this.shadowRoot.querySelector(".ed-to"); const overlay = this.shadowRoot.querySelector(".overlay"); const now = this._getNowMin(); const start = this._clamp(Math.round(now), 0, 1380); const end = this._clamp(start + 60, start + 15, 1440); if (edTemp) edTemp.value = String(this._toDisplayTemp(row.defaultTemp ?? 20)); if (edFrom) edFrom.value = this._toTimeInput(start); if (edTo) edTo.value = this._toTimeInput(end); const title = this.shadowRoot.querySelector('.modal h3'); if (title) title.textContent = this._t('ui.add_block'); const delBtn = this.shadowRoot.querySelector('.ed-delete'); if (delBtn) delBtn.style.display = 'none'; const err = this.shadowRoot.querySelector(".ed-error"); if (err) { err.style.display = "none"; err.textContent = ""; } + /* popups visible: continue */ + // Hide any tooltips while editing + try { const tips = this.shadowRoot.querySelectorAll('.wk-tooltip'); tips.forEach(t => { try { if (t._hideTimer) clearTimeout(t._hideTimer); } catch {} t.style.display='none'; }); } catch {} + try { const modalEl = this.shadowRoot.querySelector('.overlay .modal:not(.modal-week)'); if (modalEl) modalEl.classList.toggle('modal-12h', !!this._config?.time_12h); } catch {} + // Open after a frame to avoid half-rendered UI flash + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { overlay.classList.add('open'); })); } catch { overlay.classList.add('open'); } + try { const fromMer = this.shadowRoot.querySelector('.ed-from-mer'); const toMer = this.shadowRoot.querySelector('.ed-to-mer'); if (this._config?.time_12h) { if (edFrom) edFrom.value = this._toTimeInput12h(start); if (edTo) edTo.value = this._toTimeInput12h(end); if (fromMer){ fromMer.style.display=''; fromMer.value = (start>=720)?'PM':'AM'; } if (toMer){ toMer.style.display=''; toMer.value = (end>=720 && end<1440)?'PM':'AM'; } } else { if (fromMer) fromMer.style.display='none'; if (toMer) toMer.style.display='none'; } } catch {} + if (edTemp) { edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } + try { + const onKey = (ev) => { if (ev.key === 'Escape') { this._closeEditor(); } }; + window.addEventListener('keydown', onKey, { once: true }); + } catch {} + // Improved keyboard navigation between time fields (two-step Tab in "Fra") + try { + const fromInput = this.shadowRoot.querySelector(".ed-from"); + const toInput = this.shadowRoot.querySelector(".ed-to"); + if (fromInput && toInput) { + // Focus "Fra" on open for fast editing + setTimeout(() => { try { fromInput.focus(); fromInput.select?.(); } catch {} }, 0); + // Two-step Tab inside "Fra": hours -> minutes -> move to "Til" + let fraStep = 0; + fromInput.addEventListener("keydown", (e) => { + if (e.key === "Tab" && !e.shiftKey) { + e.preventDefault(); + try { + const val = fromInput.value || ""; + if (fraStep === 0) { + if (fromInput.setSelectionRange) { + setTimeout(() => { try { fromInput.setSelectionRange(3, 5); } catch {} }, 0); + } + fraStep = 1; + } else { + fraStep = 0; + toInput.focus(); + toInput.select?.(); + } + } catch { + if (fraStep == 0) { fraStep = 1; } else { fraStep = 0; toInput.focus(); toInput.select?.(); } + } + } + }); + // Shift+Tab from Til -> Fra + toInput.addEventListener("keydown", (e) => { + if (e.key === "Tab" && e.shiftKey) { + e.preventDefault(); + fraStep = 1; + fromInput.focus(); + try { fromInput.setSelectionRange?.(3,5); } catch {} + } + }); + fromInput.addEventListener("input", () => { fraStep = 0; }); + } + } catch {} + } + + // ---------- Weekdays modal ---------- + _openWeeklyEditor(entity){ + this._ensureEditModeOn(); + try { this._applySeasonalModeToRows(); } catch {} + this._weeklyEntity = entity; + const row = this._schedules[entity] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureWeeklyStruct(row); + // Reset any pending cross-room copies for this editing session + this._weeklyCopyTargets = []; + + // Away delay tracking + this._awayWant = null; // boolean | null + this._awayWantSinceMs = 0; // timestamp when away want last changed + // Deep copy as draft from the dataset for current mode + try { + const curMode = (row.weekly?.mode) || (this._config?.weekdays_mode) || 'weekday_weekend'; + const src = row.weekly_modes?.[curMode]?.days || {}; + const names = this._weekdayNames(); + const draftDays = JSON.parse(JSON.stringify(src)); + for (const k of names){ if (!Array.isArray(draftDays[k])) draftDays[k] = []; } + this._weeklyDraft = { mode: curMode, days: draftDays }; + } catch { + this._weeklyDraft = JSON.parse(JSON.stringify(row.weekly)); + } + this._weeklyDayKey = this._todayKey(); + this._weeklyOpen = true; + const ov = this.shadowRoot.querySelector('.overlay-week'); + // Mark overlaying first to hide background artifacts + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + // Build the modal while hidden + this._renderWeeklyModal(); + // Open after a paint + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { if (ov) ov.classList.add('open'); })); } catch { if (ov) ov.classList.add('open'); } + } + _closeWeeklyEditor(){ + try { this._weeklyOpen = false; this._weeklyEntity = null; this._weeklyDraft = null; this._weeklyDayKey = null; } catch {} + try { this.shadowRoot.querySelector('.overlay-week')?.classList.remove('open'); } catch {} + try { this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} + try { + if (this._resumeSettingsAfterWeekdays) { + this._resumeSettingsAfterWeekdays = false; + this._resumeSettingsPopup?.(); + } + } catch {} + } + _weeklySaveAll(){ try { if (!this._weeklyEntity || !this._weeklyDraft) { this._closeWeeklyEditor(); return; } const entity = this._weeklyEntity; const row = this._schedules[entity]; if (!row) { this._closeWeeklyEditor(); return; } + // Normalize by mode: propagate grouped days within the selected mode only + const mode = this._weeklyDraft.mode || 'weekday_weekend'; + const d = this._weeklyDraft.days || {}; + const applyGroup = (keys, srcKey)=>{ const src = JSON.parse(JSON.stringify(d[srcKey]||[])); for (const k of keys){ d[k] = JSON.parse(JSON.stringify(src)); } }; + if (mode === 'weekday_weekend') { + applyGroup(['tue','wed','thu','fri'], 'mon'); + applyGroup(['sun'], 'sat'); + } else if (mode === 'weekday_sat_sun') { + applyGroup(['tue','wed','thu','fri'], 'mon'); + } // all_7 -> keep individual + this._ensureWeeklyStruct(row); + // Save into mode bucket and set active weekly to this mode + try { row.weekly_modes[mode] = { days: JSON.parse(JSON.stringify(d)) }; } catch { row.weekly_modes[mode] = { days: d }; } + row.weekly = { mode, days: JSON.parse(JSON.stringify(d)) }; + this._schedules[entity] = row; + // Also update the global fallback mode so other rooms without per-room override follow this selection + this._config.weekdays_mode = mode; + // Also apply to any pending target rooms selected via "copy week to rooms" + const targets = Array.isArray(this._weeklyCopyTargets) ? this._weeklyCopyTargets : []; + if (targets.length) { + // Make a fresh copy for targets (don't reuse the modified 'd' which already has applyGroup applied) + const targetData = JSON.parse(JSON.stringify(this._weeklyDraft.days || {})); + for (const eid of targets){ + try { + const trow = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureWeeklyStruct(trow); + // Apply group propagation on the target copy (independent from source) + const tdata = JSON.parse(JSON.stringify(targetData)); + const applyGroupTarget = (keys, srcKey)=>{ const src = JSON.parse(JSON.stringify(tdata[srcKey]||[])); for (const k of keys){ tdata[k] = JSON.parse(JSON.stringify(src)); } }; + if (mode === 'weekday_weekend') { + applyGroupTarget(['tue','wed','thu','fri'], 'mon'); + applyGroupTarget(['sun'], 'sat'); + } else if (mode === 'weekday_sat_sun') { + applyGroupTarget(['tue','wed','thu','fri'], 'mon'); + } + trow.weekly_modes[mode] = { days: JSON.parse(JSON.stringify(tdata)) }; + trow.weekly = { mode, days: JSON.parse(JSON.stringify(tdata)) }; + this._schedules[eid] = trow; + } catch {} + } + } + // Clear pending list after Save + this._weeklyCopyTargets = []; + const before = this._desiredNowSnapshot(); + try { this._persistDraftNow(); } catch {} + this._markPendingSave(); + this._closeWeeklyEditor(); + this._render(); + if (this._config.apply_on_edit) this._applyIfDesiredChanged(before); + this._scheduleNextApply(); + } catch(e){ this._closeWeeklyEditor(); } + } + _renderWeeklyModal(){ try { + const modeSel = this.shadowRoot.querySelector('.week-mode'); + const titleEl = this.shadowRoot.querySelector('.week-title'); + const roomEl = this.shadowRoot.querySelector('.week-room'); + const roomTabs = this.shadowRoot.querySelector('.week-roomtabs'); + const note = this.shadowRoot.querySelector('.week-note'); + const tabsWrap = this.shadowRoot.querySelector('.week-tabs'); + const pasteBar = this.shadowRoot.querySelector('.week-pastebar'); + const actionsWrap = this.shadowRoot.querySelector('.wk-actions'); + const track = this.shadowRoot.querySelector('.week-track'); + const scale = this.shadowRoot.querySelector('.week-scale-inner'); + const modalWeek = this.shadowRoot.querySelector('.modal-week'); + if (!this._weeklyDraft) return; + // i18n + if (titleEl) titleEl.textContent = this._t('week.editor.title'); + // Room name on the same header line + try { + if (roomEl) { + const eid = this._weeklyEntity; + let name = ''; + if (eid) { + const custom = (this._config?.labels && this._config.labels[eid]) ? this._config.labels[eid] : null; + name = custom || this._prettyName(eid); + } + roomEl.textContent = name || ''; + } + } catch {} + // Build room tabs (switch room in weekdays editor) + try { + if (roomTabs) { + roomTabs.innerHTML = ''; + const ents = this._config?.entities || []; + if (!this._weeklyEntity || !ents.includes(this._weeklyEntity)) this._weeklyEntity = ents[0] || null; + + for (const eid of ents){ + const btn = document.createElement('button'); + btn.type='button'; + btn.className = 'wk-tab' + (eid===this._weeklyEntity ? ' active' : ''); + let label = ''; + try { label = this._config?.labels?.[eid] || (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1]||eid)); } catch { label = eid; } + btn.textContent = label; + btn.addEventListener('click', ()=>{ if (eid!==this._weeklyEntity) this._openWeeklyEditor(eid); }); + roomTabs.append(btn); + } + + // Hide tabs if only one room + roomTabs.style.display = (ents.length>1) ? 'flex' : 'none'; + } + } catch (e) {} + const modeLab = this.shadowRoot.querySelector('.week-mode-label'); + if (modeLab) modeLab.textContent = this._t('week.mode'); + if (note) note.textContent = this._t('week.editor.preview_note'); + if (modeSel) { + modeSel.value = this._weeklyDraft.mode || 'weekday_weekend'; + // set option labels i18n + for (const opt of Array.from(modeSel.options)){ + if (opt.value==='weekday_weekend') opt.textContent = this._t('week.mode.weekday_weekend'); + if (opt.value==='weekday_sat_sun') opt.textContent = this._t('week.mode.weekday_sat_sun'); + if (opt.value==='all_7') opt.textContent = this._t('week.mode.all_7'); + } + modeSel.onchange = () => { + const newMode = modeSel.value; + this._weeklyDraft.mode = newMode; + // Update global fallback so per-room view reflects latest choice + this._config.weekdays_mode = newMode; + try { + const row = this._schedules[this._weeklyEntity]; + this._ensureWeeklyStruct(row); + const src = row.weekly_modes?.[newMode]?.days || {}; + const names = this._weekdayNames(); + const draftDays = JSON.parse(JSON.stringify(src)); + for (const k of names){ if (!Array.isArray(draftDays[k])) draftDays[k] = []; } + this._weeklyDraft.days = draftDays; + } catch {} + this._renderWeeklyModal(); + }; + } + // Seasonal selector (Summer/Winter) + try { + const seasonRow = this.shadowRoot.querySelector('.week-season-row'); + const seasonLab = this.shadowRoot.querySelector('.week-season-label'); + const seasonSel = this.shadowRoot.querySelector('.week-season'); + if (seasonRow && seasonSel && seasonLab) { + const on = !!this._config?.seasonal_enabled; + seasonRow.style.display = on ? '' : 'none'; + if (on) { + seasonLab.textContent = this._t('season.mode.title') || 'Season'; + const optW = seasonSel.querySelector('.opt-season-winter'); + const optS = seasonSel.querySelector('.opt-season-summer'); + if (optW) optW.textContent = this._t('season.mode.winter') || 'Winter'; + if (optS) optS.textContent = this._t('season.mode.summer') || 'Summer'; + seasonSel.value = this._seasonKey(); + seasonSel.onchange = ()=>{ + try { + const next = (seasonSel.value === 'summer') ? 'summer' : 'winter'; + this._setSeasonMode(next); + this._renderWeeklyModal(); + } catch {} + }; + } + } + } catch {} + // Copy/Paste day logic + // Helper to resolve which real days are in a group for current mode + const groupDaysForMode = (mode, key)=>{ + if (mode === 'weekday_weekend') { + if (key==='weekdays') return ['mon','tue','wed','thu','fri']; + if (key==='weekend') return ['sat','sun']; + } else if (mode === 'weekday_sat_sun') { + if (key==='weekdays') return ['mon','tue','wed','thu','fri']; + if (key==='sat') return ['sat']; + if (key==='sun') return ['sun']; + } else if (mode === 'all_7') { + return [key]; + } + return [this._effectiveDayKey(key)]; + }; + // Build per-day action buttons (unique DOM per tab) + const renderActions = () => { + if (!actionsWrap) return; + actionsWrap.innerHTML = ''; + const curGroup = this._weeklyDayKey || 'weekdays'; + const mode = this._weeklyDraft.mode || 'weekday_weekend'; + // Global action: copy entire week to other rooms + const copyAllBtn = document.createElement('button'); + copyAllBtn.type = 'button'; + copyAllBtn.className = 'btn ghost wk-copyall'; + copyAllBtn.textContent = this._t('week.copy_to_rooms'); + copyAllBtn.onclick = (e)=>{ e.preventDefault(); this._openCopyWeekToRooms(); }; + actionsWrap.append(copyAllBtn); + const itemsKeys = []; + if (this._weeklyDraft.mode === 'weekday_weekend') itemsKeys.push('weekdays','weekend'); + else if (this._weeklyDraft.mode === 'weekday_sat_sun') itemsKeys.push('weekdays','sat','sun'); + else itemsKeys.push('mon','tue','wed','thu','fri','sat','sun'); + for (const key of itemsKeys){ + const box = document.createElement('div'); + box.style.display = key === curGroup ? 'flex' : 'none'; + box.style.gap = '8px'; + box.setAttribute('data-key', key); + // Copy/Paste button (contextual) + const cp = document.createElement('button'); + cp.type = 'button'; + cp.className = 'btn'; + const setCpStyle = (m)=>{ cp.className='btn'; if (m==='copy') cp.classList.add('ghost'); if (m==='paste') cp.classList.add('success'); }; + if (!this._weeklyCopyFromGroup) { cp.textContent = this._t('ui.copy_day'); setCpStyle('copy'); } + else if (this._weeklyCopyFromGroup === key) { cp.textContent = this._t('ui.copy_day'); setCpStyle('copy'); } + else { cp.textContent = this._t('ui.paste_day'); setCpStyle('paste'); } + // Disable on source day while copy mode is active + cp.disabled = !!this._weeklyCopyFromGroup && this._weeklyCopyFromGroup === key; + cp.onclick = (ev)=>{ + ev.preventDefault(); ev.stopPropagation(); + if (cp.disabled) return; + const modeNow = this._weeklyDraft.mode || 'weekday_weekend'; + if (!this._weeklyCopyFromGroup) { + const gDays = groupDaysForMode(modeNow, key); + const srcDay = (modeNow==='all_7') ? this._effectiveDayKey(key) : gDays[0]; + const arr = (this._weeklyDraft?.days?.[srcDay] || []); + this._weeklyCopyFromGroup = key; + this._weeklyCopyPayload = JSON.parse(JSON.stringify(arr)); + renderActions(); + return; + } + if (this._weeklyCopyFromGroup === key) { + // Refresh payload from same source, stay in copy mode + const gDays = groupDaysForMode(modeNow, key); + const srcDay = (modeNow==='all_7') ? this._effectiveDayKey(key) : gDays[0]; + const arr = (this._weeklyDraft?.days?.[srcDay] || []); + this._weeklyCopyPayload = JSON.parse(JSON.stringify(arr)); + return; + } + // Paste into this key + try { + const payload = Array.isArray(this._weeklyCopyPayload) ? this._weeklyCopyPayload.map(x=>({ ...x, id: Math.random().toString(36).slice(2,9) })) : []; + const targets = (modeNow==='all_7') ? [this._effectiveDayKey(key)] : groupDaysForMode(modeNow, key); + const daysClone = { ...(this._weeklyDraft.days || {}) }; + for (const dk of targets){ + const arr = payload.map(x=>({ ...x })); + arr.sort((a,b)=>a.startMin-b.startMin||a.endMin-b.endMin); + daysClone[dk] = arr; + } + this._weeklyDraft.days = daysClone; + this._ensureWeeklyStruct(this._schedules[this._weeklyEntity] || { weekly:{} }); + } catch(e){} + this._weeklyDayKey = key; this._renderWeeklyModal(); + }; + // Add block button (per day) — doubles as Cancel while in copy mode + const add = document.createElement('button'); + add.type = 'button'; + const applyAddState = ()=>{ + const inCopy = !!this._weeklyCopyFromGroup; + add.className = 'btn ' + (inCopy ? 'danger' : 'primary'); + add.textContent = inCopy ? this._t('ui.cancel') : ('+ ' + this._t('ui.add_block')); + }; + applyAddState(); + add.onclick = ()=>{ + if (this._weeklyCopyFromGroup) { + // Cancel copy mode + this._weeklyCopyFromGroup = null; this._weeklyCopyPayload = null; renderActions(); + } else { + this._weeklyDayKey = key; this._openWeeklyBlockEditor(null); + } + }; + box.append(cp, add); + actionsWrap.append(box); + } + }; + renderActions(); + // Ensure pastebar stays hidden with the new contextual control + try { const pb = this.shadowRoot.querySelector('.week-pastebar'); if (pb) { pb.style.display='none'; pb.innerHTML=''; } } catch{} + // Paste target bar (always real days mon..sun). Visible only in copy mode + if (pasteBar) { + if (!this._weeklyCopyFrom) { + pasteBar.style.display = 'none'; pasteBar.innerHTML = ''; + } else { + pasteBar.style.display = 'flex'; pasteBar.innerHTML = ''; + const names = this._weekdayNames(); + for (const k of names){ + const btn = document.createElement('button'); btn.type='button'; + if (k === this._weeklyCopyFrom) { + btn.className = 'btn danger'; btn.textContent = this._t('ui.cancel'); + btn.onclick = ()=>{ this._weeklyCopyFrom = null; this._weeklyCopyPayload = null; this._renderWeeklyModal(); }; + } else { + btn.className = 'btn success'; + const lab = (this._t('week.days_full.'+k) || this._t('week.days.'+k) || k); + btn.textContent = this._t('ui.paste_day') + ' ' + lab; + btn.onclick = ()=>{ + try { + const payload = Array.isArray(this._weeklyCopyPayload) ? this._weeklyCopyPayload.map(x=>({ ...x, id: Math.random().toString(36).slice(2,9) })) : []; + this._weeklyDraft.days[k] = payload.sort((a,b)=>a.startMin-b.startMin||a.endMin-b.endMin); + this._renderWeeklyModal(); + } catch(e){} + }; + } + pasteBar.append(btn); + } + } + } + // Build group/day tabs reflecting mode grouping + tabsWrap.innerHTML = ''; + const editable = this._weeklyEditableKeys(this._weeklyDraft.mode); + const items = []; + if (this._weeklyDraft.mode === 'weekday_weekend') { + items.push({key:'weekdays', label: (this._t('week.group.weekdays')||'Weekdays')}); + items.push({key:'weekend', label: (this._t('week.group.weekend')||'Weekend')}); + } else if (this._weeklyDraft.mode === 'weekday_sat_sun') { + items.push({key:'weekdays', label: (this._t('week.group.weekdays')||'Weekdays')}); + items.push({key:'sat', label: this._t('week.days_full.sat') || this._t('week.days.sat')}); + items.push({key:'sun', label: this._t('week.days_full.sun') || this._t('week.days.sun')}); + } else { // all_7 + for (const k of this._weekdayNames()) items.push({key:k, label:(this._t('week.days_full.'+k) || this._t('week.days.'+k))}); + } + if (!editable.includes(this._weeklyDayKey)) this._weeklyDayKey = editable[0] || 'weekdays'; + // Helper: group -> array of real day keys + const groupDays2 = (mode, key)=>{ + if (mode === 'weekday_weekend') { + if (key==='weekdays') return ['mon','tue','wed','thu','fri']; + if (key==='weekend') return ['sat','sun']; + } else if (mode === 'weekday_sat_sun') { + if (key==='weekdays') return ['mon','tue','wed','thu','fri']; + if (key==='sat') return ['sat']; + if (key==='sun') return ['sun']; + } else if (mode === 'all_7') { + return [key]; + } + return [this._effectiveDayKey(key)]; + }; + for (const it of items){ + const tab = document.createElement('button'); + tab.type = 'button'; + tab.className = 'wk-tab' + (it.key === this._weeklyDayKey ? ' active' : ''); + tab.textContent = it.label; + tab.addEventListener('click', ()=>{ this._weeklyDayKey = it.key; this._renderWeeklyModal(); }); + tabsWrap.append(tab); + } + // Rebuild actions so the visible set matches active tab + renderActions(); + // Compact scale ticks (every 2 hours) + if (scale) { + scale.innerHTML = ''; + for (let i=0;i<=24;i+=2){ + const t = document.createElement('div'); + t.style.position='absolute'; + t.style.left = (i/24*100)+'%'; + t.style.top='4px'; t.style.bottom='4px'; + t.style.borderLeft='1px solid var(--divider-color)'; + const lab = document.createElement('div'); lab.style.position='absolute'; lab.style.top='-2px'; lab.style.transform='translate(-50%,0)'; lab.style.left='0'; lab.style.fontSize='.75rem'; lab.style.color='var(--secondary-text-color)'; + if (this._config?.time_12h){ const p=this._timeParts(i===24?0:i*60); lab.innerHTML = `
    ${p.main}
    ${p.mer||''}
    `; } else { lab.textContent = (i===24? this._label(0) : this._label(i*60)); } + t.append(lab); scale.append(t); + } + } + // Render preview track with today's selected day blocks + track.innerHTML = ''; + const effKey = this._effectiveDayKey(this._weeklyDayKey); + const blocks = (this._weeklyDraft.days?.[effKey] || []); + // Ensure tooltip exists (scoped to the weekly modal to avoid conflicts with other tooltips) + let tooltip = (modalWeek?.querySelector('.wk-tooltip.week')) || this.shadowRoot.querySelector('.wk-tooltip.week'); + if (!tooltip) { tooltip = document.createElement('div'); tooltip.className='wk-tooltip week'; tooltip.style.display='none'; modalWeek?.append(tooltip); } + for (const b of blocks){ + const div = document.createElement('div'); + div.className = 'block'; + div.style.position='absolute'; + div.style.top='6px'; div.style.bottom='6px'; + div.style.left = (b.startMin/1440*100)+'%'; + div.style.width = ((b.endMin-b.startMin)/1440*100)+'%'; + // Only show temperature in the block in the weekly popup (hide time) + try { + const clr = this._colorFor(this._weeklyEntity, b.temp); + if (clr) { + div.style.background = clr; + div.style.borderColor = clr; + const txt = this._contrastTextColor(clr); + if (txt) div.style.color = txt; + } + } catch {} + UiHelper.genBlockPill( + this, + div, + `${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}` + ); + // double-click to edit (prevent bubbling to track handler) + div.addEventListener('dblclick', (ev)=>{ try { ev.stopPropagation(); ev.preventDefault(); } catch {} this._openWeeklyBlockEditor(b.id); }); + // Hover -> show tooltip with full time range + temp (no click needed) + const showTip = ()=>{ + const txt = `${this._label(b.startMin)} - ${this._label(b.endMin)} • ${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}`; + if (!tooltip) return; + try { if (tooltip._hideTimer) { clearTimeout(tooltip._hideTimer); tooltip._hideTimer = null; } } catch {} + // Do not show while editor popup is open + try { + const ov = this.shadowRoot.querySelector('.overlay'); + if (this._editing || (ov && ov.classList.contains('open'))) return; + } catch {} + tooltip.textContent = txt; + const box = div.getBoundingClientRect(); + const cont = modalWeek?.getBoundingClientRect(); + const left = box.left + box.width/2 - (cont?.left||0); + const top = (box.top - (cont?.top||0)) - 8; // a little above + tooltip.style.left = left + 'px'; + tooltip.style.top = top + 'px'; + tooltip.style.transform = 'translate(-50%,-100%)'; + tooltip.style.display = ''; + }; + const hideTip = ()=>{ + if (!tooltip) return; + const delay = (window.matchMedia && window.matchMedia('(pointer:coarse)').matches) ? 3000 : 120; + try { if (tooltip._hideTimer) clearTimeout(tooltip._hideTimer); } catch {} + tooltip._hideTimer = setTimeout(() => { try { tooltip.style.display='none'; tooltip._hideTimer = null; } catch {} }, delay); + }; + div.addEventListener('mouseenter', showTip); + div.addEventListener('mouseleave', hideTip); + track.append(div); + } + // Add click on empty area to add + track.addEventListener('dblclick', (e)=>{ + try { + if (e.target && (e.target.closest && e.target.closest('.block'))) return; + const box = track.getBoundingClientRect(); + const rel = (e.clientX - box.left) / box.width; const min = this._clamp(Math.round(rel*1440),0,1439); + this._openWeeklyBlockEditor(null, min); + } catch { this._openWeeklyBlockEditor(null); } + }); + track.addEventListener('mouseleave', ()=>{ + const tip = modalWeek?.querySelector('.wk-tooltip.week'); + if (!tip) return; + try { if (tip._hideTimer) { clearTimeout(tip._hideTimer); tip._hideTimer = null; } } catch {} + const delay = (window.matchMedia && window.matchMedia('(pointer:coarse)').matches) ? 3000 : 120; + tip._hideTimer = setTimeout(()=>{ try { tip.style.display='none'; tip._hideTimer = null; } catch{} }, delay); + }); + } catch(e){} + } + // ---------- Profiles modal ---------- + _openProfilesEditor(entity){ + try { + this._ensureEditModeOn(); + this._profilesEntity = entity; + const row = this._schedules[entity] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureProfilesStruct(row); + // New creation by default: empty timeline + this._profilesSelected = null; + this._profilesEditingExisting = false; + // Initialize empty draft for ALL rooms + const roomsMap = {}; for (const eid of (this._config?.entities||[])) roomsMap[eid] = []; + this._profilesDraft = { name: null, rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + this._profilesDirty = false; + this._profilesEditingNew = true; + this._profilesRoom = (this._config?.entities||[])[0] || entity; + this._profilesOpen = true; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-profiles'); + // Mark overlaying, render while hidden, then open after paint + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + this._renderProfilesModal(); + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { if (ov) ov.classList.add('open'); })); } catch { if (ov) ov.classList.add('open'); } + } catch { /* no-op */ } + } + _openConfirmCloseProfiles(){ try { + const pov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-profiles'); if (!pov) return; + // If an existing confirm is open, do nothing + if (pov.querySelector('.mini-confirm')) return; + const wrap = document.createElement('div'); wrap.className='mini-confirm'; + const box = document.createElement('div'); box.className='box'; + const h = document.createElement('h4'); h.textContent = this._t('ui.unsaved_confirm'); + const btns = document.createElement('div'); btns.className='btns'; + const keep = document.createElement('button'); keep.type='button'; keep.className='btn ghost'; keep.textContent = this._t('ui.keep_editing') || 'Keep editing'; + const discard = document.createElement('button'); discard.type='button'; discard.className='btn danger'; discard.textContent = this._t('ui.discard_changes') || 'Discard changes'; + keep.addEventListener('click', ()=>{ try { wrap.remove(); } catch {} }); + discard.addEventListener('click', ()=>{ try { wrap.remove(); } catch {}; this._closeProfilesEditor(); }); + btns.append(keep, discard); box.append(h, btns); wrap.append(box); pov.append(wrap); + } catch {} } + _closeProfilesEditor(){ + this._profilesOpen = false; this._profilesEntity = null; this._profilesSelected = null; this.shadowRoot.querySelector('.overlay-profiles')?.classList.remove('open'); + try { this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} + } + _renderProfilesModal(){ try { + const title = this.shadowRoot.querySelector('.prof-title'); if (title) title.textContent = this._t('profiles.editor.title'); + const roomEl = this.shadowRoot.querySelector('.prof-room'); if (roomEl) { const eid = this._profilesEntity; const custom = (this._config?.labels?.[eid]) || null; roomEl.textContent = custom || this._prettyName(eid||''); } + const note = this.shadowRoot.querySelector('.prof-note'); if (note) note.textContent = this._t('profiles.preview_note'); + const list = this.shadowRoot.querySelector('.prof-list'); + const addBtn = this.shadowRoot.querySelector('.prof-add'); + const closeBtn = this.shadowRoot.querySelector('.prof-close'); + const saveBtn = this.shadowRoot.querySelector('.prof-save'); + const modalProf = this.shadowRoot.querySelector('.modal-profiles'); + const scale = modalProf?.querySelector('.week-scale-inner') || this.shadowRoot.querySelector('.week-scale-inner'); + const track = modalProf?.querySelector('.week-track') || this.shadowRoot.querySelector('.week-track'); + const roomTabs = modalProf?.querySelector('.prof-roomtabs'); + if (!list || !track || !this._profilesEntity) return; + + // Keep the Save button label in sync with current mode + try { + if (saveBtn) { + const isExisting = !!(this._profilesEditingExisting && this._profilesSelected); + const key = isExisting ? 'profiles.save_existing' : 'profiles.save_new'; + saveBtn.textContent = this._t(key) || this._t('ui.save') || 'Save'; + } + } catch {} + const row = this._schedules[this._profilesEntity] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureProfilesStruct(row); + // Build room tabs (one per configured entity) + if (roomTabs) { + roomTabs.innerHTML = ''; + const ents = this._config?.entities || []; + if (!this._profilesRoom || !ents.includes(this._profilesRoom)) this._profilesRoom = ents[0] || null; + for (const eid of ents){ + const btn = document.createElement('button'); btn.type='button'; btn.className='wk-tab' + (eid===this._profilesRoom?' active':''); + let label = this._config?.labels?.[eid] || (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1]||eid)); + btn.textContent = label; btn.addEventListener('click', ()=>{ this._profilesRoom = eid; this._renderProfilesModal(); }); + roomTabs.append(btn); + } + } + // Build list + list.innerHTML = ''; + const names = Object.keys(row.profiles || {}); + if (!names.length) { + const p = document.createElement('div'); p.style.fontSize = '.9rem'; p.style.color='var(--secondary-text-color)'; p.textContent = this._t('profiles.none'); list.append(p); + } + for (const name of names){ + const item = document.createElement('div'); item.className = 'prof-item' + (this._profilesSelected===name ? ' active' : ''); + // Name label: intentionally non-interactive (only action buttons should do something) + const left = document.createElement('div'); left.className='prof-chip'; left.textContent = name; + const actions = document.createElement('div'); actions.className = 'prof-actions'; + const editBtn = document.createElement('button'); editBtn.type='button'; editBtn.className='btn ghost'; editBtn.textContent=this._t('profiles.edit'); + // Highlight Edit button when this profile is the one currently being edited + try { + if (this._profilesEditingExisting && this._profilesSelected === name) { + editBtn.classList.remove('ghost'); + editBtn.classList.add('success'); + editBtn.setAttribute('aria-pressed','true'); + } else { + editBtn.setAttribute('aria-pressed','false'); + } + } catch {} + editBtn.addEventListener('click', ()=>{ + // Enter edit mode explicitly (overwriting the selected profile on Save) + this._profilesSelected = name; + this._profilesEditingExisting = true; + this._profilesEditingNew = false; + try { + const roomsMap = {}; + for (const eid of (this._config?.entities||[])){ + const src = this._schedules[eid]?.profiles?.[name]?.blocks || []; + roomsMap[eid] = JSON.parse(JSON.stringify(src)); + } + this._profilesDraft = { name, rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + this._profilesDirty = false; + } catch { + this._profilesDraft = null; + this._profilesDirty = false; + } + this._renderProfilesModal(); + try { const trk = modalProf?.querySelector('.week-track'); trk?.scrollIntoView({ behavior:'smooth', block:'center' }); } catch {} + }); + const act = document.createElement('button'); act.type='button'; act.className = 'btn ' + (row.activeProfile===name?'success':'ghost'); act.textContent = (row.activeProfile===name)? this._t('profiles.deactivate') : this._t('profiles.activate'); + act.addEventListener('click', async ()=>{ + const before = this._desiredNowSnapshot(); + // Toggle global activation: if already globally selected, turn off; else activate this name for all rooms + const enable = (this._globalProfile !== name); + if (enable) { + this._globalProfile = name; + this._config.profiles_enabled = true; + } else { + this._globalProfile = null; + // Keep profiles_enabled as-is; user can still have per-room profiles + } + // Apply across all configured rooms + for (const eid of (this._config?.entities || [])){ + const r = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureProfilesStruct(r); + if (enable) { + r.activeProfile = (r.profiles && r.profiles[name]) ? name : null; + } else { + r.activeProfile = null; + } + this._schedules[eid] = r; + } + this._markPendingSave(); + this._render(); + this._renderProfilesModal(); + try { + if (this._config?.auto_apply && !this._config?.storage_enabled && !this._isPaused()) { + await this._applyIfDesiredChanged(before); + } + } catch {} + this._scheduleNextApply(); + // Auto-close profiles modal when a profile is (de)activated + try { this._closeProfilesEditor(); } catch {} + }); + const ren = document.createElement('button'); ren.type='button'; ren.className='btn ghost'; ren.textContent=this._t('profiles.rename'); ren.addEventListener('click', async ()=>{ + const cur = name; const txt = prompt(this._t('profiles.rename'), cur) || ''; + const nn = txt.trim(); if (!nn || nn===cur) return; + if (row.profiles[nn]) { alert(this._t('profiles.name_exists') || 'Name exists'); return; } + row.profiles[nn] = row.profiles[cur]; delete row.profiles[cur]; if (row.activeProfile===cur) row.activeProfile = nn; if (this._profilesSelected===cur) this._profilesSelected = nn; this._markPendingSave(); this._renderProfilesModal(); this._render(); + }); + const del = document.createElement('button'); del.type='button'; del.className='remove-btn'; del.innerHTML = ''+this._t('profiles.delete')+''; + del.addEventListener('click', async ()=>{ if (!confirm(this._t('profiles.delete')+'?')) return; delete row.profiles[name]; if (row.activeProfile===name) row.activeProfile=null; if (this._profilesSelected===name) this._profilesSelected = Object.keys(row.profiles||{})[0]||null; this._markPendingSave(); this._renderProfilesModal(); this._render(); this._scheduleNextApply(); }); + actions.append(editBtn, act, ren, del); item.append(left, actions); list.append(item); + } + if (addBtn) { + try { const sp = addBtn.querySelector('span'); if (sp) sp.textContent = this._t('profiles.add'); } catch {} + addBtn.classList.remove('btn','primary'); addBtn.classList.add('add-entity-btn'); + addBtn.onclick = ()=>{ + // Reset to a brand‑new draft (empty timeline) + try { + const eid = this._profilesEntity; const row = this._schedules[eid] || { defaultTemp: this._config.default_temp }; + this._profilesSelected = null; this._profilesEditingNew = true; + this._profilesEditingExisting = false; + this._profilesDraft = { name: null, blocks: [], defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + this._profilesDirty = false; this._renderProfilesModal(); + } catch {} + }; + } + if (closeBtn) { try { closeBtn.textContent = this._t('ui.close') || 'Close'; } catch {} } + // Draw scale + try { + if (scale) { + scale.innerHTML = ''; + // Match the weekly preview: ticks every 2 hours using simple borders and small labels + for (let i=0;i<=24;i+=2){ + const t = document.createElement('div'); + t.style.position='absolute'; + t.style.left = (i/24*100)+'%'; + t.style.top='4px'; t.style.bottom='4px'; + t.style.borderLeft='1px solid var(--divider-color)'; + const lab = document.createElement('div'); + lab.style.position='absolute'; lab.style.top='-2px'; lab.style.transform='translate(-50%,0)'; lab.style.left='0'; lab.style.fontSize='.75rem'; lab.style.color='var(--secondary-text-color)'; + if (this._config?.time_12h){ const p=this._timeParts(i===24?0:i*60); lab.innerHTML = `
    ${p.main}
    ${p.mer||''}
    `; } else { lab.textContent = (i===24 ? this._label(0) : this._label(i*60)); } + t.append(lab); scale.append(t); + } + } + } catch {} + // Draw track for selected profile + track.innerHTML = ''; + const sel = this._profilesSelected; + let pblocks = []; + const curRoom = this._profilesRoom || this._profilesEntity; + if (this._profilesDraft && (!sel || this._profilesDraft.name===sel)) { + const rooms = this._profilesDraft.rooms || {}; pblocks = rooms[curRoom] || []; + } else if (sel) { + // Fallback: show blocks from the current room's saved profile + const r = this._schedules[curRoom] || {}; pblocks = (r.profiles?.[sel]?.blocks) || []; + } + const addBtnInline = document.createElement('button'); addBtnInline.type='button'; addBtnInline.className='btn primary'; addBtnInline.innerHTML = ''+this._t('ui.add_block')+''; addBtnInline.addEventListener('click', ()=>{ + this._openProfileBlockEditor(null, this._getNowMin()); + }); + const toolbar = document.createElement('div'); toolbar.className='prof-toolbar'; toolbar.style.display='flex'; toolbar.style.justifyContent='flex-end'; toolbar.style.gap='8px'; toolbar.append(addBtnInline); + const topbar = document.createElement('div'); topbar.className='prof-topbar'; + // Move the existing note into the topbar so note (left) + add button (right) are on the same line + try { if (note) { note.style.whiteSpace = 'normal'; topbar.append(note); } } catch {} + topbar.append(toolbar); + // Place topbar ABOVE the time scale (before the .week-scale container) + try { + const outerScale = (scale && scale.closest) ? scale.closest('.week-scale') : (modalProf?.querySelector('.week-scale') || null); + const parent = outerScale?.parentElement || track.parentElement; + if (parent) { + Array.from(parent.querySelectorAll('.prof-topbar')).forEach(el=>el.remove()); + Array.from(parent.querySelectorAll('.prof-toolbar')).forEach(el=>el.remove()); + if (outerScale) parent.insertBefore(topbar, outerScale); else parent.insertBefore(topbar, track); + } + } catch {} + // Tooltip (same style as weekdays) + let tooltip = this.shadowRoot.querySelector('.wk-tooltip.prof'); + if (!tooltip) { tooltip = document.createElement('div'); tooltip.className='wk-tooltip prof'; tooltip.style.display='none'; modalProf?.append(tooltip); } + for (const b of pblocks){ + const pctStart = (b.startMin/1440)*100; const pctW = ((b.endMin-b.startMin)/1440)*100; + const bl = document.createElement('div'); bl.className='block'; bl.style.left=`${pctStart}%`; bl.style.width=`${pctW}%`; + try { const clr = this._colorFor(this._profilesEntity, b.temp); if (clr){ bl.style.background=clr; bl.style.borderColor=clr; const txt=this._contrastTextColor(clr); if (txt) bl.style.color=txt; } } catch {} + // Hide pill via CSS; use hover tooltip like weekdays + UiHelper.genBlockPill( + this, bl, + `${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}` + ); + bl.addEventListener('dblclick', ()=> this._openProfileBlockEditor(b.id)); + const showTip = ()=>{ + const txt = `${this._label(b.startMin)} - ${this._label(b.endMin)} • ${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}`; + if (!tooltip) return; + try { if (tooltip._hideTimer) { clearTimeout(tooltip._hideTimer); tooltip._hideTimer = null; } } catch {} + // Suppress while block editor is open + try { const ov = this.shadowRoot.querySelector('.overlay'); if (this._editing || (ov && ov.classList.contains('open'))) return; } catch {} + tooltip.textContent = txt; + const box = bl.getBoundingClientRect(); const cont = modalProf?.getBoundingClientRect(); + const left = box.left + box.width/2 - (cont?.left||0); const top = (box.top - (cont?.top||0)) - 8; + tooltip.style.left = left + 'px'; tooltip.style.top = top + 'px'; tooltip.style.transform = 'translate(-50%,-100%)'; tooltip.style.display=''; + }; + const hideTip = ()=>{ if (!tooltip) return; const delay = (window.matchMedia && window.matchMedia('(pointer:coarse)').matches) ? 3000 : 120; try { if (tooltip._hideTimer) clearTimeout(tooltip._hideTimer); } catch {}; tooltip._hideTimer = setTimeout(()=>{ try { tooltip.style.display='none'; tooltip._hideTimer=null; } catch{} }, delay); }; + bl.addEventListener('mouseenter', showTip); + bl.addEventListener('mouseleave', hideTip); + track.append(bl); + } + } catch { /* ignore */ } + } + _openProfileBlockEditor(blockId, atMin=null){ + try { + this._ensureEditModeOn(); + const eid = this._profilesRoom || this._profilesEntity; const name = this._profilesSelected; if (!eid) return; + const row = this._schedules[eid]; this._ensureProfilesStruct(row); + // Use draft while editing + if (!this._profilesDraft || this._profilesDraft.name !== name) { + try { + const roomsMap = {}; + for (const e of (this._config?.entities||[])){ + const src = name ? (this._schedules[e]?.profiles?.[name]?.blocks || []) : []; + roomsMap[e] = JSON.parse(JSON.stringify(src)); + } + this._profilesDraft = { name, rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + this._profilesDirty = false; + } catch { + const roomsMap = {}; for (const e of (this._config?.entities||[])) roomsMap[e] = []; + this._profilesDraft = { name, rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; this._profilesDirty = false; + } + } + const arr = (this._profilesDraft.rooms||{})[eid] || []; + const edTemp = this.shadowRoot.querySelector('.ed-temp'); const edFrom = this.shadowRoot.querySelector('.ed-from'); const edTo = this.shadowRoot.querySelector('.ed-to'); const overlay = this.shadowRoot.querySelector('.overlay'); const title = this.shadowRoot.querySelector('.modal h3'); const delBtn = this.shadowRoot.querySelector('.ed-delete'); const err = this.shadowRoot.querySelector('.ed-error'); const fromMer = this.shadowRoot.querySelector('.ed-from-mer'); const toMer = this.shadowRoot.querySelector('.ed-to-mer'); + if (!blockId){ + // New block default around now + const now = Number.isFinite(atMin) ? atMin : this._getNowMin(); const start = this._clamp(Math.round(now), 0, 1380); const end = this._clamp(start+60, start+15, 1440); + this._editing = { entity: eid, blockId: null, profileName: (name || '__new__') }; + if (edTemp) { const base = Number(row.defaultTemp||this._config.default_temp||20); edTemp.value = String(this._toDisplayTemp(base)); edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } + if (this._config?.time_12h) { if (edFrom) edFrom.value=this._toTimeInput12h(start); if (edTo) edTo.value=this._toTimeInput12h(end); if (fromMer){ fromMer.style.display=''; fromMer.value=(start>=720)?'PM':'AM'; } if (toMer){ toMer.style.display=''; toMer.value=(end>=720 && end<1440)?'PM':'AM'; } } else { if (edFrom) edFrom.value=this._toTimeInput(start); if (edTo) edTo.value=this._toTimeInput(end); if (fromMer) fromMer.style.display='none'; if (toMer) toMer.style.display='none'; } + if (title) title.textContent = this._t('ui.add_block'); if (delBtn) delBtn.style.display='none'; if (err){ err.style.display='none'; err.textContent=''; } + } else { + const b = arr.find(x=>x.id===blockId); if (!b) return; + this._editing = { entity: eid, blockId, profileName: (name || '__new__') }; + if (edTemp) { edTemp.value = String(this._toDisplayTemp(b.temp)); edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } + if (this._config?.time_12h) { if (edFrom) edFrom.value=this._toTimeInput12h(b.startMin); if (edTo) edTo.value=this._toTimeInput12h(b.endMin); if (fromMer){ fromMer.style.display=''; fromMer.value=(b.startMin>=720)?'PM':'AM'; } if (toMer){ toMer.style.display=''; toMer.value=(b.endMin>=720 && b.endMin<1440)?'PM':'AM'; } } else { if (edFrom) edFrom.value=this._toTimeInput(b.startMin); if (edTo) edTo.value=this._toTimeInput(b.endMin); if (fromMer) fromMer.style.display='none'; if (toMer) toMer.style.display='none'; } + if (title) title.textContent = this._t('ui.edit_block'); if (delBtn) delBtn.style.display=''; if (err){ err.style.display='none'; err.textContent=''; } + } + // Hide profiles overlay while editing + const pov = this.shadowRoot.querySelector('.overlay-profiles'); if (pov) pov.classList.remove('open'); this._profilesOverlayHiddenForEditor = true; + try { const modalEl = this.shadowRoot.querySelector('.overlay .modal:not(.modal-week)'); if (modalEl) modalEl.classList.toggle('modal-12h', !!this._config?.time_12h); } catch {} + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { overlay.classList.add('open'); })); } catch { overlay.classList.add('open'); } + } catch {} + } + async _profilesSaveDraft(){ + try { + const eid = this._profilesEntity; const sel = this._profilesSelected; const d = this._profilesDraft; + if (!eid || !sel || !d || d.name !== sel) return; + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureProfilesStruct(row); + // Persist current draft into storage structure + const blocks = (d.rooms && d.rooms[eid]) ? d.rooms[eid] : []; + row.profiles[sel] = { defaultTemp: row.defaultTemp, blocks: JSON.parse(JSON.stringify(blocks)) }; + this._schedules[eid] = row; + this._markPendingSave(); + // Clear dirty flag and revert Add/Save button immediately + this._profilesDirty = false; + try { + const btn = this.shadowRoot && this.shadowRoot.querySelector('.prof-add'); + const sp = btn && btn.querySelector && btn.querySelector('span'); + if (btn && sp) { sp.textContent = this._t('profiles.add'); btn.classList.remove('btn','primary'); btn.classList.add('add-entity-btn'); btn.onclick = null; } + } catch {} + // Refresh UI and reschedule transitions + this._render(); + this._renderProfilesModal(); + this._scheduleNextApply(); + } catch (e) {} + } + async _profilesSave(){ + try { + // Prevent double-trigger when user presses Enter to confirm the prompt + if (this._profilesSaveLock) return; this._profilesSaveLock = true; + const eid = this._profilesEntity; if (!eid) return; + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; this._ensureProfilesStruct(row); + const draft = this._profilesDraft || { name:null, rooms:{} }; + // New profile: prompt for name + if (this._profilesEditingNew || !this._profilesSelected) { + const base = this._t('profiles.name_placeholder'); + const txt = prompt(base||'Profile name', 'Profile'); + const name = (txt||'').trim(); if (!name) return; + if (row.profiles[name]) { alert(this._t('profiles.name_exists') || 'Name exists'); return; } + // Save this profile for ALL rooms from the draft map + for (const rid of (this._config?.entities||[])){ + const rrow = this._schedules[rid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureProfilesStruct(rrow); + const blocks = (draft.rooms?.[rid] || []); + rrow.profiles = rrow.profiles || {}; rrow.profiles[name] = { defaultTemp: rrow.defaultTemp, blocks: JSON.parse(JSON.stringify(blocks)) }; + this._schedules[rid] = rrow; + } + // Persist and reset timeline for a fresh new draft (ready for next) + this._profilesDirty = false; + this._markPendingSave(); + try { + this._profilesSelected = null; this._profilesEditingNew = true; + this._profilesEditingExisting = false; + const roomsMap = {}; for (const rid of (this._config?.entities||[])) roomsMap[rid] = []; + this._profilesDraft = { name: null, rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + } catch {} + // Move focus away from Save to avoid immediate re-click from Enter + try { (this.shadowRoot && this.shadowRoot.activeElement && this.shadowRoot.activeElement.blur && this.shadowRoot.activeElement.blur()); } catch {} + this._renderProfilesModal(); this._render(); this._scheduleNextApply(); + return; + } + // Edit existing: save without renaming, then reset timeline for next + const sel = this._profilesSelected; + for (const rid of (this._config?.entities||[])){ + const rrow = this._schedules[rid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureProfilesStruct(rrow); + const blocks = (draft.rooms?.[rid] || []); + rrow.profiles = rrow.profiles || {}; rrow.profiles[sel] = { defaultTemp: rrow.defaultTemp, blocks: JSON.parse(JSON.stringify(blocks)) }; + this._schedules[rid] = rrow; + } + this._profilesDirty = false; + this._markPendingSave(); + try { (this.shadowRoot && this.shadowRoot.activeElement && this.shadowRoot.activeElement.blur && this.shadowRoot.activeElement.blur()); } catch {} + // Clear selection and show empty preview ready for a new schedule + try { + this._profilesSelected = null; this._profilesEditingNew = true; + this._profilesEditingExisting = false; + const roomsMap = {}; for (const rid of (this._config?.entities||[])) roomsMap[rid] = []; + this._profilesDraft = { name: null, rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + } catch {} + this._renderProfilesModal(); this._render(); this._scheduleNextApply(); + } catch (e) {} + finally { setTimeout(()=>{ this._profilesSaveLock = false; }, 300); } + } + _openCopyWeekToRooms(){ try { + // Build list of target rooms (exclude current) + const list = this.shadowRoot.querySelector('.copyrooms-list'); + const title = this.shadowRoot.querySelector('.copyrooms-title'); + const desc = this.shadowRoot.querySelector('.copyrooms-desc'); + const ov = this.shadowRoot.querySelector('.overlay-copyrooms'); + if (title) title.textContent = this._t('week.copy_to_rooms.title'); + if (desc) desc.textContent = this._t('week.copy_to_rooms.select'); + if (list) { + list.innerHTML = ''; + const ents = (this._config?.entities || []); + for (const eid of ents){ + if (eid === this._weeklyEntity) continue; + const lbl = (this._config?.labels && this._config.labels[eid]) ? this._config.labels[eid] : this._prettyName(eid); + const row = document.createElement('label'); + const cb = document.createElement('input'); cb.type='checkbox'; cb.value=eid; + const span = document.createElement('span'); span.textContent = lbl; + row.append(cb, span); list.append(row); + } + } + const cancel = this.shadowRoot.querySelector('.copyrooms-cancel'); + const apply = this.shadowRoot.querySelector('.copyrooms-apply'); + if (cancel) cancel.onclick = ()=>{ ov?.classList.remove('open'); }; + if (apply) apply.onclick = ()=> this._applyCopyWeekToRooms(); + ov?.classList.add('open'); + } catch(e){} + } + async _applyCopyWeekToRooms(){ try { + const list = this.shadowRoot.querySelector('.copyrooms-list'); + const ov = this.shadowRoot.querySelector('.overlay-copyrooms'); + if (!this._weeklyDraft) { ov?.classList.remove('open'); return; } + const checked = Array.from(list?.querySelectorAll('input[type=checkbox]:checked') || []).map(x=>x.value); + // Queue the targets; actual copy occurs on Save in weekly editor + this._weeklyCopyTargets = checked; + ov?.classList.remove('open'); + // Optionally re-render header/note to reflect queued action (no persistence yet) + this._renderWeeklyModal(); + } catch(e){} + } + _openWeeklyBlockEditor(blockId, atMin=null){ if (!this._weeklyDraft) return; this._ensureEditModeOn(); const day = this._effectiveDayKey(this._weeklyDayKey || 'mon'); const arr = this._weeklyDraft.days?.[day] || []; if (!blockId){ + // open new + const start = Number.isFinite(atMin) ? this._clamp(Math.round(atMin),0,1380) : this._clamp(Math.round(this._getNowMin()),0,1380); + const end = this._clamp(start+60, start+15, 1440); + const row = this._schedules[this._weeklyEntity] || { defaultTemp: this._config.default_temp }; + const edTemp = this.shadowRoot.querySelector('.ed-temp'); const edFrom = this.shadowRoot.querySelector('.ed-from'); const edTo = this.shadowRoot.querySelector('.ed-to'); const overlay = this.shadowRoot.querySelector('.overlay'); const title = this.shadowRoot.querySelector('.modal h3'); const delBtn = this.shadowRoot.querySelector('.ed-delete'); const err = this.shadowRoot.querySelector('.ed-error'); const fromMer = this.shadowRoot.querySelector('.ed-from-mer'); const toMer = this.shadowRoot.querySelector('.ed-to-mer'); + this._editing = { entity: this._weeklyEntity, blockId: null, weeklyDay: day }; + // Hide any tooltips while editing + try { const tips = this.shadowRoot.querySelectorAll('.wk-tooltip'); tips.forEach(t => { try { if (t._hideTimer) clearTimeout(t._hideTimer); } catch {} t.style.display='none'; }); } catch {} + if (edTemp) { edTemp.value = String(this._toDisplayTemp(row.defaultTemp||20)); edTemp.max= String(this._maxDisplay()); } + if (this._config?.time_12h) { + if (edFrom) edFrom.value = this._toTimeInput12h(start); + if (edTo) edTo.value = this._toTimeInput12h(end); + if (fromMer){ fromMer.style.display=''; fromMer.value = (start>=720)?'PM':'AM'; } + if (toMer){ toMer.style.display=''; toMer.value = (end>=720 && end<1440)?'PM':'AM'; } + } else { + if (edFrom) edFrom.value = this._toTimeInput(start); + if (edTo) edTo.value = this._toTimeInput(end); + if (fromMer) fromMer.style.display='none'; + if (toMer) toMer.style.display='none'; + } + if (title) title.textContent = this._t('ui.add_block'); if (delBtn) delBtn.style.display='none'; if (err){ err.style.display='none'; err.textContent=''; } + // Hide weekday overlay while editing to avoid double-popups + const wov = this.shadowRoot.querySelector('.overlay-week'); if (wov) wov.classList.remove('open'); this._weeklyOverlayHiddenForEditor = true; + try { const modalEl = this.shadowRoot.querySelector('.overlay .modal:not(.modal-week)'); if (modalEl) modalEl.classList.toggle('modal-12h', !!this._config?.time_12h); } catch {} + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { overlay.classList.add('open'); })); } catch { overlay.classList.add('open'); } + } else { + let b = arr.find(x=>x.id===blockId); + + // If not found in weekly.days, try searching in _weeklyModes + if (!b && this._weeklyDraft?._weeklyModes) { + for (const modeName in this._weeklyDraft._weeklyModes) { + const modeBlocks = this._weeklyDraft._weeklyModes[modeName]?.days?.[day]; + if (Array.isArray(modeBlocks)) { + b = modeBlocks.find(x=>x.id===blockId); + if (b) break; + } + } + } + + if (!b) return; + const edTemp = this.shadowRoot.querySelector('.ed-temp'); const edFrom = this.shadowRoot.querySelector('.ed-from'); const edTo = this.shadowRoot.querySelector('.ed-to'); const overlay = this.shadowRoot.querySelector('.overlay'); const title = this.shadowRoot.querySelector('.modal h3'); const delBtn = this.shadowRoot.querySelector('.ed-delete'); const err = this.shadowRoot.querySelector('.ed-error'); const fromMer = this.shadowRoot.querySelector('.ed-from-mer'); const toMer = this.shadowRoot.querySelector('.ed-to-mer'); + this._editing = { entity: this._weeklyEntity, blockId, weeklyDay: day }; + // Hide any tooltips while editing + try { const tips = this.shadowRoot.querySelectorAll('.wk-tooltip'); tips.forEach(t => { try { if (t._hideTimer) clearTimeout(t._hideTimer); } catch {} t.style.display='none'; }); } catch {} + if (edTemp) { edTemp.value = String(this._toDisplayTemp(b.temp)); edTemp.max= String(this._maxDisplay()); } + if (this._config?.time_12h) { + if (edFrom) edFrom.value = this._toTimeInput12h(b.startMin); + if (edTo) edTo.value = this._toTimeInput12h(b.endMin); + if (fromMer){ fromMer.style.display=''; fromMer.value = (b.startMin>=720)?'PM':'AM'; } + if (toMer){ toMer.style.display=''; toMer.value = (b.endMin>=720 && b.endMin<1440)?'PM':'AM'; } + } else { + if (edFrom) edFrom.value = this._toTimeInput(b.startMin); + if (edTo) edTo.value = this._toTimeInput(b.endMin); + if (fromMer) fromMer.style.display='none'; + if (toMer) toMer.style.display='none'; + } + if (title) title.textContent = this._t('ui.edit_block'); if (delBtn) delBtn.style.display=''; if (err){ err.style.display='none'; err.textContent=''; } + const wov = this.shadowRoot.querySelector('.overlay-week'); if (wov) wov.classList.remove('open'); this._weeklyOverlayHiddenForEditor = true; + try { const modalEl = this.shadowRoot.querySelector('.overlay .modal:not(.modal-week)'); if (modalEl) modalEl.classList.toggle('modal-12h', !!this._config?.time_12h); } catch {} + overlay.classList.add('open'); + } + } + + // ---------- Holidays modal ---------- + _openHolidayEditor(entity){ + try { + this._ensureEditModeOn(); + const ents = this._config?.entities || []; + this._holidayRoom = entity || ents[0] || null; + // Build a rooms map draft from all rooms + const roomsMap = {}; + for (const eid of ents){ + const r = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureHolidayStruct(r); + roomsMap[eid] = Array.isArray(r.holiday?.blocks) ? JSON.parse(JSON.stringify(r.holiday.blocks)) : []; + } + this._holidayDraft = { rooms: roomsMap }; + this._holidayOpen = true; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-holiday'); + // Mark card overlay state first, then render while hidden + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + this._renderHolidayModal(); + // Open after a paint so timeline is fully laid out + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { if (ov) ov.classList.add('open'); })); } catch { if (ov) ov.classList.add('open'); } + } catch {} + } + _closeHolidayEditor(){ + try { this._holidayOpen = false; this._holidayEntity = null; this._holidayRoom = null; this._holidayDraft = null; } catch {} + try { this.shadowRoot.querySelector('.overlay-holiday')?.classList.remove('open'); } catch {} + try { this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} + try { + if (this._resumeSettingsAfterHoliday) { + this._resumeSettingsAfterHoliday = false; + this._resumeSettingsPopup?.(); + } + } catch {} + } + _renderHolidayModal(){ try { + const title = this.shadowRoot && this.shadowRoot.querySelector('.holiday-title'); if (title) title.textContent = this._t('holidays.editor.title'); + const roomEl = this.shadowRoot && this.shadowRoot.querySelector('.holiday-room'); if (roomEl) { const eid = this._holidayRoom; const custom = (this._config?.labels?.[eid]) || null; roomEl.textContent = custom || this._prettyName(eid||''); } + const note = this.shadowRoot && this.shadowRoot.querySelector('.holiday-note'); if (note) note.textContent = this._t('week.editor.preview_note'); + try { const sv = this.shadowRoot.querySelector('.holiday-save'); if (sv) sv.textContent = this._t('ui.save'); const cl = this.shadowRoot.querySelector('.holiday-close'); if (cl) cl.textContent = this._t('ui.cancel'); } catch {} + const modalHost = this.shadowRoot && this.shadowRoot.querySelector('.modal-holiday'); + const scale = modalHost?.querySelector('.week-scale-inner') || this.shadowRoot.querySelector('.week-scale-inner'); + const track = modalHost?.querySelector('.week-track') || this.shadowRoot.querySelector('.week-track'); + if (!track) return; + // Build room tabs + const tabs = modalHost?.querySelector('.holiday-roomtabs'); + if (tabs) { + tabs.innerHTML = ''; + const ents = this._config?.entities || []; + if (!this._holidayRoom || !ents.includes(this._holidayRoom)) this._holidayRoom = ents[0] || null; + for (const eid of ents){ const btn=document.createElement('button'); btn.type='button'; btn.className='wk-tab'+(eid===this._holidayRoom?' active':''); let label = this._config?.labels?.[eid] || (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1]||eid)); btn.textContent=label; btn.addEventListener('click', ()=>{ this._holidayRoom=eid; this._renderHolidayModal(); }); tabs.append(btn); } + } + // Draw scale every 2 hours + if (scale) { + scale.innerHTML = ''; + for (let i=0;i<=24;i+=2){ + const t = document.createElement('div'); t.style.position='absolute'; t.style.left=(i/24*100)+'%'; t.style.top='4px'; t.style.bottom='4px'; t.style.borderLeft='1px solid var(--divider-color)'; + const lab = document.createElement('div'); lab.style.position='absolute'; lab.style.top='-2px'; lab.style.transform='translate(-50%,0)'; lab.style.left='0'; lab.style.fontSize='.75rem'; lab.style.color='var(--secondary-text-color)'; + if (this._config?.time_12h){ const p=this._timeParts(i===24?0:i*60); lab.innerHTML = `
    ${p.main}
    ${p.mer||''}
    `; } else { lab.textContent = (i===24 ? this._label(0) : this._label(i*60)); } + t.append(lab); scale.append(t); + } + } + // Toolbar with Add + const addBtnInline = document.createElement('button'); addBtnInline.type='button'; addBtnInline.className='btn primary'; addBtnInline.innerHTML = ''+this._t('ui.add_block')+''; + addBtnInline.addEventListener('click', ()=>{ this._openHolidayBlockEditor(null, this._getNowMin()); }); + const copyBtn = document.createElement('button'); copyBtn.type='button'; copyBtn.className='btn ghost'; copyBtn.innerHTML = ''+ (this._t('holidays.copy_from') || this._t('ui.copy_plan')) +''; + copyBtn.addEventListener('click', ()=> this._openHolidayCopy()); + const toolbar = document.createElement('div'); toolbar.className='prof-toolbar'; toolbar.style.display='flex'; toolbar.style.justifyContent='flex-end'; toolbar.style.gap='8px'; toolbar.append(copyBtn, addBtnInline); + try { + const outerScale = (scale && scale.closest) ? scale.closest('.week-scale') : (modalHost?.querySelector('.week-scale') || null); + const parent = outerScale?.parentElement || track.parentElement; + if (parent) { + Array.from(parent.querySelectorAll('.prof-toolbar')).forEach(el=>el.remove()); + if (outerScale) parent.insertBefore(toolbar, outerScale); else parent.insertBefore(toolbar, track); + } + } catch {} + // Draw blocks + track.innerHTML = ''; + const blocks = (this._holidayDraft && this._holidayDraft.rooms) ? (this._holidayDraft.rooms[this._holidayRoom] || []) : []; + // Tooltip like weekly + let tooltip = this.shadowRoot.querySelector('.wk-tooltip.holiday'); + if (!tooltip) { tooltip = document.createElement('div'); tooltip.className='wk-tooltip holiday'; tooltip.style.display='none'; modalHost?.append(tooltip); } + for (const b of blocks){ + const pctStart=(b.startMin/1440)*100, pctW=((b.endMin-b.startMin)/1440)*100; const bl=document.createElement('div'); bl.className='block'; bl.style.left=pctStart+'%'; bl.style.width=pctW+'%'; + try { const clr=this._colorFor(this._holidayRoom, b.temp); if (clr){ bl.style.background=clr; bl.style.borderColor=clr; const txt=this._contrastTextColor(clr); if (txt) bl.style.color=txt; } } catch {} + UiHelper.genBlockPill( + this, bl, + `${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}` + ); + bl.addEventListener('dblclick', ()=> this._openHolidayBlockEditor(b.id)); + const showTip = ()=>{ const txt=`${this._label(b.startMin)} - ${this._label(b.endMin)} • ${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}`; if (!tooltip) return; try { if (tooltip._hideTimer) clearTimeout(tooltip._hideTimer); tooltip._hideTimer=null; } catch {} const box=bl.getBoundingClientRect(); const cont=modalHost?.getBoundingClientRect(); const left=box.left+box.width/2-(cont?.left||0); const top=(box.top-(cont?.top||0))-8; tooltip.textContent=txt; tooltip.style.left=left+'px'; tooltip.style.top=top+'px'; tooltip.style.transform='translate(-50%,-100%)'; tooltip.style.display=''; }; + const hideTip = ()=>{ if (!tooltip) return; const delay=(window.matchMedia&&window.matchMedia('(pointer:coarse)').matches)?3000:120; try{ if(tooltip._hideTimer) clearTimeout(tooltip._hideTimer);}catch{} tooltip._hideTimer=setTimeout(()=>{ try{ tooltip.style.display='none'; tooltip._hideTimer=null; }catch{} }, delay); }; + bl.addEventListener('mouseenter', showTip); bl.addEventListener('mouseleave', hideTip); + track.append(bl); + } + // Add on empty area + track.addEventListener('dblclick', (e)=>{ try { if (e.target && (e.target.closest && e.target.closest('.block'))) return; const box=track.getBoundingClientRect(); const rel=(e.clientX-box.left)/box.width; const min=this._clamp(Math.round(rel*1440),0,1439); this._openHolidayBlockEditor(null, min); } catch { this._openHolidayBlockEditor(null); } }); + } catch {} + } + + // ---------- Presence (advanced away) modal ---------- + _openPresenceEditor(entity){ + try { + this._ensureEditModeOn(); + // Default to first room + const ents = this._config?.entities || []; + this._presenceRoom = entity || ents[0] || null; + // Select current active combo if any; else first combo + const combos = this._presenceAllCombos(); + const activeKey = this._activePresenceComboKey(); + this._presenceCombo = activeKey || (combos[0]?.key || null); + // Build draft map for current combo from all rooms + const roomsMap = {}; + for (const eid of ents){ + const r = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensurePresenceStruct(r); + const arr = (this._presenceCombo && r.presence && r.presence[this._presenceCombo]) ? (r.presence[this._presenceCombo].blocks || []) : []; + roomsMap[eid] = JSON.parse(JSON.stringify(arr)); + } + this._presenceDraft = { combo: this._presenceCombo, rooms: roomsMap }; + this._presenceOpen = true; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-presence'); + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + this._renderPresenceModal(); + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { if (ov) ov.classList.add('open'); })); } catch { if (ov) ov.classList.add('open'); } + } catch {} + } + _closePresenceEditor(){ this._presenceOpen = false; this._presenceCombo = null; this._presenceDraft = null; this._presenceRoom = null; this.shadowRoot.querySelector('.overlay-presence')?.classList.remove('open'); try { this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} } + _renderPresenceModal(){ try { + const title = this.shadowRoot && this.shadowRoot.querySelector('.presence-title'); if (title) title.textContent = this._t('presence.editor.title'); + const roomEl = this.shadowRoot && this.shadowRoot.querySelector('.presence-room'); if (roomEl) { const eid = this._presenceRoom; const custom = (this._config?.labels?.[eid]) || null; roomEl.textContent = custom || this._prettyName(eid||''); } + // Hide toggle in modal (selection happens in editor) + try { const advRow = this.shadowRoot && this.shadowRoot.querySelector('.presence-adv-row'); if (advRow) advRow.style.display = 'none'; } catch {} + const list = this.shadowRoot && this.shadowRoot.querySelector('.presence-combos'); + const wrapActive = this.shadowRoot && this.shadowRoot.querySelector('.presence-combo-active'); + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-presence'); + const modalHost = this.shadowRoot && this.shadowRoot.querySelector('.overlay-presence .modal'); + const scale = modalHost?.querySelector('.week-scale-inner'); + const track = modalHost?.querySelector('.week-track'); + const tabs = modalHost?.querySelector('.presence-roomtabs'); + const btnSave = modalHost?.querySelector('.presence-save'); const btnClose = modalHost?.querySelector('.presence-close'); + const seasonRow = modalHost?.querySelector('.presence-season-row'); + const seasonLab = modalHost?.querySelector('.presence-season-label'); + const seasonSel = modalHost?.querySelector('.presence-season'); + + const buildPresenceDraft = (comboKey)=>{ + try { + const roomsMap = {}; + for (const eid of (this._config?.entities||[])){ + const r = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensurePresenceStruct(r); + const arr = (comboKey && r.presence && r.presence[comboKey]) ? (r.presence[comboKey].blocks || []) : []; + roomsMap[eid] = JSON.parse(JSON.stringify(arr)); + } + this._presenceDraft = { combo: comboKey, rooms: roomsMap }; + } catch {} + }; + // Build readonly vertical list of enabled combos (click to select) + if (list) { + list.innerHTML=''; + const all = this._presenceAllCombos(); + const conf = (this._config?.away?.combos) || {}; + const enabled = all.filter(it => !!conf[it.key]?.enabled); + const grid = document.createElement('div'); grid.className = 'presence-rows'; grid.style.border='1px solid var(--divider-color)'; grid.style.borderRadius='8px'; grid.style.padding='6px'; grid.style.maxHeight='180px'; grid.style.overflow='auto'; + const titleRow = document.createElement('div'); titleRow.style.fontSize='.9rem'; titleRow.style.color='var(--secondary-text-color)'; titleRow.textContent = this._t('presence.combos'); list.append(titleRow); + for (const it of enabled){ + const btn = document.createElement('button'); btn.type='button'; btn.className='presence-row' + (it.key===this._presenceCombo?' active':''); + const full = it.label; btn.textContent = full; btn.title = full; + btn.addEventListener('click', ()=>{ + this._presenceCombo = it.key; + buildPresenceDraft(it.key); + this._renderPresenceModal(); + }); + grid.append(btn); + } + list.append(grid); + } + // Seasonal selector (Summer/Winter) + try { + if (seasonRow && seasonSel && seasonLab) { + const on = !!this._config?.seasonal_enabled; + seasonRow.style.display = on ? '' : 'none'; + if (on) { + seasonLab.textContent = this._t('season.mode.title') || 'Season'; + const optW = seasonSel.querySelector('.opt-season-winter'); + const optS = seasonSel.querySelector('.opt-season-summer'); + if (optW) optW.textContent = this._t('season.mode.winter') || 'Winter'; + if (optS) optS.textContent = this._t('season.mode.summer') || 'Summer'; + seasonSel.value = this._seasonKey(); + seasonSel.onchange = ()=>{ + try { + const next = (seasonSel.value === 'summer') ? 'summer' : 'winter'; + this._setSeasonMode(next); + buildPresenceDraft(this._presenceCombo); + this._renderPresenceModal(); + } catch {} + }; + } + } + } catch {} + // Active combo label + if (wrapActive) { + const combos = this._presenceAllCombos(); const conf=(this._config?.away?.combos)||{}; + const cur = combos.find(x=>x.key===this._presenceCombo && !!conf[x.key]?.enabled); + if (cur) { + const nameOf = (eid)=>{ + try { return this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1]||eid); } catch { return eid; } + }; + const homeNames = (cur.home||[]).map(nameOf).join(', ') || '-'; + const awayNames = (cur.away||[]).map(nameOf).join(', ') || '-'; + const lh = this._t('presence.who_home') || 'Who is home'; + const la = this._t('presence.who_away') || 'Who is not home'; + wrapActive.innerHTML = `
    ${lh}: ${homeNames}
    ${la}: ${awayNames}
    `; + } else { + wrapActive.textContent = ''; + wrapActive.title = ''; + } + } + // (combo tabs removed; selection happens via the list above) + // Room tabs + if (tabs) { + tabs.innerHTML=''; + const ents = this._config?.entities || []; + if (!this._presenceRoom || !ents.includes(this._presenceRoom)) this._presenceRoom = ents[0] || null; + for (const eid of ents){ const btn=document.createElement('button'); btn.type='button'; btn.className='wk-tab'+(eid===this._presenceRoom?' active':''); let label = this._config?.labels?.[eid] || (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1]||eid)); btn.textContent=label; btn.addEventListener('click', ()=>{ this._presenceRoom=eid; this._renderPresenceModal(); }); tabs.append(btn); } + try { tabs.style.display = (ents.length>1) ? 'flex' : 'none'; } catch {} + } + // Scale + if (scale) { scale.innerHTML=''; for (let i=0;i<=24;i+=2){ const t=document.createElement('div'); t.style.position='absolute'; t.style.left=(i/24*100)+'%'; t.style.top='4px'; t.style.bottom='4px'; t.style.borderLeft='1px solid var(--divider-color)'; const lab=document.createElement('div'); lab.style.position='absolute'; lab.style.top='-2px'; lab.style.transform='translate(-50%,0)'; lab.style.left='0'; lab.style.fontSize='.75rem'; lab.style.color='var(--secondary-text-color)'; if (this._config?.time_12h){ const p=this._timeParts(i===24?0:i*60); lab.innerHTML=`
    ${p.main}
    ${p.mer||''}
    `; } else { lab.textContent=(i===24? this._label(0) : this._label(i*60)); } t.append(lab); scale.append(t);} } + // Toolbar Add + if (modalHost) { + const addBtnInline = document.createElement('button'); addBtnInline.type='button'; addBtnInline.className='btn primary'; addBtnInline.innerHTML=''+this._t('ui.add_block')+''; + addBtnInline.addEventListener('click', ()=>{ this._openPresenceBlockEditor(null, this._getNowMin()); }); + const toolbar = document.createElement('div'); toolbar.className='prof-toolbar'; toolbar.style.display='flex'; toolbar.style.justifyContent='flex-end'; toolbar.style.gap='8px'; toolbar.append(addBtnInline); + const outerScale = (scale && scale.closest) ? scale.closest('.week-scale') : (modalHost.querySelector('.week-scale') || null); + const parent = outerScale?.parentElement || track?.parentElement; + if (parent) { Array.from(parent.querySelectorAll('.prof-toolbar')).forEach(el=>el.remove()); if (outerScale) parent.insertBefore(toolbar, outerScale); else if (track) parent.insertBefore(toolbar, track); } + } + // Track blocks for current room/combo + if (track) { + track.innerHTML=''; + const combo = this._presenceCombo; const room = this._presenceRoom; + const arr = (this._presenceDraft && this._presenceDraft.rooms) ? (this._presenceDraft.rooms[room] || []) : []; + // Tooltip reuse + let tooltip = this.shadowRoot.querySelector('.wk-tooltip.presence'); if (!tooltip) { tooltip = document.createElement('div'); tooltip.className='wk-tooltip presence'; tooltip.style.display='none'; (modalHost||document.body).append(tooltip); } + for (const b of arr){ + const pctStart=(b.startMin/1440)*100, pctW=((b.endMin-b.startMin)/1440)*100; + const bl=document.createElement('div'); + bl.className='block'; bl.style.left=pctStart+'%'; + bl.style.width=pctW+'%'; + try { + const clr=this._colorFor(room, b.temp); + if (clr){ + bl.style.background=clr; + bl.style.borderColor=clr; + const txt=this._contrastTextColor(clr); + if (txt) bl.style.color=txt; + } + } catch {} + UiHelper.genBlockPill( + this, bl, + `${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}` + ); + bl.addEventListener('dblclick', ()=> this._openPresenceBlockEditor(b.id)); + const showTip = ()=>{ const txt=`${this._label(b.startMin)} - ${this._label(b.endMin)} • ${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}`; + if (!tooltip) return; + try { + if (tooltip._hideTimer) clearTimeout(tooltip._hideTimer); + tooltip._hideTimer=null; + } catch {} + const box=bl.getBoundingClientRect(); + const cont=modalHost?.getBoundingClientRect(); + const left=box.left+box.width/2-(cont?.left||0); + const top=(box.top-(cont?.top||0))-8; + tooltip.textContent=txt; + tooltip.style.left=left+'px'; + tooltip.style.top=top+'px'; + tooltip.style.transform='translate(-50%,-100%)'; + tooltip.style.display=''; }; + const hideTip = ()=>{ + if (!tooltip) return; + const delay=(window.matchMedia&&window.matchMedia('(pointer:coarse)').matches)?3000:120; + try{ + if(tooltip._hideTimer) clearTimeout(tooltip._hideTimer); + }catch{} + tooltip._hideTimer=setTimeout( + ()=>{ + try{ + tooltip.style.display='none'; + tooltip._hideTimer=null; + } catch{} + }, delay + ); + }; + bl.addEventListener('mouseenter', showTip); + bl.addEventListener('mouseleave', hideTip); + track.append(bl); } + // dblclick to add + track.addEventListener('dblclick', (e)=>{ try { if (e.target && (e.target.closest && e.target.closest('.block'))) return; const box=track.getBoundingClientRect(); const rel=(e.clientX-box.left)/box.width; const min=this._clamp(Math.round(rel*1440),0,1439); this._openPresenceBlockEditor(null, min); } catch { this._openPresenceBlockEditor(null); } }); + } + // Save/Close + if (btnSave) btnSave.onclick = ()=> this._presenceSave(); + if (btnClose) btnClose.onclick = ()=> this._closePresenceEditor(); + } catch {} + } + _openPresenceBlockEditor(blockId, atMin=null){ try { + this._ensureEditModeOn(); + const eid = this._presenceRoom; if (!eid) return; + if (!this._presenceDraft || !this._presenceDraft.rooms) { + const roomsMap={}; for (const rid of (this._config?.entities||[])){ const r=this._schedules[rid] || { defaultTemp: this._config.default_temp, blocks: [] }; this._ensurePresenceStruct(r); const arr=(this._presenceCombo && r.presence && r.presence[this._presenceCombo]) ? (r.presence[this._presenceCombo].blocks || []) : []; roomsMap[rid]=JSON.parse(JSON.stringify(arr)); } + this._presenceDraft = { combo: this._presenceCombo, rooms: roomsMap }; + } + const arr = (this._presenceDraft.rooms||{})[eid] || []; + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + const edTemp = this.shadowRoot.querySelector('.ed-temp'); const edFrom = this.shadowRoot.querySelector('.ed-from'); const edTo = this.shadowRoot.querySelector('.ed-to'); const overlay = this.shadowRoot.querySelector('.overlay'); const title = this.shadowRoot.querySelector('.modal h3'); const delBtn = this.shadowRoot.querySelector('.ed-delete'); const err = this.shadowRoot.querySelector('.ed-error'); const fromMer = this.shadowRoot.querySelector('.ed-from-mer'); const toMer = this.shadowRoot.querySelector('.ed-to-mer'); + if (!blockId){ const now = Number.isFinite(atMin) ? atMin : this._getNowMin(); const start = this._clamp(Math.round(now), 0, 1380); const end = this._clamp(start+60, start+15, 1440); this._editing = { entity: eid, blockId: null, presence: true }; if (edTemp) { const base = Number(row.defaultTemp||this._config.default_temp||20); edTemp.value = String(this._toDisplayTemp(base)); edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } if (this._config?.time_12h) { if (edFrom) edFrom.value=this._toTimeInput12h(start); if (edTo) edTo.value=this._toTimeInput12h(end); if (fromMer){ fromMer.style.display=''; fromMer.value=(start>=720)?'PM':'AM'; } if (toMer){ toMer.style.display=''; toMer.value=(end>=720 && end<1440)?'PM':'AM'; } } else { if (edFrom) edFrom.value=this._toTimeInput(start); if (edTo) edTo.value=this._toTimeInput(end); if (fromMer) fromMer.style.display='none'; if (toMer) toMer.style.display='none'; } if (title) title.textContent = this._t('ui.add_block'); if (delBtn) delBtn.style.display='none'; if (err){ err.style.display='none'; err.textContent=''; } } + else { const b = arr.find(x=>x.id===blockId); if (!b) return; this._editing = { entity: eid, blockId, presence: true }; if (edTemp) { edTemp.value = String(this._toDisplayTemp(b.temp)); edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } if (this._config?.time_12h) { if (edFrom) edFrom.value=this._toTimeInput12h(b.startMin); if (edTo) edTo.value=this._toTimeInput12h(b.endMin); if (fromMer){ fromMer.style.display=''; fromMer.value=(b.startMin>=720)?'PM':'AM'; } if (toMer){ toMer.style.display=''; toMer.value=(b.endMin>=720 && b.endMin<1440)?'PM':'AM'; } } else { if (edFrom) edFrom.value=this._toTimeInput(b.startMin); if (edTo) edTo.value=this._toTimeInput(b.endMin); if (fromMer) fromMer.style.display='none'; if (toMer) toMer.style.display='none'; } if (title) title.textContent = this._t('ui.edit_block'); if (delBtn) delBtn.style.display=''; if (err){ err.style.display='none'; err.textContent=''; } } + // Hide presence overlay while editing + const pov = this.shadowRoot.querySelector('.overlay-presence'); if (pov) pov.classList.remove('open'); this._profilesOverlayHiddenForEditor = false; this._holidayOverlayHiddenForEditor = false; // independent + try { const modalEl = this.shadowRoot.querySelector('.overlay .modal:not(.modal-week)'); if (modalEl) modalEl.classList.toggle('modal-12h', !!this._config?.time_12h); } catch {} + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { overlay.classList.add('open'); })); } catch { overlay.classList.add('open'); } + } catch {} + } + async _presenceSave(){ try { + if (!this._presenceDraft || !this._presenceDraft.rooms || !this._presenceCombo) { this._closePresenceEditor(); return; } + // Persist for each room under row.presence[combo].blocks + for (const eid of (this._config?.entities||[])){ + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensurePresenceStruct(row); + const arr = JSON.parse(JSON.stringify(this._presenceDraft.rooms?.[eid] || [])); + row.presence = row.presence || {}; row.presence[this._presenceCombo] = { blocks: arr }; + this._schedules[eid] = row; + } + const before = this._desiredNowSnapshot(); + this._markPendingSave(); + this._closePresenceEditor(); this._render(); + if (this._config.apply_on_edit) await this._applyIfDesiredChanged(before); this._scheduleNextApply(); + } catch {} + } + _openHolidayBlockEditor(blockId, atMin=null){ try { + this._ensureEditModeOn(); + const eid = this._holidayRoom; if (!eid) return; const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; this._ensureHolidayStruct(row); + if (!this._holidayDraft || !this._holidayDraft.rooms) { + const roomsMap={}; for (const rid of (this._config?.entities||[])){ const src=this._schedules[rid]?.holiday?.blocks || []; roomsMap[rid]=JSON.parse(JSON.stringify(src)); } + this._holidayDraft = { rooms: roomsMap }; + } + const arr = (this._holidayDraft.rooms||{})[eid] || []; + const edTemp = this.shadowRoot.querySelector('.ed-temp'); const edFrom = this.shadowRoot.querySelector('.ed-from'); const edTo = this.shadowRoot.querySelector('.ed-to'); const overlay = this.shadowRoot.querySelector('.overlay'); const title = this.shadowRoot.querySelector('.modal h3'); const delBtn = this.shadowRoot.querySelector('.ed-delete'); const err = this.shadowRoot.querySelector('.ed-error'); const fromMer = this.shadowRoot.querySelector('.ed-from-mer'); const toMer = this.shadowRoot.querySelector('.ed-to-mer'); + if (!blockId){ + const now = Number.isFinite(atMin) ? atMin : this._getNowMin(); const start = this._clamp(Math.round(now), 0, 1380); const end = this._clamp(start+60, start+15, 1440); + this._editing = { entity: eid, blockId: null, holiday: true }; + if (edTemp) { const base = Number(row.defaultTemp||this._config.default_temp||20); edTemp.value = String(this._toDisplayTemp(base)); edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } + if (this._config?.time_12h) { if (edFrom) edFrom.value=this._toTimeInput12h(start); if (edTo) edTo.value=this._toTimeInput12h(end); if (fromMer){ fromMer.style.display=''; fromMer.value=(start>=720)?'PM':'AM'; } if (toMer){ toMer.style.display=''; toMer.value=(end>=720 && end<1440)?'PM':'AM'; } } else { if (edFrom) edFrom.value=this._toTimeInput(start); if (edTo) edTo.value=this._toTimeInput(end); if (fromMer) fromMer.style.display='none'; if (toMer) toMer.style.display='none'; } + if (title) title.textContent = this._t('ui.add_block'); if (delBtn) delBtn.style.display='none'; if (err){ err.style.display='none'; err.textContent=''; } + } else { + const b = arr.find(x=>x.id===blockId); if (!b) return; this._editing = { entity: eid, blockId, holiday: true }; + if (edTemp) { edTemp.value = String(this._toDisplayTemp(b.temp)); edTemp.max = String(this._maxDisplay()); edTemp.min = String(this._minDisplay()); } + if (this._config?.time_12h) { if (edFrom) edFrom.value=this._toTimeInput12h(b.startMin); if (edTo) edTo.value=this._toTimeInput12h(b.endMin); if (fromMer){ fromMer.style.display=''; fromMer.value=(b.startMin>=720)?'PM':'AM'; } if (toMer){ toMer.style.display=''; toMer.value=(b.endMin>=720 && b.endMin<1440)?'PM':'AM'; } } else { if (edFrom) edFrom.value=this._toTimeInput(b.startMin); if (edTo) edTo.value=this._toTimeInput(b.endMin); if (fromMer) fromMer.style.display='none'; if (toMer) toMer.style.display='none'; } + if (title) title.textContent = this._t('ui.edit_block'); if (delBtn) delBtn.style.display=''; if (err){ err.style.display='none'; err.textContent=''; } + } + // Hide holiday overlay while editing to avoid double-popups + const hov = this.shadowRoot.querySelector('.overlay-holiday'); if (hov) hov.classList.remove('open'); this._holidayOverlayHiddenForEditor = true; + try { const modalEl = this.shadowRoot.querySelector('.overlay .modal:not(.modal-week)'); if (modalEl) modalEl.classList.toggle('modal-12h', !!this._config?.time_12h); } catch {} + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { overlay.classList.add('open'); })); } catch { overlay.classList.add('open'); } + } catch {} + } + async _holidaySave(){ try { + if (!this._holidayDraft || !this._holidayDraft.rooms) { this._closeHolidayEditor(); return; } + const rooms = this._holidayDraft.rooms || {}; + for (const eid of (this._config?.entities||[])){ + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureHolidayStruct(row); + row.holiday = { blocks: JSON.parse(JSON.stringify(rooms[eid] || [])) }; + this._schedules[eid] = row; + } + const before = this._desiredNowSnapshot(); + this._markPendingSave(); + this._closeHolidayEditor(); this._render(); + if (this._config.apply_on_edit) await this._applyIfDesiredChanged(before); + this._scheduleNextApply(); + } catch {} + } + + // ---- Holiday: Copy from other schedule types ---- + _holidayCopySourcesForRoom(eid){ + const out = []; + try { + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureWeeklyStruct(row); this._ensureProfilesStruct(row); this._ensurePresenceStruct(row); this._ensureHolidayStruct(row); + // Main plan + out.push({ group: 'main', label: this._t('holidays.copy.group.main'), items: [{ value: 'main::main', label: this._t('holidays.copy.group.main') }] }); + // Weekdays – list all 7 days, regardless of mode + try { + const days = this._weekdayNames(); + const items = days.map(k=>({ value: `weekday::${k}`, label: (this._t('week.days_full.'+k) || this._t('week.days.'+k) || k) })); + out.push({ group: 'weekdays', label: this._t('holidays.copy.group.weekdays'), items }); + } catch {} + // Profiles + try { + const names = Object.keys(row.profiles || {}); + if (names.length) { + const items = []; + // Put active profile first if any + const ap = row.activeProfile && names.includes(row.activeProfile) ? row.activeProfile : null; + if (ap) items.push({ value: `profile::${ap}`, label: `${(this._t('profiles.button')||'Manual schedule')}: ${ap}` }); + for (const n of names){ if (n!==ap) items.push({ value: `profile::${n}`, label: n }); } + out.push({ group: 'profiles', label: this._t('holidays.copy.group.profiles'), items }); + } + } catch {} + // Presence combinations (show enabled + any with saved blocks) + try { + const conf = (this._config?.away?.combos) || {}; + const all = this._presenceAllCombos(); + const items = []; + for (const it of all){ + const meta = conf[it.key] || {}; + const hasBlocks = !!(row.presence && row.presence[it.key] && Array.isArray(row.presence[it.key].blocks) && row.presence[it.key].blocks.length); + if (meta.enabled || hasBlocks) { + const lab = meta.enabled ? it.label : `${it.label} (off)`; + items.push({ value:`presence::${it.key}`, label: lab }); + } + } + if (items.length) out.push({ group: 'presence', label: this._t('holidays.copy.group.presence'), items }); + } catch {} + } catch {} + return out; + } + _holidayCopyBlocks(eid, value){ + try { + const [type, keyRaw] = String(value||'').split('::'); + const row = this._schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + this._ensureWeeklyStruct(row); this._ensureProfilesStruct(row); this._ensurePresenceStruct(row); + if (type === 'main') return JSON.parse(JSON.stringify(row.blocks||[])); + if (type === 'weekday') { + const k = keyRaw || 'mon'; + const src = row.weekly?.days?.[k] || []; + return JSON.parse(JSON.stringify(src)); + } + if (type === 'profile') { + const src = row.profiles?.[keyRaw]?.blocks || []; + return JSON.parse(JSON.stringify(src)); + } + if (type === 'presence') { + const src = row.presence?.[keyRaw]?.blocks || null; + if (Array.isArray(src)) return JSON.parse(JSON.stringify(src)); + // Fallback: all‑day away + const awayC = Number(this._config?.away?.target_c ?? 17); + const fallback = Number.isFinite(awayC) ? awayC : (row.defaultTemp || this._config.default_temp || 20); + return [{ id: '__presence_away__', startMin: 0, endMin: 1440, temp: fallback }]; + } + } catch {} + return []; + } + _openHolidayCopy(){ + try { + this._ensureEditModeOn(); + const eid = this._holidayRoom; if (!eid) return; + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-hcopy'); + try { this.shadowRoot.querySelector('ha-card')?.classList.add('overlay-open'); } catch {} + this._holidayCopyOpen = true; + this._holidayCopySel = 'main::main'; + this._renderHolidayCopy(); + try { requestAnimationFrame(()=> requestAnimationFrame(()=> { ov?.classList.add('open'); })); } catch { ov?.classList.add('open'); } + } catch {} + } + _closeHolidayCopy(){ try { this._holidayCopyOpen = false; this._holidayCopySel = null; this.shadowRoot.querySelector('.overlay-hcopy')?.classList.remove('open'); this.shadowRoot.querySelector('ha-card')?.classList.remove('overlay-open'); } catch {} } + _renderHolidayCopy(){ try { + const modal = this.shadowRoot && this.shadowRoot.querySelector('.overlay-hcopy .modal-hcopy'); if (!modal) return; + const eid = this._holidayRoom; const rowName = this._config?.labels?.[eid] || (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid?.split?.('.')[1]||eid)); + const title = modal.querySelector('.hcopy-title'); if (title) title.textContent = this._t('holidays.copy.title'); + const roomEl = modal.querySelector('.hcopy-room'); if (roomEl) roomEl.textContent = rowName || ''; + const selLab = modal.querySelector('.hcopy-label'); if (selLab) selLab.textContent = this._t('holidays.copy.select'); + const applyBtn = modal.querySelector('.hcopy-apply'); if (applyBtn) { applyBtn.textContent = this._t('holidays.copy.apply'); applyBtn.onclick = ()=>{ + try { + const cur = (this._holidayDraft?.rooms?.[eid] || []); + if (cur.length) { const ok = confirm(this._t('holidays.copy.replace_confirm')); if (!ok) return; } + } catch {} + const blocks = this._holidayCopyBlocks(eid, this._holidayCopySel); + if (!this._holidayDraft || !this._holidayDraft.rooms) { + const roomsMap = {}; for (const r of (this._config?.entities||[])) { const arr = (this._schedules[r]?.holiday?.blocks || []); roomsMap[r] = JSON.parse(JSON.stringify(arr)); } + this._holidayDraft = { rooms: roomsMap }; + } + this._holidayDraft.rooms[eid] = blocks.map(b=>({ ...b, id: (b.id && String(b.id).startsWith('__')? b.id : Math.random().toString(36).slice(2)) })); + this._renderHolidayModal(); this._closeHolidayCopy(); + }; } + const closeBtn = modal.querySelector('.hcopy-close'); if (closeBtn) { closeBtn.textContent = this._t('ui.cancel'); closeBtn.onclick = ()=> this._closeHolidayCopy(); } + // Build select options + const select = modal.querySelector('.hcopy-select'); + if (select) { + select.innerHTML = ''; + const groups = this._holidayCopySourcesForRoom(eid); + for (const g of groups){ + const og = document.createElement('optgroup'); og.label = g.label || g.group; + for (const it of (g.items||[])){ + const opt = document.createElement('option'); opt.value = it.value; opt.textContent = it.label; if (this._holidayCopySel===it.value) opt.selected = true; og.append(opt); + } + select.append(og); + } + select.onchange = ()=>{ this._holidayCopySel = String(select.value||''); this._renderHolidayCopy(); }; + } + // Draw scale and preview + const scale = modal.querySelector('.week-scale-inner'); const track = modal.querySelector('.week-track'); + if (scale) { + scale.innerHTML = ''; + for (let i=0;i<=24;i+=2){ + const t = document.createElement('div'); t.style.position='absolute'; t.style.left=(i/24*100)+'%'; t.style.top='4px'; t.style.bottom='4px'; t.style.borderLeft='1px solid var(--divider-color)'; + const lab = document.createElement('div'); lab.style.position='absolute'; lab.style.top='-2px'; lab.style.transform='translate(-50%,0)'; lab.style.left='0'; lab.style.fontSize='.75rem'; lab.style.color='var(--secondary-text-color)'; + if (this._config?.time_12h){ const p=this._timeParts(i===24?0:i*60); lab.innerHTML = `
    ${p.main}
    ${p.mer||''}
    `; } else { lab.textContent = (i===24 ? this._label(0) : this._label(i*60)); } + t.append(lab); scale.append(t); + } + } + if (track) { + track.innerHTML = ''; + const blocks = this._holidayCopyBlocks(eid, this._holidayCopySel); + for (const b of blocks){ + const pctStart=(b.startMin/1440)*100, pctW=((b.endMin-b.startMin)/1440)*100; const bl=document.createElement('div'); bl.className='block'; bl.style.left=pctStart+'%'; bl.style.width=pctW+'%'; + try { const clr=this._colorFor(eid, b.temp); if (clr){ bl.style.background=clr; bl.style.borderColor=clr; const txt=this._contrastTextColor(clr); if (txt) bl.style.color=txt; } } catch {} + UiHelper.genBlockPill( + this, bl, + `${this._toDisplayTemp(b.temp)} ${this._unitSymbol()}` + ); + track.append(bl); + } + } + } catch {} + } + + async _saveEditor(){ const errElGlobal = this.shadowRoot?.querySelector('.ed-error'); try { if (!this._editing) return; const { entity, blockId, weeklyDay, profileName, holiday } = this._editing; const row = this._schedules[entity]; if (!row) return; let b = null; let sourceBlocks = row.blocks; if (profileName) { this._ensureProfilesStruct(row); // Use the current draft for overlap/checks instead of stored profile + const curRoom = this._profilesRoom || this._profilesEntity || entity; const draftArr = (this._profilesDraft?.rooms?.[curRoom]) || []; sourceBlocks = draftArr; b = blockId ? draftArr.find(x=>x.id===blockId) : null; } else if (weeklyDay && this._weeklyDraft) { sourceBlocks = (this._weeklyDraft.days?.[weeklyDay] || []); b = blockId ? sourceBlocks.find(x=>x.id===blockId) : null; if (!b && this._weeklyDraft?._weeklyModes) { for (const modeName in this._weeklyDraft._weeklyModes) { const modeBlocks = this._weeklyDraft._weeklyModes[modeName]?.days?.[weeklyDay]; if (Array.isArray(modeBlocks)) { b = modeBlocks.find(x=>x.id===blockId); if (b) { sourceBlocks = modeBlocks; break; } } } } } else if (holiday && this._holidayDraft?.rooms) { sourceBlocks = (this._holidayDraft.rooms?.[entity] || []); b = blockId ? sourceBlocks.find(x=>x.id===blockId) : null; } else { if (blockId) b = row.blocks.find(x => x.id === blockId); } + const edTemp = this.shadowRoot.querySelector(".ed-temp"); const edFrom = this.shadowRoot.querySelector(".ed-from"); const edTo = this.shadowRoot.querySelector(".ed-to"); + const fromMerSel = this.shadowRoot.querySelector('.ed-from-mer'); const toMerSel = this.shadowRoot.querySelector('.ed-to-mer'); + const rawFrom = String(edFrom.value || ""); + const rawTo = String(edTo.value || ""); + let start = this._fromTimeInput(rawFrom); + let end = this._fromTimeInput(rawTo); + // If 12h mode, adjust using AM/PM selects + if (this._config?.time_12h) { + const merF = (fromMerSel?.value || 'AM').toUpperCase(); + const merT = (toMerSel?.value || 'AM').toUpperCase(); + let sh = Math.floor(start/60), sm = start%60; + let eh = Math.floor(end/60), em = end%60; + if (sh <= 12) { if (merF==='AM' && sh===12) sh = 0; if (merF==='PM' && sh<12) sh += 12; } + if (eh <= 12) { if (merT==='AM' && eh===12) eh = 0; if (merT==='PM' && eh<12) eh += 12; } + start = sh*60 + sm; end = eh*60 + em; + } + // Treat end time 00:00 as end of day (24:00 -> 1440 minutes) + if (rawTo === '00:00') end = 1440; + let tempRaw = String(edTemp.value || "").replace(",", "."); let temp = parseFloat(tempRaw); + if (isNaN(start)) start = 0; if (isNaN(end)) end = 60; if (isNaN(temp)) temp = row.defaultTemp || 20; + const maxLimit = this._maxDisplay(); const minLimit = this._minDisplay(); if (Number.isFinite(maxLimit)) temp = Math.min(temp, maxLimit); if (Number.isFinite(minLimit)) temp = Math.max(temp, minLimit); + start = this._clamp(Math.floor(start), 0, 1439); end = this._clamp(Math.ceil(end), 1, 1440); + + // Helper to check overlap in an array of blocks (ignore current 'b' if provided) + const hasOverlap = (arr, s, e, ignoreId=null) => { + const othersArr = (arr||[]).filter(x=>!ignoreId || x.id !== ignoreId); + return othersArr.find(o => !(e <= o.startMin || s >= o.endMin)); + }; + + // If interval crosses midnight (end <= start), split into two blocks + const crossesMidnight = end <= start; + // Early overlap check only for base/weekly editors; profiles and holiday are handled below using the draft state + if (!crossesMidnight && !profileName && !holiday) { + const overlap = hasOverlap(sourceBlocks, start, end, b?.id); + if (overlap) { + const overlapStart = Math.max(start, overlap.startMin); + const overlapEnd = Math.min(end, overlap.endMin); + const errEl = this.shadowRoot.querySelector('.ed-error'); + if (errEl) { + const msg = this._t('ui.overlap_msg').replace('{start}', this._label(overlapStart)).replace('{end}', this._label(overlapEnd)); + const fixStartLab = this._t('ui.fix_start_to').replace('{time}', this._label(overlap.endMin)); + const fixEndLab = this._t('ui.fix_end_to').replace('{time}', this._label(overlap.startMin)); + // Decide which fixes are relevant + const startInside = start >= overlap.startMin && start < overlap.endMin; + const endInside = end > overlap.startMin && end <= overlap.endMin; + const showStart = startInside || (!startInside && endInside && end > overlap.endMin); // overlap at right edge + const showEnd = endInside || (!endInside && startInside && start < overlap.startMin); // overlap at left edge + let btns = ''; + if (showStart) btns += ``; + if (showEnd) btns += ``; + // No extra cancel button here; bottom Cancel handles it + errEl.innerHTML = `
    ${msg}
    ${btns}
    `; + errEl.style.display = 'block'; + const applyFix = (kind)=>{ + // Adjust inputs then re-run save + try { + const edFrom = this.shadowRoot.querySelector('.ed-from'); + const edTo = this.shadowRoot.querySelector('.ed-to'); + const fromMerSel = this.shadowRoot.querySelector('.ed-from-mer'); + const toMerSel = this.shadowRoot.querySelector('.ed-to-mer'); + if (kind==='start') { + const ns = overlap.endMin; + if (this._config?.time_12h) { + if (edFrom) edFrom.value = this._toTimeInput12h(ns); + if (fromMerSel) fromMerSel.value = (ns>=720 && ns<1440)?'PM':'AM'; + } else { + if (edFrom) edFrom.value = this._toTimeInput(ns); + } + } else if (kind==='end') { + const ne = overlap.startMin; + if (this._config?.time_12h) { + if (edTo) edTo.value = this._toTimeInput12h(ne); + if (toMerSel) toMerSel.value = (ne>=720 && ne<1440)?'PM':'AM'; + } else { + if (edTo) edTo.value = this._toTimeInput(ne); + } + } + } catch {} + try { errEl.style.display='none'; errEl.textContent=''; } catch {} + // Defer to allow DOM to update + setTimeout(()=>{ this._saveEditor(); }, 0); + }; + errEl.querySelector('.ed-fix-start')?.addEventListener('click', ()=>applyFix('start')); + errEl.querySelector('.ed-fix-end')?.addEventListener('click', ()=>applyFix('end')); + // No extra cancel handler + } + return; + } + } + temp = this._fromDisplayTemp(temp); + const before = this._desiredNowSnapshot(); + if (profileName) { + // Save into a draft for the selected profile; persist only when user presses Save + const hasOverlap = (arr, s, e, ignoreId=null) => { const othersArr = (arr||[]).filter(x=>!ignoreId || x.id !== ignoreId); return othersArr.find(o => !(e <= o.startMin || s >= o.endMin)); }; + const crossesMidnight = end <= start; + // Ensure draft exists for current room; don't re-init when creating a new (unnamed) profile + const isNewProf = (profileName === '__new__'); + const curRoom = this._profilesRoom || this._profilesEntity || Object.keys(this._schedules)[0]; + if (!this._profilesDraft || (!isNewProf && this._profilesDraft.name !== profileName)) { + try { + const roomsMap = {}; + for (const e of (this._config?.entities||[])){ + const src = !isNewProf ? (this._schedules[e]?.profiles?.[profileName]?.blocks || []) : []; + roomsMap[e] = JSON.parse(JSON.stringify(src)); + } + this._profilesDraft = { name: (isNewProf? null : profileName), rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + } catch { + const roomsMap = {}; for (const e of (this._config?.entities||[])) roomsMap[e] = []; + this._profilesDraft = { name: (isNewProf? null : profileName), rooms: roomsMap, defaultTemp: Number(row.defaultTemp||this._config.default_temp||20) }; + } + } + const arr = (this._profilesDraft.rooms||{})[curRoom] || []; + // Find the block within the draft, not the stored profile + let b = blockId ? arr.find(x=>x.id===blockId) : null; + if (crossesMidnight) { + const ov1 = hasOverlap(arr, start, 1440, b?.id); + const ov2 = hasOverlap(arr, 0, end, null); + if (ov1 || ov2) { const errEl = this.shadowRoot.querySelector('.ed-error'); if (errEl) { const overlap = ov1 || ov2; const msg = this._t('ui.overlap_msg').replace('{start}', this._label(ov1?Math.max(start, ov1.startMin):0)).replace('{end}', this._label(ov1?Math.min(1440, ov1.endMin):end)); errEl.innerHTML = `
    ${msg}
    `; errEl.style.display='block'; } return; } + if (!b) { const id1=Math.random().toString(36).slice(2,9); const id2=Math.random().toString(36).slice(2,9); arr.push({ id:id1, startMin:start, endMin:1440, temp: this._fromDisplayTemp(temp) }); arr.push({ id:id2, startMin:0, endMin:end, temp: this._fromDisplayTemp(temp) }); } else { b.temp=this._fromDisplayTemp(temp); b.startMin=start; b.endMin=1440; const id2=Math.random().toString(36).slice(2,9); arr.push({ id:id2, startMin:0, endMin:end, temp: this._fromDisplayTemp(temp) }); } + } else { + const overlap = hasOverlap(arr, start, end, b?.id); + if (overlap) { const errEl = this.shadowRoot.querySelector('.ed-error'); if (errEl) { const msg = this._t('ui.overlap_msg').replace('{start}', this._label(Math.max(start, overlap.startMin))).replace('{end}', this._label(Math.min(end, overlap.endMin))); errEl.innerHTML = `
    ${msg}
    `; errEl.style.display='block'; } return; } + if (!b) { const id=Math.random().toString(36).slice(2,9); arr.push({ id, startMin:start, endMin:end, temp:this._fromDisplayTemp(temp) }); } else { b.temp=this._fromDisplayTemp(temp); b.startMin=start; b.endMin=end; } + } + (this._profilesDraft.rooms||{})[curRoom] = arr.sort((a,bx)=>a.startMin-bx.startMin||a.endMin-bx.endMin); + this._profilesDirty = true; + // If the profile edit originated from the main timeline, persist immediately + if (this._autoSaveProfileFromTimeline) { + try { + this._autoSaveProfileFromTimeline = false; + // Persist draft for current room/profile + await this._profilesSaveDraft(); + // Apply immediately if edit affects current time + try { + if (this._config?.auto_apply && this._config?.apply_on_edit) { + const nowMin = this._getNowMin(); + const arrNow = Array.isArray((this._profilesDraft?.rooms||{})[curRoom]) ? this._profilesDraft.rooms[curRoom] : []; + const hit = arrNow.find(x => nowMin >= x.startMin && nowMin < x.endMin); + if (hit) { await this._applyImmediateForEntity(entity, Number(hit.temp), nowMin); this._scheduleNextApply(); } + } + } catch {} + this._closeEditor(); + return; + } catch { + // Fall back to non-persisting flow + } + } + this._render(); this._closeEditor(); this._renderProfilesModal(); + return; + } + if (weeklyDay && this._weeklyDraft) { + if (crossesMidnight) { + // Split into two parts on the SAME day: [start..1440] and [0..end] + const dayKey = weeklyDay; + const arrToday = this._weeklyDraft.days?.[dayKey] || []; + // Check overlaps for each part against the same day + const ov1 = hasOverlap(arrToday, start, 1440, b?.id); + const ov2 = hasOverlap(arrToday, 0, end, b?.id); + if (ov1 || ov2){ + const errEl = this.shadowRoot.querySelector('.ed-error'); + if (errEl) { + const overlap = ov1 || ov2; + const msg = this._t('ui.overlap_msg').replace('{start}', this._label(Math.max((ov1?start:0), overlap.startMin))).replace('{end}', this._label(Math.min((ov1?1440:end), overlap.endMin))); + errEl.innerHTML = `
    ${msg}
    `; errEl.style.display='block'; + } + return; + } + // Apply changes on same day + if (!b) { + const id1 = Math.random().toString(36).slice(2,9); + const id2 = Math.random().toString(36).slice(2,9); + arrToday.push({ id:id1, startMin:start, endMin:1440, temp }); + arrToday.push({ id:id2, startMin:0, endMin:end, temp }); + } else { + b.temp = temp; b.startMin = start; b.endMin = 1440; + const id2 = Math.random().toString(36).slice(2,9); + arrToday.push({ id:id2, startMin:0, endMin:end, temp }); + } + this._weeklyDraft.days[dayKey] = arrToday.sort((a,bx)=>a.startMin-bx.startMin||a.endMin-bx.endMin); + } else { + if (!b) { + const id = Math.random().toString(36).slice(2,9); + b = { id, startMin: start, endMin: end, temp }; + sourceBlocks.push(b); + } else { + b.temp = temp; b.startMin = start; b.endMin = end; + } + sourceBlocks.sort((a,bx)=>a.startMin - bx.startMin || a.endMin - bx.endMin); + } + // If started from main timeline, auto-save the whole weekly plan now + if (this._autoSaveWeeklyFromTimeline) { + this._autoSaveWeeklyFromTimeline = false; + this._closeEditor(); + // Persist weekly draft (propagate groups) and apply if needed + this._weeklySaveAll(); + } else { + // Close editor and re-render weekly preview; no immediate apply + this._closeEditor(); + this._renderWeeklyModal(); + + // If "apply on edit" is enabled, and this weekly edit affects "right now", + // apply the new temperature immediately without requiring full Week Save. + try { + if (this._config?.auto_apply && this._config?.apply_on_edit) { + const today = this._todayKey(); + // Determine if the edit impacts today given the current week mode and selected group + const mode = (this._weeklyDraft?.mode) || (row.weekly?.mode) || (this._config?.weekdays_mode) || 'weekday_weekend'; + const selKey = this._weeklyDayKey || weeklyDay || today; // group key the user was editing + + const isWeekday = ['mon','tue','wed','thu','fri'].includes(today); + let editImpactsToday = false; + let sourceKeyForToday = today; // which draft day array reflects today after grouping + if (mode === 'weekday_weekend') { + if (selKey === 'weekdays' && isWeekday) { editImpactsToday = true; sourceKeyForToday = 'mon'; } + if (selKey === 'weekend' && (today==='sat' || today==='sun')) { editImpactsToday = true; sourceKeyForToday = 'sat'; } + if (selKey === today) { editImpactsToday = true; sourceKeyForToday = today; } + } else if (mode === 'weekday_sat_sun') { + if (selKey === 'weekdays' && isWeekday) { editImpactsToday = true; sourceKeyForToday = 'mon'; } + if ((selKey === 'sat' && today==='sat') || (selKey==='sun' && today==='sun')) { editImpactsToday = true; sourceKeyForToday = today; } + if (selKey === today) { editImpactsToday = true; sourceKeyForToday = today; } + } else { // all_7 + if (selKey === today) { editImpactsToday = true; sourceKeyForToday = today; } + } + + if (editImpactsToday) { + const nowMin = this._getNowMin(); + const arrNow = Array.isArray(this._weeklyDraft?.days?.[sourceKeyForToday]) ? this._weeklyDraft.days[sourceKeyForToday] : []; + const hit = arrNow.find(x => nowMin >= x.startMin && nowMin < x.endMin); + if (hit) { + // Apply immediately to reflect the new block that currently covers "now" + await this._applyImmediateForEntity(entity, Number(hit.temp), nowMin); + this._scheduleNextApply(); + } + } + } + } catch {} + } + return; + } + if (this._editing.presence && this._presenceDraft) { + // Work on presence draft only; persist on Presence Save + const eid = this._editing.entity; + const arr = (this._presenceDraft.rooms||{})[eid] || []; + // Ensure we reference the correct block when editing (so overlap checks ignore itself) + let b = blockId ? arr.find(x=>x.id===blockId) : null; + if (crossesMidnight) { + const ov1 = hasOverlap(arr, start, 1440, b?.id); + const ov2 = hasOverlap(arr, 0, end, null); + if (ov1 || ov2) { const errEl = this.shadowRoot.querySelector('.ed-error'); if (errEl) { const overlap = ov1 || ov2; const msg = this._t('ui.overlap_msg').replace('{start}', this._label(ov1?Math.max(start, ov1.startMin):0)).replace('{end}', this._label(ov1?Math.min(1440, ov1.endMin):end)); errEl.innerHTML = `
    ${msg}
    `; errEl.style.display='block'; } return; } + if (!b) { const id1=Math.random().toString(36).slice(2,9); const id2=Math.random().toString(36).slice(2,9); arr.push({ id:id1, startMin:start, endMin:1440, temp }); arr.push({ id:id2, startMin:0, endMin:end, temp }); } else { b.temp=temp; b.startMin=start; b.endMin=1440; const id2=Math.random().toString(36).slice(2,9); arr.push({ id:id2, startMin:0, endMin:end, temp }); } + } else { + const overlap = hasOverlap(arr, start, end, b?.id); + if (overlap) { const errEl = this.shadowRoot.querySelector('.ed-error'); if (errEl) { const msg = this._t('ui.overlap_msg').replace('{start}', this._label(Math.max(start, overlap.startMin))).replace('{end}', this._label(Math.min(end, overlap.endMin))); errEl.innerHTML = `
    ${msg}
    `; errEl.style.display='block'; } return; } + if (!b) { const id=Math.random().toString(36).slice(2,9); arr.push({ id, startMin:start, endMin:end, temp }); } else { b.temp=temp; b.startMin=start; b.endMin=end; } + } + (this._presenceDraft.rooms||{})[eid] = arr.sort((a,bx)=>a.startMin-bx.startMin||a.endMin-bx.endMin); + this._render(); this._closeEditor(); if (this._presenceOpen) this._renderPresenceModal(); + return; + } + if (holiday && this._holidayDraft?.rooms) { + // Work on holiday draft only; persist on Holiday Save + const eid = entity; + const arr = (this._holidayDraft.rooms||{})[eid] || []; + if (crossesMidnight) { + const ov1 = hasOverlap(arr, start, 1440, b?.id); + const ov2 = hasOverlap(arr, 0, end, null); + if (ov1 || ov2) { const errEl = this.shadowRoot.querySelector('.ed-error'); if (errEl) { const overlap = ov1 || ov2; const msg = this._t('ui.overlap_msg').replace('{start}', this._label(ov1?Math.max(start, ov1.startMin):0)).replace('{end}', this._label(ov1?Math.min(1440, ov1.endMin):end)); errEl.innerHTML = `
    ${msg}
    `; errEl.style.display='block'; } return; } + if (!b) { const id1=Math.random().toString(36).slice(2,9); const id2=Math.random().toString(36).slice(2,9); arr.push({ id:id1, startMin:start, endMin:1440, temp: this._fromDisplayTemp(temp) }); arr.push({ id:id2, startMin:0, endMin:end, temp: this._fromDisplayTemp(temp) }); } else { b.temp=this._fromDisplayTemp(temp); b.startMin=start; b.endMin=1440; const id2=Math.random().toString(36).slice(2,9); arr.push({ id:id2, startMin:0, endMin:end, temp: this._fromDisplayTemp(temp) }); } + } else { + const overlap = hasOverlap(arr, start, end, b?.id); + if (overlap) { const errEl = this.shadowRoot.querySelector('.ed-error'); if (errEl) { const msg = this._t('ui.overlap_msg').replace('{start}', this._label(Math.max(start, overlap.startMin))).replace('{end}', this._label(Math.min(end, overlap.endMin))); errEl.innerHTML = `
    ${msg}
    `; errEl.style.display='block'; } return; } + if (!b) { const id=Math.random().toString(36).slice(2,9); arr.push({ id, startMin:start, endMin:end, temp:this._fromDisplayTemp(temp) }); } else { b.temp=this._fromDisplayTemp(temp); b.startMin=start; b.endMin=end; } + } + (this._holidayDraft.rooms||{})[eid] = arr.sort((a,bx)=>a.startMin-bx.startMin||a.endMin-bx.endMin); + this._render(); this._closeEditor(); this._renderHolidayModal(); + return; + } + // Normal (non-weekly) save + if (crossesMidnight) { + // Check overlaps for both parts + const ov1 = hasOverlap(row.blocks, start, 1440, b?.id); + const ov2 = hasOverlap(row.blocks, 0, end, null); + if (ov1 || ov2) { + const errEl = this.shadowRoot.querySelector('.ed-error'); + if (errEl) { + const overlap = ov1 || ov2; + const msg = this._t('ui.overlap_msg').replace('{start}', this._label(ov1?Math.max(start, ov1.startMin):0)).replace('{end}', this._label(ov1?Math.min(1440, ov1.endMin):end)); + const fixStartLab = this._t('ui.fix_start_to').replace('{time}', this._label((ov1?1440:overlap.endMin))); + const fixEndLab = this._t('ui.fix_end_to').replace('{time}', this._label((ov2?0:overlap.startMin))); + // Relevance: if first part [start..1440] overlaps -> offer start fix; if second part [0..end] overlaps -> offer end fix + const showStart = !!ov1; + const showEnd = !!ov2; + let btns = ''; + if (showStart) btns += ``; + if (showEnd) btns += ``; + // No extra cancel here; use the modal cancel button + errEl.innerHTML = `
    ${msg}
    ${btns}
    `; + errEl.style.display='block'; + const applyFix = (kind)=>{ + try { + const edFrom = this.shadowRoot.querySelector('.ed-from'); + const edTo = this.shadowRoot.querySelector('.ed-to'); + const fromMerSel = this.shadowRoot.querySelector('.ed-from-mer'); + const toMerSel = this.shadowRoot.querySelector('.ed-to-mer'); + if (kind==='start') { + const ns = ov1 ? Math.max(start, (ov1.endMin||start)) : (overlap.endMin||start); + if (this._config?.time_12h) { if (edFrom) edFrom.value = this._toTimeInput12h(ns); if (fromMerSel) fromMerSel.value = (ns>=720 && ns<1440)?'PM':'AM'; } else { if (edFrom) edFrom.value = this._toTimeInput(ns); } + } else { + const ne = ov2 ? Math.min(end, (ov2.startMin||end)) : (overlap.startMin||end); + if (this._config?.time_12h) { if (edTo) edTo.value = this._toTimeInput12h(ne); if (toMerSel) toMerSel.value = (ne>=720 && ne<1440)?'PM':'AM'; } else { if (edTo) edTo.value = this._toTimeInput(ne); } + } + } catch {} + try { errEl.style.display='none'; errEl.textContent=''; } catch {} + setTimeout(()=>{ this._saveEditor(); }, 0); + }; + errEl.querySelector('.ed-fix-start')?.addEventListener('click', ()=>applyFix('start')); + errEl.querySelector('.ed-fix-end')?.addEventListener('click', ()=>applyFix('end')); + // No extra cancel handler + } + return; + } + if (!b) { + const id1 = Math.random().toString(36).slice(2,9); + const id2 = Math.random().toString(36).slice(2,9); + row.blocks.push({ id:id1, startMin:start, endMin:1440, temp }); + row.blocks.push({ id:id2, startMin:0, endMin:end, temp }); + } else { + // Update current b to first part and add second + b.temp = temp; b.startMin = start; b.endMin = 1440; + const id2 = Math.random().toString(36).slice(2,9); + row.blocks.push({ id:id2, startMin:0, endMin:end, temp }); + } + row.blocks.sort((a,bx)=>a.startMin-bx.startMin||a.endMin-bx.endMin); + } else { + if (!b) { const id = Math.random().toString(36).slice(2,9); b = { id, startMin: start, endMin: end, temp }; row.blocks.push(b); } else { b.temp = temp; b.startMin = start; b.endMin = end; } + this._applyNoOverlapResize(entity, b, "left", b.startMin); this._applyNoOverlapResize(entity, b, "right", b.endMin); + } + // Persist local draft immediately for resilience + try { this._persistDraftNow(); } catch {} + this._markPendingSave(); this._render(); this._closeEditor(); + // Immediate apply if a block now covers the current time (non-weekly path) + try { + if (this._config?.auto_apply && this._config?.apply_on_edit) { + const nowMin = this._getNowMin(); + const arrNow = Array.isArray(row.blocks) ? row.blocks : []; + const hitNow = arrNow.find(x => nowMin >= x.startMin && nowMin < x.endMin); + if (hitNow) await this._applyImmediateForEntity(entity, Number(hitNow.temp), nowMin); + } + } catch {} + if (this._config.apply_on_edit) await this._applyIfDesiredChanged(before); this._scheduleNextApply(); } catch (e) { if (errElGlobal) { errElGlobal.style.display = 'block'; errElGlobal.textContent = this._t('editor.error_prefix') + (e && e.message ? e.message : String(e)); } }} + + _toTimeInput(min){ if (!Number.isFinite(min)) min = 0; const m = ((Math.floor(min) % 1440) + 1440) % 1440; const hh=Math.floor(m/60), mm=Math.floor(m%60); return `${String(hh).padStart(2,"0")}:${String(mm).padStart(2,"0")}`; } + _fromTimeInput(str){ const m=/(\d{1,2}):(\d{2})$/.exec(str||""); if(!m) return 0; const h=Math.max(0,Math.min(23,parseInt(m[1],10))); const mi=Math.max(0,Math.min(59,parseInt(m[2],10))); return h*60+mi; } + _toTimeInput12h(min){ if (!Number.isFinite(min)) min=0; const m=((Math.floor(min)%1440)+1440)%1440; let hh=Math.floor(m/60); const mm=Math.floor(m%60); let h=hh%12; if(h===0) h=12; return `${String(h).padStart(2,'0')}:${String(mm).padStart(2,'0')}`; } + + // ---------- Onboarding ---------- + _onboardStorageKey(){ return 'thermostat_timeline_onboard_seen_v1'; } + _maybeOpenOnboard(){ + try { + // Respect explicit disable flag + if (this._disableOnboard) return; + const flag = localStorage.getItem(this._onboardStorageKey()); + if (!flag) this._openOnboard(); + } catch {} + } + _openOnboard(){ + try { + this._onboardOpen = true; this._onboardStep = 0; this._renderOnboard(); + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-onboard'); + if (ov) ov.classList.add('open'); + } catch {} + } + _closeOnboard(markSeen=false){ + try { + const ov = this.shadowRoot && this.shadowRoot.querySelector('.overlay-onboard'); if (ov) ov.classList.remove('open'); + this._onboardOpen = false; + // store preference + const cb = this.shadowRoot && this.shadowRoot.querySelector('.onb-hide'); + if (markSeen || (cb && cb.checked)) { + try { localStorage.setItem(this._onboardStorageKey(), '1'); } catch {} + } + } catch {} + } + _renderOnboard(){ + try { + const img = this.shadowRoot && this.shadowRoot.querySelector('.onb-img'); + const vid = this.shadowRoot && this.shadowRoot.querySelector('.onb-vid'); + const hero = this.shadowRoot && this.shadowRoot.querySelector('.ob-hero'); + const txt = this.shadowRoot && this.shadowRoot.querySelector('.onb-text'); + const dots = this.shadowRoot && this.shadowRoot.querySelector('.onb-dots'); + const next = this.shadowRoot && this.shadowRoot.querySelector('.onb-next'); + const done = this.shadowRoot && this.shadowRoot.querySelector('.onb-done'); + const step = Math.max(0, Math.min(this._onboardSlides.length-1, this._onboardStep)); + const slide = this._onboardSlides[step] || { key:'onboard.step_main', img:'', fallback:'' }; + if (img && vid) { + // First try HACS/relative asset; on error, try /local fallback if provided + const nextUrl = slide.img || slide.fallback || ''; + if (this._onboardLastUrl !== nextUrl) { + this._onboardLastUrl = nextUrl; + const isVideo = /\.(mp4|webm|mov)(\?|#|$)/i.test(nextUrl); + // Reset state + img.style.display = 'none'; + vid.style.display = 'none'; + // Remove any previous message + try { const m = hero && hero.querySelector('.onb-missing'); if (m) m.remove(); } catch {} + if (isVideo) { + // Setup video + vid.onerror = null; vid.onloadeddata = null; vid.oncanplay = null; + vid.muted = true; vid.playsInline = true; vid.loop = true; vid.autoplay = true; + vid.onloadeddata = ()=>{ try { vid.style.display=''; img.style.display='none'; vid.play().catch(()=>{}); } catch{} }; + vid.onerror = ()=>{ + try { + if (slide.fallback && vid.src !== slide.fallback) { vid.src = slide.fallback; vid.load?.(); return; } + vid.style.display='none'; + if (hero) { let msg = hero.querySelector('.onb-missing'); if (!msg){ msg=document.createElement('div'); msg.className='onb-missing'; msg.style.fontSize='.9rem'; msg.style.color='var(--secondary-text-color)'; msg.style.padding='8px'; hero.append(msg); } msg.textContent=this._t('onboard.video_not_found') || 'Video not found'; } + } catch{} + }; + vid.src = nextUrl; vid.load?.(); + } else { + // Setup image + img.onerror = null; img.onload = null; + img.onerror = ()=>{ + try { + if (slide.fallback && img.src !== slide.fallback) { img.src = slide.fallback; return; } + img.style.display='none'; + if (hero) { let msg = hero.querySelector('.onb-missing'); if (!msg){ msg=document.createElement('div'); msg.className='onb-missing'; msg.style.fontSize='.9rem'; msg.style.color='var(--secondary-text-color)'; msg.style.padding='8px'; hero.append(msg); } msg.textContent=this._t('onboard.image_not_found') || 'Image not found'; } + } catch{} + }; + img.onload = ()=>{ try { img.style.display=''; vid.style.display='none'; } catch{} }; + img.src = nextUrl; + } + } + } + if (txt) txt.textContent = this._t(slide.key); + if (dots) { + dots.innerHTML = ''; + for (let i=0;ib.id!==id); if(this._active&&this._active.entity===entity&&this._active.id===id) this._active=null; this._debouncedSaveStore(); this._render(); if (this._config.apply_on_edit) this._applyIfDesiredChanged(before); this._scheduleNextApply(); } + + _t(k){ return ttLocalize(k, this._hass || this._lang || 'en'); } + + _applyCardI18n(){ + try { + const t = (k)=> this._t(k); + // Main modal: Edit block + const modal = this.shadowRoot && this.shadowRoot.querySelector('.overlay:not(.overlay-week):not(.overlay-profiles):not(.overlay-holiday):not(.overlay-presence):not(.overlay-copyrooms):not(.overlay-onboard):not(.overlay-pause):not(.overlay-test):not(.overlay-settings) .modal'); + if (modal) { + modal.setAttribute('aria-label', t('ui.edit_block')); + const h3 = modal.querySelector('.ed-modal-title'); + if (h3) h3.textContent = t('ui.edit_block'); + const fromLab = modal.querySelector('.ed-from-label'); + if (fromLab) fromLab.textContent = t('ui.from'); + const toLab = modal.querySelector('.ed-to-label'); + if (toLab) toLab.textContent = t('ui.to'); + const tempLab = modal.querySelector('.ed-temp-label'); + if (tempLab) tempLab.textContent = t('ui.temperature') + ' ('+ this._unitSymbol() +')'; + const del = modal.querySelector('.ed-delete'); + if (del) del.textContent = t('ui.delete'); + const cancel = modal.querySelector('.ed-cancel'); + if (cancel) cancel.textContent = t('ui.cancel'); + const save = modal.querySelector('.ed-save'); + if (save) save.textContent = t('ui.save'); + } + + // Week modal + try { + const wt = this.shadowRoot && this.shadowRoot.querySelector('.week-title'); + if (wt) wt.textContent = t('week.editor.title'); + const wml = this.shadowRoot && this.shadowRoot.querySelector('.week-mode-label'); + if (wml) wml.textContent = t('week.mode'); + const wnote = this.shadowRoot && this.shadowRoot.querySelector('.week-note'); + if (wnote) wnote.textContent = t('week.editor.preview_note'); + const wsel = this.shadowRoot && this.shadowRoot.querySelector('.week-mode'); + if (wsel) { + const opt1 = wsel.querySelector('.opt-weekday-weekend'); + if (opt1) opt1.textContent = t('week.mode.weekday_weekend'); + const opt2 = wsel.querySelector('.opt-weekday-sat-sun'); + if (opt2) opt2.textContent = t('week.mode.weekday_sat_sun'); + const opt3 = wsel.querySelector('.opt-all-7'); + if (opt3) opt3.textContent = t('week.mode.all_7'); + } + const wcancelBtn = this.shadowRoot && this.shadowRoot.querySelector('.wk-cancel'); + if (wcancelBtn) wcancelBtn.textContent = t('ui.cancel'); + const wsaveBtn = this.shadowRoot && this.shadowRoot.querySelector('.wk-save'); + if (wsaveBtn) wsaveBtn.textContent = t('ui.save'); + } catch {} + + // Profiles modal + try { + const proftitle = this.shadowRoot && this.shadowRoot.querySelector('.prof-title'); + if (proftitle) proftitle.textContent = t('profiles.editor.title'); + const profadd = this.shadowRoot && this.shadowRoot.querySelector('.prof-add-text'); + if (profadd) profadd.textContent = t('profiles.add'); + const ps = this.shadowRoot && this.shadowRoot.querySelector('.prof-save'); + if (ps) { + const isExisting = !!(this._profilesEditingExisting && this._profilesSelected); + const key = isExisting ? 'profiles.save_existing' : 'profiles.save_new'; + ps.textContent = t(key) || t('ui.save'); + } + const pc = this.shadowRoot && this.shadowRoot.querySelector('.prof-close'); + if (pc) pc.textContent = t('ui.close'); + } catch {} + + // Holiday modal + try { + const ht = this.shadowRoot && this.shadowRoot.querySelector('.holiday-title'); + if (ht) ht.textContent = t('holidays.button'); + const hs = this.shadowRoot && this.shadowRoot.querySelector('.holiday-save'); + if (hs) hs.textContent = t('ui.save'); + const hc = this.shadowRoot && this.shadowRoot.querySelector('.holiday-close'); + if (hc) hc.textContent = t('ui.close'); + } catch {} + + // Presence modal + try { + const pttl = this.shadowRoot && this.shadowRoot.querySelector('.presence-title'); + if (pttl) pttl.textContent = t('presence.editor.title'); + const padv = this.shadowRoot && this.shadowRoot.querySelector('.presence-adv-label'); + if (padv) padv.textContent = t('presence.enable_advanced'); + const ps = this.shadowRoot && this.shadowRoot.querySelector('.presence-save'); + if (ps) ps.textContent = t('ui.save'); + const pcl = this.shadowRoot && this.shadowRoot.querySelector('.presence-close'); + if (pcl) pcl.textContent = t('ui.close'); + } catch {} + + // Copy to rooms modal + try { + const crt = this.shadowRoot && this.shadowRoot.querySelector('.copyrooms-title'); + if (crt) crt.textContent = t('week.copy_to_rooms.title'); + const crd = this.shadowRoot && this.shadowRoot.querySelector('.copyrooms-desc'); + if (crd) crd.textContent = t('week.copy_to_rooms.select'); + const crc = this.shadowRoot && this.shadowRoot.querySelector('.copyrooms-cancel'); + if (crc) crc.textContent = t('ui.cancel'); + const cra = this.shadowRoot && this.shadowRoot.querySelector('.copyrooms-apply'); + if (cra) cra.textContent = t('week.copy_to_rooms.apply'); + } catch {} + + // Onboarding modal + try { + const ttl = this.shadowRoot && this.shadowRoot.querySelector('.onb-title'); + if (ttl) ttl.textContent = t('onboard.title'); + const prev = this.shadowRoot && this.shadowRoot.querySelector('.onb-prev'); + if (prev) prev.textContent = t('onboard.prev'); + const next = this.shadowRoot && this.shadowRoot.querySelector('.onb-next'); + if (next) next.textContent = t('onboard.next'); + const skip = this.shadowRoot && this.shadowRoot.querySelector('.onb-skip'); + if (skip) skip.textContent = t('onboard.skip'); + const done = this.shadowRoot && this.shadowRoot.querySelector('.onb-done'); + if (done) done.textContent = t('onboard.done'); + const hideLbl = this.shadowRoot && this.shadowRoot.querySelector('.onb-hide-lbl'); + if (hideLbl) hideLbl.textContent = t('onboard.dont_show'); + } catch {} + + // Pause modal + try { + const pt = this.shadowRoot && this.shadowRoot.querySelector('.pause-title'); + if (pt) pt.textContent = t('pause.menu_title'); + const p1 = this.shadowRoot && this.shadowRoot.querySelector('.pause-1h'); + if (p1) p1.textContent = t('pause.off_1h'); + const p5 = this.shadowRoot && this.shadowRoot.querySelector('.pause-5h'); + if (p5) p5.textContent = t('pause.off_5h'); + const pcu = this.shadowRoot && this.shadowRoot.querySelector('.pause-custom'); + if (pcu) pcu.textContent = t('pause.off_custom'); + const pi = this.shadowRoot && this.shadowRoot.querySelector('.pause-indef'); + if (pi) pi.textContent = t('pause.off_indef'); + const pr = this.shadowRoot && this.shadowRoot.querySelector('.pause-resume'); + if (pr) pr.textContent = t('pause.resume'); + const px = this.shadowRoot && this.shadowRoot.querySelector('.pause-close'); + if (px) px.textContent = t('pause.close'); + } catch {} + + // Test modal + try { + const testtitle = this.shadowRoot && this.shadowRoot.querySelector('.test-title'); + if (testtitle) testtitle.textContent = t('editor.test_tool') || 'Test tool'; + const testlabel = this.shadowRoot && this.shadowRoot.querySelector('.test-label'); + if (testlabel) testlabel.textContent = t('editor.test_select_entity') || 'Select entity'; + const testempty = this.shadowRoot && this.shadowRoot.querySelector('.test-empty'); + if (testempty) testempty.textContent = t('editor.test_empty') || 'Select an entity to see available commands…'; + const testexport = this.shadowRoot && this.shadowRoot.querySelector('.test-export-btn'); + if (testexport) testexport.textContent = t('editor.test_export') || 'Export .md'; + const testclose = this.shadowRoot && this.shadowRoot.querySelector('.test-close'); + if (testclose) testclose.textContent = t('ui.close'); + } catch {} + + // Settings modal (in-card) + try { + const sm = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .modal'); + if (sm) sm.setAttribute('aria-label', t('editor.tabs.settings')); + const st = this.shadowRoot && this.shadowRoot.querySelector('.settings-title'); + if (st) st.textContent = t('editor.tabs.settings'); + + // Integration availability status chip (moved from HA editor into popup header) + try { + const chip = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-int-status'); + const icon = chip?.querySelector('ha-icon'); + const txt = chip?.querySelector('.text'); + const hasService = !!(this._hass && this._hass.services && this._hass.services['thermostat_timeline']); + if (chip && icon && txt) { + if (hasService) { + chip.classList.remove('bad'); chip.classList.add('ok'); + icon.setAttribute('icon','mdi:cloud-check-outline'); + txt.textContent = t('editor.integration.connected') || 'Connected to integration'; + } else { + chip.classList.remove('ok'); chip.classList.add('bad'); + icon.setAttribute('icon','mdi:cloud-off-outline'); + txt.textContent = t('editor.integration.disconnected') || 'Not connected to integration'; + } + } + } catch {} + + // Use the draft unit for the settings popup (so unit toggles update labels immediately, before Save). + const popupUnit = (() => { + try { + if (this._settingsDraft && this._settingsDraft.temp_unit != null) { + const u = String(this._settingsDraft.temp_unit).toUpperCase(); + if (u === 'F' || u === 'FAHRENHEIT') return '°F'; + if (u === 'C' || u === 'CELSIUS') return '°C'; + } + } catch {} + try { + return this._unitSymbol(); + } catch {} + return '°C'; + })(); + + // Tab strip (same labels as editor tabs; excludes Sync/Backup intentionally) + const tabMap = { + settings: 'editor.tabs.settings', + pause: 'editor.tabs.pause', + weekdays: 'editor.tabs.weekdays', + rooms: 'editor.tabs.rooms', + boiler: 'editor.tabs.boiler', + colors: 'editor.tabs.colors', + presence_sensor: 'editor.tabs.presence_sensor', + owd: 'editor.tabs.owd', + holidays: 'editor.tabs.holidays', + away: 'editor.tabs.away', + reset: 'editor.tabs.reset' + }; + const tabFallback = { + settings: 'Settings', + pause: 'Pause', + weekdays: 'Weekdays', + rooms: 'Rooms', + boiler: 'Boiler', + colors: 'Colors', + presence_sensor: 'Presence sensor', + owd: 'Open Window Detection', + holidays: 'Holidays', + away: 'Away from home', + reset: 'Reset' + }; + const btns = this.shadowRoot ? Array.from(this.shadowRoot.querySelectorAll('.overlay-settings .settings-tab-btn')) : []; + for (const b of btns){ + const tab = String(b.getAttribute('data-tab') || ''); + const key = tabMap[tab] || null; + if (!key) continue; + b.textContent = t(key) || tabFallback[tab] || tab; + } + const ss = this.shadowRoot && this.shadowRoot.querySelector('.settings-save'); + if (ss) ss.textContent = t('ui.save'); + const sc = this.shadowRoot && this.shadowRoot.querySelector('.settings-close'); + if (sc) sc.textContent = t('ui.close'); + + // Settings tab content labels + const defLab = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-slabel-def'); + if (defLab) defLab.textContent = String(t('editor.default_c')||'').replace('°C', popupUnit); + const rowHLab = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-slabel-rowh'); + if (rowHLab) rowHLab.textContent = t('editor.row_height_px'); + const minLab = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-slabel-minc'); + if (minLab) minLab.textContent = String(t('editor.min_c')||'').replace('°C', popupUnit); + const maxLab = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-slabel-maxc'); + if (maxLab) maxLab.textContent = String(t('editor.max_c')||'').replace('°C', popupUnit); + + const at = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-auto-title'); + if (at) at.textContent = t('editor.auto_apply.title'); + const ad = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-auto-desc'); + if (ad) ad.textContent = t('editor.auto_apply.desc'); + + const et = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-applyedit-title'); + if (et) et.textContent = t('editor.apply_edit.title'); + const ed = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-applyedit-desc'); + if (ed) ed.textContent = t('editor.apply_edit.desc'); + + const dt = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-applydef-title'); + if (dt) dt.textContent = t('editor.apply_default.title'); + const dd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-applydef-desc'); + if (dd) dd.textContent = t('editor.apply_default.desc'); + + const prt = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-perroom-title'); + if (prt) prt.textContent = t('editor.perroom.title'); + const prd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-perroom-desc'); + if (prd) prd.textContent = t('editor.perroom.desc'); + + const srt = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-showroomtemp-title'); + if (srt) srt.textContent = t('editor.show_room_temp.title'); + const srd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-showroomtemp-desc'); + if (srd) srd.textContent = t('editor.show_room_temp.desc'); + + const pbt = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-pausebtn-title'); + if (pbt) pbt.textContent = t('editor.pausebtn.title'); + const pbd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-pausebtn-desc'); + if (pbd) pbd.textContent = t('editor.pausebtn.desc'); + + const pst = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-pausesensor-title'); + if (pst) pst.textContent = t('editor.pause_sensor.title'); + const psd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-pausesensor-desc'); + if (psd) psd.textContent = t('editor.pause_sensor.desc'); + const psEt = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-pausesensor-entity-title'); + if (psEt) psEt.textContent = t('editor.pause_sensor.entity_title'); + const psEd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-pausesensor-entity-desc'); + if (psEd) psEd.textContent = t('editor.pause_sensor.entity_desc'); + + // New popup-only settings (moved from HA editor) + const tft = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-timefmt-title'); + if (tft) tft.textContent = t('editor.timefmt.title'); + const tfd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-timefmt-desc'); + if (tfd) tfd.textContent = t('editor.timefmt.desc'); + const tst = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-timesrc-title'); + if (tst) tst.textContent = t('editor.timesrc.title'); + const tsd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-timesrc-desc'); + if (tsd) tsd.textContent = t('editor.timesrc.desc'); + const sb = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-srcBrowser'); + if (sb) sb.textContent = t('editor.timesrc.browser'); + const sh = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-srcHa'); + if (sh) sh.textContent = t('editor.timesrc.ha'); + const tut = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-tempfmt-title'); + if (tut) tut.textContent = t('editor.tempfmt.title'); + const tud = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-tempfmt-desc'); + if (tud) tud.textContent = t('editor.tempfmt.desc'); + const pt = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-profiles-title'); + if (pt) pt.textContent = t('profiles.enable'); + const pd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-profiles-desc'); + if (pd) pd.textContent = t('profiles.enable.desc'); + + const seasonTitle = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-seasonal-title'); + if (seasonTitle) seasonTitle.textContent = t('season.enable.title'); + const seasonDesc = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-seasonal-desc'); + if (seasonDesc) seasonDesc.textContent = t('season.enable.desc'); + + const instTitle = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-instance-title'); + if (instTitle) instTitle.textContent = t('editor.instance.title'); + const instDesc = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-instance-desc'); + if (instDesc) instDesc.textContent = t('editor.instance.desc'); + const instBtn = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-instance-regenerate-btn'); + if (instBtn) { + const label = instBtn.querySelector('span') || instBtn; + if (label) label.textContent = t('editor.instance.new_id'); + } + + // Weekdays tab content labels + const weT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-enable-title'); + if (weT) weT.textContent = t('week.enable'); + const weD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-enable-desc'); + if (weD) weD.textContent = t('week.enable.desc'); + const wvT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-view-title'); + if (wvT) wvT.textContent = t('week.view.title'); + const wvR = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-view-rooms'); + if (wvR) wvR.textContent = t('week.view.rooms_one_day'); + const wvD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-view-days'); + if (wvD) wvD.textContent = t('week.view.days_one_room'); + + const wvsT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-view-switch-title'); + if (wvsT) wvsT.textContent = t('week.view_switch.title'); + const wvsD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-view-switch-desc'); + if (wvsD) wvsD.textContent = t('week.view_switch.desc'); + + const woT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-open-title'); + if (woT) woT.textContent = t('week.editor.title'); + const woD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-open-desc'); + if (woD) woD.textContent = t('week.open_editor.desc'); + const woB = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-week-open-btn'); + if (woB) woB.textContent = t('week.open_editor.btn'); + + // Boiler tab content labels + const beT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-enable-title'); + if (beT) beT.textContent = t('boiler.enable'); + const beD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-enable-desc'); + if (beD) beD.textContent = t('boiler.enable.desc'); + const bsT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-switch-title'); + if (bsT) bsT.textContent = t('boiler.switch'); + const bsdL = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-switch-domain-label'); + if (bsdL) bsdL.textContent = t('boiler.switch_type'); + try { + const sel = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-switch-domain'); + if (sel) { + const oSw = sel.querySelector('option[value="switch"]'); + if (oSw) oSw.textContent = t('boiler.switch_type.switch') || 'Switch'; + const oIb = sel.querySelector('option[value="input_boolean"]'); + if (oIb) oIb.textContent = t('boiler.switch_type.input_boolean') || 'Input boolean'; + } + } catch {} + const brT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-rooms-title'); + if (brT) brT.textContent = t('boiler.rooms'); + const brD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-rooms-desc'); + if (brD) brD.textContent = t('boiler.rooms.desc'); + const bmT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-multi-title'); + if (bmT) bmT.textContent = t('boiler.multi_enable'); + const bmD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-multi-desc'); + if (bmD) bmD.textContent = t('boiler.multi_enable.desc'); + const bmrT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-multi-rooms-title'); + if (bmrT) bmrT.textContent = t('boiler.multi_rooms'); + const bmrD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-multi-rooms-desc'); + if (bmrD) bmrD.textContent = t('boiler.multi_rooms.desc'); + const boT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-offsets-title'); + if (boT) boT.textContent = t('boiler.offsets'); + const boD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-offsets-desc'); + if (boD) boD.textContent = t('boiler.offsets.desc'); + const bon = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-on-offset-label'); + if (bon) bon.textContent = String(t('boiler.on_offset')||'').replace('°C', popupUnit); + const bof = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-off-offset-label'); + if (bof) bof.textContent = String(t('boiler.off_offset')||'').replace('°C', popupUnit); + const btT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-sensor-title'); + if (btT) btT.textContent = t('boiler.temp_sensor'); + const btD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-sensor-desc'); + if (btD) btD.textContent = t('boiler.temp_sensor.desc'); + const bmin = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-min-temp-label'); + if (bmin) bmin.textContent = String(t('boiler.min_temp')||'').replace('°C', popupUnit); + const bmax = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-boiler-max-temp-label'); + if (bmax) bmax.textContent = String(t('boiler.max_temp')||'').replace('°C', popupUnit); + + // Open Window Detection tab content labels + const owdT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-owd-enable-title'); + if (owdT) owdT.textContent = t('owd.enable'); + const owdD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-owd-enable-desc'); + if (owdD) owdD.textContent = t('owd.enable.desc'); + const owdS = this.shadowRoot ? Array.from(this.shadowRoot.querySelectorAll('.overlay-settings .sp-owd-sensors-label')) : []; + for (const el of owdS) { try { el.textContent = t('owd.sensors'); } catch {} } + const owdDT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-owd-delays-title'); + if (owdDT) owdDT.textContent = t('owd.delays.title'); + const owdDD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-owd-delays-desc'); + if (owdDD) owdDD.textContent = t('owd.delays.desc'); + const owdOpen = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-owd-open-delay-label'); + if (owdOpen) owdOpen.textContent = t('owd.open_delay'); + const owdClose = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-owd-close-delay-label'); + if (owdClose) owdClose.textContent = t('owd.close_delay'); + + // Holidays tab content labels + const heT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-enable-title'); + if (heT) heT.textContent = t('holidays.enable'); + const heD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-enable-desc'); + if (heD) heD.textContent = t('holidays.desc'); + const hsT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-source-title'); + if (hsT) hsT.textContent = t('holidays.source'); + const hCal = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-source-cal'); + if (hCal) hCal.textContent = t('holidays.source.calendar'); + const hMan = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-source-man'); + if (hMan) hMan.textContent = t('holidays.source.manual'); + const hEntT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-entity-title'); + if (hEntT) hEntT.textContent = t('holidays.entity'); + const hEntD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-entity-desc'); + if (hEntD) hEntD.textContent = t('holidays.entity_desc'); + const hDatesT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-dates-title'); + if (hDatesT) hDatesT.textContent = t('holidays.dates'); + const hSinglesT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-single-title'); + if (hSinglesT) hSinglesT.textContent = t('holidays.single_dates'); + const hAdd = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-add-date'); + if (hAdd) hAdd.textContent = t('holidays.add_date'); + const hAddR = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-add-range'); + if (hAddR) hAddR.textContent = t('holidays.add_range'); + const hRangesT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-ranges-title'); + if (hRangesT) hRangesT.textContent = t('holidays.ranges'); + const hEditT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-edit-title'); + if (hEditT) hEditT.textContent = t('holidays.editor.title'); + const hEditB = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-holidays-edit-btn'); + if (hEditB) hEditB.textContent = t('holidays.edit'); + + // Away tab content labels + const aeT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-enable-title'); + if (aeT) aeT.textContent = t('away.enable'); + const aeD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-enable-desc'); + if (aeD) aeD.textContent = t('away.desc'); + const atT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-temp-title'); + if (atT) atT.textContent = String(t('away.temp')||'').replace('°C', popupUnit); + const apT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-persons-title'); + if (apT) apT.textContent = t('away.persons'); + const adT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-delay-title'); + if (adT) adT.textContent = t('away.delay_enable'); + const adD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-delay-desc'); + if (adD) adD.textContent = t('away.delay_desc'); + const adVL = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-delay-value-label'); + if (adVL) adVL.textContent = t('away.delay_value'); + const adUL = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-delay-unit-label'); + if (adUL) adUL.textContent = t('away.delay_unit'); + try { const sel = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-delay-unit'); if (sel) { const opts = sel.querySelectorAll('option'); if (opts[0]) opts[0].textContent = t('away.delay_unit.seconds'); if (opts[1]) opts[1].textContent = t('away.delay_unit.minutes'); } } catch {} + const aaT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-adv-title'); + if (aaT) aaT.textContent = t('presence.enable_advanced'); + const aaD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-away-adv-desc'); + if (aaD) aaD.textContent = t('presence.enable_advanced.desc'); + const plT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-presence-live-title'); + if (plT) plT.textContent = t('presence.live_header'); + const plD = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-presence-live-desc'); + if (plD) plD.textContent = t('presence.live_header.desc'); + const pcT = this.shadowRoot && this.shadowRoot.querySelector('.overlay-settings .sp-presence-combos-title'); + if (pcT) pcT.textContent = t('presence.combos'); + } catch {} + + } catch (e) { /* ignore */ } + } + + _colorFor(eid, tempC){ + try { + + // Onboarding i18n + try { + const ttl = this.shadowRoot && this.shadowRoot.querySelector('.onb-title'); if (ttl) ttl.textContent = t('onboard.title'); + const prev = this.shadowRoot && this.shadowRoot.querySelector('.onb-prev'); if (prev) prev.textContent = t('onboard.prev'); + const next = this.shadowRoot && this.shadowRoot.querySelector('.onb-next'); if (next) next.textContent = t('onboard.next'); + const skip = this.shadowRoot && this.shadowRoot.querySelector('.onb-skip'); if (skip) skip.textContent = t('onboard.skip'); + const done = this.shadowRoot && this.shadowRoot.querySelector('.onb-done'); if (done) done.textContent = t('onboard.done'); + const hideLbl = this.shadowRoot && this.shadowRoot.querySelector('.onb-hide-lbl'); if (hideLbl) hideLbl.textContent = t('onboard.dont_show'); + } catch {} + // Support merged entities by always resolving to primary + const primary = this._groupPrimaryOf ? this._groupPrimaryOf(eid) : eid; + const cr = (this._config?.color_ranges) || {}; + const globalFirst = !!this._config?.color_global; + const candidates = globalFirst ? ['*', 'default', primary, eid] : [primary, eid, '*', 'default']; + let ranges = []; + for (const k of candidates){ if (Array.isArray(cr[k]) && cr[k].length){ ranges = cr[k]; break; } } + for (const r of ranges){ + const from = Number(r.from); + const to = Number(r.to); + if (!Number.isFinite(from) || !Number.isFinite(to)) continue; + if (tempC >= Math.min(from,to) && tempC <= Math.max(from,to)) return r.color || ''; + } + } catch {} + return ''; + } + + // Compute readable text color for a given background + _contrastTextColor(bg){ + try { + const rgb = (c)=>{ + if(!c) return null; + let s=String(c).trim(); + if (s.startsWith('rgb')){ + const m=s.match(/rgba?\((\d+)\s*,\s*(\d+)\s*,\s*(\d+)/i); if(!m) return null; return {r:+m[1], g:+m[2], b:+m[3]}; + } + if (s[0]==='#') s=s.slice(1); + if (s.length===3) s = s.split('').map(x=>x+x).join(''); + if (s.length!==6) return null; + const r=parseInt(s.slice(0,2),16), g=parseInt(s.slice(2,4),16), b=parseInt(s.slice(4,6),16); + if ([r,g,b].some(x=>Number.isNaN(x))) return null; return {r,g,b}; + }; + const o = rgb(bg); if (!o) return null; + // Perceived luminance (sRGB) + const lum = (v)=>{ v/=255; return v<=0.03928? v/12.92 : Math.pow((v+0.055)/1.055, 2.4); }; + const L = 0.2126*lum(o.r) + 0.7152*lum(o.g) + 0.0722*lum(o.b); + return L > 0.5 ? '#000000' : '#ffffff'; + } catch { return null; } + } + +} +// Avoid hard failures if the resource is loaded multiple times. +try { + if (!customElements.get("thermostat-timeline-card")) { + customElements.define("thermostat-timeline-card", ThermostatTimelineCard); + } +} catch {} + +/* ----------------- LIGHTWEIGHT PICKER (TTEntityPicker) ----------------- */ +class TTEntityPicker extends HTMLElement { + static get observedAttributes(){ return ['include-domains','label','disabled']; } + constructor(){ + super(); + this.attachShadow({ mode: 'open' }); + this._hass = null; + this._value = ''; + this._filter = null; // function + this._domains = []; + this._disabled = false; + } + set hass(h){ this._hass = h; this._render(); } + get hass(){ return this._hass; } + set value(v){ this._value = v || ''; this._render(); } + get value(){ return this._value || ''; } + set entityFilter(fn){ this._filter = typeof fn === 'function' ? fn : null; this._render(); } + get entityFilter(){ return this._filter; } + set disabled(v){ this._disabled = !!v; this._render(); } + get disabled(){ return !!this._disabled; } + attributeChangedCallback(name, _old, val){ + if (name === 'disabled') this._disabled = val !== null; + this._render(); + } + connectedCallback(){ this._render(); } + _parseDomains(){ + try { + const a = this.getAttribute('include-domains'); + if (!a) return []; + const v = typeof a === 'string' ? JSON.parse(a) : a; + return Array.isArray(v) ? v.map(x=>String(x)) : []; + } catch { return []; } + } + _render(){ + const label = this.getAttribute('label') || ''; + this._domains = this._parseDomains(); + const opts = []; + try { + const states = this._hass?.states || {}; + for (const id of Object.keys(states)){ + const dom = id.split('.')[0]; + if (this._domains.length && !this._domains.includes(dom)) continue; + const st = states[id]; + if (this._filter && !this._filter(st)) continue; + const name = st?.attributes?.friendly_name || (id.split('.')[1] || id); + opts.push({ id, name }); + } + } catch {} + opts.sort((a,b)=> a.name.localeCompare(b.name,'en',{sensitivity:'base'})); + const selOpts = [''].concat( + opts.map(o=>``) + ); + const css = ` + :host{ display:block; } + .wrap{ width:100%; min-width:250px; max-width:440px; } + .sel{ position: relative; } + select{ width:100%; height:36px; min-height:36px; appearance:none; -webkit-appearance:none; -moz-appearance:none; padding:6px 34px 6px 10px; border:1px solid var(--divider-color); border-radius:8px; background: var(--card-background-color); color: var(--primary-text-color); box-sizing: border-box; } + .backup-slot-select{ width:auto; min-width:260px; height:40px; min-height:40px; padding:8px 42px 8px 14px; font-size:14px; font-weight:500; border:1px solid var(--divider-color); border-radius:10px; background: var(--card-background-color); color: var(--primary-text-color); box-shadow: 0 1px 2px rgba(0,0,0,0.08) inset, 0 1px 4px rgba(0,0,0,0.12); transition: border-color 120ms ease, box-shadow 120ms ease; } + .backup-slot-select:focus{ outline:none; border-color: var(--primary-color); box-shadow: 0 0 0 1px color-mix(in srgb, var(--primary-color) 60%, transparent), 0 1px 4px rgba(0,0,0,0.14); } + select:focus{ outline:none; border-color: var(--primary-color); box-shadow: 0 0 0 2px color-mix(in oklab, var(--primary-color) 35%, transparent); } + select:hover{ border-color: color-mix(in oklab, var(--primary-text-color) 20%, var(--divider-color)); } + select:disabled{ opacity:.6; cursor: not-allowed; } + .dd-arrow{ position:absolute; right:12px; top:50%; transform: translateY(-50%); width:0; height:0; border-left:6px solid transparent; border-right:6px solid transparent; border-top:6px solid var(--secondary-text-color); pointer-events:none; } + select:disabled + .dd-arrow{ opacity:.6; } + .label{ font-size:.85rem; color: var(--secondary-text-color); margin: 2px 0 4px; display:none; } + `; + this.shadowRoot.innerHTML = `
    `; + const select = this.shadowRoot.querySelector('select'); + if (select) { + try { select.value = this._value || ''; } catch {} + try { select.disabled = !!this._disabled; } catch {} + // Signal picker open early (helps pause editor re-render on macOS in Safari/Chrome) + try { + select.addEventListener('pointerdown', () => { + this.dispatchEvent(new CustomEvent('picker-opened', { bubbles: true, composed: true })); + }, { passive: true }); + select.addEventListener('focus', () => { + this.dispatchEvent(new CustomEvent('picker-opened', { bubbles: true, composed: true })); + }, { passive: true }); + } catch {} + select.onchange = (e)=>{ + this._value = e.target.value || ''; + this.dispatchEvent(new CustomEvent('value-changed', { detail: { value: this._value }, bubbles: true, composed: true })); + // Signal picker closed after a selection is made + try { this.dispatchEvent(new CustomEvent('picker-closed', { bubbles: true, composed: true })); } catch {} + }; + // Also signal closed on blur (if user clicks away without changing) + try { + select.addEventListener('blur', ()=>{ + this.dispatchEvent(new CustomEvent('picker-closed', { bubbles: true, composed: true })); + }); + } catch {} + } + } +} +// Avoid hard failures if another resource already defined the picker. +try { + if (!customElements.get('tt-entity-picker')) { + customElements.define('tt-entity-picker', TTEntityPicker); + } +} catch {} + +/* ----------------- CONFIG EDITOR ----------------- */ +class ThermostatTimelineCardEditor extends HTMLElement { + setConfig(config) { + const hadUid = !!(config && typeof config === 'object' && String(config.instance_uid || '').trim()); + this._config = { ...ThermostatTimelineCard.getStubConfig(), ...(config||{}) }; + this._storageLoaded = false; + this._render(); + // If this card predates instance_uid, persist it immediately so local per-card + // storage keys remain stable across reloads. + if (!hadUid) { + try { this._emit(); } catch {} + } + } + // Helper to check if API is available + _apiSupported(){ try { return !!(this._hass?.callApi); } catch { return false; } } + + _normInstanceId(raw){ + try { + const s = String(raw ?? '').trim(); + if (!s) return 'default'; + const out = []; + for (const ch of s) { + const ok = (ch >= '0' && ch <= '9') || (ch >= 'A' && ch <= 'Z') || (ch >= 'a' && ch <= 'z') || ch === '_' || ch === '-' || ch === '.'; + out.push(ok ? ch : '_'); + } + const s2 = out.join('').slice(0, 64); + return s2 || 'default'; + } catch { return 'default'; } + } + + _instanceId(){ + try { + if (!this._config?.instance_enabled) return 'default'; + return this._normInstanceId(this._config?.instance_id || 'default'); + } catch { return 'default'; } + } + + _localStoreKey(){ + try { + const iid = this._instanceId(); + return (this._config?.instance_enabled) ? `thermostat_timeline_store:${iid}` : 'thermostat_timeline_store'; + } catch { return 'thermostat_timeline_store'; } + } + + async _apiFetchState(instanceId=null){ + if (!this._apiSupported()) return null; + try { + const iid = instanceId ? this._normInstanceId(instanceId) : this._instanceId(); + const q = (this._config?.instance_enabled && iid) ? `?instance_id=${encodeURIComponent(iid)}` : ''; + return await this._hass.callApi('GET', `thermostat_timeline/state${q}`); + } catch { return null; } + } + set hass(hass) { this._hass = hass; this._lang = ttGetLangFromHass(hass); + // On first hass set, merge settings from file storage via API + if (!this._storageLoaded && this._config?.storage_enabled) { + this._storageLoaded = true; + (async () => { + try { + const resp = await this._apiFetchState(); + const s = resp?.settings; + if (s && typeof s === 'object') { + if (typeof s.time_source === 'string') this._config.time_source = (s.time_source === 'ha' ? 'ha' : 'browser'); + if (typeof s.time_12h === 'boolean') this._config.time_12h = !!s.time_12h; + if (typeof s.temp_unit === 'string') this._config.temp_unit = (String(s.temp_unit).toUpperCase()==='F' ? 'F' : 'C'); + if (Number.isFinite(s.default_temp)) this._config.default_temp = Number(s.default_temp); + if (Number.isFinite(s.row_height)) this._config.row_height = Math.max(30, Math.min(200, Math.round(Number(s.row_height)))); + if (Number.isFinite(s.min_temp)) this._config.min_temp = Number(s.min_temp); + if (Number.isFinite(s.max_temp)) this._config.max_temp = Number(s.max_temp); + if (typeof s.auto_apply_enabled === 'boolean') this._config.auto_apply = !!s.auto_apply_enabled; + if (typeof s.profiles_enabled === 'boolean') this._config.profiles_enabled = !!s.profiles_enabled; + if (typeof s.weekdays_enabled === 'boolean') this._config.weekdays_enabled = !!s.weekdays_enabled; + if (typeof s.holidays_enabled === 'boolean') this._config.holidays_enabled = !!s.holidays_enabled; + if (typeof s.show_pause_button === 'boolean') this._config.show_pause_button = !!s.show_pause_button; + if (typeof s.pause_sensor_enabled === 'boolean') this._config.pause_sensor_enabled = !!s.pause_sensor_enabled; + if (typeof s.pause_sensor_entity === 'string') this._config.pause_sensor_entity = String(s.pause_sensor_entity || ''); + if (typeof s.apply_on_edit === 'boolean') this._config.apply_on_edit = !!s.apply_on_edit; + if (typeof s.apply_on_default_change === 'boolean') this._config.apply_on_default_change = !!s.apply_on_default_change; + if (typeof s.per_room_defaults === 'boolean') this._config.per_room_defaults = !!s.per_room_defaults; + if (typeof s.backup_auto_enabled === 'boolean') this._config.backup_auto_enabled = !!s.backup_auto_enabled; + if (Number.isFinite(s.backup_interval_days)) this._config.backup_interval_days = s.backup_interval_days; + if (s.open_window && typeof s.open_window === 'object') this._config.open_window = s.open_window; + } + // Re-render after merging settings + try { this._render(); } catch {} + } catch (err) {} + })(); + } + // Avoid clobbering inputs while user is typing (preserve previous behavior) + const ae = this.shadowRoot && this.shadowRoot.activeElement; + if (this._suspendRender) return; + if (ae) { + try { + if (ae.classList?.contains("title") || ae.classList?.contains('label-input') + || ae.classList?.contains('mi-from') || ae.classList?.contains('mi-to') || ae.classList?.contains('mi-temp') + || (ae.closest && ae.closest('.mini-ed'))) return; + if (ae.closest && (ae.closest('ha-entity-picker') || ae.closest('tt-entity-picker'))) return; + } catch {} + } + // Keep title preset in sync with language when user hasn't customized it + try { + if (this._config) { + const cur = String(this._config.title || ''); + if (!cur || this._isDefaultTitle(cur)) { + const next = ttLocalize('card.title_default', this._hass || this._lang || 'en'); + if (next && next !== cur) { this._config.title = next; this._emit(); } + } + } + } catch {} + // Full render and i18n apply (normal path) + this._render(); this._applyEditorI18n(); + // Ensure entity pickers are hydrated with hass after hard refresh + try { this._forcePickerRefresh && this._forcePickerRefresh(); } catch {} + // Also run a lightweight service-availability-only check in case services changed while editor open + try { + const root = this.shadowRoot; + if (root) { + const hasService = !!(this._hass && this._hass.services && this._hass.services['thermostat_timeline']); + if (!hasService) { + const warnText = this._t ? this._t('editor.store_missing') : null; + const msg = warnText && warnText !== 'editor.store_missing' ? warnText : 'Integration "Thermostat Pro Timeline Sync" is not installed. Shared storage is unavailable.'; + let warn = root.querySelector('.store-missing'); + if (!warn) { + warn = document.createElement('div'); warn.className = 'store-missing'; warn.style.color = 'var(--error-color)'; warn.style.fontSize = '.9rem'; warn.style.marginTop = '6px'; + const controls = root.querySelector('.store-controls'); if (controls && controls.parentNode) controls.parentNode.insertBefore(warn, controls); + } + while (warn.firstChild) warn.removeChild(warn.firstChild); + warn.appendChild(document.createTextNode(msg.replace(/\.$/, '') + '. ')); + (function(){ + let link = document.createElement('a'); + // Use my.home-assistant.io deep link to the HACS repository page + link.href = 'https://my.home-assistant.io/redirect/hacs_repository/?owner=qlerup&repository=thermostat-pro-timeline-sync'; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + link.style.color = 'var(--error-color)'; + link.style.textDecoration = 'underline'; + link.style.fontWeight = '500'; + link.textContent = this._t('editor.open_in_hacs'); + warn.appendChild(link); + })(); + const sw = root.querySelector('.store-enable'); if (sw) sw.disabled = true; + const picker = root.querySelector('.storage'); if (picker) picker.disabled = true; + } else { + const rootWarn = root.querySelector('.store-missing'); if (rootWarn && rootWarn.parentNode) rootWarn.parentNode.removeChild(rootWarn); + const sw = root.querySelector('.store-enable'); if (sw) sw.disabled = false; + const picker = root.querySelector('.storage'); if (picker) picker.disabled = false; + } + // Update integration status chip in tabs + try { + const chip = root.querySelector('.int-status'); + const icon = chip?.querySelector('ha-icon'); + const txt = chip?.querySelector('.text'); + if (chip && icon && txt) { + if (hasService) { + chip.classList.remove('bad'); chip.classList.add('ok'); + icon.setAttribute('icon','mdi:cloud-check-outline'); + txt.textContent = this._t ? this._t('editor.integration.connected') : 'Connected to integration'; + } else { + chip.classList.remove('ok'); chip.classList.add('bad'); + icon.setAttribute('icon','mdi:cloud-off-outline'); + txt.textContent = this._t ? this._t('editor.integration.disconnected') : 'Not connected to integration'; + } + // Click to jump to Sync tab + try { chip.style.userSelect='none'; chip.onclick = ()=>{ this._activeTab='sync'; this._render(); }; } catch {} + } + } catch {} + } + } catch {} + } + + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this._openCount = 0; + this._pendingEmit = false; + this._openRows = new Set(); + this._openColorRows = new Set(); + this._openOwdRows = new Set(); + this._suspendRender = false; + this._activeTab = 'settings'; + // Holidays inline editor state + this._holDraft = null; // { rooms: { [eid]: [ {id,startMin,endMin,temp} ] } } + this._holRoom = null; // active room eid + this._holEditing = null; // { room, blockId } + this._holMini = null; // { room, blk } + // Import state (Backup tab) + this._importStoreError = ''; + this._importStoreBusy = false; + this.shadowRoot.innerHTML = ` + +
    +
    +
    Titel
    + + + + + + +
    +
    +
    +
    +
    +
    Shared storage + background control
    +
    Saves schedules to file storage and enables background control.
    +
    + +
    + +
    + +
    + +
    +
    +
    +
    + +
    + + +
    +
    + + `; this._applyEditorI18n(); + + const qs = (s)=>this.shadowRoot.querySelector(s); + qs(".title")?.addEventListener("change", e => this._upd("title", e.target.value)); + // Note: storage_entity_* pickers removed - file-based storage is always used + qs('.store-enable')?.addEventListener('change', e => { + const on = !!e.target.checked; + this._upd('storage_enabled', on); + // Disable the bottom "Clear local only" button when shared storage is ON + try { + const clrLocal = this.shadowRoot.querySelector('.clear-local-only'); + if (clrLocal) { + clrLocal.disabled = on; + clrLocal.setAttribute('aria-disabled', on ? 'true' : 'false'); + clrLocal.title = on ? 'Disabled while shared storage is ON' : ''; + // Hide button entirely when shared storage is enabled (requested) + clrLocal.style.display = on ? 'none' : ''; + } + } catch {} + // Show/hide storage controls when shared storage is toggled + try { + const ctrls = this.shadowRoot.querySelector('.store-controls'); + if (ctrls) ctrls.style.display = on ? 'flex' : 'none'; + } catch {} + // Sync controls removed (instant-only via Save button) + this._applyEditorI18n(); + // Re-render so Colors tab shared-clear button visibility updates immediately + try { this._render(); } catch {} + // Ensure integration side knows current auto-apply state when enabling shared storage + try { if (on) this._pushSettingsToStoreDebounced(); } catch {} + // On enabling shared storage: offer to migrate local browser data to storage + (async ()=>{ + try { + if (on) { + const localRaw = localStorage.getItem(this._localStoreKey()); + const hasLocal = !!(localRaw && localRaw.length > 2); + if (hasLocal) { + const ok = confirm(this._t('editor.migrate_confirm')); + if (ok) { + // Properly unwrap the local payload and send both schedules and settings + let parsed = {}; + try { parsed = JSON.parse(localRaw || '{}'); } catch { parsed = {}; } + let schedules = parsed && typeof parsed === 'object' && parsed.schedules ? parsed.schedules : (parsed || {}); + let settings = parsed && typeof parsed === 'object' && parsed.settings ? parsed.settings : { time_12h: this._config.time_12h, temp_unit: this._config.temp_unit, entities: Array.isArray(this._config.entities) ? this._config.entities : [], room_use_input_number: Array.isArray(this._config.room_use_input_number) ? this._config.room_use_input_number : [], room_use_temp_sensor: Array.isArray(this._config.room_use_temp_sensor) ? this._config.room_use_temp_sensor : [], color_ranges: this._config.color_ranges, color_global: !!this._config.color_global, row_height: Number(this._config.row_height ?? 64), default_temp: Number(this._config.default_temp||20), min_temp: Number(this._config.min_temp ?? 5), max_temp: Number(this._config.max_temp ?? 25), away: this._config.away, merges: this._config.merges, labels: this._config.labels, temp_sensors: this._config.temp_sensors, per_room_defaults: !!(this._config.per_room_defaults ?? false), apply_on_default_change: !!(this._config.apply_on_default_change ?? true), holidays_enabled: !!this._config.holidays_enabled, holidays_source: this._config.holidays_source || 'calendar', holidays_entity: this._config.holidays_entity || '', holidays_dates: Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : [], sync_mode: (this._config.storage_sync_mode||'instant'), sync_delay_min: Number(Math.max(0, Math.round((this._config.storage_sync_sec||0)/60))), sync_delay_sec: Number(this._config.storage_sync_sec||0) }; + let colors = parsed && typeof parsed === 'object' && parsed.colors ? parsed.colors : { color_ranges: settings?.color_ranges || this._config.color_ranges, color_global: typeof settings?.color_global === 'boolean' ? settings.color_global : !!this._config.color_global }; + settings = { ...settings, entities: Array.isArray(settings?.entities) ? settings.entities : (Array.isArray(this._config.entities) ? this._config.entities : []), room_use_input_number: Array.isArray(settings?.room_use_input_number) ? settings.room_use_input_number : (Array.isArray(this._config.room_use_input_number) ? this._config.room_use_input_number : []), room_use_temp_sensor: Array.isArray(settings?.room_use_temp_sensor) ? settings.room_use_temp_sensor : (Array.isArray(this._config.room_use_temp_sensor) ? this._config.room_use_temp_sensor : []), row_height: Number(settings?.row_height ?? (this._config.row_height ?? 64)), min_temp: Number(settings?.min_temp ?? (this._config.min_temp ?? 5)), max_temp: Number(settings?.max_temp ?? (this._config.max_temp ?? 25)), apply_on_default_change: (typeof settings?.apply_on_default_change === 'boolean') ? settings.apply_on_default_change : !!(this._config.apply_on_default_change ?? true), show_pause_button: !!(this._config.show_pause_button ?? true), show_room_temp: !!(this._config.show_room_temp ?? true), pause_sensor_enabled: !!(this._config.pause_sensor_enabled ?? false), pause_sensor_entity: String(this._config.pause_sensor_entity || ''), auto_apply_enabled: !!(this._config.auto_apply ?? true), pause_indef: !!this._pauseIndef, pause_until_ms: Number(this._pauseUntilMs||0) }; + if (this._hass) { + // Use set_store service directly - file-based storage + const p = { schedules, settings, colors }; + if (this._config?.instance_enabled) { p.instance_id = this._instanceId(); p.activate = true; } + await this._hass.callService('thermostat_timeline', 'set_store', p); + } + } else { + // If user cancels, offer to import schedules FROM storage into local browser + try { + // Fetch from API instead of sensor states + const resp = await this._apiFetchState(); + if (resp && typeof resp === 'object') { + let sch = resp.schedules || {}; + let set = resp.settings || { time_12h: this._config.time_12h, temp_unit: this._config.temp_unit, entities: Array.isArray(this._config.entities) ? this._config.entities : [], room_use_input_number: Array.isArray(this._config.room_use_input_number) ? this._config.room_use_input_number : [], room_use_temp_sensor: Array.isArray(this._config.room_use_temp_sensor) ? this._config.room_use_temp_sensor : [], color_ranges: this._config.color_ranges, color_global: !!this._config.color_global, row_height: Number(this._config.row_height ?? 64), default_temp: Number(this._config.default_temp||20), min_temp: Number(this._config.min_temp ?? 5), max_temp: Number(this._config.max_temp ?? 25), away: this._config.away, merges: this._config.merges, labels: this._config.labels, temp_sensors: this._config.temp_sensors, per_room_defaults: !!(this._config.per_room_defaults ?? false), apply_on_default_change: !!(this._config.apply_on_default_change ?? true), holidays_enabled: !!this._config.holidays_enabled, holidays_source: this._config.holidays_source || 'calendar', holidays_entity: this._config.holidays_entity || '', holidays_dates: Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : [], sync_mode: (this._config.storage_sync_mode||'instant'), sync_delay_min: Number(Math.max(0, Math.round((this._config.storage_sync_sec||0)/60))), sync_delay_sec: Number(this._config.storage_sync_sec||0) }; + let colors = resp.colors || { color_ranges: set.color_ranges || this._config.color_ranges, color_global: !!set.color_global }; + if (Object.keys(sch || {}).length) { + const pull = confirm(this._t('editor.migrate_pull_confirm')); + if (pull) { + try { localStorage.setItem(this._localStoreKey(), JSON.stringify({ schedules: sch, settings: set, colors })); } catch {} + } + } + } + } catch {} + } + } + } + } catch {} + })(); + }); + // Sync mode controls removed (instant-only via Save button) + // (reload buttons removed) + // Holidays enable (migrated into in-card settings popup) + // (clear storage only button removed) + + // Clear all data (storage + local) + qs('.clear-all')?.addEventListener('click', async () => { + const msg = this._t('editor.clear_all_confirm'); + if (!confirm(msg)) return; + try { + try { await this._clearAllDataEverywhereConfirmed(); } catch {} + } catch (e) {} + }); + + // Clear local only (bottom) + qs('.clear-local-only')?.addEventListener('click', async (ev) => { + // Hard guard: do nothing if shared storage is enabled + if (this._config?.storage_enabled) { try { ev.preventDefault(); ev.stopPropagation(); } catch {} return; } + const msg = this._t('editor.clear_local_only_confirm'); + if (!confirm(msg)) return; + try { + try { localStorage.removeItem(this._localStoreKey()); } catch {} + // Clear in-memory schedules when using local storage only + this._schedules = {}; + this._profilesSelected = null; this._profilesDraft = null; this._profilesDirty = false; this._profilesEditingNew = false; + this._emit(true); + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + } catch {} + }); + // Reset onboarding (show intro again) + qs('.reset-onboard')?.addEventListener('click', async () => { + try { localStorage.removeItem('thermostat_timeline_onboard_seen_v1'); } catch {} + try { alert(this._t('onboard.reset_ok') || 'Welcome will be shown next time.'); } catch {} + }); + qs('.migrate-to-store')?.addEventListener('click', async (ev) => { + try { + // File-based storage only - no sensor entity IDs needed + const raw = localStorage.getItem(this._localStoreKey()) || ''; + if (!raw) { alert(this._t('editor.no_local_data')); return; } + const ok = confirm(this._t('editor.migrate_confirm')); + if (!ok) return; + const parsed = JSON.parse(raw || '{}'); + const schedules = parsed.schedules || parsed || {}; + const weekdays = parsed.weekdays || {}; + const profiles = parsed.profiles || {}; + let settings = parsed.settings || { time_12h: this._config.time_12h, temp_unit: this._config.temp_unit, color_ranges: this._config.color_ranges, color_global: !!this._config.color_global, row_height: Number(this._config.row_height ?? 64), default_temp: Number(this._config.default_temp||20), min_temp: Number(this._config.min_temp ?? 5), max_temp: Number(this._config.max_temp ?? 25), away: this._config.away, merges: this._config.merges, labels: this._config.labels, temp_sensors: this._config.temp_sensors, per_room_defaults: !!(this._config.per_room_defaults ?? false), apply_on_default_change: !!(this._config.apply_on_default_change ?? true), profiles_enabled: !!this._config.profiles_enabled, holidays_enabled: !!this._config.holidays_enabled, holidays_source: this._config.holidays_source || 'calendar', holidays_entity: this._config.holidays_entity || '', holidays_dates: Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : [] }; + let colors = parsed.colors || { color_ranges: settings?.color_ranges || this._config.color_ranges, color_global: typeof settings?.color_global === 'boolean' ? settings.color_global : !!this._config.color_global }; + settings = { ...settings, row_height: Number(settings?.row_height ?? (this._config.row_height ?? 64)), min_temp: Number(settings?.min_temp ?? (this._config.min_temp ?? 5)), max_temp: Number(settings?.max_temp ?? (this._config.max_temp ?? 25)), apply_on_default_change: (typeof settings?.apply_on_default_change === 'boolean') ? settings.apply_on_default_change : !!(this._config.apply_on_default_change ?? true), show_pause_button: !!(this._config.show_pause_button ?? true), pause_sensor_enabled: !!(this._config.pause_sensor_enabled ?? false), pause_sensor_entity: String(this._config.pause_sensor_entity || ''), auto_apply_enabled: !!(this._config.auto_apply ?? true), pause_indef: !!this._pauseIndef, pause_until_ms: Number(this._pauseUntilMs||0) }; + if (this._hass) { + // Use set_store service directly - file-based storage + const p = { schedules, weekdays, profiles, settings, colors }; + if (this._config?.instance_enabled) { p.instance_id = this._instanceId(); p.activate = true; } + await this._hass.callService('thermostat_timeline', 'set_store', p); + } + } catch (e) {} + }); + // Tabs behavior is handled via robust click delegation in _render() + } + + _roomUsesInputNumber(idx){ + try { + const ent = (this._config?.entities||[])[idx] || ''; + const inferred = String(ent).startsWith('input_number.'); + const arr = Array.isArray(this._config?.room_use_input_number) ? this._config.room_use_input_number : []; + const explicit = (typeof arr[idx] === 'boolean') ? arr[idx] : null; + return (explicit === null) ? inferred : explicit; + } catch { return false; } + } + + _setRoomUseInputNumber(idx, on){ + try { + const ents = [...(this._config.entities||[])]; + const modes = Array.isArray(this._config.room_use_input_number) ? [...this._config.room_use_input_number] : []; + while (modes.length < ents.length) modes.push(false); + modes[idx] = !!on; + + const oldEid = ents[idx]; + // If existing selection doesn't match new mode, clear and clean mappings + const mismatch = !!oldEid && ((!!on && !String(oldEid).startsWith('input_number.')) || (!on && !String(oldEid).startsWith('climate.'))); + if (mismatch) { + // Clean up labels/merges/temp_sensors for the old primary + try { + const labels = { ...(this._config.labels || {}) }; + if (oldEid && labels[oldEid]) delete labels[oldEid]; + this._config.labels = labels; + } catch {} + try { + const merges = { ...(this._config.merges || {}) }; + if (oldEid && merges[oldEid]) delete merges[oldEid]; + for (const k of Object.keys(merges)) { + merges[k] = (merges[k] || []).filter((e) => e !== oldEid); + if (!merges[k].length) delete merges[k]; + } + this._config.merges = merges; + } catch {} + try { + const sensors = { ...(this._config.temp_sensors || {}) }; + if (oldEid && sensors[oldEid]) delete sensors[oldEid]; + this._config.temp_sensors = sensors; + } catch {} + ents[idx] = ''; + } + + this._config.entities = ents; + this._config.room_use_input_number = modes; + this._emit(true); + try { this._pushSettingsToStoreDebounced(); } catch {} + } catch { this._emit(true); } + } + + // --- Local helpers for unit/time conversions (editor context) --- + _isF(){ try { return (this._config?.temp_unit||'C')==='F'; } catch { return false; } } + _unitSymbol(){ return this._isF() ? '°F' : '°C'; } + _toDisplayTemp(c){ + // Always round display values to avoid long decimals (e.g. whole °F becomes fractional °C internally). + try { + const v = Number(c); + if (!Number.isFinite(v)) return c; + const disp = this._isF() ? (v*9/5+32) : v; + return Math.round(disp * 10) / 10; + } catch { return c; } + } + _fromDisplayTemp(v){ try { return this._isF() ? ((Number(v)-32)*5/9) : Number(v); } catch { return Number(v); } } + + + + // Force re-hydration of entity pickers on the Rooms tab with the latest hass + // reference. This helps cases where the tab was hidden during first render + // or after a hard refresh and the picker didn't populate. + _forcePickerRefresh(){ + try { + const tab = this.shadowRoot && this.shadowRoot.querySelector('.tab-rooms'); + if (!tab) return; + const pickers = Array.from(tab.querySelectorAll('ha-entity-picker, tt-entity-picker')); + for (const p of pickers){ + try { + p.hass = this._hass; + p.style.display = 'block'; + p.style.minHeight = '48px'; + if (typeof p.requestUpdate === 'function') p.requestUpdate(); + // Fallback: if element still has no height, poke its internal input later + setTimeout(()=>{ try { if (p.offsetHeight < 8 && typeof p.requestUpdate === 'function') p.requestUpdate(); } catch {} }, 50); + } catch {} + } + } catch {} + } + + connectedCallback(){ + this._render(); + try { + // Extra-robust suspend on pointer start inside any entity picker (captures from Shadow DOM) + const capHandler = (e)=>{ + try { + const path = typeof e.composedPath === 'function' ? e.composedPath() : []; + const hit = path && path.some(n => { + const t = (n && n.tagName ? n.tagName.toLowerCase() : ''); + return t === 'ha-entity-picker' || t === 'tt-entity-picker'; + }); + if (hit) this._suspendRender = true; + } catch {} + }; + this.shadowRoot.addEventListener('pointerdown', capHandler, { capture: true }); + this.shadowRoot.addEventListener('touchstart', capHandler, { capture: true, passive: true }); + + // Suspend render while interacting with any entity picker to avoid closing dropdowns + this.shadowRoot.addEventListener('focusin', (e)=>{ + try { if (e.target?.closest && (e.target.closest('ha-entity-picker') || e.target.closest('tt-entity-picker'))) this._suspendRender = true; } catch {} + try { + // Also suspend while editing inputs in Colors tab (prevents HA hass updates from re-rendering and killing focus) + const t = e.target; + if (t && t.closest && t.closest('.tab-colors')) { + if (t.tagName === 'INPUT' || t.classList?.contains('remove-btn')) { + this._suspendRender = true; + } + } + } catch {} + }); + this.shadowRoot.addEventListener('focusout', ()=>{ + // Give a tiny delay so click into next field doesn't flicker + setTimeout(()=>{ this._suspendRender = false; }, 150); + }); + // React to explicit picker open/close signals (from tt-entity-picker) + this.shadowRoot.addEventListener('picker-opened', ()=>{ try { this._suspendRender = true; } catch {} }); + this.shadowRoot.addEventListener('picker-closed', ()=>{ try { this._suspendRender = false; } catch {} }); + this.shadowRoot.addEventListener('value-changed', (e)=>{ + try { + const tag = e.target?.tagName?.toLowerCase(); + if (tag === 'ha-entity-picker' || tag === 'tt-entity-picker') { this._suspendRender = false; } + } catch {} + }); + // After hard refresh, wait for HA to define pickers and hydrate them + try { + customElements.whenDefined('ha-entity-picker').then(()=>{ try { this._forcePickerRefresh(); } catch {} }); + } catch {} + // Extra retry shortly after mount + try { setTimeout(()=>{ try { this._forcePickerRefresh(); } catch {} }, 300); } catch {} + } catch {} + } + + // ---------- Storage helpers (editor context) ---------- + // Note: File-based storage is always used now - no sensor entities needed + // Stub function for backwards compatibility - always returns empty string + _storageEntity(kind = 'schedules') { return ''; } + + // Debounced push of settings (labels/merges/colors/min/max/away/unit/format) to shared storage + _pushSettingsToStoreDebounced(){ + clearTimeout(this._storePushTimer); + this._storePushTimer = setTimeout(()=>this._pushSettingsToStore(), 500); + } + async _pushSettingsToStore(){ + try { + if (!this._hass || !this._config?.storage_enabled) { return; } + // IMPORTANT: The backend replaces the entire settings object when provided. + // Base our write on the current stored settings to avoid wiping popup-managed keys. + const resp = await this._apiFetchState(); + const base = resp?.settings; + if (!base || typeof base !== 'object') return; + + let settings = { + ...base, + + // Editor-managed settings only (popup-managed keys are preserved from base) + away: this._config.away, + away_bypass: !!this._config.away_bypass, + open_window: (this._config.open_window && typeof this._config.open_window === 'object') ? this._config.open_window : (base.open_window || { enabled:false, sensors:{}, open_delay_min:2, close_delay_min:5 }), + + entities: Array.isArray(this._config.entities) ? this._config.entities.filter(Boolean).map(String) : [], + room_use_input_number: Array.isArray(this._config.room_use_input_number) ? this._config.room_use_input_number.slice() : [], + room_use_temp_sensor: Array.isArray(this._config.room_use_temp_sensor) ? this._config.room_use_temp_sensor.slice() : [], + + merges: this._config.merges, + labels: this._config.labels, + temp_sensors: this._config.temp_sensors, + turn_on: this._config.turn_on, + + backup_auto_enabled: !!this._config.backup_auto_enabled, + backup_interval_min: Number(this._config.backup_interval_min||1440), + + holidays_enabled: !!this._config.holidays_enabled, + holidays_source: this._config.holidays_source || base.holidays_source || 'calendar', + holidays_entity: this._config.holidays_entity || '', + holidays_dates: Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : (Array.isArray(base.holidays_dates) ? base.holidays_dates : []), + holidays_groups: Array.isArray(this._config.holidays_groups) ? this._config.holidays_groups : (Array.isArray(base.holidays_groups) ? base.holidays_groups : []), + + sync_mode: (this._config.storage_sync_mode||base.sync_mode||'instant'), + sync_delay_min: Number(this._config.storage_sync_min ?? base.sync_delay_min ?? 0), + }; + // Prevent accidental flip of holidays_enabled=true->false on load when nothing holiday-related changed + try { + const bHE = typeof base.holidays_enabled === 'boolean' ? base.holidays_enabled : undefined; + const iHE = typeof settings.holidays_enabled === 'boolean' ? settings.holidays_enabled : undefined; + if (bHE === true && iHE === false) { + const normArr = (a)=>{ try { return Array.from(new Set((a||[]).map(String))).sort(); } catch { return []; } }; + const eqDates = JSON.stringify(normArr(settings.holidays_dates)) === JSON.stringify(normArr(base.holidays_dates)); + const eqSource = String(settings.holidays_source||'') === String(base.holidays_source||''); + const eqEntity = String(settings.holidays_entity||'') === String(base.holidays_entity||''); + const eqGroups = JSON.stringify(settings.holidays_groups||[]) === JSON.stringify(base.holidays_groups||[]); + if (eqDates && eqSource && eqEntity && eqGroups) { + settings = { ...settings, holidays_enabled: true }; + } + } + } catch {} + // If user hasn't toggled holidays explicitly, keep backend value + try { + if (!this._holidaysToggleTouched && typeof base.holidays_enabled === 'boolean') { + settings = { ...settings, holidays_enabled: base.holidays_enabled }; + } + } catch {} + const colors = { + color_ranges: (this._config?.color_ranges ?? base?.color_ranges ?? {}), + color_global: (typeof this._config?.color_global === 'boolean') ? this._config.color_global : !!(base?.color_global), + }; + // When a dedicated colors sensor is configured, avoid duplicating colors inside settings payload + if (this._config?.storage_enabled && this._storageEntity('colors')) { + delete settings.color_ranges; + delete settings.color_global; + } else { + settings.color_ranges = colors.color_ranges; + settings.color_global = colors.color_global; + } + const p = { settings, colors }; + if (this._config?.instance_enabled) { p.instance_id = this._instanceId(); p.activate = true; } + await this._hass.callService('thermostat_timeline','set_store', p); + } catch (err) {} + } + + // Replace this editor element with a fresh instance (simulates closing/reopening) + _hardReloadEditor(){ + try { + const cfg = JSON.parse(JSON.stringify(this._config || {})); + const hass = this._hass; + const parent = this.parentNode || (this.getRootNode && this.getRootNode().host && this.getRootNode().host.parentNode) || null; + const next = this.nextSibling || null; + const fresh = document.createElement('thermostat-timeline-card-editor'); + try { fresh.setConfig(cfg); } catch {} + try { if (hass) fresh.hass = hass; } catch {} + if (parent) { + parent.insertBefore(fresh, next); + parent.removeChild(this); + } else { + // Fallback: force re-render + picker refresh + this._render(); + this._forcePickerRefresh && this._forcePickerRefresh(); + } + } catch (e) { + try { this._render(); this._forcePickerRefresh && this._forcePickerRefresh(); } catch {} + } + } + + // Full dashboard reload with cache-busting query param + _hardReloadPage(){ + try { + const url = new URL(window.location.href); + url.searchParams.set('ttcb', String(Date.now())); + window.location.replace(url.toString()); + } catch { + try { window.location.reload(); } catch {} + } + } + + _normalizeImportedStoreFile(obj){ + // Accept either raw data object ({schedules,...}) or full HA .storage wrapper ({key,data:{...}}). + const isObj = (v)=>!!v && typeof v === 'object' && !Array.isArray(v); + const root = isObj(obj) ? obj : null; + const data = (root && isObj(root.data)) ? root.data : root; + if (!isObj(data)) return null; + + const schedules = data.schedules; + if (!isObj(schedules)) return null; + + const out = { schedules }; + if (isObj(data.settings)) out.settings = data.settings; + if (isObj(data.weekdays)) out.weekdays = data.weekdays; + if (isObj(data.profiles)) out.profiles = data.profiles; + if (isObj(data.colors)) out.colors = data.colors; + return out; + } + + async _importThermostatTimelineJsonFile(file){ + const safeMsg = (e)=>{ try { return String(e?.message || e || ''); } catch { return 'Error'; } }; + this._importStoreError = ''; + if (!file) return; + + try { + if (!this._hass) throw new Error((this._t && (this._t('backup.import.err_no_hass') || 'Home Assistant not ready')) || 'Home Assistant not ready'); + if (!this._config?.storage_enabled) throw new Error((this._t && (this._t('backup.import.err_storage_off') || 'Shared storage is OFF. Turn it on first.')) || 'Shared storage is OFF. Turn it on first.'); + const hasService = !!(this._hass?.services && this._hass.services['thermostat_timeline']); + if (!hasService) throw new Error((this._t && (this._t('backup.import.err_no_integration') || 'Integration thermostat_timeline not available')) || 'Integration thermostat_timeline not available'); + + this._importStoreBusy = true; + try { this._render(); } catch {} + + let parsed; + try { + const text = await file.text(); + parsed = JSON.parse(text); + } catch { + throw new Error((this._t && (this._t('backup.import.err_json') || 'Invalid JSON')) || 'Invalid JSON'); + } + + const store = this._normalizeImportedStoreFile(parsed); + if (!store) throw new Error(this._t ? (this._t('backup.import.err_invalid') || 'Invalid file') : 'Invalid file'); + + // 1) Create a new backup slot of the current store + await this._hass.callService('thermostat_timeline', 'backup_now', {}); + // Give HA a moment to persist the backup + await new Promise((res)=>setTimeout(res, 600)); + + // 2) Overwrite store with imported data + const payload = { force: true, schedules: store.schedules }; + if (store.settings) payload.settings = store.settings; + if (store.weekdays) payload.weekdays = store.weekdays; + if (store.profiles) payload.profiles = store.profiles; + if (store.colors) payload.colors = store.colors; + if (this._config?.instance_enabled) { payload.instance_id = this._instanceId(); payload.activate = true; } + await this._hass.callService('thermostat_timeline', 'set_store', payload); + + // 3) Sync editor UI with imported settings (best-effort) + try { + const s = store.settings; + if (s && Array.isArray(s.entities)) { + const ents = s.entities.filter(Boolean).map(String); + this._config.entities = ents; + const rawModes = Array.isArray(s.room_use_input_number) ? s.room_use_input_number.map(v=>!!v) : []; + while (rawModes.length < ents.length) rawModes.push(false); + this._config.room_use_input_number = ents.map((eid, idx)=>{ + if (String(eid).startsWith('input_number.')) return true; + if (String(eid).startsWith('climate.')) return false; + return !!rawModes[idx]; + }); + this._emit(true); + } + } catch {} + + // 4) Update local browser cache and refresh open cards + try { + const st = await this._apiFetchState(); + if (st && typeof st === 'object') { + localStorage.setItem(this._localStoreKey(), JSON.stringify({ + schedules: st.schedules || {}, + weekdays: st.weekdays || {}, + profiles: st.profiles || {}, + settings: st.settings || {}, + colors: st.colors || {}, + })); + } + } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + + // Re-render backup tab so the new slot shows up + await new Promise((res)=>setTimeout(res, 600)); + try { this._render(); } catch {} + } catch (e) { + this._importStoreError = safeMsg(e); + try { this._render(); } catch {} + } finally { + this._importStoreBusy = false; + try { this._render(); } catch {} + } + } + + _render(){ + if(!this._hass || !this.shadowRoot || !this._config) return; + if(this._openCount > 0) return; + + // Boiler tab was migrated into the in-card settings popup + if (this._activeTab === 'boiler') this._activeTab = 'settings'; + + // Open Window Detection tab was migrated into the in-card settings popup + if (this._activeTab === 'owd') this._activeTab = 'settings'; + + // Colors tab was migrated into the in-card settings popup + if (this._activeTab === 'colors') this._activeTab = 'settings'; + + // Holidays tab was migrated into the in-card settings popup + if (this._activeTab === 'holidays') this._activeTab = 'settings'; + + // Away tab was migrated into the in-card settings popup + if (this._activeTab === 'away') this._activeTab = 'settings'; + + // Rooms tab was migrated into the in-card settings popup + if (this._activeTab === 'rooms') this._activeTab = 'settings'; + + // If HA hasn't registered core editor elements yet (after hard refresh), + // re-render once they are defined so pickers/textfields upgrade properly. + try { + if (!customElements.get('ha-entity-picker')) { + customElements.whenDefined('ha-entity-picker').then(()=>{ try { this._render(); } catch {} }); + } + if (!customElements.get('ha-textfield')) { + customElements.whenDefined('ha-textfield').then(()=>{ try { this._render(); } catch {} }); + } + } catch {} + + // Robust tab delegation (ensure any tab can be opened reliably) + if (!this._tabHandlersBound) { + this._tabHandlersBound = true; + try { + this.shadowRoot.addEventListener('click', (ev)=>{ + // Shadow DOM can yield a Text node as event.target; use composedPath() + // to find the actual button element. + const path = typeof ev.composedPath === 'function' ? ev.composedPath() : []; + let el = null; + for (const n of path) { + if (n && n.classList && n.classList.contains('tab-btn')) { el = n; break; } + } + if (!el) { + const t = (ev.target && ev.target.closest) ? ev.target : null; + el = t && t.closest ? t.closest('.tab-btn') : null; + } + if (!el) return; + if (el.classList.contains('tab-settings-btn')) { this._activeTab = 'settings'; this._render(); } + if (el.classList.contains('tab-backup-btn')) { this._activeTab = 'backup'; this._render(); } + }); + } catch {} + } + + const t = this.shadowRoot.querySelector(".title"); + // Note: storage entity pickers removed - file-based storage is always used + const seEn = this.shadowRoot.querySelector('.store-enable'); + const hol = this.shadowRoot.querySelector('.holidays-enable'); + if (t) t.value = this._config.title ?? ""; + if (seEn) { + const enabled = !!this._config.storage_enabled; + seEn.checked = enabled; + // Note: storage entity pickers removed - file-based storage is always used + // Ensure controls visibility matches toggle + try { + const ctrls = this.shadowRoot.querySelector('.store-controls'); + if (ctrls) ctrls.style.display = enabled ? 'flex' : 'none'; + } catch {} + // Sync controls removed (instant-only via Save button) + // Reflect state for bottom "Clear local only" button + try { + const clrLocal = this.shadowRoot.querySelector('.clear-local-only'); + if (clrLocal) { + clrLocal.disabled = enabled; + clrLocal.setAttribute('aria-disabled', enabled ? 'true' : 'false'); + clrLocal.title = enabled ? 'Disabled while shared storage is ON' : ''; + // Hide when shared storage is enabled + clrLocal.style.display = enabled ? 'none' : ''; + } + } catch {} + } + this._applyEditorI18n(); + if (hol){ hol.checked = !!this._config.holidays_enabled; } + // Note: storage entity pickers removed - file-based storage is always used + // Backup tab controls + try { + const bAuto = this.shadowRoot.querySelector('.backup-auto'); + const bMin = this.shadowRoot.querySelector('.backup-days'); + const bRow = this.shadowRoot.querySelector('.backup-interval-row'); + const bBtn = this.shadowRoot.querySelector('.btn-backup-now'); + const rBtn = this.shadowRoot.querySelector('.btn-restore-now'); + const dBtn = this.shadowRoot.querySelector('.btn-delete-backup'); + const impInput = this.shadowRoot.querySelector('.backup-import-file'); + const impBtn = this.shadowRoot.querySelector('.btn-import-store'); + const impErr = this.shadowRoot.querySelector('.backup-import-error'); + const slotSel = this.shadowRoot.querySelector('.backup-slot-select'); + const lastDesc = this.shadowRoot.querySelector('.last-backup-desc'); + if (bAuto) bAuto.checked = !!this._config.backup_auto_enabled; + if (bRow) bRow.style.display = (!!this._config.backup_auto_enabled) ? '' : 'none'; + if (bMin && bMin !== this.shadowRoot.activeElement) bMin.value = String(this._config.backup_interval_days || 1); + + // Import UI state + try { + const msg = String(this._importStoreError || ''); + if (impErr) { + impErr.textContent = msg; + impErr.style.display = msg ? '' : 'none'; + } + if (impBtn) impBtn.disabled = !!this._importStoreBusy; + if (impInput) impInput.disabled = !!this._importStoreBusy; + } catch {} + // No master toggle; all individual checkboxes remain enabled + const fmt = (iso)=>{ + try { + if(!iso) return ''; + const d=new Date(iso); + const lang = ttGetLangFromHass(this._hass || this._lang || 'en'); + const opts={ dateStyle:'medium', timeStyle:'short', hour12: !!this._config?.time_12h }; + if (this._config?.time_source==='ha' && this._hass?.config?.time_zone) opts.timeZone=this._hass.config.time_zone; + return new Intl.DateTimeFormat(lang, opts).format(d); + } catch { + return String(iso||''); + } + }; + // Fetch backup info from API + const fetchBackupInfo = async ()=>{ + try { + if (!this._apiSupported?.()) return null; + return await this._hass.callApi('GET', 'thermostat_timeline/state'); + } catch { return null; } + }; + (async ()=>{ + const api = await fetchBackupInfo(); + if (lastDesc && api){ + const ts = api?.backup?.last_backup_ts; + lastDesc.textContent = ts ? fmt(ts) : ''; + } + if (slotSel && api){ + const prevSel = slotSel.value; + const slots = Array.isArray(api?.backup?.slots) ? api.backup.slots : []; + const opts=[]; + slots.forEach((entry, i)=>{ + if(!entry) return; + const ts = entry.ts || entry.created_at || entry.created || null; + const labelTs = ts ? fmt(ts) : ''; + let label = labelTs || 'Backup'; + try { if (entry.imported) { const suf = (this._t && this._t('backup.imported_suffix')) || ' - imported'; label += suf; } } catch {} + opts.push({value:String(i+1), label}); + }); + slotSel.innerHTML=''; + const newOpt = document.createElement('option'); + newOpt.value = 'new'; + newOpt.textContent = (this._t && this._t('backup.new')) ? this._t('backup.new') : 'New backup'; + slotSel.append(newOpt); + opts.forEach((o)=>{ const opt=document.createElement('option'); opt.value=o.value; opt.textContent=o.label; slotSel.append(opt); }); + + // Restore previous selection if still valid, else default to first backup (if any) otherwise new + const validValues = new Set(['new', ...opts.map(o=>o.value)]); + let targetVal = 'new'; + if (prevSel && validValues.has(prevSel)) { + targetVal = prevSel; + } else if (opts.length > 0) { + targetVal = opts[0].value; + } + slotSel.value = targetVal; + const applyStates = ()=>{ + const val = slotSel.value; + const isNew = (val === 'new' || !val); + const setState = (btn, disabled)=>{ + if (!btn) return; + btn.disabled = !!disabled; + btn.style.opacity = disabled ? '0.55' : ''; + btn.style.pointerEvents = disabled ? 'none' : ''; + btn.style.filter = disabled ? 'grayscale(0.35)' : ''; + }; + setState(rBtn, isNew); + setState(dBtn, isNew); + setState(bBtn, !isNew); + }; + applyStates(); + slotSel.onchange = applyStates; + } + })(); + bBtn && (bBtn.onclick = async ()=>{ + try { + // Always full backup + await this._hass.callService('thermostat_timeline','backup_now', {}); + // Note: sensor update removed - file-based storage is used + // give HA a moment to persist + await new Promise(res=>setTimeout(res, 800)); + setTimeout(()=>{ try { this._render(); } catch{} }, 1200); + } catch {} + }); + rBtn && (rBtn.onclick = async ()=>{ + try { + const slot = Number(this.shadowRoot.querySelector('.backup-slot-select')?.value || '0') || undefined; + if (confirm((this._t && this._t('backup.restore.confirm_overwrite')) || 'Restore from backup? This will overwrite current schedules and settings.')) { + const payload = { mode: 'replace' }; + if (slot) payload.slot = slot; + await this._hass.callService('thermostat_timeline','restore_now', payload); + setTimeout(()=>{ try { this._render(); } catch{} }, 800); + } + } catch {} + }); + dBtn && (dBtn.onclick = async ()=>{ + try { + const slot = Number(this.shadowRoot.querySelector('.backup-slot-select')?.value || '0') || undefined; + if (!slot) return; + const msg = this._t ? this._t('backup.delete.confirm') : null; + if (!confirm(msg || 'Delete selected backup?')) return; + await this._hass.callService('thermostat_timeline','delete_backup', { slot }); + setTimeout(()=>{ try { this._render(); } catch{} }, 800); + } catch {} + }); + // Import from thermostat_timeline.json file + const runImport = async ()=>{ + try { + const file = impInput?.files?.[0]; + await this._importThermostatTimelineJsonFile(file); + } finally { + try { if (impInput) impInput.value = ''; } catch {} + } + }; + if (impBtn) impBtn.onclick = runImport; + if (impInput) impInput.onchange = runImport; + // Import full backup from file (thermostat_timeline_backup.json) + const impBkBtn = this.shadowRoot.querySelector('.btn-import-backup'); + const impBkInput = this.shadowRoot.querySelector('.backup-importbk-file'); + const impBkErr = this.shadowRoot.querySelector('.backup-importbk-error'); + const impBkRow = this.shadowRoot.querySelector('.backup-file-select-row'); + const impBkSel = this.shadowRoot.querySelector('.backup-file-slot-select'); + const impBkAddSel = this.shadowRoot.querySelector('.btn-import-slot'); + const impBkAddAll = this.shadowRoot.querySelector('.btn-import-all'); + const impBkStatus = this.shadowRoot.querySelector('.backup-importbk-status'); + const runImportBackup = async ()=>{ + try { + if (impBkErr) { impBkErr.style.display='none'; impBkErr.textContent=''; } + const file = impBkInput?.files?.[0]; + if (!file) return; + const text = await file.text(); + let obj = null; try { obj = JSON.parse(text); } catch { obj = null; } + if (!obj) throw new Error('json'); + let data = obj?.data || obj; + let slots = Array.isArray(data?.slots) ? data.slots : null; + if (!slots) { + const hasSections = (data && (data.schedules || data.settings || data.weekdays || data.profiles)); + if (hasSections) { + slots = [{ ts: new Date().toISOString(), sections: { schedules: data.schedules||{}, settings: data.settings||{}, weekdays: data.weekdays||{}, profiles: data.profiles||{}, colors: data.colors||{} }, partial_flags: data.partial_flags||null, imported: true }]; + } + } + if (!Array.isArray(slots) || !slots.length) throw new Error('invalid'); + // Keep in memory for user selection + this._pendingImportSlots = slots.map((s)=>{ try { return { ...s, imported: true }; } catch { return s; } }); + if (impBkSel){ + impBkSel.innerHTML = ''; + const labels = []; + for (let i=0;i{ + try { + const idx = Number(impBkSel?.value||'0')-1; + if (!this._pendingImportSlots || idx<0 || idx>=this._pendingImportSlots.length) return; + await this._hass.callService('thermostat_timeline','import_backup', { slots: [ this._pendingImportSlots[idx] ] }); + if (impBkStatus){ + try { + const t = (this._t && this._t('backup.file.imported_ok')) || 'Imported ({n})'; + impBkStatus.textContent = t.replace('{n}', '1'); + impBkStatus.style.display = ''; + } catch {} + } + setTimeout(()=>{ try { this._render(); } catch{} }, 800); + } catch {} + }; + if (impBkAddAll) impBkAddAll.onclick = async ()=>{ + try { + if (!Array.isArray(this._pendingImportSlots) || this._pendingImportSlots.length===0) return; + await this._hass.callService('thermostat_timeline','import_backup', { slots: this._pendingImportSlots }); + if (impBkStatus){ + try { + const n = this._pendingImportSlots.length; + const t = (this._t && this._t('backup.file.imported_ok')) || 'Imported ({n})'; + impBkStatus.textContent = t.replace('{n}', String(n)); + impBkStatus.style.display = ''; + } catch {} + } + setTimeout(()=>{ try { this._render(); } catch{} }, 800); + } catch {} + }; + // Export full backup (thermostat_timeline_backup.json) + const expBtn = this.shadowRoot.querySelector('.btn-export-backup'); + if (expBtn) expBtn.onclick = async ()=>{ + try { + const resp = await this._apiFetchState(); + const bk = (resp && resp.backup) ? resp.backup : {}; + const data = { + schedules: bk.schedules || {}, + settings: bk.settings || {}, + weekdays: bk.weekdays || {}, + profiles: bk.profiles || {}, + version: bk.version || 1, + last_backup_ts: bk.last_backup_ts || null, + partial_flags: bk.partial_flags || null, + slots: Array.isArray(bk.slots) ? bk.slots : [], + slot_index: Number(bk.slot_index || 0), + }; + const wrapper = { + version: 1, + minor_version: 1, + key: 'thermostat_timeline_backup.json', + data, + }; + const nameTs = (new Date()).toISOString().replace(/[:T]/g,'-').slice(0,19); + const fname = `thermostat_timeline_backup_${nameTs}.json`; + const blob = new Blob([JSON.stringify(wrapper, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; a.download = fname; document.body.appendChild(a); a.click(); + setTimeout(()=>{ try{ URL.revokeObjectURL(url); a.remove(); }catch{} }, 1500); + } catch (e) { + // best-effort; no UI error noise + } + }; + bAuto && bAuto.addEventListener('change', (e)=>{ const on=!!e.target.checked; this._upd('backup_auto_enabled', on); if (bRow) bRow.style.display = on? '' : 'none'; try { this._pushSettingsToStoreDebounced(); } catch {} }); + bMin && bMin.addEventListener('change', (e)=>{ let v=Math.round(Number(e.target.value||1)); if(!Number.isFinite(v)) v=1; v=Math.max(1, Math.min(365, v)); e.target.value=String(v); this._upd('backup_interval_days', v); try { this._pushSettingsToStoreDebounced(); } catch {} }); + } catch {} + // No mode dropdown in editor + + const wrap = this.shadowRoot.querySelector(".entities"); + const want = (this._config.entities || []).length; + if (wrap) { + // Rebuild rows; preserve which are open using a stable key per row (entity id or placeholder) + wrap.innerHTML = ''; + for (let i=0;i { + try { + const id = st?.entity_id || st?.entityId || st?.entityID || ""; + if (!id) return false; + const chosenPrim = (this._config.entities || []).filter(Boolean); + const allLinked = Object.values(this._config.merges || {}).flat(); + const blocked = new Set([...chosenPrim, ...allLinked]); + const current = this._config.entities[i]; + return !blocked.has(id) || id === current; + } catch { return true; } + }; + } + + // Update header title and label input to reflect current selection/override + try { + const eid = this._config.entities[i] || ""; + const labels = this._config.labels || {}; + const titleEl = row.querySelector('.summary-title'); + const subEl = row.querySelector('.summary-sub'); + const nameInp = row.querySelector('.label-input'); + const friendly = eid ? (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid)) : ''; + const shown = labels[eid] || friendly || this._t('editor.entity_placeholder'); + if (titleEl) titleEl.textContent = shown; + if (nameInp && nameInp !== this.shadowRoot.activeElement) nameInp.value = labels[eid] || ''; + // Build merged subtitle text + if (subEl) { + const linked = (this._config.merges?.[eid] || []).map(id => ( + this._hass?.states?.[id]?.attributes?.friendly_name || (id.split('.')[1] || id) + )); + const parts = []; + if (eid) parts.push(eid); + if (linked.length) parts.push(`${this._t('editor.merged_with')}: ${linked.join(', ')}`); + subEl.textContent = parts.join(' • '); + } + } catch {} + + // Rebuild merge chips every render + try { + const eid = this._config.entities[i] || ''; + const chips = row.querySelector('.linked-chips'); + const mergePicker = row.querySelector('.merge-picker'); + if (mergePicker) mergePicker.hass = this._hass; + if (chips && eid){ + chips.innerHTML = ''; + const links = (this._config.merges?.[eid] || []); + for (const l of links){ + const chip = document.createElement('span'); + chip.className = 'pill-chip'; + const txt = document.createElement('span'); + txt.textContent = this._hass?.states?.[l]?.attributes?.friendly_name || (l.split('.')[1]||l); + const rm = document.createElement('button'); + rm.type = 'button'; rm.className = 'rm'; rm.textContent = '×'; + rm.setAttribute('aria-label', this._t('editor.remove')); + rm.addEventListener('click', ()=>{ + const merges = { ...(this._config.merges || {}) }; + merges[eid] = (merges[eid]||[]).filter(x=>x!==l); + if (!merges[eid]?.length) delete merges[eid]; + this._config.merges = merges; + this._emit(true); + }); + chip.append(txt, rm); + chips.append(chip); + } + } + + // (colors tab content and visibility now rendered once outside the entity loop) + } catch {} + } + } + + // Build colors tab content (always render once, even with zero rooms) + // Build colors tab content (always render once, even with zero rooms) + try { + const root = this.shadowRoot.querySelector('.tab-colors'); + if (root) { + root.innerHTML = ''; + + // Colors mode toggle (Per room | All rooms) + const modeWrap = document.createElement('div'); + modeWrap.style.display = 'flex'; + modeWrap.style.alignItems = 'center'; + modeWrap.style.justifyContent = 'space-between'; + modeWrap.style.gap = '10px'; + modeWrap.style.marginBottom = '6px'; + const modeText = document.createElement('div'); + modeText.style.display = 'grid'; + modeText.style.gap = '2px'; + const modeTitle = document.createElement('div'); modeTitle.className = 'colors-mode-title'; modeTitle.style.fontWeight = '600'; modeTitle.textContent = this._t('editor.colors.mode') || 'Color mode'; + const modeDesc = document.createElement('div'); modeDesc.className = 'colors-mode-desc'; modeDesc.style.fontSize = '.85rem'; modeDesc.style.color = 'var(--secondary-text-color)'; modeDesc.textContent = this._t('editor.colors.mode.desc') || 'Choose per-room colors or one set for all rooms.'; + modeText.append(modeTitle, modeDesc); + const seg = document.createElement('div'); seg.className = 'seg colors-mode'; + const bRoom = document.createElement('button'); bRoom.type = 'button'; bRoom.className = 'mode-room'; bRoom.textContent = this._t('editor.colors.mode_per_room') || 'Per room'; + const bGlob = document.createElement('button'); bGlob.type = 'button'; bGlob.className = 'mode-global'; bGlob.textContent = this._t('editor.colors.mode_global') || 'All rooms'; + const applySegUi = ()=>{ const on = !!this._config.color_global; bGlob.classList.toggle('active', on); bRoom.classList.toggle('active', !on); }; + bRoom.onclick = ()=>{ this._config.color_global = false; this._emit(true); this._pushSettingsToStoreDebounced(); }; + bGlob.onclick = ()=>{ this._config.color_global = true; this._emit(true); this._pushSettingsToStoreDebounced(); }; + seg.append(bRoom, bGlob); + applySegUi(); + modeWrap.append(modeText, seg); + root.append(modeWrap); + + const isGlobal = !!this._config.color_global; + + // If per-room mode and there are no rooms yet, show helper box + if (!isGlobal && want === 0) { + const box = document.createElement('div'); box.className = 'empty-box'; + const p = document.createElement('div'); p.textContent = this._t('editor.colors.empty') || 'No rooms added yet.'; + const btn = document.createElement('button'); btn.type = 'button'; btn.className = 'add-entity-btn'; btn.innerHTML = '' + (this._t('editor.colors.go_add') || 'Add room') + ''; + btn.onclick = () => { + this._activeTab = 'rooms'; this._render(); + const add = this.shadowRoot.querySelector('.add-room-btn'); + if (add) { add.classList.add('pulse-attn','pulse'); setTimeout(() => add.classList.remove('pulse'), 3500); add.scrollIntoView({ behavior:'smooth', block:'center' }); } + }; + box.append(p, btn); root.append(box); + } + + // --- FIXED: GLOBAL EDITOR --- + if (isGlobal) { + const eid = '*'; + const line = document.createElement('div'); line.className = 'inline'; + const summary = document.createElement('div'); summary.className = 'summary'; + const sumLeft = document.createElement('div'); sumLeft.className = 'summary-left'; + const exp = document.createElement('button'); exp.className = 'expander'; exp.innerHTML = ''; + const txtWrap = document.createElement('div'); txtWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); titleSpan.className = 'summary-title'; titleSpan.textContent = this._t('editor.colors.global_title') || 'All rooms'; + txtWrap.append(titleSpan); sumLeft.append(exp, txtWrap); summary.append(sumLeft); + line.append(summary); + const details = document.createElement('div'); details.className = 'details'; line.append(details); + const colorsWrap = document.createElement('div'); colorsWrap.style.display = 'grid'; colorsWrap.style.gap = '0px'; details.append(colorsWrap); + const head = document.createElement('div'); head.className = 'colors-head'; + head.innerHTML = `${this._t('editor.colors.col_from')}${this._t('editor.colors.col_to')}${this._t('editor.colors.col_color')}`; + try { if (this._openColorRows.has(eid)) line.classList.add('open'); } catch {} + const renderRanges = () => { + colorsWrap.innerHTML = ''; + // Get current list (treat as read-only) + const ranges = (this._config.color_ranges?.[eid]) || []; + if (ranges.length > 0) colorsWrap.append(head); + ranges.forEach((r, ri) => { + const outer = document.createElement('div'); outer.className = 'color-item'; + const row = document.createElement('div'); row.className = 'colors-row'; + const f = document.createElement('input'); f.type = 'number'; f.step = '0.5'; f.value = String(this._toDisplayTemp(r.from ?? 0)); f.setAttribute('aria-label', this._t('editor.colors.col_from')); f.title = this._t('editor.colors.col_from'); + const t = document.createElement('input'); t.type = 'number'; t.step = '0.5'; t.value = String(this._toDisplayTemp(r.to ?? 0)); t.setAttribute('aria-label', this._t('editor.colors.col_to')); t.title = this._t('editor.colors.col_to'); + const c = document.createElement('input'); c.type = 'color'; c.value = r.color || '#ffb347'; c.setAttribute('aria-label', this._t('editor.colors.col_color')); c.title = this._t('editor.colors.col_color'); + const maxDisp = this._toDisplayTemp(this._config.max_temp ?? 25); + const minDisp = this._toDisplayTemp(this._config.min_temp ?? 5); + f.min = String(minDisp); t.min = String(minDisp); + f.max = String(maxDisp); t.max = String(maxDisp); + const rm = document.createElement('button'); rm.type = 'button'; rm.className = 'remove-btn'; rm.innerHTML = '' + (this._t('editor.remove') || 'Remove') + ''; + + const repaint = ()=>{ this._emit(true); try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} try { document.querySelectorAll('thermostat-timeline-card').forEach(el=>el?.devRefreshFromEditor?.()); } catch {} }; + + // --- SAFE IMMUTABLE UPDATES --- + f.onchange = () => { + let v=parseFloat(f.value); if (isNaN(v)) v=minDisp; v=Math.max(minDisp, Math.min(maxDisp, v)); f.value=String(v); + // Copy array + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + // Copy object and update + if(copied_config[ri]) copied_config[ri] = { ...copied_config[ri], from: this._fromDisplayTemp(v) }; + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + repaint(); + }; + t.onchange = () => { + let v=parseFloat(t.value); if (isNaN(v)) v=minDisp; v=Math.max(minDisp, Math.min(maxDisp, v)); t.value=String(v); + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + if(copied_config[ri]) copied_config[ri] = { ...copied_config[ri], to: this._fromDisplayTemp(v) }; + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + repaint(); + }; + c.onchange = () => { + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + if(copied_config[ri]) copied_config[ri] = { ...copied_config[ri], color: c.value }; + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + repaint(); + }; + rm.onclick = () => { + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + copied_config.splice(ri, 1); + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + this._emit(true); renderRanges(); + }; + // ----------------------------- + + row.append(f, t, c, rm); outer.append(row); colorsWrap.append(outer); + }); + const add = document.createElement('button'); add.className = 'add-entity-btn'; add.type = 'button'; add.innerHTML = '' + (this._t('editor.heat_colors.add') || 'Add color range') + ''; + add.onclick = () => { const arr = [...(this._config.color_ranges?.[eid] || [])]; arr.push({ from: this._config.default_temp || 20, to: this._config.default_temp || 20, color: '#ffb347' }); this._config.color_ranges = { ...(this._config.color_ranges || {}), [eid]: arr }; this._emit(true); renderRanges(); }; + colorsWrap.append(add); + }; + renderRanges(); + exp.onclick = () => { line.classList.toggle('open'); const key = eid; if (line.classList.contains('open')) this._openColorRows.add(key); else this._openColorRows.delete(key); }; + root.append(line); + } + + // --- FIXED: PER-ROOM EDITOR --- + for (let i = 0; !isGlobal && i < want; i++) { + const eid = this._config.entities[i]; if (!eid) continue; + const line = document.createElement('div'); line.className = 'inline'; + const summary = document.createElement('div'); summary.className = 'summary'; + const sumLeft = document.createElement('div'); sumLeft.className = 'summary-left'; + const exp = document.createElement('button'); exp.className = 'expander'; exp.innerHTML = ''; + const txtWrap = document.createElement('div'); txtWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); titleSpan.className = 'summary-title'; + try { + const labels = this._config.labels || {}; + const display = labels[eid] || this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid); + titleSpan.textContent = display; + } catch { titleSpan.textContent = this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1] || eid); } + txtWrap.append(titleSpan); sumLeft.append(exp, txtWrap); summary.append(sumLeft); + line.append(summary); + const details = document.createElement('div'); details.className = 'details'; line.append(details); + const colorsWrap = document.createElement('div'); colorsWrap.style.display = 'grid'; colorsWrap.style.gap = '0px'; details.append(colorsWrap); + const head = document.createElement('div'); head.className = 'colors-head'; + head.innerHTML = `${this._t('editor.colors.col_from')}${this._t('editor.colors.col_to')}${this._t('editor.colors.col_color')}`; + try { if (this._openColorRows.has(eid)) line.classList.add('open'); } catch {} + const renderRanges = () => { + colorsWrap.innerHTML = ''; + // Get current list (treat as read-only) + const ranges = (this._config.color_ranges?.[eid]) || []; + if (ranges.length > 0) colorsWrap.append(head); + ranges.forEach((r, ri) => { + const outer = document.createElement('div'); outer.className = 'color-item'; + const row = document.createElement('div'); row.className = 'colors-row'; + const f = document.createElement('input'); f.type = 'number'; f.step = '0.5'; f.value = String(this._toDisplayTemp(r.from ?? 0)); f.setAttribute('aria-label', this._t('editor.colors.col_from')); f.title = this._t('editor.colors.col_from'); + const t = document.createElement('input'); t.type = 'number'; t.step = '0.5'; t.value = String(this._toDisplayTemp(r.to ?? 0)); t.setAttribute('aria-label', this._t('editor.colors.col_to')); t.title = this._t('editor.colors.col_to'); + const c = document.createElement('input'); c.type = 'color'; c.value = r.color || '#ffb347'; c.setAttribute('aria-label', this._t('editor.colors.col_color')); c.title = this._t('editor.colors.col_color'); + const maxDisp = this._toDisplayTemp(this._config.max_temp ?? 25); + const minDisp = this._toDisplayTemp(this._config.min_temp ?? 5); + f.min = String(minDisp); t.min = String(minDisp); + f.max = String(maxDisp); t.max = String(maxDisp); + const rm = document.createElement('button'); rm.type = 'button'; rm.className = 'remove-btn'; rm.innerHTML = '' + (this._t('editor.remove') || 'Remove') + ''; + + const repaint = ()=>{ + this._emit(true); + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + try { document.querySelectorAll('thermostat-timeline-card').forEach(el=>el?.devRefreshFromEditor?.()); } catch {} + }; + + // --- SAFE IMMUTABLE UPDATES --- + f.onchange = () => { + let v=parseFloat(f.value); if (isNaN(v)) v=minDisp; v=Math.max(minDisp, Math.min(maxDisp, v)); f.value=String(v); + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + if(copied_config[ri]) copied_config[ri] = { ...copied_config[ri], from: this._fromDisplayTemp(v) }; + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + repaint(); + }; + t.onchange = () => { + let v=parseFloat(t.value); if (isNaN(v)) v=minDisp; v=Math.max(minDisp, Math.min(maxDisp, v)); t.value=String(v); + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + if(copied_config[ri]) copied_config[ri] = { ...copied_config[ri], to: this._fromDisplayTemp(v) }; + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + repaint(); + }; + c.onchange = () => { + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + if(copied_config[ri]) copied_config[ri] = { ...copied_config[ri], color: c.value }; + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + repaint(); + }; + rm.onclick = () => { + const copied_config = [...(this._config.color_ranges?.[eid] || [])]; + copied_config.splice(ri, 1); + this._config.color_ranges = { ...(this._config.color_ranges||{}), [eid]: copied_config }; + this._emit(true); renderRanges(); + }; + // ----------------------------- + + row.append(f, t, c, rm); outer.append(row); colorsWrap.append(outer); + }); + const add = document.createElement('button'); add.className = 'add-entity-btn'; add.type = 'button'; add.innerHTML = '' + (this._t('editor.heat_colors.add') || 'Add color range') + ''; + add.onclick = () => { const arr = [...(this._config.color_ranges?.[eid] || [])]; arr.push({ from: this._config.default_temp || 20, to: this._config.default_temp || 20, color: '#ffb347' }); this._config.color_ranges = { ...(this._config.color_ranges || {}), [eid]: arr }; this._emit(true); renderRanges(); }; + colorsWrap.append(add); + }; + renderRanges(); + exp.onclick = () => { + line.classList.toggle('open'); + const key = eid; + if (line.classList.contains('open')) this._openColorRows.add(key); + else this._openColorRows.delete(key); + }; + root.append(line); + } + + // Add explicit refresh/apply button for colors + const refresh = document.createElement('button'); + refresh.className = 'add-entity-btn'; + refresh.type = 'button'; + refresh.innerHTML = '' + (this._t('editor.colors.refresh') || 'Update schedule') + ''; + refresh.onclick = async () => { + // Persist color settings to local/shared storage so live card instances pick them up + try { + let settings = { time_12h: this._config.time_12h, temp_unit: this._config.temp_unit, time_source: this._config.time_source, row_height: Number(this._config.row_height ?? 64), default_temp: Number(this._config.default_temp || 20), min_temp: Number(this._config.min_temp ?? 5), max_temp: Number(this._config.max_temp ?? 25), per_room_defaults: !!(this._config.per_room_defaults ?? false), apply_on_edit: !!(this._config.apply_on_edit ?? true), apply_on_default_change: !!(this._config.apply_on_default_change ?? true), color_ranges: this._config.color_ranges, color_global: !!this._config.color_global, away: this._config.away, merges: this._config.merges, labels: this._config.labels, temp_sensors: this._config.temp_sensors, profiles_enabled: !!this._config.profiles_enabled, holidays_enabled: !!this._config.holidays_enabled, holidays_source: this._config.holidays_source || 'calendar', holidays_entity: this._config.holidays_entity || '', holidays_dates: Array.isArray(this._config.holidays_dates) ? this._config.holidays_dates : [], sync_mode: (this._config.storage_sync_mode||'instant'), sync_delay_min: Number(this._config.storage_sync_min||0) }; + try { + if (!this._holidaysToggleTouched) { + const st = await this._apiFetchState?.(); + const base = st?.settings; + if (typeof base?.holidays_enabled === 'boolean') settings.holidays_enabled = base.holidays_enabled; else delete settings.holidays_enabled; + } + } catch {} + const colors = { color_ranges: this._config.color_ranges, color_global: !!this._config.color_global }; + let schedules = {}; + // Prefer current file storage schedules when enabled - use API + try { + if (this._config?.storage_enabled && this._apiSupported?.()) { + const resp = await this._apiFetchState?.(); + if (resp && typeof resp === 'object' && resp.schedules) { + schedules = JSON.parse(JSON.stringify(resp.schedules)); + } + } + } catch {} + // Fallback to localStorage copy + if (!schedules || Object.keys(schedules).length===0){ + try { + const raw = localStorage.getItem(this._localStoreKey()) || ''; + const parsed = JSON.parse(raw||'{}'); + schedules = parsed.schedules || {}; + // Unwrap if accidentally saved nested + if (schedules && schedules.schedules && typeof schedules.schedules === 'object') schedules = schedules.schedules; + } catch {} + } + // Write local copy + try { localStorage.setItem(this._localStoreKey(), JSON.stringify({ schedules, settings: { ...settings, show_pause_button: !!(this._config.show_pause_button ?? true), show_room_temp: !!(this._config.show_room_temp ?? true), pause_sensor_enabled: !!(this._config.pause_sensor_enabled ?? false), pause_sensor_entity: String(this._config.pause_sensor_entity || ''), auto_apply_enabled: !!(this._config.auto_apply ?? true), pause_indef: !!this._pauseIndef, pause_until_ms: Number(this._pauseUntilMs||0) }, colors })); } catch {} + // And update shared file storage if available + try { + if (this._config.storage_enabled) { + const setPayload = { ...settings, show_pause_button: !!(this._config.show_pause_button ?? true), show_room_temp: !!(this._config.show_room_temp ?? true), pause_sensor_enabled: !!(this._config.pause_sensor_enabled ?? false), pause_sensor_entity: String(this._config.pause_sensor_entity || ''), auto_apply_enabled: !!(this._config.auto_apply ?? true), pause_indef: !!this._pauseIndef, pause_until_ms: Number(this._pauseUntilMs||0) }; + const p = { schedules, settings: setPayload, colors }; + if (this._config?.instance_enabled) { p.instance_id = this._instanceId(); p.activate = true; } + await this._hass.callService('thermostat_timeline','set_store', p); + } + } catch {} + } catch {} + // Emit config change and force preview card re-render + this._emit(true); + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + try { document.querySelectorAll('thermostat-timeline-card').forEach(el=>el?.devRefreshFromEditor?.()); } catch {} + }; + // Append buttons stacked (each on its own line) and aligned left + // Update schedule button + root.append(refresh); + + // Single button: clear all color data (local + shared if available) + const clrAll = document.createElement('button'); + clrAll.className = 'remove-btn clr-colors-all-btn'; + clrAll.type = 'button'; + try { clrAll.style.width = 'max-content'; clrAll.style.justifySelf = 'start'; } catch {} + clrAll.innerHTML = '' + (this._t('editor.colors.clear_all') || 'Clear all color data') + ''; + clrAll.onclick = async () => { + const ok = confirm(this._t('editor.colors.clear_all_confirm') || 'This will delete all color ranges from both local and shared storage. Continue?'); + if (!ok) return; + try { + // 1) Clear in-memory config + this._config.color_ranges = {}; + // 2) Clear localStorage copy + try { + const raw = localStorage.getItem(this._localStoreKey()) || ''; + let schedules = {}; + let settings = { time_12h: this._config.time_12h, temp_unit: this._config.temp_unit, time_source: this._config.time_source, row_height: Number(this._config.row_height ?? 64), default_temp: Number(this._config.default_temp || 20), color_ranges: {}, color_global: !!this._config.color_global, min_temp: Number(this._config.min_temp ?? 5), max_temp: Number(this._config.max_temp ?? 25), per_room_defaults: !!(this._config.per_room_defaults ?? false), apply_on_edit: !!(this._config.apply_on_edit ?? true), apply_on_default_change: !!(this._config.apply_on_default_change ?? true), away: this._config.away, merges: this._config.merges, labels: this._config.labels, temp_sensors: this._config.temp_sensors, sync_mode: (this._config.storage_sync_mode||'instant'), sync_delay_min: Number(this._config.storage_sync_min||0) }; + let colors = { color_ranges: {}, color_global: !!this._config.color_global }; + if (raw) { + try { + const parsed = JSON.parse(raw || '{}'); + schedules = parsed.schedules || {}; + const prev = parsed.settings || {}; + settings = { ...prev, color_ranges: {} }; + colors = parsed.colors || colors; + } catch {} + } + localStorage.setItem(this._localStoreKey(), JSON.stringify({ schedules, settings, colors })); + } catch {} + // 3) Clear shared file storage if enabled + try { + if (this._hass && this._config?.storage_enabled) { + let schedules = {}; + let settings = { time_12h: this._config.time_12h, temp_unit: this._config.temp_unit, time_source: this._config.time_source, row_height: Number(this._config.row_height ?? 64), default_temp: Number(this._config.default_temp || 20), color_ranges: {}, color_global: !!this._config.color_global, min_temp: Number(this._config.min_temp ?? 5), max_temp: Number(this._config.max_temp ?? 25), per_room_defaults: !!(this._config.per_room_defaults ?? false), apply_on_edit: !!(this._config.apply_on_edit ?? true), apply_on_default_change: !!(this._config.apply_on_default_change ?? true), away: this._config.away, merges: this._config.merges, labels: this._config.labels, temp_sensors: this._config.temp_sensors, sync_mode: (this._config.storage_sync_mode||'instant'), sync_delay_min: Number(this._config.storage_sync_min||0) }; + let colors = { color_ranges: {}, color_global: !!this._config.color_global }; + // Fetch current schedules from API to preserve them while clearing colors + try { + const resp = await this._apiFetchState?.(); + if (resp && typeof resp === 'object') { + if (resp.schedules) schedules = JSON.parse(JSON.stringify(resp.schedules)); + if (resp.settings) settings = { ...resp.settings, color_ranges: {} }; + if (resp.colors) colors = { ...resp.colors, color_ranges: {} }; + } + } catch {} + const p = { schedules, settings, colors }; + if (this._config?.instance_enabled) { p.instance_id = this._instanceId(); p.activate = true; } + await this._hass.callService('thermostat_timeline', 'set_store', p); + } + } catch (e) {} + } catch {} + // Repaint editor + live cards + this._emit(true); + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + try { document.querySelectorAll('thermostat-timeline-card').forEach(el=>el?.devRefreshFromEditor?.()); } catch {} + }; + root.append(clrAll); + } + } catch {} + + // Build Holidays tab content + try { + const root = this.shadowRoot.querySelector('.tab-holidays'); + if (root) { + root.innerHTML = ''; + // Toggle enable inside Holidays tab + const enableCard = document.createElement('div'); enableCard.className = 'settings-card'; + const enRow = document.createElement('div'); enRow.className='setting'; + const enText = document.createElement('div'); enText.className='text'; + const enTitle = document.createElement('div'); enTitle.className='title'; enTitle.textContent = this._t('holidays.enable'); + const enDesc = document.createElement('div'); enDesc.className='desc'; enDesc.textContent = this._t('holidays.desc'); + enText.append(enTitle, enDesc); + const enSwitch = document.createElement('ha-switch'); enSwitch.className = 'holidays-enable'; enSwitch.checked = !!this._config.holidays_enabled; + enSwitch.addEventListener('change', (e)=>{ const on = !!e.target.checked; this._upd('holidays_enabled', on); this._holidaysToggleTouched = true; try { contentWrap.style.display = on ? '' : 'none'; } catch {} try { this._pushSettingsToStoreDebounced(); } catch {} }); + enRow.append(enText, enSwitch); enableCard.append(enRow); root.append(enableCard); + // Container for remaining content (shown only when enabled) + const contentWrap = document.createElement('div'); contentWrap.className='holidays-content'; contentWrap.style.display = this._config.holidays_enabled ? '' : 'none'; root.append(contentWrap); + // Enable hint if no rooms + if ((this._config.entities||[]).length === 0) { + const box = document.createElement('div'); box.className='empty-box'; + const p = document.createElement('div'); p.textContent = this._t('editor.select_entities'); + const btn = document.createElement('button'); btn.type='button'; btn.className='add-entity-btn'; btn.innerHTML = ''+(this._t('editor.add_entity')||'Add room')+''; + btn.onclick = ()=>{ this._activeTab='rooms'; this._render(); }; + box.append(p, btn); contentWrap.append(box); + } + // Source selector + const sourceWrap = document.createElement('div'); sourceWrap.className='settings-card'; + const sRow = document.createElement('div'); sRow.className='setting'; + const sText = document.createElement('div'); sText.className='text'; + const sTitle = document.createElement('div'); sTitle.className='title'; sTitle.textContent = this._t('holidays.source'); + const sDesc = document.createElement('div'); sDesc.className='desc'; sDesc.textContent = this._t('holidays.desc'); + sText.append(sTitle, sDesc); + const sSeg = document.createElement('div'); sSeg.className='seg'; + const bCal = document.createElement('button'); bCal.type='button'; bCal.className='h-src-cal'; bCal.textContent = this._t('holidays.source.calendar'); + const bMan = document.createElement('button'); bMan.type='button'; bMan.className='h-src-man'; bMan.textContent = this._t('holidays.source.manual'); + const applySegUi = ()=>{ const src = this._config.holidays_source || 'calendar'; bCal.classList.toggle('active', src==='calendar'); bMan.classList.toggle('active', src==='manual'); }; + bCal.onclick = ()=>{ this._config.holidays_source='calendar'; applySegUi(); this._emit(true); try { this._pushSettingsToStoreDebounced(); } catch {} }; + bMan.onclick = ()=>{ this._config.holidays_source='manual'; applySegUi(); this._emit(true); try { this._pushSettingsToStoreDebounced(); } catch {} }; + sSeg.append(bCal, bMan); sRow.append(sText, sSeg); sourceWrap.append(sRow); applySegUi(); + // Calendar entity picker + const rowCal = document.createElement('div'); rowCal.className='setting'; rowCal.style.display = (this._config.holidays_source||'calendar')==='calendar' ? 'grid' : 'none'; + // Override layout to single-column so description can sit under the dropdown + try { rowCal.style.gridTemplateColumns = '1fr'; } catch {} + const calTxt = document.createElement('div'); calTxt.className='text'; + const calTitle = document.createElement('div'); calTitle.className='title'; calTitle.textContent = this._t('holidays.entity'); + calTxt.append(calTitle); + const picker = document.createElement('tt-entity-picker'); picker.className='holiday-entity'; picker.setAttribute('include-domains','["calendar"]'); picker.setAttribute('label', this._t('holidays.entity')); + try { picker.hass = this._hass; picker.value = this._config.holidays_entity || ''; } catch {} + picker.addEventListener('value-changed', (e)=>{ this._upd('holidays_entity', e.detail.value||''); try { this._pushSettingsToStoreDebounced(); } catch {} }); + // Description moved below the dropdown to avoid cramped layout + const calDesc = document.createElement('div'); calDesc.className='desc'; calDesc.textContent = this._t('holidays.entity_desc'); + rowCal.append(calTxt, picker, calDesc); sourceWrap.append(rowCal); + // Manual dates list + const rowMan = document.createElement('div'); rowMan.className='setting'; rowMan.style.display = (this._config.holidays_source||'calendar')==='manual' ? 'grid' : 'none'; + const manTxt = document.createElement('div'); manTxt.className='text'; + const manTitle = document.createElement('div'); manTitle.className='title'; manTitle.textContent = this._t('holidays.dates'); + const manDesc = document.createElement('div'); manDesc.className='desc'; manDesc.textContent=''; manTxt.append(manTitle, manDesc); + const manWrap = document.createElement('div'); manWrap.style.display='grid'; manWrap.style.gap='8px'; + const addRow = document.createElement('div'); addRow.style.display='flex'; addRow.style.gap='8px'; addRow.style.alignItems='center'; addRow.style.flexWrap='wrap'; + const inp = document.createElement('input'); inp.type='date'; inp.className='tt-input'; + const addBtn = document.createElement('button'); addBtn.type='button'; addBtn.className='add-entity-btn'; addBtn.innerHTML = ''+(this._t('holidays.add_date')||'Add date')+''; + const dateMsg = document.createElement('span'); dateMsg.style.color='var(--error-color)'; dateMsg.style.fontSize='.85rem'; dateMsg.style.marginLeft='6px'; + addBtn.onclick = ()=>{ + const v=inp.value; if (!v) return; + // If date is already covered by a range, show hint and do nothing + try { + const groups = Array.isArray(this._config.holidays_groups) ? this._config.holidays_groups : []; + const covered = groups.some(g => Array.isArray(g?.dates) && g.dates.map(String).includes(String(v))); + if (covered) { dateMsg.textContent = this._t('holidays.date_in_range') || 'Date already covered by a range'; return; } + } catch {} + const arr = Array.isArray(this._config.holidays_dates)?[...this._config.holidays_dates]:[]; if (!arr.includes(v)) arr.push(v); + this._config.holidays_dates = arr.sort(); inp.value=''; dateMsg.textContent=''; this._emit(true); try { this._pushSettingsToStoreDebounced(); } catch {} + }; + addRow.append(inp, addBtn, dateMsg); manWrap.append(addRow); + + // Range add: from date .. to date + const rangeRow = document.createElement('div'); rangeRow.style.display='flex'; rangeRow.style.gap='8px'; rangeRow.style.alignItems='center'; rangeRow.style.flexWrap='wrap'; + const inpFrom = document.createElement('input'); inpFrom.type='date'; inpFrom.className='tt-input'; + const inpTo = document.createElement('input'); inpTo.type='date'; inpTo.className='tt-input'; + const addRangeBtn = document.createElement('button'); addRangeBtn.type='button'; addRangeBtn.className='add-entity-btn'; addRangeBtn.innerHTML = ''+(this._t('holidays.add_range')||'Add range')+''; + addRangeBtn.onclick = ()=>{ + try { + const f = String(inpFrom.value||''); const t = String(inpTo.value||''); + if (!f || !t) return; + let d1 = new Date(f+'T00:00:00'); let d2 = new Date(t+'T00:00:00'); + if (isNaN(d1.getTime()) || isNaN(d2.getTime())) return; + if (d1.getTime() > d2.getTime()) { const tmp=d1; d1=d2; d2=tmp; } + const fmt = (d)=>{ const y=d.getFullYear(); const m=String(d.getMonth()+1).padStart(2,'0'); const dd=String(d.getDate()).padStart(2,'0'); return `${y}-${m}-${dd}`; }; + const cur = Array.isArray(this._config.holidays_dates)? new Set(this._config.holidays_dates.map(String)) : new Set(); + const grpDates = []; + for (let d=new Date(d1); d.getTime()<=d2.getTime(); d.setDate(d.getDate()+1)) { const s=fmt(d); cur.add(s); grpDates.push(s); } + const next = Array.from(cur).sort(); + this._config.holidays_dates = next; + // Create UI range group tag for easier removal later + try { + const fromS = fmt(d1), toS = fmt(d2); + const gid = 'hg_'+Date.now()+'_'+Math.floor(Math.random()*1e6); + const groups = Array.isArray(this._config.holidays_groups) ? [...this._config.holidays_groups] : []; + groups.push({ id: gid, from: fromS, to: toS, dates: grpDates }); + this._config.holidays_groups = groups; + } catch {} + try { inpFrom.value=''; inpTo.value=''; } catch {} + this._emit(true); try { this._pushSettingsToStoreDebounced(); } catch {} + } catch {} + }; + rangeRow.append(inpFrom, inpTo, addRangeBtn); manWrap.append(rangeRow); + const singlesLbl = document.createElement('div'); singlesLbl.className='title'; singlesLbl.style.fontSize='.9rem'; singlesLbl.textContent = this._t('holidays.single_dates')||'Single dates'; + const chips = document.createElement('div'); chips.className='linked-chips'; + const arr = Array.isArray(this._config.holidays_dates)?this._config.holidays_dates:[]; + const groupDatesSet = (()=>{ try { const gs = Array.isArray(this._config.holidays_groups)?this._config.holidays_groups:[]; return new Set([].concat(...gs.map(g=>g.dates||[])).map(String)); } catch { return new Set(); } })(); + const singles = arr.filter(d=>!groupDatesSet.has(String(d))); + chips.innerHTML=''; + for (const d of singles){ const chip=document.createElement('span'); chip.className='pill-chip'; const txt=document.createElement('span'); txt.textContent=d; const rm=document.createElement('button'); rm.type='button'; rm.className='rm'; rm.textContent='×'; rm.addEventListener('click', ()=>{ const v=(this._config.holidays_dates||[]).filter(x=>x!==d); this._config.holidays_dates=v; this._emit(true); try { this._pushSettingsToStoreDebounced(); } catch {} }); chip.append(txt, rm); chips.append(chip); } + manWrap.append(singlesLbl, chips); + // Range group chips + const grpLbl = document.createElement('div'); grpLbl.className='title'; grpLbl.style.fontSize='.9rem'; grpLbl.textContent = this._t('holidays.ranges')||'Holiday ranges'; + const grpChips = document.createElement('div'); grpChips.className='linked-chips'; + const renderGrp = ()=>{ + try { + grpChips.innerHTML=''; + const groups = Array.isArray(this._config.holidays_groups) ? this._config.holidays_groups : []; + for (const g of groups){ + const chip=document.createElement('span'); chip.className='pill-chip'; + const txt=document.createElement('span'); txt.textContent = `${g.from} – ${g.to}`; + const rm=document.createElement('button'); rm.type='button'; rm.className='rm'; rm.textContent='×'; + rm.addEventListener('click', ()=>{ + try { + const all = Array.isArray(this._config.holidays_groups) ? this._config.holidays_groups : []; + const others = all.filter(x=>x!==g); + const othersDates = new Set([].concat(...others.map(o=>o.dates||[])).map(String)); + const curSet = new Set((this._config.holidays_dates||[]).map(String)); + for (const d of (g.dates||[])) { if (!othersDates.has(String(d))) curSet.delete(String(d)); } + this._config.holidays_dates = Array.from(curSet).sort(); + this._config.holidays_groups = others; + this._emit(true); try { this._pushSettingsToStoreDebounced(); } catch {} + // rerender both lists + chips.innerHTML=''; + for (const d of this._config.holidays_dates){ const c=document.createElement('span'); c.className='pill-chip'; const t=document.createElement('span'); t.textContent=d; const r=document.createElement('button'); r.type='button'; r.className='rm'; r.textContent='×'; r.addEventListener('click', ()=>{ const v=(this._config.holidays_dates||[]).filter(x=>x!==d); this._config.holidays_dates=v; this._emit(true); try { this._pushSettingsToStoreDebounced(); } catch {} }); c.append(t,r); chips.append(c); } + renderGrp(); + } catch {} + }); + chip.append(txt, rm); grpChips.append(chip); + } + } catch {} + }; + renderGrp(); + manWrap.append(grpLbl, grpChips); + rowMan.append(manTxt, manWrap); sourceWrap.append(rowMan); + contentWrap.append(sourceWrap); + // Inline Holidays editor (preview + edit) inside editor + const edWrap = document.createElement('div'); edWrap.className = 'holiday-ed'; edWrap.style.position = 'relative'; + // Room tabs + const tabs = document.createElement('div'); tabs.className='hol-tabs'; tabs.style.display='flex'; tabs.style.gap='8px'; tabs.style.flexWrap='wrap'; edWrap.append(tabs); + const note = document.createElement('div'); note.style.fontSize='.85rem'; note.style.color='var(--secondary-text-color)'; note.textContent = this._t('week.editor.preview_note'); edWrap.append(note); + // Scale + const scaleOuter = document.createElement('div'); scaleOuter.className='week-scale'; const scale = document.createElement('div'); scale.className='week-scale-inner'; scaleOuter.append(scale); edWrap.append(scaleOuter); + // Track + const track = document.createElement('div'); track.className='week-track'; edWrap.append(track); + // Toolbar + const tools = document.createElement('div'); tools.className='hol-toolbar'; + const addBlkBtn = document.createElement('button'); addBlkBtn.className='add-entity-btn'; addBlkBtn.innerHTML = ''+ (this._t('ui.add_block') || 'Add block') +''; + const saveBtn = document.createElement('button'); saveBtn.className='add-entity-btn'; saveBtn.innerHTML = ''+ (this._t('ui.save')||'Save') +''; + tools.append(addBlkBtn, saveBtn); edWrap.append(tools); + // Inline mini block editor panel + const mini = document.createElement('div'); mini.className='mini-ed'; mini.style.display='none'; mini.innerHTML = ` +
    +
    +
    +
    + + + +
    + `; + edWrap.append(mini); + contentWrap.append(edWrap); + + // Helpers + const toTime = (min)=>{ min=(Number(min)||0)%1440; const hh=String(Math.floor(min/60)).padStart(2,'0'); const mm=String(min%60).padStart(2,'0'); return `${hh}:${mm}`; }; + const fromTime = (s)=>{ const m=(/^(\d{1,2}):(\d{2})$/).exec(String(s||'')); if(!m) return 0; let h=Number(m[1]); let mi=Number(m[2]); h=Math.min(23,Math.max(0,h)); mi=Math.min(59,Math.max(0,mi)); return h*60+mi; }; + const toDisp = (c)=> this._toDisplayTemp(Number(c)); + const fromDisp = (v)=> this._fromDisplayTemp(Number(v)); + + // Color helpers (mirror of card logic) + const colorFor = (eid, tempC)=>{ + try { + const cr = this._config?.color_ranges || {}; + const list = this._config?.color_global ? (cr['*'] || []) : (cr[eid] || cr['*'] || []); + for (const r of (list||[])){ + const f = Number(r.from); const t = Number(r.to); + if (Number.isFinite(f) && Number.isFinite(t) && tempC >= f && tempC <= t) return r.color || null; + } + } catch {} + return null; + }; + const contrastText = (hex)=>{ + try { const h = String(hex||'').replace('#',''); if (h.length!==6) return '#000000'; const r=parseInt(h.slice(0,2),16), g=parseInt(h.slice(2,4),16), b=parseInt(h.slice(4,6),16); const yiq=(r*299+g*587+b*114)/1000; return yiq>=140 ? '#000000' : '#ffffff'; } catch { return '#000000'; } + }; + + const ensureHolDraft = async ()=>{ + if (this._holDraft) return; + let schedules = {}; let settings={}; + // Load from file storage via API + try { + if (this._config?.storage_enabled && this._apiSupported?.()) { + const resp = await this._apiFetchState?.(); + if (resp && typeof resp === 'object') { + schedules = resp.schedules || {}; + if (schedules.schedules) schedules = schedules.schedules; + settings = resp.settings || {}; + } + } + } catch {} + if (!Object.keys(schedules||{}).length){ + try { + const raw = localStorage.getItem(this._localStoreKey()) || ''; + const parsed = JSON.parse(raw||'{}'); + schedules = parsed.schedules||{}; + if (schedules.schedules) schedules = schedules.schedules; + settings = parsed.settings||{}; + } catch {} + } + const roomsMap={}; for (const eid of (this._config?.entities||[])){ const row=schedules[eid]||{}; const arr=(row.holiday && Array.isArray(row.holiday.blocks))?row.holiday.blocks:[]; roomsMap[eid]=JSON.parse(JSON.stringify(arr)); } + this._holDraft={ rooms: roomsMap }; this._holRoom = this._holRoom || (this._config?.entities||[])[0] || null; + }; + + const paintScale = ()=>{ scale.innerHTML=''; for(let i=0;i<=24;i+=2){ const t=document.createElement('div'); t.style.position='absolute'; t.style.left=(i/24*100)+'%'; t.style.top='4px'; t.style.bottom='4px'; t.style.borderLeft='1px solid var(--divider-color)'; const lab=document.createElement('div'); lab.style.position='absolute'; lab.style.top='-2px'; lab.style.transform='translate(-50%,0)'; lab.style.left='0'; lab.style.fontSize='.75rem'; lab.style.color='var(--secondary-text-color)'; lab.textContent=(i===24?'00:00':toTime(i*60)); t.append(lab); scale.append(t);} }; + + const paintTabs = ()=>{ tabs.innerHTML=''; const ents=this._config?.entities||[]; if (!this._holRoom || !ents.includes(this._holRoom)) this._holRoom=ents[0]||null; for (const eid of ents){ const b=document.createElement('button'); b.type='button'; b.className='wk-tab'+(eid===this._holRoom?' active':''); let lbl=this._config?.labels?.[eid] || (this._hass?.states?.[eid]?.attributes?.friendly_name || (eid.split('.')[1]||eid)); b.textContent=lbl; b.onclick=()=>{ this._holRoom=eid; paintTrack(); }; tabs.append(b);} }; + + const openMini = (blk)=>{ mini.style.display=''; const f=mini.querySelector('.mi-from'); const t=mini.querySelector('.mi-to'); const tmp=mini.querySelector('.mi-temp'); f.value=toTime(blk.startMin); t.value=toTime(blk.endMin%1440); tmp.value=String(toDisp(blk.temp)); this._holMini = { room: this._holRoom, blk: { id: blk.id, startMin: blk.startMin, endMin: blk.endMin, temp: blk.temp } }; + // Focus first field so global editor render guard keeps panel open + try { f.focus(); f.select && f.select(); } catch {} + mini.querySelector('.mi-save').onclick=()=>{ const s=fromTime(f.value); let e=fromTime(t.value); if (e===0 && t.value==='00:00') e=1440; const arr=(this._holDraft.rooms||{})[this._holRoom]||[]; let b=arr.find(x=>x.id===blk.id); if (!b) { b = { id: blk.id, startMin:s, endMin:e, temp: fromDisp(tmp.value) }; arr.push(b); } else { b.startMin=Math.max(0,Math.min(1439,s)); b.endMin=Math.max(1,Math.min(1440,e)); b.temp=fromDisp(tmp.value); } arr.sort((a,bx)=>a.startMin-bx.startMin||a.endMin-bx.endMin); (this._holDraft.rooms||{})[this._holRoom]=arr; this._holMini = null; mini.style.display='none'; paintTrack(); }; + mini.querySelector('.mi-cancel').onclick=()=>{ this._holMini = null; mini.style.display='none'; }; + mini.querySelector('.mi-del').onclick=()=>{ const arr=(this._holDraft.rooms||{})[this._holRoom]||[]; const i=arr.findIndex(x=>x.id===blk.id); if (i>=0) { arr.splice(i,1); (this._holDraft.rooms||{})[this._holRoom]=arr; paintTrack(); } this._holMini = null; mini.style.display='none'; }; + }; + + const paintTrack = async ()=>{ await ensureHolDraft(); paintTabs(); paintScale(); track.innerHTML=''; + // Ensure tooltip exists once + let tip = edWrap.querySelector('.ed-hol-tooltip'); + if (!tip) { tip = document.createElement('div'); tip.className='wk-tooltip ed-hol-tooltip'; tip.style.display='none'; edWrap.append(tip); } + const arr=(this._holDraft.rooms||{})[this._holRoom]||[]; + for (const b of arr){ const div=document.createElement('div'); div.className='block'; div.style.left=(b.startMin/1440*100)+'%'; div.style.width=((b.endMin-b.startMin)/1440*100)+'%'; + // Apply heat color if configured + try { const clr = colorFor(this._holRoom, Number(b.temp)); if (clr) { div.style.background = clr; div.style.borderColor = clr; const txt=contrastText(clr); if (txt) div.style.color = txt; } } catch {} + const showTip = ()=>{ + const txt = `${toTime(b.startMin)} - ${toTime(b.endMin)} • ${toDisp(b.temp)} ${this._isF()? '°F':'°C'}`; + tip.textContent = txt; tip.style.display = ''; + const box = div.getBoundingClientRect(); + const cont = edWrap.getBoundingClientRect(); + const tbox = track.getBoundingClientRect(); + // Base center: visual center of the hovered block element + let center = box.left + box.width/2 - cont.left; + const tipW = tip.offsetWidth || 0; const tipH = tip.offsetHeight || 0; const pad = 8; + const minC = (tbox.left - cont.left) + pad + tipW/2; + const maxC = (tbox.right - cont.left) - pad - tipW/2; + center = Math.max(minC, Math.min(maxC, center)); + // Prefer above the block; if not enough space, flip below + const yAbove = box.top - cont.top - 8; // anchor a bit above block + const yBelow = box.bottom - cont.top + 8; // a bit below block + if (yAbove - tipH < pad) { + tip.style.top = Math.min(cont.height - pad, yBelow) + 'px'; + tip.style.transform = 'translate(-50%,0)'; + } else { + // Place right above the block top edge + tip.style.top = Math.max(pad, box.top - cont.top + 6) + 'px'; + tip.style.transform = 'translate(-50%,-100%)'; + } + tip.style.left = center + 'px'; + }; + const hideTip = ()=>{ const delay = (window.matchMedia && window.matchMedia('(pointer:coarse)').matches) ? 3000 : 120; if (tip._hideTimer) try{ clearTimeout(tip._hideTimer);}catch{}; tip._hideTimer = setTimeout(()=>{ try { tip.style.display='none'; tip._hideTimer = null; } catch{} }, delay); }; + div.addEventListener('mouseenter', showTip); div.addEventListener('mouseleave', hideTip); + div.ondblclick=()=>openMini(b); track.append(div); + } + // Re-open mini editor if it was open before a re-render + if (this._holMini && this._holMini.room === this._holRoom) { + openMini(this._holMini.blk); + } + }; + + addBlkBtn.onclick = async ()=>{ await ensureHolDraft(); const now=new Date(); const m=now.getHours()*60+now.getMinutes(); const start=Math.max(0,Math.min(1380, Math.round(m))); const end=Math.min(1440,start+60); const blk={ id: Math.random().toString(36).slice(2,9), startMin:start, endMin:end, temp: fromDisp(this._config.default_temp||20) }; openMini(blk); }; + + saveBtn.onclick = async ()=>{ + await ensureHolDraft(); + let schedules={}; + let settings={}; + // Load current schedules from file storage via API + try { + if (this._config?.storage_enabled && this._apiSupported?.()) { + const resp = await this._apiFetchState?.(); + if (resp && typeof resp === 'object') { + schedules = resp.schedules || {}; + if (schedules.schedules) schedules = schedules.schedules; + settings = resp.settings || {}; + } + } + } catch {} + if (!Object.keys(schedules||{}).length){ + try { + const raw=localStorage.getItem(this._localStoreKey())||''; + const parsed=JSON.parse(raw||'{}'); + schedules=parsed.schedules||{}; + if (schedules.schedules) schedules=schedules.schedules; + settings=parsed.settings||{}; + } catch {} + } + for (const eid of (this._config?.entities||[])){ + const row=schedules[eid] || { defaultTemp: this._config.default_temp, blocks: [] }; + const arr=(this._holDraft.rooms||{})[eid]||[]; + row.holiday={ blocks: JSON.parse(JSON.stringify(arr)) }; + schedules[eid]=row; + } + const wantF = this._isF(); + const schOut = wantF ? this._convertSchedulesTemps(schedules,'F') : schedules; + let settingsOut = { ...settings, holidays_enabled: !!this._config.holidays_enabled, holidays_source: this._config.holidays_source || 'calendar', holidays_entity: this._config.holidays_entity || '', holidays_dates: Array.isArray(this._config.holidays_dates)?this._config.holidays_dates:[], holidays_groups: Array.isArray(this._config.holidays_groups)?this._config.holidays_groups:[] }; + // Preserve backend holidays_enabled unless explicitly toggled in this session + try { + if (!this._holidaysToggleTouched) { + const st = await this._apiFetchState?.(); + const base = st?.settings; + if (typeof base?.holidays_enabled === 'boolean') settingsOut.holidays_enabled = base.holidays_enabled; else delete settingsOut.holidays_enabled; + } + } catch {} + try { + if (wantF) { + const toStore=(n)=>{ const v=Number(n); return Number.isFinite(v)? this._cToF(v): n; }; + if (settingsOut.default_temp!==undefined) settingsOut.default_temp = toStore(settingsOut.default_temp); + if (settingsOut.min_temp!==undefined) settingsOut.min_temp = toStore(settingsOut.min_temp); + if (settingsOut.max_temp!==undefined) settingsOut.max_temp = toStore(settingsOut.max_temp); + if (settingsOut.away && typeof settingsOut.away==='object' && settingsOut.away.target_c!==undefined) { + settingsOut.away = { ...settingsOut.away, target_c: toStore(settingsOut.away.target_c) }; + } + } + } catch {} + try { localStorage.setItem(this._localStoreKey(), JSON.stringify({ schedules: schOut, settings: settingsOut })); } catch {} + // Save to file storage via service + try { + if (this._config.storage_enabled) { + const p = { schedules: schOut, settings: settingsOut }; + if (this._config?.instance_enabled) { p.instance_id = this._instanceId(); p.activate = true; } + await this._hass.callService('thermostat_timeline','set_store', p); + } + } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + }; + + // Initial render + (async()=>{ await ensureHolDraft(); await paintTrack(); })(); + } + } catch {} + + // Apply tab visibility/class based on current state + try { + const sBtn = this.shadowRoot.querySelector('.tab-settings-btn'); + const bBtn = this.shadowRoot.querySelector('.tab-backup-btn'); + + const sSec = this.shadowRoot.querySelector('.tab-settings'); + const bSec = this.shadowRoot.querySelector('.tab-backup'); + + const setActive = (btn, on) => { + if (!btn) return; + try { btn.classList.toggle('active', !!on); } catch {} + try { btn.setAttribute('aria-selected', on ? 'true' : 'false'); } catch {} + }; + const setShow = (sec, on) => { + if (!sec) return; + try { sec.style.display = on ? '' : 'none'; } catch {} + }; + + const showBackup = !!this._config.storage_enabled; + if (!showBackup && this._activeTab === 'backup') this._activeTab = 'settings'; + if (this._activeTab === 'sync') this._activeTab = 'settings'; + if (!['settings', 'backup'].includes(this._activeTab)) this._activeTab = 'settings'; + + const isSettings = this._activeTab === 'settings'; + const isBackup = this._activeTab === 'backup'; + + setActive(sBtn, isSettings); + if (bBtn) { + setActive(bBtn, isBackup); + try { bBtn.style.display = showBackup ? '' : 'none'; } catch {} + } + + setShow(sSec, isSettings); + setShow(bSec, showBackup && isBackup); + } catch {} + + } + + // Pointer-driven Drag & Drop (works reliably inside HA editors) + _startPointerDnd(line, idx){ + const wrap = line.parentElement; + if (!wrap) return; + if (this._pointerDndActive) return; + this._pointerDndActive = true; + // Avoid any re-render while dragging which could disrupt DOM + try { this._suspendRender = true; } catch {} + + const rect = line.getBoundingClientRect(); + const ghost = line.cloneNode(true); + ghost.classList.add('drag-ghost'); + ghost.style.width = rect.width + 'px'; + ghost.style.height = rect.height + 'px'; + ghost.style.top = rect.top + 'px'; + ghost.style.left = rect.left + 'px'; + + const placeholder = document.createElement('div'); + placeholder.className = 'placeholder'; + placeholder.style.height = rect.height + 'px'; + + // place placeholder where line was, and hide line + wrap.insertBefore(placeholder, line); + line.style.display = 'none'; + document.body.appendChild(ghost); + + const readClient = (e)=>{ + if (e.touches && e.touches[0]) return { x: e.touches[0].clientX, y: e.touches[0].clientY }; + return { x: e.clientX, y: e.clientY }; + }; + + const onMove = (ev)=>{ + try { ev.preventDefault(); } catch {} + const { y, x } = readClient(ev); + ghost.style.top = (y - rect.height/2) + 'px'; + + // find element under pointer within this shadowRoot + let el = this.shadowRoot && this.shadowRoot.elementFromPoint(x, y); + // climb to row in the same wrap + while (el && el !== wrap && !el.classList?.contains('inline')) el = el.parentElement; + if (!el || el===placeholder || el===wrap) return; + const targetRect = el.getBoundingClientRect(); + const before = (y - targetRect.top) < targetRect.height/2; + if (before) wrap.insertBefore(placeholder, el); + else wrap.insertBefore(placeholder, el.nextSibling); + }; + + const endDrag = ()=>{ + window.removeEventListener('mousemove', onMove, true); + window.removeEventListener('mouseup', onUp, true); + window.removeEventListener('touchmove', onMove, true); + window.removeEventListener('touchend', onUp, true); + ghost.remove(); + // compute new order + const from = idx; + const to = Array.prototype.indexOf.call(wrap.children, placeholder); + const arr = [...(this._config.entities||[])]; + const modes = Array.isArray(this._config.room_use_input_number) ? [...this._config.room_use_input_number] : []; + while (modes.length < arr.length) modes.push(false); + const [item] = arr.splice(from,1); + const [mItem] = modes.splice(from,1); + const correctedTo = Math.max(0, Math.min(to, arr.length)); + arr.splice(correctedTo, 0, item); + modes.splice(correctedTo, 0, mItem); + placeholder.remove(); + line.style.display = ''; + this._pointerDndActive = false; + this._config.entities = arr; + this._config.room_use_input_number = modes; + try { this._suspendRender = false; } catch {} + this._emit(true); + }; + + const onUp = (ev)=>{ try { ev.preventDefault?.(); } catch {} endDrag(); }; + + window.addEventListener('mousemove', onMove, true); + window.addEventListener('mouseup', onUp, true); + window.addEventListener('touchmove', onMove, true); + window.addEventListener('touchend', onUp, true); + } + + // Async creator used with lit's `until` to delay row insertion until HA elements are ready + async _createRoomLine(ent, idx){ + try { await customElements.whenDefined('ha-entity-picker'); } catch {} + try { await customElements.whenDefined('ha-textfield'); } catch {} + const line = this._makeEntityRow(idx); + // Restore open state for this row + try { + const key = ent || `#idx:${idx}`; + if (this._openRows.has(key)) line.classList.add('open'); + } catch {} + return line; + } + + _makeEntityRow(idx){ + const line = document.createElement("div"); line.className = "inline"; + + // Summary bar (collapsed row header) + const summary = document.createElement('div'); + summary.className = 'summary'; + const sumLeft = document.createElement('div'); sumLeft.className = 'summary-left'; + const expander = document.createElement('button'); expander.className = 'expander'; expander.innerHTML = ``; sumLeft.append(expander); + // Title and subtitle container + const textWrap = document.createElement('div'); textWrap.className = 'summary-text'; + const titleSpan = document.createElement('span'); titleSpan.className = 'summary-title'; titleSpan.textContent = this._t('editor.entity_placeholder'); + const subSpan = document.createElement('span'); subSpan.className = 'summary-sub'; subSpan.textContent = ''; + textWrap.append(titleSpan, subSpan); + sumLeft.append(textWrap); + summary.append(sumLeft); + const actions = document.createElement("div"); actions.className = "actions"; + const handle = document.createElement("button"); handle.className = "drag-handle"; handle.setAttribute("draggable","true"); handle.setAttribute("title", this._t('editor.drag_reorder')); handle.innerHTML = ``; actions.append(handle); + // Start pointer-driven drag on mousedown (more reliable inside HA editors than native DnD) + handle.addEventListener('mousedown', (ev)=>{ + ev.preventDefault(); + try { + const wrap = line.parentElement; + const curIdx = wrap ? Array.prototype.indexOf.call(wrap.children, line) : idx; + this._startPointerDnd(line, curIdx); + } catch { this._startPointerDnd(line, idx); } + }); + // Touch support + handle.addEventListener('touchstart', (ev)=>{ + try { ev.preventDefault(); } catch {} + try { + const wrap = line.parentElement; + const curIdx = wrap ? Array.prototype.indexOf.call(wrap.children, line) : idx; + this._startPointerDnd(line, curIdx); + } catch { this._startPointerDnd(line, idx); } + }, { passive: false }); + const del = document.createElement("button"); del.className = "remove-btn"; del.innerHTML = `${this._t('editor.remove')}`; actions.append(del); + summary.append(actions); + + // Details (expanded content) + const details = document.createElement('div'); details.className = 'details'; + + // Left column: entity picker + optional label + merge UI + const left = document.createElement("div"); left.style.display = "grid"; left.style.gap = "6px"; + + const useInput = this._roomUsesInputNumber(idx); + + // Per-room mode toggle + const modeRow = document.createElement('div'); + modeRow.style.display = 'flex'; + modeRow.style.alignItems = 'center'; + modeRow.style.justifyContent = 'space-between'; + modeRow.style.gap = '10px'; + const modeText = document.createElement('div'); + modeText.style.display = 'grid'; + modeText.style.gap = '2px'; + const modeTitle = document.createElement('div'); + modeTitle.style.fontWeight = '600'; + modeTitle.textContent = this._t('editor.room_mode.title') || 'Use input_number instead of climate'; + const modeDesc = document.createElement('div'); + modeDesc.style.fontSize = '.85rem'; + modeDesc.style.color = 'var(--secondary-text-color)'; + modeDesc.textContent = this._t('editor.room_mode.desc') || ''; + modeText.append(modeTitle, modeDesc); + const modeSwitch = document.createElement('ha-switch'); + modeSwitch.className = 'room-mode-switch'; + modeSwitch.checked = !!useInput; + modeSwitch.addEventListener('change', (e)=>{ this._setRoomUseInputNumber(idx, !!e.target.checked); }); + modeRow.append(modeText, modeSwitch); + + const pick = document.createElement("tt-entity-picker"); + try { pick.style.display = 'block'; pick.style.minHeight = '48px'; } catch {} + try { pick.hass = this._hass; } catch {} + try { pick.value = this._config.entities[idx] || ""; } catch {} + pick.setAttribute("include-domains", useInput ? '["input_number"]' : '["climate"]'); + pick.setAttribute("label", this._t("editor.entity_placeholder")); + // Filter out already-selected entities in other pickers (but allow the current one) + pick.entityFilter = (st) => { + try { + const id = st?.entity_id || st?.entityId || st?.entityID || ""; + if (!id) return false; + const chosenPrim = (this._config.entities || []).filter(Boolean); + const allLinked = Object.values(this._config.merges || {}).flat(); + const blocked = new Set([...chosenPrim, ...allLinked]); + const current = this._config.entities[idx]; + return !blocked.has(id) || id === current; + } catch { return true; } + }; + + const nameInp = document.createElement("input"); + nameInp.type = 'text'; + nameInp.className = "label-input tt-input"; + nameInp.setAttribute("placeholder", this._t('editor.display_name_optional')); + try { + const curEid = this._config.entities[idx]; + const curLbl = (this._config.labels || {})[curEid] || ''; + nameInp.value = curLbl; + } catch {} + nameInp.addEventListener("change", (e)=>{ + const eid = this._config.entities[idx]; + if (!eid) return; + const labels = { ...(this._config.labels || {}) }; + const val = (e.target.value || "").trim(); + if (val) labels[eid] = val; else delete labels[eid]; + this._config.labels = labels; + // Repaint so the summary header updates immediately with the custom name + this._emit(true); + // Persist label to local browser storage so live preview doesn’t get + // overwritten by periodic hass() reloads when shared storage is OFF + try { + const raw = localStorage.getItem(this._localStoreKey()) || ''; + let parsed = {}; + try { parsed = JSON.parse(raw || '{}'); } catch {} + const schedules = parsed.schedules || {}; + const prevSettings = parsed.settings || {}; + const settings = { ...prevSettings, labels: { ...(prevSettings.labels||{}), ...labels } }; + localStorage.setItem(this._localStoreKey(), JSON.stringify({ schedules, settings })); + } catch {} + // Nudge any live preview card instances to refresh immediately + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + try { document.querySelectorAll('thermostat-timeline-card').forEach(el=>el?.devRefreshFromEditor?.()); } catch {} + try { this._pushSettingsToStoreDebounced(); } catch {} + }); + + // Per-room: optional turn_on behavior + const turnOnWrap = document.createElement('div'); + turnOnWrap.style.display = 'grid'; + turnOnWrap.style.gap = '6px'; + const turnOnRow = document.createElement('div'); + turnOnRow.style.display = 'flex'; + turnOnRow.style.alignItems = 'center'; + turnOnRow.style.justifyContent = 'space-between'; + turnOnRow.style.gap = '10px'; + const turnOnText = document.createElement('div'); + turnOnText.style.display = 'grid'; + turnOnText.style.gap = '2px'; + const turnOnTitle = document.createElement('div'); + turnOnTitle.style.fontWeight = '600'; + turnOnTitle.textContent = this._t('editor.turn_on.title') || 'Send turn_on command'; + const turnOnDesc = document.createElement('div'); + turnOnDesc.style.fontSize = '.85rem'; + turnOnDesc.style.color = 'var(--secondary-text-color)'; + turnOnDesc.textContent = this._t('editor.turn_on.desc') || ''; + turnOnText.append(turnOnTitle, turnOnDesc); + const turnOnSwitch = document.createElement('ha-switch'); + turnOnSwitch.className = 'turn-on-switch'; + // Dropdown (only visible when enabled) + const turnOnOrderRow = document.createElement('div'); + turnOnOrderRow.style.display = 'grid'; + turnOnOrderRow.style.gap = '2px'; + const orderLbl = document.createElement('div'); + orderLbl.className = 'slabel'; + orderLbl.textContent = this._t('editor.turn_on.order') || 'Order'; + const orderSel = document.createElement('select'); + orderSel.className = 'turn-on-order'; + try { + orderSel.style.height = '32px'; + orderSel.style.border = '1px solid var(--divider-color)'; + orderSel.style.borderRadius = '8px'; + orderSel.style.background = 'var(--card-background-color)'; + orderSel.style.color = 'var(--primary-text-color)'; + orderSel.style.padding = '0 10px'; + orderSel.style.width = '100%'; + orderSel.style.maxWidth = '440px'; + } catch {} + const optBefore = document.createElement('option'); + optBefore.value = 'before'; + optBefore.textContent = this._t('editor.turn_on.order.before') || 'Before set_temperature'; + const optAfter = document.createElement('option'); + optAfter.value = 'after'; + optAfter.textContent = this._t('editor.turn_on.order.after') || 'After set_temperature'; + orderSel.append(optBefore, optAfter); + + const readTurnOnCfg = ()=>{ + try { + const eid = this._config.entities[idx]; + const map = (this._config.turn_on && typeof this._config.turn_on === 'object') ? this._config.turn_on : {}; + const cfg = (eid && map[eid] && typeof map[eid] === 'object') ? map[eid] : {}; + const enabled = !!cfg.enabled; + const order = String(cfg.order || 'before'); + return { eid, enabled, order: (order === 'after' ? 'after' : 'before') }; + } catch { return { eid: this._config.entities[idx], enabled: false, order: 'before' }; } + }; + const writeTurnOnCfg = (eid, patch)=>{ + try { + if (!eid) return; + const prev = (this._config.turn_on && typeof this._config.turn_on === 'object') ? this._config.turn_on : {}; + const cur = (prev[eid] && typeof prev[eid] === 'object') ? prev[eid] : {}; + const next = { ...prev, [eid]: { ...cur, ...patch } }; + // If disabling, keep order but store enabled=false (so it can be re-enabled without losing selection) + this._config.turn_on = next; + } catch {} + // Persist to local browser storage for immediate live preview when Sync is OFF + try { + const raw = localStorage.getItem(this._localStoreKey()) || ''; + let parsed = {}; + try { parsed = JSON.parse(raw || '{}'); } catch { parsed = {}; } + const schedules = parsed.schedules || {}; + const prevSettings = parsed.settings || {}; + const settings = { ...prevSettings, turn_on: { ...(prevSettings.turn_on||{}), ...(this._config.turn_on||{}) } }; + localStorage.setItem(this._localStoreKey(), JSON.stringify({ schedules, settings })); + } catch {} + try { this._pushSettingsToStoreDebounced(); } catch {} + try { window.dispatchEvent(new CustomEvent('thermostat-timeline-refresh')); } catch {} + try { document.querySelectorAll('thermostat-timeline-card').forEach(el=>el?.devRefreshFromEditor?.()); } catch {} + }; + + const applyTurnOnUi = ()=>{ + const { enabled, order } = readTurnOnCfg(); + try { turnOnSwitch.checked = !!enabled; } catch {} + try { orderSel.value = (order === 'after' ? 'after' : 'before'); } catch {} + try { turnOnOrderRow.style.display = enabled ? '' : 'none'; } catch {} + }; + + turnOnSwitch.addEventListener('change', (e)=>{ + try { + const { eid, order } = readTurnOnCfg(); + const enabled = !!e.target.checked; + writeTurnOnCfg(eid, { enabled, order }); + applyTurnOnUi(); + this._emit(true); + } catch {} + }); + orderSel.addEventListener('change', ()=>{ + try { + const { eid, enabled } = readTurnOnCfg(); + const order = String(orderSel.value || 'before'); + writeTurnOnCfg(eid, { enabled: !!enabled, order: (order === 'after' ? 'after' : 'before') }); + this._emit(true); + } catch {} + }); + // Initial values + applyTurnOnUi(); + turnOnRow.append(turnOnText, turnOnSwitch); + turnOnOrderRow.append(orderLbl, orderSel); + turnOnWrap.append(turnOnRow, turnOnOrderRow); + + // Merge/Link section + const linkWrap = document.createElement('div'); + linkWrap.style.display = 'grid'; + linkWrap.style.gap = '6px'; + // Visible label for merge/link section + const linkLabel = document.createElement('div'); + linkLabel.className = 'label'; + linkLabel.textContent = this._t('editor.merge_label') || 'Flet med (tilføj ekstra termostat)'; + const linkPicker = document.createElement('tt-entity-picker'); + try { linkPicker.style.display = 'block'; linkPicker.style.minHeight = '48px'; } catch {} + linkPicker.className = 'merge-picker'; + linkPicker.setAttribute('include-domains', '["climate"]'); + linkPicker.setAttribute('label', this._t('editor.merge_label') || 'Flet med (tilføj ekstra termostat)'); + linkPicker.hass = this._hass; + const linkFilter = (st) => { + try { + const id = st?.entity_id || st?.entityId || st?.entityID || ''; + if (!id) return false; + const prims = (this._config.entities || []).filter(Boolean); + const allLinked = Object.values(this._config.merges || {}).flat(); + const used = new Set([...prims, ...allLinked]); + // Allow selecting if not already used anywhere and not equal to current primary + const cur = this._config.entities[idx]; + return !used.has(id) && id !== cur; + } catch { return true; } + }; + linkPicker.entityFilter = linkFilter; + linkPicker.addEventListener('value-changed', (e)=>{ + const toAdd = e.detail.value; + linkPicker.value = ''; + const primary = this._config.entities[idx]; + if (!primary || !toAdd) return; + const merges = { ...(this._config.merges || {}) }; + const arr = Array.from(new Set([...(merges[primary] || []), toAdd])); + merges[primary] = arr; + this._config.merges = merges; + this._emit(true); + try { this._pushSettingsToStoreDebounced(); } catch {} + }); + + const chips = document.createElement('div'); + chips.className = 'linked-chips'; + + left.append(pick, nameInp, turnOnWrap, linkWrap, chips); + // Insert mode toggle at the top + try { left.insertBefore(modeRow, left.firstChild); } catch { left.prepend(modeRow); } + // Hide merge UI when controlling input_number + try { if (useInput) { linkWrap.style.display = 'none'; chips.style.display = 'none'; turnOnWrap.style.display = 'none'; } } catch {} + details.append(left); + + // Expand/collapse + const toggle = ()=>{ + line.classList.toggle('open'); + try { + const eid = this._config.entities[idx] || `#idx:${idx}`; + if (line.classList.contains('open')) this._openRows.add(eid); + else this._openRows.delete(eid); + } catch {} + // When a row is opened, force-hydrate the pickers so they render immediately + try { + if (line.classList.contains('open')) { + // Primary picker + try { + pick.hass = this._hass; + pick.style.display = 'block'; + pick.style.minHeight = '48px'; + if (typeof pick.requestUpdate === 'function') pick.requestUpdate(); + // Nudge again after layout for stubborn cases + setTimeout(()=>{ try { if (typeof pick.requestUpdate === 'function') pick.requestUpdate(); } catch {} }, 50); + } catch {} + // Merge picker + try { + linkPicker.hass = this._hass; + linkPicker.style.display = 'block'; + linkPicker.style.minHeight = '48px'; + if (typeof linkPicker.requestUpdate === 'function') linkPicker.requestUpdate(); + setTimeout(()=>{ try { if (typeof linkPicker.requestUpdate === 'function') linkPicker.requestUpdate(); } catch {} }, 50); + } catch {} + } + } catch {} + }; + expander.addEventListener('click', (e)=>{ e.stopPropagation(); toggle(); }); + sumLeft.addEventListener('click', (e)=>{ if (e.target === expander) return; toggle(); }); + + line.append(summary, details); + + // populate chips on render + const refreshChips = () => { + try { + linkPicker.hass = this._hass; + const primary = this._config.entities[idx]; + const links = (this._config.merges?.[primary] || []); + chips.innerHTML = ''; + for (const l of links){ + const chip = document.createElement('span'); + chip.className = 'pill-chip'; + const txt = document.createElement('span'); + txt.textContent = this._hass?.states?.[l]?.attributes?.friendly_name || (l.split('.')[1]||l); + const rm = document.createElement('button'); + rm.type = 'button'; rm.className = 'rm'; rm.textContent = '×'; + rm.setAttribute('aria-label', this._t('editor.remove')); + rm.addEventListener('click', ()=>{ + const merges = { ...(this._config.merges || {}) }; + merges[primary] = (merges[primary]||[]).filter(x=>x!==l); + if (!merges[primary]?.length) delete merges[primary]; + this._config.merges = merges; + this._emit(true); + try { this._pushSettingsToStoreDebounced(); } catch {} + }); + chip.append(txt, rm); + chips.append(chip); + } + // Update subtitle after chip changes + try { + const subEl = line.querySelector('.summary-sub'); + const linkedNames = links.map(id => this._hass?.states?.[id]?.attributes?.friendly_name || (id.split('.')[1]||id)); + const parts = []; + const eid = primary; + if (eid) parts.push(eid); + if (linkedNames.length) parts.push(`${this._t('editor.merged_with')}: ${linkedNames.join(', ')}`); + if (subEl) subEl.textContent = parts.join(' • '); + } catch {} + linkWrap.innerHTML = ''; + linkWrap.append(linkLabel, linkPicker); + } catch {} + }; + + // Initial render of merge section + if (!useInput) refreshChips(); + + // Remove this room/entity + del.addEventListener('click', () => { + try { + const arr = [...(this._config.entities || [])]; + const modes = Array.isArray(this._config.room_use_input_number) ? [...this._config.room_use_input_number] : []; + const primary = arr[idx]; + // Remove the primary from entities + arr.splice(idx, 1); + try { if (modes.length) modes.splice(idx, 1); } catch {} + // Clean up labels + const labels = { ...(this._config.labels || {}) }; + if (primary && labels[primary]) delete labels[primary]; + // Clean up turn_on + const turnOn = { ...(this._config.turn_on || {}) }; + if (primary && turnOn[primary]) delete turnOn[primary]; + // Clean up merges: remove mapping for this primary and remove it from other arrays + const merges = { ...(this._config.merges || {}) }; + if (primary && merges[primary]) delete merges[primary]; + for (const k of Object.keys(merges)) { + merges[k] = (merges[k] || []).filter((e) => e !== primary); + if (!merges[k].length) delete merges[k]; + } + this._config.entities = arr; + this._config.room_use_input_number = modes; + this._config.labels = labels; + this._config.merges = merges; + this._config.turn_on = turnOn; + this._emit(true); + } catch (e) {} + }); + + // When picker changes primary value, migrate merges + pick.addEventListener("value-changed", (e)=>{ + const oldPrimary = this._config.entities[idx]; + const newPrimary = e.detail.value; + + // Auto-sync mode based on chosen domain (defensive) + try { + const modes = Array.isArray(this._config.room_use_input_number) ? [...this._config.room_use_input_number] : []; + const ents = [...(this._config.entities || [])]; + while (modes.length < ents.length) modes.push(false); + if (typeof newPrimary === 'string') { + if (newPrimary.startsWith('input_number.')) modes[idx] = true; + else if (newPrimary.startsWith('climate.')) modes[idx] = false; + } + this._config.room_use_input_number = modes; + } catch {} + if (oldPrimary && newPrimary && oldPrimary !== newPrimary){ + const merges = { ...(this._config.merges || {}) }; + if (merges[oldPrimary] && !merges[newPrimary]) { merges[newPrimary] = merges[oldPrimary]; delete merges[oldPrimary]; } + else if (merges[oldPrimary] && merges[newPrimary]) { merges[newPrimary] = Array.from(new Set([...(merges[newPrimary]||[]), ...(merges[oldPrimary]||[])])); delete merges[oldPrimary]; } + this._config.merges = merges; + // migrate temp sensor mapping to new primary key + try { + const sensors = { ...(this._config.temp_sensors || {}) }; + if (sensors[oldPrimary] && !sensors[newPrimary]) sensors[newPrimary] = sensors[oldPrimary]; + if (sensors[oldPrimary]) delete sensors[oldPrimary]; + this._config.temp_sensors = sensors; + } catch {} + // migrate per-room turn_on mapping to new primary key + try { + const to = { ...(this._config.turn_on || {}) }; + if (to[oldPrimary] && !to[newPrimary]) to[newPrimary] = to[oldPrimary]; + if (to[oldPrimary]) delete to[oldPrimary]; + this._config.turn_on = to; + } catch {} + } + // Avoid mutating a potentially frozen array (some browsers/HA paths freeze config) + try { + const ents = [...(this._config.entities || [])]; + ents[idx] = newPrimary; + this._config.entities = ents; + } catch { + // Fallback (should not happen, but keep previous behavior if cloning fails) + try { this._config.entities[idx] = newPrimary; } catch {} + } + // Move open-state key from old -> new entity id + try { + const oldKey = oldPrimary || `#idx:${idx}`; + const newKey = newPrimary || `#idx:${idx}`; + if (this._openRows.has(oldKey)) { this._openRows.delete(oldKey); this._openRows.add(newKey); } + } catch {} + try { nameInp.value = (this._config.labels || {})[newPrimary] || ""; if (nameInp && typeof nameInp.requestUpdate === 'function') nameInp.requestUpdate(); } catch {} + if (this._openCount > 0) this._pendingEmit = true; else this._emit(true); + try { this._pushSettingsToStoreDebounced(); } catch {} + if (!this._roomUsesInputNumber(idx)) refreshChips(); + // refresh per-room turn_on UI after primary change + try { applyTurnOnUi(); } catch {} + // Also update subtitle on primary change + try { + const subEl = line.querySelector('.summary-sub'); + const eid = this._config.entities[idx]; + const linkedNames = (this._config.merges?.[eid] || []).map(id => this._hass?.states?.[id]?.attributes?.friendly_name || (id.split('.')[1]||id)); + const parts = []; + if (eid) parts.push(eid); + if (linkedNames.length) parts.push(`${this._t('editor.merged_with')}: ${linkedNames.join(', ')}`); + if (subEl) subEl.textContent = parts.join(' • '); + } catch {} + }); + + return line; + } + + // (Removed unused pointer-driven drag & drop prototype) + + _addEntity(){ + this._config.entities = [...(this._config.entities||[]), ""]; + try { + const modes = Array.isArray(this._config.room_use_input_number) ? [...this._config.room_use_input_number] : []; + modes.push(false); + this._config.room_use_input_number = modes; + } catch {} + this._emit(true); + } + _upd(key,val){ this._config[key] = val; this._emit(); } + _emit(repaint=false){ + if (this._openCount > 0) { this._pendingEmit = true; return; } + // Emit a fresh cloned config to ensure HA editor detects changes immediately + try { this._config.respect_storage_weekdays = false; } catch {} + let cfg; + try { cfg = JSON.parse(JSON.stringify(this._config)); } + catch { cfg = { ...this._config }; } + // These settings are now edited in the in-card popup + stored in shared/local storage, + // so they should not be emitted into YAML config. + try { + for (const k of ['row_height','default_temp','min_temp','max_temp','auto_apply','apply_on_edit','apply_on_default_change','per_room_defaults','show_pause_button','show_room_temp','pause_sensor_enabled','pause_sensor_entity','presence_sensor_enabled','presence_sensors','presence_sensor_temps','presence_sensor_delays','presence_sensor_delay_units','time_12h','time_source','temp_unit','profiles_enabled','weekdays_enabled','weekdays_view','weekdays_selected_room','boiler_enabled','boiler_switch','boiler_switch_domain','boiler_temp_sensor','boiler_min_temp','boiler_max_temp','boiler_multi_enabled','boiler_room_settings','open_window','color_ranges','color_global','respect_storage_weekdays']) { + if (k in cfg) delete cfg[k]; + } + } catch {} + // Always keep instance config in YAML (per-card) + try { + cfg.instance_enabled = !!this._config?.instance_enabled; + cfg.instance_id = String(this._config?.instance_id || ''); + cfg.instance_uid = String(this._config?.instance_uid || ''); + } catch {} + this.dispatchEvent(new CustomEvent("config-changed", { detail:{ config: cfg }, bubbles:true, composed:true })); + if (repaint) this._render(); + } + + _t(k){ return ttLocalize(k, this._hass || this._lang || 'en'); } + + _applyEditorI18n(){ + try { + + const t = (k)=> this._t(k); + const root = this.shadowRoot; + if (!root) return; + // Title row + const titleRow = root.querySelector('.row .title')?.closest('.row'); + const titleLabel = titleRow?.querySelector('.label') || root.querySelector('.row .label'); + if (titleLabel) titleLabel.textContent = t('editor.title_label'); + const titleField = root.querySelector('ha-textfield.title'); + if (titleField) { + titleField.setAttribute('label', t('editor.title_label')); + // Show localized preset when empty, and keep it updated with language + try { titleField.setAttribute('placeholder', this._t('card.title_default')); } catch {} + } + + // Storage entity (use the .storage picker to find its label robustly) + const storagePicker = root.querySelector('.storage'); + if (storagePicker) { + const storageLabel = storagePicker.closest('.row')?.querySelector('.label'); + // Hide the label text for the storage section + if (storageLabel) storageLabel.textContent = ''; + // toggle texts + const stTitle = root.querySelector('.store-title'); + const stDesc = root.querySelector('.store-desc'); + // Title + info icon (click to open popover) + const stTitleText = root.querySelector('.store-title .store-title-text'); + if (stTitleText) stTitleText.textContent = t('editor.store_enable.title'); + if (stDesc) { + const shortTxt = this._t('editor.store_enable.short'); + const icon = root.querySelector('.info-icon'); + const textSpan = root.querySelector('.store-desc-text'); + if (textSpan) textSpan.textContent = shortTxt !== 'editor.store_enable.short' ? shortTxt : (this._t('editor.store_enable.title') || ''); + + // Build popover once + let pop = root.querySelector('.info-pop'); + if (!pop) { + pop = document.createElement('div'); + pop.className = 'info-pop hidden'; + pop.setAttribute('role','dialog'); + root.append(pop); + } + const openPop = () => { + const desc = this._t('editor.store_enable.desc'); + pop.textContent = desc; + const r = icon.getBoundingClientRect(); + const maxW = Math.min(520, Math.max(240, Math.floor(window.innerWidth*0.9))); + pop.style.maxWidth = maxW + 'px'; + let left = Math.max(8, Math.min(window.innerWidth - maxW - 8, r.left)); + let top = r.bottom + 8; + pop.style.left = left + 'px'; + pop.style.top = top + 'px'; + pop.classList.remove('hidden'); + }; + const closePop = () => { if (pop) pop.classList.add('hidden'); }; + + // Bind click/tap once + if (icon && !icon.dataset.bound) { + icon.dataset.bound = '1'; + icon.addEventListener('click', (e) => { e.stopPropagation(); if (pop.classList.contains('hidden')) openPop(); else closePop(); }); + icon.addEventListener('touchend', (e) => { try { e.preventDefault(); } catch{}; e.stopPropagation(); if (pop.classList.contains('hidden')) openPop(); else closePop(); }, { passive:false }); + // Close on outside click or ESC + const onOutside = (ev)=>{ + const path = ev.composedPath ? ev.composedPath() : []; + if (path.includes(icon) || path.includes(pop)) return; closePop(); + }; + window.addEventListener('click', onOutside); + window.addEventListener('keydown', (ev)=>{ if (ev.key==='Escape') closePop(); }); + } + } + // Check that the thermostat_timeline integration/service exists in Home Assistant + try { + const hasService = !!(this._hass && this._hass.services && this._hass.services['thermostat_timeline']); + let warn = root.querySelector('.store-missing'); + const warnText = this._t ? this._t('editor.store_missing') : null; + const msg = warnText && warnText !== 'editor.store_missing' + ? warnText + : 'Integration "Thermostat Pro Timeline Sync" is not installed. Shared storage is unavailable.'; + var githubLink = ' [Åbn i HACS]'; + if (!warn) { + warn = document.createElement('div'); + warn.className = 'store-missing'; + warn.style.color = 'var(--error-color)'; + warn.style.fontSize = '.9rem'; + warn.style.marginTop = '6px'; + warn.style.display = 'none'; + const controls = root.querySelector('.store-controls'); + if (controls && controls.parentNode) controls.parentNode.insertBefore(warn, controls); + } + // Always update warning content + // Sæt tekst og link som separate child nodes + // Fjern alle eksisterende child nodes + while (warn.firstChild) warn.removeChild(warn.firstChild); + // Tilføj tekst og link igen + warn.appendChild(document.createTextNode(msg.replace(/\.$/, '') + '. ')); + let link = document.createElement('a'); + link.href = 'https://my.home-assistant.io/redirect/hacs_repository/?owner=qlerup&repository=thermostat-pro-timeline-sync'; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + link.style.color = 'var(--error-color)'; + link.style.textDecoration = 'underline'; + link.style.fontWeight = '500'; + link.textContent = this._t('editor.open_in_hacs'); + warn.appendChild(link); + if (!hasService) { + warn.style.display = 'block'; + const sw = root.querySelector('.store-enable'); if (sw) sw.disabled = true; + const picker = root.querySelector('.storage'); if (picker) { try { picker.disabled = true; picker.setAttribute('disabled',''); } catch {} } + } else { + warn.style.display = 'none'; + const sw = root.querySelector('.store-enable'); if (sw) sw.disabled = false; + const picker = root.querySelector('.storage'); if (picker) { try { picker.disabled = false; picker.removeAttribute('disabled'); } catch {} } + } + // Update small status chip in tabs + try { + const chip = root.querySelector('.int-status'); + const icon = chip?.querySelector('ha-icon'); + const txt = chip?.querySelector('.text'); + if (chip && icon && txt) { + if (hasService) { + chip.classList.remove('bad'); chip.classList.add('ok'); + icon.setAttribute('icon', 'mdi:cloud-check-outline'); + txt.textContent = t('editor.integration.connected') || 'Connected to integration'; + } else { + chip.classList.remove('ok'); chip.classList.add('bad'); + icon.setAttribute('icon', 'mdi:cloud-off-outline'); + txt.textContent = t('editor.integration.disconnected') || 'Not connected to integration'; + } + // Make the chip clickable to jump to Settings (Sync is inside Settings) + chip.style.userSelect = 'none'; + chip.addEventListener('click', ()=>{ try { this._activeTab = 'settings'; this._render(); } catch {} }); + } + } catch {} + } catch {} + // (clear storage only button removed) + const clrAllBtn = root.querySelector('.clear-all span'); + if (clrAllBtn) clrAllBtn.textContent = t('editor.clear_all'); + const migBtn = root.querySelector('.migrate-to-store span'); + if (migBtn) migBtn.textContent = t('editor.migrate_to_store'); + } + const clrLocalBtn = root.querySelector('.clear-local-only span'); + if (clrLocalBtn) clrLocalBtn.textContent = t('editor.clear_local_only'); + const resetOnbBtn = root.querySelector('.reset-onboard span'); + if (resetOnbBtn) resetOnbBtn.textContent = t('onboard.reset'); + + // Entities section label (use the .entities container to find its label robustly) + const entitiesWrap = root.querySelector('.entities'); + if (entitiesWrap) { + const entitiesLabel = entitiesWrap.closest('.row')?.querySelector('.label'); + if (entitiesLabel) entitiesLabel.textContent = t('editor.select_entities'); + } + + // Add room button text (target only the actual add-room control) + const addBtn = root.querySelector('.add-room-btn span'); + if (addBtn) addBtn.textContent = t('editor.add_entity'); + + const bMinLbl = root.querySelector('.boiler-min-temp-label'); + if (bMinLbl) bMinLbl.textContent = (t('boiler.min_temp') || 'Min boiler temp (°C)').replace('°C', this._unitSymbol()); + const bMaxLbl = root.querySelector('.boiler-max-temp-label'); + if (bMaxLbl) bMaxLbl.textContent = (t('boiler.max_temp') || 'Max boiler temp (°C)').replace('°C', this._unitSymbol()); + const bEnTitle = root.querySelector('.boiler-enable-title'); + if (bEnTitle) bEnTitle.textContent = t('boiler.enable') || 'Enable boiler controls'; + const bEnDesc = root.querySelector('.boiler-enable-desc'); + if (bEnDesc) bEnDesc.textContent = t('boiler.enable.desc') || ''; + const bSensTitle = root.querySelector('.boiler-sensor-title'); + if (bSensTitle) bSensTitle.textContent = t('boiler.temp_sensor') || 'Boiler temperature sensor'; + const bSensDesc = root.querySelector('.boiler-sensor-desc'); + if (bSensDesc) bSensDesc.textContent = t('boiler.temp_sensor.desc') || ''; + // Tabs labels + try { + const sBtn = root.querySelector('.tab-settings-btn'); if (sBtn) sBtn.textContent = t('editor.tabs.settings'); + const rBtn = root.querySelector('.tab-rooms-btn'); if (rBtn) rBtn.textContent = t('editor.tabs.rooms'); + const oBtn = root.querySelector('.tab-boiler-btn'); if (oBtn) oBtn.textContent = t('editor.tabs.boiler') || 'Boiler'; + const cBtn = root.querySelector('.tab-colors-btn'); if (cBtn) cBtn.textContent = t('editor.tabs.colors'); + const owBtn = root.querySelector('.tab-owd-btn'); if (owBtn) owBtn.textContent = t('editor.tabs.owd') || 'Open Window Detection'; + const bBtn = root.querySelector('.tab-backup-btn'); if (bBtn) bBtn.textContent = t('editor.tabs.backup') || 'Backup'; + const hBtn = root.querySelector('.tab-holidays-btn'); if (hBtn) hBtn.textContent = t('editor.tabs.holidays'); + const boTitle = root.querySelector('.boiler-switch-title'); if (boTitle) boTitle.textContent = t('boiler.switch') || 'Boiler switch'; + // Holidays tab inner labels + try { + const srcTitle = root.querySelector('.tab-holidays .settings-card .setting .title'); if (srcTitle) srcTitle.textContent = t('holidays.source'); + const srcDesc = root.querySelector('.tab-holidays .settings-card .setting .desc'); if (srcDesc) srcDesc.textContent = t('holidays.desc'); + const calTitle = root.querySelector('.tab-holidays .settings-card .setting:nth-child(2) .title'); if (calTitle) calTitle.textContent = t('holidays.entity'); + const manTitle = root.querySelector('.tab-holidays .settings-card .setting:nth-child(3) .title'); if (manTitle) manTitle.textContent = t('holidays.dates'); + const editBtns = root.querySelectorAll('.tab-holidays .inline .actions .btn'); editBtns.forEach(b=>{ try { b.textContent = t('holidays.edit'); } catch{} }); + const segCal = root.querySelector('.tab-holidays .h-src-cal'); if (segCal) segCal.textContent = t('holidays.source.calendar'); + const segMan = root.querySelector('.tab-holidays .h-src-man'); if (segMan) segMan.textContent = t('holidays.source.manual'); + const addDate = root.querySelector('.tab-holidays .add-entity-btn span'); if (addDate) addDate.textContent = t('holidays.add_date'); + } catch {} + // (reload buttons removed) + // Colors mode labels + const cmTitle = root.querySelector('.colors-mode-title'); if (cmTitle) cmTitle.textContent = t('editor.colors.mode'); + const cmDesc = root.querySelector('.colors-mode-desc'); if (cmDesc) cmDesc.textContent = t('editor.colors.mode.desc'); + const cmRoom = root.querySelector('.colors-mode .mode-room'); if (cmRoom) cmRoom.textContent = t('editor.colors.mode_per_room'); + const cmGlob = root.querySelector('.colors-mode .mode-global'); if (cmGlob) cmGlob.textContent = t('editor.colors.mode_global'); + // Colors tab action label (single clear-all button) + const clrAllBtn = root.querySelector('.clr-colors-all-btn span'); if (clrAllBtn) clrAllBtn.textContent = t('editor.colors.clear_all'); + // Away tab labels + const awayTitle = root.querySelector('.away-enable-title'); if (awayTitle) awayTitle.textContent = t('away.enable'); + const awayDesc = root.querySelector('.away-desc'); if (awayDesc) awayDesc.textContent = t('away.desc'); + const awayTemp = root.querySelector('.away-temp-title'); if (awayTemp) awayTemp.textContent = t('away.temp'); + const awayPersons = root.querySelector('.away-persons-title'); if (awayPersons) awayPersons.textContent = t('away.persons'); + // Presence labels in Away tab + const advTitle = root.querySelector('.away-adv-title'); if (advTitle) advTitle.textContent = t('presence.enable_advanced'); + const advDesc = root.querySelector('.away-adv-desc'); if (advDesc) advDesc.textContent = t('presence.enable_advanced.desc'); + const pcTitle = root.querySelector('.presence-combos-title'); if (pcTitle) pcTitle.textContent = t('presence.combos'); + const plTitle = root.querySelector('.presence-live-title'); if (plTitle) plTitle.textContent = t('presence.live_header'); + const plDesc = root.querySelector('.presence-live-desc'); if (plDesc) plDesc.textContent = t('presence.live_header.desc'); + const pcDesc = root.querySelector('.presence-combos-desc'); if (pcDesc) pcDesc.textContent = ''; + } catch {} + + // Sync delay labels removed (controls removed) + + // Backup tab labels + try { + const bt = root.querySelector('.backup-title'); if (bt) bt.textContent = t('backup.title') || 'Backup'; + const bd = root.querySelector('.backup-desc'); if (bd) bd.textContent = t('backup.desc') || 'Copy schedules/settings to backup and restore when needed.'; + // Selection UI removed; no per-section labels to set + const bn = root.querySelector('.backup-now-label'); if (bn) bn.textContent = t('backup.now') || 'Backup now'; + const brl = root.querySelector('.backup-restore-label'); if (brl) brl.textContent = t('backup.restore') || 'Restore'; + const bdl = root.querySelector('.backup-delete-label'); if (bdl) bdl.textContent = t('backup.delete') || 'Delete'; + const bitl = root.querySelector('.backup-import-title'); if (bitl) bitl.textContent = t('backup.import.title') || 'Import thermostat_timeline.json'; + const bidl = root.querySelector('.backup-import-desc'); if (bidl) bidl.textContent = t('backup.import.desc') || ''; + const bibl = root.querySelector('.backup-import-btn-label'); if (bibl) bibl.textContent = t('backup.import.btn') || 'Import file'; + const betl = root.querySelector('.backup-export-title'); if (betl) betl.textContent = t('backup.export.title') || 'Export backup file'; + const bedl = root.querySelector('.backup-export-desc'); if (bedl) bedl.textContent = t('backup.export.desc') || 'Download a full thermostat_timeline_backup.json with all slots and data.'; + const bebl = root.querySelector('.backup-export-btn-label'); if (bebl) bebl.textContent = t('backup.export.btn') || 'Download file'; + const ibt = root.querySelector('.backup-importbk-title'); if (ibt) ibt.textContent = t('backup.importbk.title') || 'Import backup file'; + const ibd = root.querySelector('.backup-importbk-desc'); if (ibd) ibd.textContent = t('backup.importbk.desc') || 'Upload a thermostat_timeline_backup.json file to append its backups to the list.'; + const ibl = root.querySelector('.backup-importbk-btn-label'); if (ibl) ibl.textContent = t('backup.importbk.btn') || 'Import backup'; + const ifc = root.querySelector('.backup-file-choose'); if (ifc) ifc.textContent = t('backup.file.choose') || 'Choose imported backup'; + const ifas = root.querySelector('.backup-file-add-selected'); if (ifas) ifas.textContent = t('backup.file.add_selected') || 'Add selected'; + const ifaa = root.querySelector('.backup-file-add-all'); if (ifaa) ifaa.textContent = t('backup.file.add_all') || 'Add all'; + const bat = root.querySelector('.backup-auto-title'); if (bat) bat.textContent = t('backup.auto'); + const bad = root.querySelector('.backup-auto-desc'); if (bad) bad.textContent = t('backup.auto.desc'); + const bit = root.querySelector('.backup-int-title'); if (bit) bit.textContent = t('backup.interval'); + const bid = root.querySelector('.backup-int-desc'); if (bid) bid.textContent = t('backup.interval.desc'); + const bst = root.querySelector('.backup-sensor-title'); if (bst) bst.textContent = t('backup.sensor'); + const blt = root.querySelector('.backup-last-title'); if (blt) blt.textContent = t('backup.last'); + } catch {} + + } catch(e){ /* ignore */ } + } + + // Detect whether a given title equals any built-in localized default + _isDefaultTitle(txt){ + try { + const s = String(txt || '').trim(); + for (const k of Object.keys(TT_I18N || {})){ + const v = TT_I18N[k] && TT_I18N[k]['card.title_default']; + if (v && String(v).trim() === s) return true; + } + } catch {} + return false; + } +} + +class UiHelper { + // BinarydustFF - fix + // Block's pills (showing temperature) were rendered differently between modes + // Creating a unique helper to unify the rendering method + /** + * Generate a pill element inside a parent block + * @param {ThermostatTimelineCard} ttcInst Current `ThermostatTimelineCard` instance + * @param {HTMLDivElement} parentBlock + * @param {string} content The string content + * @returns {HTMLSpanElement} The generated pill + */ + static genBlockPill(ttcInst, parentBlock, content) { + // Pill creation + const pill = document.createElement('span'); + pill.className = 'pill'; + pill.textContent = content; + parentBlock.append(pill); + + // Pill styling + try { + const blcBgClr = parentBlock.style.backgroundColor || ''; + if (!blcBgClr) { // possible? + // console.warn("ThermostatTimelineCard: Block without background color", parentBlock) + // Skip the styling part + return pill + } + const pillTxtClr = parentBlock.style.color || ttcInst._contrastTextColor(blcBgClr) || ''; + pill.style.backgroundColor = pillTxtClr === '#ffffff' ? 'rgba(255,255,255,0.18)' : 'rgba(0,0,0,0.06)'; + pill.style.backgroundColor = pillTxtClr === '#ffffff' ? 'rgba(255,255,255,0.28)' : 'rgba(0,0,0,0.12)'; + } catch {} + return pill; + } +} + +// Avoid hard failures if the editor gets registered elsewhere/earlier. +try { + if (!customElements.get("thermostat-timeline-card-editor")) { + customElements.define("thermostat-timeline-card-editor", ThermostatTimelineCardEditor); + } +} catch {} + +// Registrér i “Custom cards” +window.customCards = window.customCards || []; +window.customCards.push({ type: "thermostat-timeline-card", name: "Thermostat Pro Timeline Card", description: "24h tidslinje – transition-baseret set_temperature + smart replan & apply-on-change", version: TT_CARD_VERSION }); + +function loadCard() {} +loadCard(); diff --git a/zigbee2mqtt/configuration.yaml b/zigbee2mqtt/configuration.yaml index 626b770..be3285f 100644 --- a/zigbee2mqtt/configuration.yaml +++ b/zigbee2mqtt/configuration.yaml @@ -5,7 +5,8 @@ mqtt: user: addons password: Yie0AhFul1jei0piecootheen2au2opuughoThe0Oishaej6iek3eish8aiyeefe serial: - port: /dev/ttyUSB0 + port: >- + /dev/serial/by-id/usb-Silicon_Labs_CP2102N_USB_to_UART_Bridge_Controller_84c5eaa66cf7f011936a1412364a576b-if00-port0 adapter: zstack baudrate: 115200 rtscts: false @@ -39,7 +40,7 @@ advanced: - 95 - 42 - 151 - enable_external_js: false + enable_external_js: true frontend: enabled: true port: 8099 @@ -66,3 +67,11 @@ devices: friendly_name: Basement Water Sensor '0xffffb40e060893d8': friendly_name: General + '0xa4c1380d0679ffff': + friendly_name: Pool +availability: + passive: + timeout: 4320 + active: + timeout: 10 + pause_on_backoff_gt: 0 diff --git a/zigbee2mqtt/coordinator_backup.json b/zigbee2mqtt/coordinator_backup.json index 27d2c55..83fb9c4 100644 --- a/zigbee2mqtt/coordinator_backup.json +++ b/zigbee2mqtt/coordinator_backup.json @@ -2,9 +2,9 @@ "metadata": { "format": "zigpy/open-coordinator-backup", "version": 1, - "source": "zigbee-herdsman@10.4.0", + "source": "zigbee-herdsman@10.6.1", "internal": { - "date": "2026-06-16T13:48:31.675Z", + "date": "2026-07-07T17:46:32.639Z", "znpVersion": 1 } }, @@ -25,7 +25,7 @@ "network_key": { "key": "0a7ed3caf6adb653119697df9d04ec25", "sequence_number": 0, - "frame_counter": 515761 + "frame_counter": 2037561 }, "devices": [ { @@ -35,7 +35,7 @@ "link_key": { "key": "a59c7f704c547dca7033b287b04ffc4b", "rx_counter": 0, - "tx_counter": 66 + "tx_counter": 661 } }, { @@ -45,7 +45,7 @@ "link_key": { "key": "91fa9ab38f0a04fc102f357e78f61f7d", "rx_counter": 0, - "tx_counter": 66 + "tx_counter": 638 } }, { @@ -55,7 +55,7 @@ "link_key": { "key": "a59a9f9bbb354104241b4a3476c2bd1f", "rx_counter": 0, - "tx_counter": 66 + "tx_counter": 638 } }, { @@ -65,7 +65,7 @@ "link_key": { "key": "d6d47144ee36ca67791986b8f5b74bb2", "rx_counter": 0, - "tx_counter": 66 + "tx_counter": 638 } }, { @@ -75,7 +75,7 @@ "link_key": { "key": "9b47a21f8d33962956b05e040cb24386", "rx_counter": 0, - "tx_counter": 66 + "tx_counter": 638 } }, { @@ -85,7 +85,7 @@ "link_key": { "key": "3537b9fd0d00b2cdf8c045e68c816762", "rx_counter": 0, - "tx_counter": 66 + "tx_counter": 702 } }, { @@ -95,7 +95,7 @@ "link_key": { "key": "85676341f405baf92aaa94bdef843b2c", "rx_counter": 0, - "tx_counter": 33 + "tx_counter": 605 } }, { @@ -105,7 +105,7 @@ "link_key": { "key": "fea770704c547dca2b08bd87b04ffc4b", "rx_counter": 0, - "tx_counter": 55 + "tx_counter": 627 } }, { @@ -115,7 +115,7 @@ "link_key": { "key": "facb3a7e78f61f7d7b1e95b38f0a04fc", "rx_counter": 0, - "tx_counter": 44 + "tx_counter": 627 } }, { @@ -125,7 +125,27 @@ "link_key": { "key": "452689b8f5b74bb2eaeb7e44ee36ca67", "rx_counter": 0, - "tx_counter": 33 + "tx_counter": 621 + } + }, + { + "nwk_address": "9faa", + "ieee_address": "a4c1380d0679ffff", + "is_child": false, + "link_key": { + "key": "89bd998438a05c117e418205b975f3dc", + "rx_counter": 0, + "tx_counter": 0 + } + }, + { + "nwk_address": "2ff8", + "ieee_address": "54ef4410014f65c1", + "is_child": false, + "link_key": { + "key": "7f9e4cb55d7697228385cd3488d95ad5", + "rx_counter": 0, + "tx_counter": 0 } } ]