Merge branch 'dev' into media-player-group-members-without-grouping

This commit is contained in:
Marcel van der Veldt
2026-08-11 00:56:56 +02:00
committed by GitHub
729 changed files with 12293 additions and 4324 deletions
+27 -17
View File
@@ -16,6 +16,10 @@ env:
# Base image version from https://github.com/home-assistant/docker
BASE_IMAGE_VERSION: "2026.07.0"
ARCHITECTURES: '["amd64", "aarch64"]'
MACHINES: >-
["generic-x86-64", "khadas-vim3", "odroid-c2", "odroid-c4", "odroid-m1",
"odroid-n2", "qemuarm-64", "qemux86-64", "raspberrypi3-64",
"raspberrypi4-64", "raspberrypi5-64", "yellow", "green"]
permissions: {}
@@ -35,6 +39,7 @@ jobs:
channel: ${{ steps.version.outputs.channel }}
publish: ${{ steps.version.outputs.publish }}
architectures: ${{ env.ARCHITECTURES }}
machines: ${{ env.MACHINES }}
base_image_version: ${{ env.BASE_IMAGE_VERSION }}
steps:
- name: Checkout the repository
@@ -218,20 +223,7 @@ jobs:
id-token: write # For cosign signing
strategy:
matrix:
machine:
- generic-x86-64
- khadas-vim3
- odroid-c2
- odroid-c4
- odroid-m1
- odroid-n2
- qemuarm-64
- qemux86-64
- raspberrypi3-64
- raspberrypi4-64
- raspberrypi5-64
- yellow
- green
machine: ${{ fromJson(needs.init.outputs.machines) }}
include:
# Default: aarch64 on native ARM runner
- arch: aarch64
@@ -282,11 +274,29 @@ jobs:
push: true
version: ${{ needs.init.outputs.version }}
run_e2e_tests_base:
name: Run E2E tests on base images
if: github.repository_owner == 'home-assistant'
needs: ["init", "build_base"]
uses: ./.github/workflows/e2e-tests.yml
with:
version: ${{ needs.init.outputs.version }}
image_prefixes: ${{ needs.init.outputs.architectures }}
run_e2e_tests_machine:
name: Run E2E tests on machine images
if: github.repository_owner == 'home-assistant'
needs: ["init", "build_machine"]
uses: ./.github/workflows/e2e-tests.yml
with:
version: ${{ needs.init.outputs.version }}
image_prefixes: ${{ needs.init.outputs.machines }}
publish_ha:
name: Publish version files
environment: ${{ needs.init.outputs.channel }}
if: github.repository_owner == 'home-assistant'
needs: ["init", "build_machine"]
needs: ["init", "build_machine", "run_e2e_tests_machine"]
runs-on: ubuntu-latest
permissions:
contents: read
@@ -326,7 +336,7 @@ jobs:
name: Publish to ${{ matrix.registry }}
environment: ${{ needs.init.outputs.channel }}
if: github.repository_owner == 'home-assistant'
needs: ["init", "build_base"]
needs: ["init", "build_base", "run_e2e_tests_base"]
runs-on: ubuntu-latest
permissions:
contents: read # To check out the repository
@@ -461,7 +471,7 @@ jobs:
build_python:
name: Build PyPi package
environment: ${{ needs.init.outputs.channel }}
needs: ["init", "build_base"]
needs: ["init", "build_base", "run_e2e_tests_base"]
runs-on: ubuntu-latest
permissions:
contents: read # To check out the repository
+2 -2
View File
@@ -28,11 +28,11 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
uses: github/codeql-action/init@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@e4fba868fa4b1b91e1fdab776edc8cfbe6e9fb81 # v4.37.3
uses: github/codeql-action/analyze@d1ba80a13dd99fba24a470575428917156a28b43 # v4.37.5
with:
category: "/language:python"
+28 -12
View File
@@ -8,32 +8,48 @@ on:
description: "Image tag or digest to test (e.g. dev, 2026.7.1, 2026.8.0b0, sha256:0a1b2c3d…)"
default: "dev"
required: true
image_prefixes:
description: "JSON array of image prefixes to test. Will be prefixed to `-homeassistant`"
default: >-
["generic-x86-64", "khadas-vim3", "odroid-c2", "odroid-c4",
"odroid-m1", "odroid-n2", "qemuarm-64", "qemux86-64",
"raspberrypi3-64", "raspberrypi4-64", "raspberrypi5-64",
"yellow", "green"]
required: true
workflow_call:
inputs:
version:
description: "Image tag or digest to test (e.g. dev, 2026.7.1, 2026.8.0b0, sha256:0a1b2c3d…)"
required: true
type: string
image_prefixes:
description: "JSON array of image prefixes to test. Will be prefixed to `-homeassistant`"
required: true
type: string
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version }}
group: ${{ github.workflow }}-${{ github.ref }}-${{ inputs.version }}-${{ inputs.image_prefixes }}
cancel-in-progress: true
jobs:
boot_check:
name: Boot check ${{ matrix.arch }} core image
name: Boot check ${{ matrix.image_prefix }} image
if: github.repository_owner == 'home-assistant'
runs-on: ${{ matrix.runs-on }}
# amd64 images need an x86 runner; all others are aarch64
runs-on: ${{ contains(fromJson('["amd64", "generic-x86-64", "qemux86-64"]'), matrix.image_prefix) && 'ubuntu-24.04' || 'ubuntu-24.04-arm' }}
timeout-minutes: 20
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runs-on: ubuntu-24.04
- arch: aarch64
runs-on: ubuntu-24.04-arm
image_prefix: ${{ fromJson(inputs.image_prefixes) }}
env:
BASE_URL: http://localhost:8123
services:
homeassistant:
image: ghcr.io/home-assistant/home-assistant${{ startsWith(inputs.version, 'sha256:') && '@' || ':' }}${{ inputs.version }} # zizmor: ignore[unpinned-images]
# Digests are referenced with '@', tags with ':'
image: ghcr.io/home-assistant/${{ matrix.image_prefix }}-homeassistant${{ startsWith(inputs.version, 'sha256:') && '@' || ':' }}${{ inputs.version }} # zizmor: ignore[unpinned-images]
ports:
- 8123:8123
# Gate steps until Home Assistant answers (60 x 5s ≈ 300s startup budget)
@@ -49,7 +65,7 @@ jobs:
persist-credentials: false
- name: Set up pnpm
uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
uses: pnpm/action-setup@0977fd99725f1db4007ccb2928dbb4e90d06cc86 # v6.0.10
with:
package_json_file: tests/e2e/package.json
@@ -76,7 +92,7 @@ jobs:
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: playwright-report-${{ matrix.arch }}
name: playwright-report-${{ matrix.image_prefix }}
path: |
tests/e2e/playwright-report/
tests/e2e/test-results/
@@ -91,5 +107,5 @@ jobs:
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: container-logs-${{ matrix.arch }}
name: container-logs-${{ matrix.image_prefix }}
path: homeassistant.log
Generated
+1
View File
@@ -2134,6 +2134,7 @@ CLAUDE.md @home-assistant/core
/tests/components/zeversolar/ @kvanzuijlen @mhuiskes
/homeassistant/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
/tests/components/zha/ @dmulcahey @adminiuga @puddly @TheJulianJES
/homeassistant/components/zhong_hong/ @crhan
/homeassistant/components/zimi/ @markhannon
/tests/components/zimi/ @markhannon
/homeassistant/components/zinvolt/ @joostlek
+11 -7
View File
@@ -440,14 +440,18 @@ class APIDomainServicesView(HomeAssistantView):
try:
# shield the service call from cancellation on connection drop
# and track the task so it cannot be garbage collected mid-run
response = await shield(
hass.services.async_call(
domain,
service,
data, # type: ignore[arg-type]
blocking=True,
context=context,
return_response=response_requested,
hass.async_create_task(
hass.services.async_call(
domain,
service,
data, # type: ignore[arg-type]
blocking=True,
context=context,
return_response=response_requested,
),
f"api service call {domain}.{service}",
)
)
except (vol.Invalid, ServiceNotFound) as ex:
@@ -89,7 +89,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: BackblazeConfigEntry) ->
translation_key="cannot_connect",
) from err
except exception.MissingAccountData as err:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
@@ -21,6 +21,9 @@ BUTTON_TYPES: dict[str, ButtonEntityDescription] = {
"fav": ButtonEntityDescription(key="fav", translation_key="fav"),
"open": ButtonEntityDescription(key="open", translation_key="open"),
"close": ButtonEntityDescription(key="close", translation_key="close"),
"second_output": ButtonEntityDescription(
key="second_output", translation_key="second_output"
),
}
_DEFAULT_BUTTON = ButtonEntityDescription(key="button")
@@ -23,6 +23,7 @@ async def async_get_config_entry_diagnostics(
"name": product.name,
"type": product.type,
"model": product.model,
"product": product.product,
"unique_id": product.unique_id,
"firmware_version": product.firmware_version,
"hardware_version": product.hardware_version,
+2 -1
View File
@@ -23,8 +23,9 @@ class BleBoxEntity[_FeatureT: Feature](CoordinatorEntity[BleBoxCoordinator]):
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, product.unique_id)},
manufacturer=product.brand,
model=product.model,
model=product.product,
name=product.name,
sw_version=product.firmware_version,
hw_version=product.hardware_version,
configuration_url=f"http://{product.address}",
)
@@ -46,6 +46,7 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["blebox_uniapi"],
"requirements": ["blebox-uniapi==2.5.5"],
"quality_scale": "gold",
"requirements": ["blebox-uniapi==2.5.6"],
"zeroconf": ["_bbxsrv._tcp.local."]
}
@@ -0,0 +1,85 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: This integration does not provide additional actions.
docs-conditions:
status: exempt
comment: This integration does not provide conditions.
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
docs-triggers:
status: exempt
comment: This integration does not provide triggers.
entity-event-setup:
status: exempt
comment: |
Entities of this integration do not subscribe to events; they use polling.
entity-unique-id: done
has-entity-name: done
runtime-data: done
test-before-configure: done
test-before-setup: done
unique-config-entry: done
# Silver
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters:
status: exempt
comment: This integration does not have an options flow.
docs-installation-parameters: done
entity-unavailable: done
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: done
test-coverage: done
# Gold
devices: done
diagnostics: done
discovery-update-info: done
discovery: done
docs-data-update: done
docs-examples: done
docs-known-limitations: done
docs-supported-devices: done
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices:
status: exempt
comment: |
This integration has a fixed single device per config entry.
entity-category: done
entity-device-class: done
entity-disabled-by-default: done
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow: done
repair-issues:
status: exempt
comment: |
This integration has no cases where raising a repair issue is needed.
stale-devices:
status: exempt
comment: |
This integration has a fixed single device per config entry.
# Platinum
async-dependency: done
inject-websession: done
strict-typing: todo
@@ -78,6 +78,7 @@
"down": { "name": "Down" },
"fav": { "name": "Favorite" },
"open": { "name": "Open" },
"second_output": { "name": "Second output" },
"up": { "name": "Up" }
},
"light": { "channel": { "name": "Channel {index}" } },
@@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["bsblan"],
"quality_scale": "silver",
"requirements": ["python-bsblan==6.1.6"],
"requirements": ["python-bsblan==6.1.8"],
"zeroconf": [
{
"name": "bsb-lan*",
@@ -190,6 +190,10 @@ async def websocket_remove_config_entry_from_device(
config_entry_id = msg["config_entry_id"]
device_id = msg["device_id"]
# A composite device id has no single underlying device to remove; reject it.
if registry.async_is_composite_device_id(device_id):
raise HomeAssistantError("Cannot remove a composite device")
if (config_entry := hass.config_entries.async_get_entry(config_entry_id)) is None:
raise HomeAssistantError("Unknown config entry")
@@ -215,14 +219,8 @@ async def websocket_remove_config_entry_from_device(
"Failed to remove device entry, rejected by integration"
)
# Integration might have removed the config entry already, that is fine.
# The integration might have removed the device already, that is fine.
if registry.async_get(device_id):
entry = registry.async_update_device(
device_id, remove_config_entry_id=config_entry_id
)
registry.async_remove_device(device_id)
entry_as_dict = entry.dict_repr if entry else None
else:
entry_as_dict = None
connection.send_message(websocket_api.result_message(msg["id"], entry_as_dict))
connection.send_message(websocket_api.result_message(msg["id"], None))
@@ -48,8 +48,8 @@ DATA_SCHEMA = vol.Schema(
)
async def _validate_connection(host: str, send_wakeup_prompt: bool) -> bool:
cool = CoolMasterNet(host, DEFAULT_PORT, send_initial_line_feed=send_wakeup_prompt)
async def _validate_connection(host: str, port: int, send_wakeup_prompt: bool) -> bool:
cool = CoolMasterNet(host, port, send_initial_line_feed=send_wakeup_prompt)
units = await cool.status()
return bool(units)
@@ -62,15 +62,14 @@ class CoolmasterConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def _async_get_entry(self, data: dict[str, Any]) -> ConfigFlowResult:
more_options = data.get(CONF_MORE_OPTIONS, {})
supported_modes = [
key for (key, value) in data.items() if key in AVAILABLE_MODES and value
]
return self.async_create_entry(
title=data[CONF_HOST],
data={
CONF_HOST: data[CONF_HOST],
CONF_PORT: DEFAULT_PORT,
CONF_SUPPORTED_MODES: supported_modes,
CONF_SUPPORTED_MODES: [
mode for mode in AVAILABLE_MODES if data.get(mode)
],
CONF_SWING_SUPPORT: data[CONF_SWING_SUPPORT],
CONF_SEND_WAKEUP_PROMPT: more_options.get(
CONF_SEND_WAKEUP_PROMPT, False
@@ -78,6 +77,25 @@ class CoolmasterConfigFlow(ConfigFlow, domain=DOMAIN):
},
)
async def _async_validate_input(
self, user_input: dict[str, Any], port: int
) -> dict[str, str]:
"""Check we can still talk to the bridge and that it reports units."""
more_options = user_input.get(CONF_MORE_OPTIONS, {})
errors: dict[str, str] = {}
try:
has_units = await _validate_connection(
user_input[CONF_HOST],
port,
more_options.get(CONF_SEND_WAKEUP_PROMPT, False),
)
except OSError:
errors["base"] = "cannot_connect"
else:
if not has_units:
errors["base"] = "no_units"
return errors
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -88,23 +106,61 @@ class CoolmasterConfigFlow(ConfigFlow, domain=DOMAIN):
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
errors = {}
host = user_input[CONF_HOST]
more_options = user_input.get(CONF_MORE_OPTIONS, {})
try:
result = await _validate_connection(
host, more_options.get(CONF_SEND_WAKEUP_PROMPT, False)
)
if not result:
errors["base"] = "no_units"
except OSError:
errors["base"] = "cannot_connect"
if errors:
if errors := await self._async_validate_input(user_input, DEFAULT_PORT):
return self.async_show_form(
step_id="user", data_schema=DATA_SCHEMA, errors=errors
)
return self._async_get_entry(user_input)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle reconfiguration of an existing entry."""
reconfigure_entry = self._get_reconfigure_entry()
entry_data = reconfigure_entry.data
errors: dict[str, str] = {}
if user_input is not None:
self._async_abort_entries_match({CONF_HOST: user_input[CONF_HOST]})
more_options = user_input.get(CONF_MORE_OPTIONS, {})
# The port is not part of the form, so keep validating the stored one.
if not (
errors := await self._async_validate_input(
user_input, entry_data[CONF_PORT]
)
):
return self.async_update_reload_and_abort(
reconfigure_entry,
title=user_input[CONF_HOST],
data_updates={
CONF_HOST: user_input[CONF_HOST],
CONF_SUPPORTED_MODES: [
mode for mode in AVAILABLE_MODES if user_input.get(mode)
],
CONF_SWING_SUPPORT: user_input[CONF_SWING_SUPPORT],
CONF_SEND_WAKEUP_PROMPT: more_options.get(
CONF_SEND_WAKEUP_PROMPT, False
),
},
)
supported_modes = entry_data.get(CONF_SUPPORTED_MODES, AVAILABLE_MODES)
return self.async_show_form(
step_id="reconfigure",
data_schema=self.add_suggested_values_to_schema(
DATA_SCHEMA,
user_input
or {
CONF_HOST: entry_data[CONF_HOST],
**{mode: mode in supported_modes for mode in AVAILABLE_MODES},
CONF_SWING_SUPPORT: entry_data.get(CONF_SWING_SUPPORT, False),
CONF_MORE_OPTIONS: {
CONF_SEND_WAKEUP_PROMPT: entry_data.get(
CONF_SEND_WAKEUP_PROMPT, False
)
},
},
),
errors=errors,
)
@@ -1,13 +1,48 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"no_units": "Could not find any HVAC units in CoolMasterNet host."
},
"step": {
"reconfigure": {
"data": {
"cool": "[%key:component::coolmaster::config::step::user::data::cool%]",
"dry": "[%key:component::coolmaster::config::step::user::data::dry%]",
"fan_only": "[%key:component::coolmaster::config::step::user::data::fan_only%]",
"heat": "[%key:component::coolmaster::config::step::user::data::heat%]",
"heat_cool": "[%key:component::coolmaster::config::step::user::data::heat_cool%]",
"host": "[%key:common::config_flow::data::host%]",
"off": "[%key:component::coolmaster::config::step::user::data::off%]",
"swing_support": "[%key:component::coolmaster::config::step::user::data::swing_support%]"
},
"data_description": {
"cool": "[%key:component::coolmaster::config::step::user::data_description::cool%]",
"dry": "[%key:component::coolmaster::config::step::user::data_description::dry%]",
"fan_only": "[%key:component::coolmaster::config::step::user::data_description::fan_only%]",
"heat": "[%key:component::coolmaster::config::step::user::data_description::heat%]",
"heat_cool": "[%key:component::coolmaster::config::step::user::data_description::heat_cool%]",
"host": "[%key:component::coolmaster::config::step::user::data_description::host%]",
"off": "[%key:component::coolmaster::config::step::user::data_description::off%]",
"swing_support": "[%key:component::coolmaster::config::step::user::data_description::swing_support%]"
},
"description": "Update your CoolMasterNet connection details and supported modes.",
"sections": {
"more_options": {
"data": {
"send_wakeup_prompt": "[%key:component::coolmaster::config::step::user::sections::more_options::data::send_wakeup_prompt%]"
},
"data_description": {
"send_wakeup_prompt": "[%key:component::coolmaster::config::step::user::sections::more_options::data_description::send_wakeup_prompt%]"
},
"name": "[%key:component::coolmaster::config::step::user::sections::more_options::name%]"
}
}
},
"user": {
"data": {
"cool": "Support cool mode",
@@ -20,7 +55,14 @@
"swing_support": "Control swing mode"
},
"data_description": {
"host": "The hostname or IP address of your CoolMasterNet device."
"cool": "Enable if your units can cool.",
"dry": "Enable if your units have a dry (dehumidify) mode.",
"fan_only": "Enable if your units can run the fan without heating or cooling.",
"heat": "Enable if your units can heat.",
"heat_cool": "Enable if your units can switch between heating and cooling automatically.",
"host": "The hostname or IP address of your CoolMasterNet device.",
"off": "Allow the units to be turned off from Home Assistant.",
"swing_support": "Expose a swing mode control. This adds one request per unit on every update, so polling is slower."
},
"description": "Set up your CoolMasterNet connection details.",
"sections": {
@@ -7,6 +7,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"loggers": ["pydaikin"],
"requirements": ["pydaikin==2.18.4"],
"requirements": ["pydaikin==2.18.5"],
"zeroconf": ["_dkapi._tcp.local."]
}
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.18.0"]
"requirements": ["env-canada==0.19.0"]
}
@@ -20,7 +20,7 @@ from evohomeasync2.const import (
SZ_USE_DAYLIGHT_SAVE_SWITCHING,
SZ_ZONES,
)
from evohomeasync2.typedefs import EvoLocStatusResponseT, EvoTcsConfigResponseT
from evohomeasync2.typedefs import EvoLocStatusT, EvoTcsConfigResponseT
from homeassistant.const import CONF_SCAN_INTERVAL
from homeassistant.core import HomeAssistant
@@ -60,7 +60,7 @@ class EvoDataUpdateCoordinator(DataUpdateCoordinator):
self.loc_idx = location_idx
self.data: EvoLocStatusResponseT = None # type: ignore[assignment]
self.data: EvoLocStatusT = None # type: ignore[assignment]
self.temps: dict[str, float | None] = {}
self._first_refresh_done = False # get schedules only after first refresh
@@ -114,8 +114,10 @@ class EvoDataUpdateCoordinator(DataUpdateCoordinator):
SZ_USE_DAYLIGHT_SAVE_SWITCHING
],
}
tcs_info: EvoTcsConfigResponseT = self.tcs.config # type: ignore[assignment]
tcs_info[SZ_ZONES] = [zone.config for zone in self.tcs.zones]
tcs_info: EvoTcsConfigResponseT = {
**self.tcs.config,
SZ_ZONES: [zone.config for zone in self.tcs.zones],
}
if self.tcs.hotwater:
tcs_info[SZ_DHW] = self.tcs.hotwater.config
gwy_info = {
@@ -223,7 +225,7 @@ class EvoDataUpdateCoordinator(DataUpdateCoordinator):
self.logger.warning("DHW has an invalid/missing schedule: %r", err)
@override
async def _async_update_data(self) -> EvoLocStatusResponseT: # type: ignore[override]
async def _async_update_data(self) -> EvoLocStatusT: # type: ignore[override]
"""Fetch the latest state of an entire TCC Location.
This includes state data for a Controller and all its child devices, such as the
+4 -2
View File
@@ -14,7 +14,7 @@ from evohomeasync2.const import (
ZoneModelType as EvoZoneModelType,
ZoneType as EvoZoneType,
)
from evohomeasync2.typedefs import EvoDayOfWeekDhwT
from evohomeasync2.typedefs import EvoDhwScheduleDayOfWeekT, EvoZonScheduleDayOfWeekT
from homeassistant.core import callback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@@ -118,7 +118,9 @@ class EvoChild(EvoEntity):
self._evo_id = evo_device.id
self._evo_tcs = evo_device.tcs
self._schedule: list[EvoDayOfWeekDhwT] | None = None
self._schedule: (
list[EvoDhwScheduleDayOfWeekT] | list[EvoZonScheduleDayOfWeekT] | None
) = None
self._setpoints: dict[str, Any] = {}
@property
@@ -6,5 +6,5 @@
"iot_class": "cloud_polling",
"loggers": ["evohomeasync", "evohomeasync2"],
"quality_scale": "legacy",
"requirements": ["evohome-async==2.0.1"]
"requirements": ["evohome-async==2.1.0"]
}
@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260729.5"]
"requirements": ["home-assistant-frontend==20260729.6"]
}
@@ -48,7 +48,7 @@ async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> Any:
) as error:
LOGGER.debug(error.args, exc_info=True)
raise CannotConnect from error
except Exception as error: # pylint: disable=broad-except
except Exception as error:
LOGGER.exception("Unexpected exception")
raise UnknownError from error
@@ -28,7 +28,7 @@ class GardenaBluetoothBinarySensorEntityDescription(BinarySensorEntityDescriptio
@property
def context(self) -> set[str]:
"""Context needed for update coordinator."""
return {self.char.uuid}
return {self.char.unique_id}
DESCRIPTIONS = (
@@ -24,7 +24,7 @@ class GardenaBluetoothButtonEntityDescription(ButtonEntityDescription):
@property
def context(self) -> set[str]:
"""Context needed for update coordinator."""
return {self.char.uuid}
return {self.char.unique_id}
DESCRIPTIONS = (
@@ -2,7 +2,7 @@
from datetime import timedelta
import logging
from typing import override
from typing import Any, override
from gardena_bluetooth.client import Client
from gardena_bluetooth.const import AquaContour, DeviceConfiguration, DeviceInformation
@@ -38,7 +38,7 @@ class DeviceUnavailable(HomeAssistantError):
"""Raised if device can't be found."""
class GardenaBluetoothCoordinator(DataUpdateCoordinator[dict[str, bytes]]):
class GardenaBluetoothCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Class to manage fetching data."""
config_entry: GardenaBluetoothConfigEntry
@@ -62,7 +62,7 @@ class GardenaBluetoothCoordinator(DataUpdateCoordinator[dict[str, bytes]]):
self.address = address
self.data = {}
self.client = client
self.characteristics: set[str] = set()
self.characteristics: dict[str, Characteristic] = {}
self.device_info = DeviceInfo(
identifiers={(DOMAIN, address)},
connections={(dr.CONNECTION_BLUETOOTH, address)},
@@ -98,7 +98,7 @@ class GardenaBluetoothCoordinator(DataUpdateCoordinator[dict[str, bytes]]):
await self._update_timestamp(DeviceConfiguration.unix_timestamp)
await self._update_timestamp(AquaContour.unix_timestamp)
self.characteristics = set(chars.keys())
self.characteristics = chars
self.device_info = DeviceInfo(
{
**self.device_info,
@@ -122,23 +122,30 @@ class GardenaBluetoothCoordinator(DataUpdateCoordinator[dict[str, bytes]]):
LOGGER.debug("No access to update internal time")
@override
async def _async_update_data(self) -> dict[str, bytes]:
async def _async_update_data(self) -> dict[str, Any]:
"""Poll the device."""
uuids: set[str] = {
uuid for context in self.async_contexts() for uuid in context
# A context may name a characteristic this device lacks, so filter up front.
unique_ids: set[str] = {
unique_id
for context in self.async_contexts()
for unique_id in context
if unique_id in self.characteristics
}
if not uuids:
if not unique_ids:
return {}
data: dict[str, bytes] = {}
for uuid in uuids:
data: dict[str, Any] = {}
for unique_id in unique_ids:
char = self.characteristics[unique_id]
try:
data[uuid] = await self.client.read_char_raw(uuid)
data[unique_id] = await self.client.read_char(char)
except CharacteristicNoAccess as exception:
LOGGER.debug("Unable to get data for %s due to %s", uuid, exception)
LOGGER.debug(
"Unable to get data for %s due to %s", unique_id, exception
)
except (GardenaBluetoothException, DeviceUnavailable) as exception:
raise UpdateFailed(
f"Unable to update data for {uuid} due to {exception}"
f"Unable to update data for {unique_id} due to {exception}"
) from exception
return data
@@ -146,9 +153,7 @@ class GardenaBluetoothCoordinator(DataUpdateCoordinator[dict[str, bytes]]):
self, char: Characteristic[CharacteristicType]
) -> CharacteristicType | None:
"""Read cached characteristic."""
if data := self.data.get(char.uuid):
return char.decode(data)
return None
return self.data.get(char.unique_id)
async def write(
self, char: Characteristic[CharacteristicType], value: CharacteristicType
@@ -161,5 +166,5 @@ class GardenaBluetoothCoordinator(DataUpdateCoordinator[dict[str, bytes]]):
f"Unable to write characteristic {char} dur to {exception}"
) from exception
self.data[char.uuid] = char.encode(value)
self.data[char.unique_id] = value
await self.async_refresh()
@@ -44,9 +44,9 @@ class GardenaBluetoothNumberEntityDescription(NumberEntityDescription):
@property
def context(self) -> set[str]:
"""Context needed for update coordinator."""
data = {self.char.uuid}
data = {self.char.unique_id}
if self.connected_state:
data.add(self.connected_state.uuid)
data.add(self.connected_state.unique_id)
return data
@@ -178,7 +178,8 @@ class GardenaBluetoothNumber(GardenaBluetoothDescriptorEntity, NumberEntity):
else:
self._attr_native_value = float(data) / self.entity_description.scale
if char := self.entity_description.connected_state:
char = self.entity_description.connected_state
if char and char.unique_id in self.coordinator.characteristics:
self._attr_available = bool(self.coordinator.get_cached(char))
else:
self._attr_available = True
@@ -210,7 +211,7 @@ class GardenaBluetoothRemainingOpenSetNumber(GardenaBluetoothEntity, NumberEntit
coordinator: GardenaBluetoothCoordinator,
) -> None:
"""Initialize the remaining time entity."""
super().__init__(coordinator, {Valve.remaining_open_time.uuid})
super().__init__(coordinator, {Valve.remaining_open_time.unique_id})
self._attr_unique_id = f"{coordinator.address}-remaining_open_set"
@override
@@ -48,7 +48,7 @@ class GardenaBluetoothSelectEntityDescription(SelectEntityDescription):
@property
def context(self) -> set[str]:
"""Context needed for update coordinator."""
return {self.char.uuid}
return {self.char.unique_id}
DESCRIPTIONS = (
@@ -67,9 +67,9 @@ class GardenaBluetoothSensorEntityDescription[T](SensorEntityDescription):
@property
def context(self) -> set[str]:
"""Context needed for update coordinator."""
data = {self.char.uuid}
data = {self.char.unique_id}
if self.connected_state:
data.add(self.connected_state.uuid)
data.add(self.connected_state.unique_id)
return data
@@ -273,7 +273,8 @@ class GardenaBluetoothSensor(GardenaBluetoothDescriptorEntity, SensorEntity):
value = self.entity_description.get(value)
self._attr_native_value = value
if char := self.entity_description.connected_state:
char = self.entity_description.connected_state
if char and char.unique_id in self.coordinator.characteristics:
self._attr_available = bool(self.coordinator.get_cached(char))
else:
self._attr_available = True
@@ -294,7 +295,7 @@ class GardenaBluetoothRemainSensor(GardenaBluetoothEntity, SensorEntity):
key: str,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, {char.uuid})
super().__init__(coordinator, {char.unique_id})
self._attr_unique_id = f"{coordinator.address}-{key}"
self._attr_translation_key = key
self._char = char
@@ -44,7 +44,7 @@ class GardenaBluetoothValveSwitch(GardenaBluetoothEntity, SwitchEntity):
) -> None:
"""Initialize the switch."""
super().__init__(
coordinator, {Valve.state.uuid, Valve.manual_watering_time.uuid}
coordinator, {Valve.state.unique_id, Valve.manual_watering_time.unique_id}
)
self._attr_unique_id = f"{coordinator.address}-{Valve.state.unique_id}"
self._attr_translation_key = "state"
@@ -59,10 +59,10 @@ class GardenaBluetoothValveSwitch(GardenaBluetoothEntity, SwitchEntity):
@override
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the entity on."""
if not (data := self.coordinator.data.get(Valve.manual_watering_time.uuid)):
value = self.coordinator.get_cached(Valve.manual_watering_time)
if value is None:
raise HomeAssistantError("Unable to get manual activation time.")
value = Valve.manual_watering_time.decode(data)
await self.coordinator.write(Valve.remaining_open_time, value)
self._attr_is_on = True
self.async_write_ha_state()
@@ -24,7 +24,7 @@ class GardenaBluetoothTextEntityDescription(TextEntityDescription):
@property
def context(self) -> set[str]:
"""Context needed for update coordinator."""
return {self.char.uuid}
return {self.char.unique_id}
DESCRIPTIONS = (
@@ -53,7 +53,7 @@ class GardenaBluetoothValve(GardenaBluetoothEntity, ValveEntity):
) -> None:
"""Initialize the switch."""
super().__init__(
coordinator, {Valve.state.uuid, Valve.manual_watering_time.uuid}
coordinator, {Valve.state.unique_id, Valve.manual_watering_time.unique_id}
)
self._attr_unique_id = f"{coordinator.address}-{Valve.state.unique_id}"
+1 -1
View File
@@ -5,7 +5,7 @@ from homeassistant.core import HomeAssistant
from .coordinator import GatusConfigEntry, GatusDataUpdateCoordinator
_PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR]
_PLATFORMS: list[Platform] = [Platform.BINARY_SENSOR, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: GatusConfigEntry) -> bool:
+25 -63
View File
@@ -1,20 +1,16 @@
"""Support for Gatus binary sensors."""
from typing import override
from gatus_api import EndpointStatus, Result
from typing import TYPE_CHECKING, override
from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import GatusConfigEntry, GatusDataUpdateCoordinator
from .entity import GatusEndpointEntity
PARALLEL_UPDATES = 0
@@ -27,19 +23,28 @@ async def async_setup_entry(
"""Set up the Gatus binary sensor platform."""
coordinator = entry.runtime_data
async_add_entities(
GatusEndpointBinarySensor(coordinator, entry, endpoint_key)
for endpoint_key in coordinator.data
)
known_endpoints: set[str] = set()
@callback
def _check_endpoints() -> None:
current_endpoints = set(coordinator.data)
new_endpoints = current_endpoints - known_endpoints
if new_endpoints:
known_endpoints.update(new_endpoints)
async_add_entities(
GatusEndpointBinarySensor(coordinator, entry, endpoint_key)
for endpoint_key in new_endpoints
)
known_endpoints.intersection_update(current_endpoints)
_check_endpoints()
entry.async_on_unload(coordinator.async_add_listener(_check_endpoints))
class GatusEndpointBinarySensor(
CoordinatorEntity[GatusDataUpdateCoordinator], BinarySensorEntity
):
class GatusEndpointBinarySensor(GatusEndpointEntity, BinarySensorEntity):
"""Representation of a Gatus endpoint status."""
_attr_device_class = BinarySensorDeviceClass.CONNECTIVITY
_attr_has_entity_name = True
_attr_name = None
def __init__(
@@ -49,57 +54,14 @@ class GatusEndpointBinarySensor(
endpoint_key: str,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)
self._endpoint_key = endpoint_key
endpoint_data = self.endpoint_data
endpoint_name = endpoint_data.name
if endpoint_data.group is not None:
device_name = f"{endpoint_data.group} {endpoint_name}"
else:
device_name = endpoint_name
super().__init__(coordinator, entry, endpoint_key)
self._attr_unique_id = f"{entry.entry_id}_{endpoint_key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{entry.entry_id}_{endpoint_key}")},
name=device_name,
manufacturer="Gatus",
entry_type=DeviceEntryType.SERVICE,
)
@property
@override
def is_on(self) -> bool | None:
def is_on(self) -> bool:
"""Return true if the endpoint is up and healthy."""
latest_result = self.latest_result
if latest_result is None:
return None
if TYPE_CHECKING:
assert self.latest_result is not None
return latest_result.success
@property
@override
def available(self) -> bool:
"""Return True if entity is available."""
data = self.coordinator.data
# Guard for empty results list, which could imply a brand new endpoint
return (
super().available
and self._endpoint_key in data
and bool(data[self._endpoint_key].results)
)
@property
def endpoint_data(self) -> EndpointStatus:
"""Return this specific endpoint's data from the coordinator."""
return self.coordinator.data[self._endpoint_key]
@property
def latest_result(self) -> Result | None:
"""Return the most recent monitoring result (Gatus appends newest last)."""
results = self.endpoint_data.results
if not results:
return None
return results[-1]
return self.latest_result.success
+63
View File
@@ -0,0 +1,63 @@
"""Base entity for the Gatus integration."""
from typing import override
from gatus_api import EndpointStatus, Result
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import GatusConfigEntry, GatusDataUpdateCoordinator
class GatusEndpointEntity(CoordinatorEntity[GatusDataUpdateCoordinator]):
"""Base class for Gatus endpoint entities."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: GatusDataUpdateCoordinator,
entry: GatusConfigEntry,
endpoint_key: str,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)
self._endpoint_key = endpoint_key
endpoint_data = self.endpoint_data
device_name = (
f"{endpoint_data.group} {endpoint_data.name}"
if endpoint_data.group is not None
else endpoint_data.name
)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, f"{entry.entry_id}_{endpoint_key}")},
name=device_name,
manufacturer="Gatus",
entry_type=DeviceEntryType.SERVICE,
)
@property
@override
def available(self) -> bool:
"""Return True if entity is available."""
data = self.coordinator.data
return (
super().available
and self._endpoint_key in data
and bool(data[self._endpoint_key].results)
)
@property
def endpoint_data(self) -> EndpointStatus:
"""Return this specific endpoint's data from the coordinator."""
return self.coordinator.data[self._endpoint_key]
@property
def latest_result(self) -> Result | None:
"""Return the most recent monitoring result (Gatus appends newest last)."""
return results[-1] if (results := self.endpoint_data.results) else None
+1 -1
View File
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["gatus_api"],
"quality_scale": "silver",
"requirements": ["gatus-api==1.0.3"]
"requirements": ["gatus-api==1.2.0"]
}
@@ -63,7 +63,7 @@ rules:
docs-supported-functions: done
docs-troubleshooting: done
docs-use-cases: done
dynamic-devices: todo
dynamic-devices: done
entity-category: done
entity-device-class: done
entity-disabled-by-default:
+93
View File
@@ -0,0 +1,93 @@
"""Support for Gatus sensors."""
from collections.abc import Callable
from dataclasses import dataclass
from typing import TYPE_CHECKING, override
from gatus_api import Result
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.const import EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import GatusConfigEntry, GatusDataUpdateCoordinator
from .entity import GatusEndpointEntity
PARALLEL_UPDATES = 0
@dataclass(frozen=True, kw_only=True)
class GatusSensorEntityDescription(SensorEntityDescription):
"""Class describing Gatus sensor entities."""
value_fn: Callable[[Result], float | int | str | None]
SENSOR_TYPES: tuple[GatusSensorEntityDescription, ...] = (
GatusSensorEntityDescription(
key="response_time",
translation_key="response_time",
device_class=SensorDeviceClass.DURATION,
native_unit_of_measurement=UnitOfTime.MILLISECONDS,
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda result: (
round(result.duration / 1_000_000, 2)
if result.duration is not None
else None
),
),
GatusSensorEntityDescription(
key="status_code",
translation_key="status_code",
entity_category=EntityCategory.DIAGNOSTIC,
value_fn=lambda result: result.status,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: GatusConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Gatus sensor platform."""
coordinator = entry.runtime_data
async_add_entities(
GatusEndpointSensor(coordinator, entry, endpoint_key, description)
for endpoint_key in coordinator.data
for description in SENSOR_TYPES
)
class GatusEndpointSensor(GatusEndpointEntity, SensorEntity):
"""Representation of a Gatus endpoint sensor."""
entity_description: GatusSensorEntityDescription
def __init__(
self,
coordinator: GatusDataUpdateCoordinator,
entry: GatusConfigEntry,
endpoint_key: str,
description: GatusSensorEntityDescription,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator, entry, endpoint_key)
self.entity_description = description
self._attr_unique_id = f"{entry.entry_id}_{endpoint_key}_{description.key}"
@property
@override
def native_value(self) -> float | int | str | None:
"""Return the state of the sensor."""
if TYPE_CHECKING:
assert self.latest_result is not None
return self.entity_description.value_fn(self.latest_result)
@@ -29,6 +29,16 @@
}
}
},
"entity": {
"sensor": {
"response_time": {
"name": "Response time"
},
"status_code": {
"name": "Status code"
}
}
},
"exceptions": {
"update_failed": {
"message": "Error communicating with Gatus API"
+5 -6
View File
@@ -2,8 +2,8 @@
import asyncio
from collections.abc import Mapping
from datetime import datetime, timedelta
import logging
import time
from typing import Any, override
import httpx
@@ -75,7 +75,7 @@ class GenericCamera(Camera):
"""A generic implementation of an IP camera."""
_last_image: bytes | None
_last_update: datetime
_last_update: float
_update_lock: asyncio.Lock
def __init__(
@@ -114,7 +114,7 @@ class GenericCamera(Camera):
self._last_url = None
self._last_image = None
self._last_update = datetime.min
self._last_update = 0.0
self._update_lock = asyncio.Lock()
self._attr_device_info = DeviceInfo(
@@ -155,13 +155,12 @@ class GenericCamera(Camera):
if (
self._last_image is not None
and url == self._last_url
and self._last_update + timedelta(0, self._attr_frame_interval)
> datetime.now() # pylint: disable=home-assistant-enforce-naive-now
and self._last_update + self._attr_frame_interval > time.time()
):
return self._last_image
try:
update_time = datetime.now() # pylint: disable=home-assistant-enforce-naive-now
update_time = time.time()
async_client = get_async_client(self.hass, verify_ssl=self.verify_ssl)
response = await async_client.get(
url,
@@ -3,10 +3,10 @@
import asyncio
from collections.abc import Mapping
import contextlib
from datetime import datetime
from errno import EHOSTUNREACH, EIO
import io
import logging
import time
from typing import Any, cast, override
from aiohttp import web
@@ -587,8 +587,7 @@ async def ws_start_preview(
if user_input.get(CONF_STILL_IMAGE_URL):
ha_still_url = (
"/api/generic/preview_flow_image"
f"/{msg['flow_id']}?t={datetime.now().isoformat()}" # pylint: disable=home-assistant-enforce-naive-now
f"/api/generic/preview_flow_image/{msg['flow_id']}?t={time.time()}"
)
_LOGGER.debug("Got preview still URL: %s", ha_still_url)
@@ -8,5 +8,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["googleapiclient"],
"requirements": ["gcal-sync==9.1.0", "oauth2client==4.1.3", "ical==14.0.1"]
"requirements": ["gcal-sync==9.1.0", "oauth2client==4.1.3", "ical==14.1.0"]
}
@@ -29,6 +29,7 @@ set_program_and_options:
- consumer_products_coffee_maker_program_beverage_caffe_latte
- consumer_products_coffee_maker_program_beverage_cappuccino
- consumer_products_coffee_maker_program_beverage_coffee
- consumer_products_coffee_maker_program_beverage_coffee_pot
- consumer_products_coffee_maker_program_beverage_espresso
- consumer_products_coffee_maker_program_beverage_espresso_doppio
- consumer_products_coffee_maker_program_beverage_espresso_macchiato
@@ -38,11 +39,27 @@ set_program_and_options:
- consumer_products_coffee_maker_program_beverage_ristretto
- consumer_products_coffee_maker_program_beverage_warm_milk
- consumer_products_coffee_maker_program_beverage_x_l_coffee
- consumer_products_coffee_maker_program_cleaning_modes_appliance_off_rinsing
- consumer_products_coffee_maker_program_cleaning_modes_appliance_on_rinsing
- consumer_products_coffee_maker_program_cleaning_modes_auto_clean
- consumer_products_coffee_maker_program_cleaning_modes_auto_descale
- consumer_products_coffee_maker_program_cleaning_modes_calc_n_clean
- consumer_products_coffee_maker_program_cleaning_modes_clean
- consumer_products_coffee_maker_program_cleaning_modes_clean_brewing_unit_manually
- consumer_products_coffee_maker_program_cleaning_modes_clean_brewing_unit_manually_detailed
- consumer_products_coffee_maker_program_cleaning_modes_clean_outlet_manually
- consumer_products_coffee_maker_program_cleaning_modes_descale
- consumer_products_coffee_maker_program_cleaning_modes_frost_protection
- consumer_products_coffee_maker_program_cleaning_modes_remove_water_filter
- consumer_products_coffee_maker_program_cleaning_modes_replace_water_filter
- consumer_products_coffee_maker_program_cleaning_modes_rinse_milk_system
- consumer_products_coffee_maker_program_coffee_world_americano
- consumer_products_coffee_maker_program_coffee_world_black_eye
- consumer_products_coffee_maker_program_coffee_world_cafe_au_lait
- consumer_products_coffee_maker_program_coffee_world_cafe_con_leche
- consumer_products_coffee_maker_program_coffee_world_cafe_cortado
- consumer_products_coffee_maker_program_coffee_world_cold_brew
- consumer_products_coffee_maker_program_coffee_world_cold_brew_macchiato
- consumer_products_coffee_maker_program_coffee_world_cortado
- consumer_products_coffee_maker_program_coffee_world_dead_eye
- consumer_products_coffee_maker_program_coffee_world_doppio
@@ -54,12 +71,41 @@ set_program_and_options:
- consumer_products_coffee_maker_program_coffee_world_kleiner_brauner
- consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd
- consumer_products_coffee_maker_program_coffee_world_red_eye
- consumer_products_coffee_maker_program_coffee_world_slow_brew
- consumer_products_coffee_maker_program_coffee_world_verlaengerter
- consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun
- consumer_products_coffee_maker_program_coffee_world_wiener_melange
- consumer_products_coffee_maker_program_my_coffee_my_coffee_1
- consumer_products_coffee_maker_program_my_coffee_my_coffee_2
- consumer_products_coffee_maker_program_my_coffee_my_coffee_3
- consumer_products_coffee_maker_program_my_coffee_my_coffee_4
- consumer_products_coffee_maker_program_my_coffee_my_coffee_5
- consumer_products_coffee_maker_program_my_coffee_my_coffee_6
- consumer_products_coffee_maker_program_my_coffee_my_coffee_7
- consumer_products_coffee_maker_program_my_coffee_my_coffee_8
- cooking_common_program_hood_automatic
- cooking_common_program_hood_delayed_shut_off
- cooking_common_program_hood_interval
- cooking_common_program_hood_venting
- cooking_hob_program_frying_sensor_mode
- cooking_hob_program_power_level_mode
- cooking_hob_program_power_move_mode
- cooking_oven_program_cleaning_draining
- cooking_oven_program_cleaning_drying
- cooking_oven_program_cleaning_ecolysis
- cooking_oven_program_cleaning_modes_auto_steam_calibration
- cooking_oven_program_cleaning_pyrolysis
- cooking_oven_program_dish_automatic_conv_steam_boneless_pork_neck_joint
- cooking_oven_program_dish_automatic_microwave_gefluegelteile_auftauen
- cooking_oven_program_dish_automatic_microwave_haehnchenteile
- cooking_oven_program_dish_automatic_microwave_pommes_frites
- cooking_oven_program_dish_recommendation_conv_meat_probe_goose_breast
- cooking_oven_program_dish_recommendation_conv_steam_meat_probe_boneless_pork_neck_joint
- cooking_oven_program_dish_recommendation_conv_steam_meat_probe_turkey_breast
- cooking_oven_program_dish_recommendation_conv_steam_part_cooked_bread_rolls_or_baguette
- cooking_oven_program_dish_recommendation_full_steam_broccoli
- cooking_oven_program_dish_subsequent_cooking
- cooking_oven_program_heating_mode_2_d_hot_air
- cooking_oven_program_heating_mode_3_d_hot_air
- cooking_oven_program_heating_mode_air_fry
- cooking_oven_program_heating_mode_bottom_heating
@@ -68,6 +114,7 @@ set_program_and_options:
- cooking_oven_program_heating_mode_desiccation
- cooking_oven_program_heating_mode_dough_proving
- cooking_oven_program_heating_mode_frozen_heatup_special
- cooking_oven_program_heating_mode_full_surface_grill
- cooking_oven_program_heating_mode_grill_large_area
- cooking_oven_program_heating_mode_grill_small_area
- cooking_oven_program_heating_mode_hot_air
@@ -80,6 +127,7 @@ set_program_and_options:
- cooking_oven_program_heating_mode_hot_air_grilling
- cooking_oven_program_heating_mode_intensive_heat
- cooking_oven_program_heating_mode_keep_warm
- cooking_oven_program_heating_mode_let_rest
- cooking_oven_program_heating_mode_pizza_setting
- cooking_oven_program_heating_mode_pre_heating
- cooking_oven_program_heating_mode_preheat_ovenware
@@ -97,7 +145,13 @@ set_program_and_options:
- cooking_oven_program_microwave_900_watt
- cooking_oven_program_microwave_90_watt
- cooking_oven_program_microwave_max
- cooking_oven_program_steam_modes_dough_proving
- cooking_oven_program_steam_modes_reheat
- cooking_oven_program_steam_modes_steam
- cooking_oven_program_subsequent_mode_continue_cooking
- cooking_oven_program_subsequent_mode_keep_warm
- cooking_oven_program_subsequent_mode_leave_to_rest
- cooking_oven_program_subsequent_mode_microwave
- dishcare_dishwasher_program_auto_1
- dishcare_dishwasher_program_auto_2
- dishcare_dishwasher_program_auto_3
@@ -106,6 +160,7 @@ set_program_and_options:
- dishcare_dishwasher_program_express_sparkle_65
- dishcare_dishwasher_program_glas_40
- dishcare_dishwasher_program_glass_care
- dishcare_dishwasher_program_glass_shine
- dishcare_dishwasher_program_intensiv_45
- dishcare_dishwasher_program_intensiv_70
- dishcare_dishwasher_program_intensiv_power
@@ -114,6 +169,7 @@ set_program_and_options:
- dishcare_dishwasher_program_learning_dishwasher
- dishcare_dishwasher_program_machine_care
- dishcare_dishwasher_program_magic_daily
- dishcare_dishwasher_program_max_efficient
- dishcare_dishwasher_program_maximum_cleaning
- dishcare_dishwasher_program_mixed_load
- dishcare_dishwasher_program_night_wash
@@ -122,6 +178,7 @@ set_program_and_options:
- dishcare_dishwasher_program_pre_rinse
- dishcare_dishwasher_program_quick_45
- dishcare_dishwasher_program_quick_65
- dishcare_dishwasher_program_quick_d
- dishcare_dishwasher_program_steam_fresh
- dishcare_dishwasher_program_super_60
- heating_ventilation_air_conditioning_air_conditioner_program_active_clean
@@ -131,24 +188,39 @@ set_program_and_options:
- heating_ventilation_air_conditioning_air_conditioner_program_fan
- heating_ventilation_air_conditioning_air_conditioner_program_heat
- laundry_care_dryer_program_anti_shrink
- laundry_care_dryer_program_bedlinens
- laundry_care_dryer_program_blankets
- laundry_care_dryer_program_business_shirts
- laundry_care_dryer_program_business_shirts_easy_iron
- laundry_care_dryer_program_cold_refresh_1_piece
- laundry_care_dryer_program_cold_refresh_5_piece
- laundry_care_dryer_program_cold_refresh_business
- laundry_care_dryer_program_cold_refresh_cold_refresh_cold_refresh
- laundry_care_dryer_program_connected_dry
- laundry_care_dryer_program_cotton
- laundry_care_dryer_program_cotton_cotton_eco
- laundry_care_dryer_program_cotton_eco_4060
- laundry_care_dryer_program_delicates
- laundry_care_dryer_program_dessous
- laundry_care_dryer_program_down_feathers
- laundry_care_dryer_program_easy_care_easy_care
- laundry_care_dryer_program_hygiene
- laundry_care_dryer_program_in_basket
- laundry_care_dryer_program_in_basket_wool_basket
- laundry_care_dryer_program_jeans
- laundry_care_dryer_program_maintenance_care_1_maintenance_care_1_quick_care
- laundry_care_dryer_program_maintenance_care_2_maintenance_care_2_depth_care
- laundry_care_dryer_program_mix
- laundry_care_dryer_program_my_time_my_drying_time
- laundry_care_dryer_program_outdoor
- laundry_care_dryer_program_outdoor_sportswear
- laundry_care_dryer_program_pillow
- laundry_care_dryer_program_shirts_15
- laundry_care_dryer_program_super_40
- laundry_care_dryer_program_synthetic
- laundry_care_dryer_program_synthetic_refresh
- laundry_care_dryer_program_time_cold
- laundry_care_dryer_program_time_cold_air_fluff
- laundry_care_dryer_program_time_cold_fix_time_cold_20
- laundry_care_dryer_program_time_cold_fix_time_cold_30
- laundry_care_dryer_program_time_cold_fix_time_cold_60
@@ -157,12 +229,19 @@ set_program_and_options:
- laundry_care_dryer_program_time_warm_fix_time_warm_40
- laundry_care_dryer_program_time_warm_fix_time_warm_60
- laundry_care_dryer_program_towels
- laundry_care_dryer_program_wool_finish
- laundry_care_washer_dryer_program_cotton
- laundry_care_washer_dryer_program_cotton_eco_4060
- laundry_care_washer_dryer_program_delicates_silk
- laundry_care_washer_dryer_program_drum_clean_and_dry_drum_care
- laundry_care_washer_dryer_program_easy_care
- laundry_care_washer_dryer_program_mix
- laundry_care_washer_dryer_program_rinse
- laundry_care_washer_dryer_program_sensitive
- laundry_care_washer_dryer_program_spin
- laundry_care_washer_dryer_program_wash_and_dry_60
- laundry_care_washer_dryer_program_wash_and_dry_90
- laundry_care_washer_dryer_program_wool
- laundry_care_washer_program_auto_30
- laundry_care_washer_program_auto_40
- laundry_care_washer_program_auto_60
@@ -182,6 +261,7 @@ set_program_and_options:
- laundry_care_washer_program_mix
- laundry_care_washer_program_mix_night_wash
- laundry_care_washer_program_monsoon
- laundry_care_washer_program_my_time
- laundry_care_washer_program_outdoor
- laundry_care_washer_program_plush_toy
- laundry_care_washer_program_power_speed_59
@@ -191,9 +271,14 @@ set_program_and_options:
- laundry_care_washer_program_shirts_blouses
- laundry_care_washer_program_spin_spin_drain
- laundry_care_washer_program_sport_fitness
- laundry_care_washer_program_sport_shoes
- laundry_care_washer_program_steaming_steaming
- laundry_care_washer_program_super_153045_super_15
- laundry_care_washer_program_super_153045_super_1530
- laundry_care_washer_program_super_153045_super_30
- laundry_care_washer_program_towels
- laundry_care_washer_program_wash_and_dry_60
- laundry_care_washer_program_wash_and_dry_90
- laundry_care_washer_program_water_proof
- laundry_care_washer_program_wool
air_conditioner_options:
@@ -229,6 +229,7 @@
"consumer_products_coffee_maker_program_beverage_caffe_latte": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_caffe_latte%]",
"consumer_products_coffee_maker_program_beverage_cappuccino": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_cappuccino%]",
"consumer_products_coffee_maker_program_beverage_coffee": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_coffee%]",
"consumer_products_coffee_maker_program_beverage_coffee_pot": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_coffee_pot%]",
"consumer_products_coffee_maker_program_beverage_espresso": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_espresso%]",
"consumer_products_coffee_maker_program_beverage_espresso_doppio": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_espresso_doppio%]",
"consumer_products_coffee_maker_program_beverage_espresso_macchiato": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_espresso_macchiato%]",
@@ -238,11 +239,27 @@
"consumer_products_coffee_maker_program_beverage_ristretto": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_ristretto%]",
"consumer_products_coffee_maker_program_beverage_warm_milk": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_warm_milk%]",
"consumer_products_coffee_maker_program_beverage_x_l_coffee": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_x_l_coffee%]",
"consumer_products_coffee_maker_program_cleaning_modes_appliance_off_rinsing": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_appliance_off_rinsing%]",
"consumer_products_coffee_maker_program_cleaning_modes_appliance_on_rinsing": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_appliance_on_rinsing%]",
"consumer_products_coffee_maker_program_cleaning_modes_auto_clean": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_auto_clean%]",
"consumer_products_coffee_maker_program_cleaning_modes_auto_descale": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_auto_descale%]",
"consumer_products_coffee_maker_program_cleaning_modes_calc_n_clean": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_calc_n_clean%]",
"consumer_products_coffee_maker_program_cleaning_modes_clean": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_clean%]",
"consumer_products_coffee_maker_program_cleaning_modes_clean_brewing_unit_manually": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_clean_brewing_unit_manually%]",
"consumer_products_coffee_maker_program_cleaning_modes_clean_brewing_unit_manually_detailed": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_clean_brewing_unit_manually_detailed%]",
"consumer_products_coffee_maker_program_cleaning_modes_clean_outlet_manually": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_clean_outlet_manually%]",
"consumer_products_coffee_maker_program_cleaning_modes_descale": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_descale%]",
"consumer_products_coffee_maker_program_cleaning_modes_frost_protection": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_frost_protection%]",
"consumer_products_coffee_maker_program_cleaning_modes_remove_water_filter": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_remove_water_filter%]",
"consumer_products_coffee_maker_program_cleaning_modes_replace_water_filter": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_replace_water_filter%]",
"consumer_products_coffee_maker_program_cleaning_modes_rinse_milk_system": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_rinse_milk_system%]",
"consumer_products_coffee_maker_program_coffee_world_americano": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_americano%]",
"consumer_products_coffee_maker_program_coffee_world_black_eye": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_black_eye%]",
"consumer_products_coffee_maker_program_coffee_world_cafe_au_lait": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_cafe_au_lait%]",
"consumer_products_coffee_maker_program_coffee_world_cafe_con_leche": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_cafe_con_leche%]",
"consumer_products_coffee_maker_program_coffee_world_cafe_cortado": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_cafe_cortado%]",
"consumer_products_coffee_maker_program_coffee_world_cold_brew": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_cold_brew%]",
"consumer_products_coffee_maker_program_coffee_world_cold_brew_macchiato": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_cold_brew_macchiato%]",
"consumer_products_coffee_maker_program_coffee_world_cortado": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_cortado%]",
"consumer_products_coffee_maker_program_coffee_world_dead_eye": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_dead_eye%]",
"consumer_products_coffee_maker_program_coffee_world_doppio": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_doppio%]",
@@ -254,12 +271,41 @@
"consumer_products_coffee_maker_program_coffee_world_kleiner_brauner": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_kleiner_brauner%]",
"consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd%]",
"consumer_products_coffee_maker_program_coffee_world_red_eye": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_red_eye%]",
"consumer_products_coffee_maker_program_coffee_world_slow_brew": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_slow_brew%]",
"consumer_products_coffee_maker_program_coffee_world_verlaengerter": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_verlaengerter%]",
"consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun%]",
"consumer_products_coffee_maker_program_coffee_world_wiener_melange": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_wiener_melange%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_1": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_1%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_2": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_2%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_3": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_3%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_4": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_4%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_5": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_5%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_6": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_6%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_7": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_7%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_8": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_8%]",
"cooking_common_program_hood_automatic": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_automatic%]",
"cooking_common_program_hood_delayed_shut_off": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_delayed_shut_off%]",
"cooking_common_program_hood_interval": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_interval%]",
"cooking_common_program_hood_venting": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_venting%]",
"cooking_hob_program_frying_sensor_mode": "[%key:component::home_connect::selector::programs::options::cooking_hob_program_frying_sensor_mode%]",
"cooking_hob_program_power_level_mode": "[%key:component::home_connect::selector::programs::options::cooking_hob_program_power_level_mode%]",
"cooking_hob_program_power_move_mode": "[%key:component::home_connect::selector::programs::options::cooking_hob_program_power_move_mode%]",
"cooking_oven_program_cleaning_draining": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_cleaning_draining%]",
"cooking_oven_program_cleaning_drying": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_cleaning_drying%]",
"cooking_oven_program_cleaning_ecolysis": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_cleaning_ecolysis%]",
"cooking_oven_program_cleaning_modes_auto_steam_calibration": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_cleaning_modes_auto_steam_calibration%]",
"cooking_oven_program_cleaning_pyrolysis": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_cleaning_pyrolysis%]",
"cooking_oven_program_dish_automatic_conv_steam_boneless_pork_neck_joint": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_automatic_conv_steam_boneless_pork_neck_joint%]",
"cooking_oven_program_dish_automatic_microwave_gefluegelteile_auftauen": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_automatic_microwave_gefluegelteile_auftauen%]",
"cooking_oven_program_dish_automatic_microwave_haehnchenteile": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_automatic_microwave_haehnchenteile%]",
"cooking_oven_program_dish_automatic_microwave_pommes_frites": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_automatic_microwave_pommes_frites%]",
"cooking_oven_program_dish_recommendation_conv_meat_probe_goose_breast": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_recommendation_conv_meat_probe_goose_breast%]",
"cooking_oven_program_dish_recommendation_conv_steam_meat_probe_boneless_pork_neck_joint": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_recommendation_conv_steam_meat_probe_boneless_pork_neck_joint%]",
"cooking_oven_program_dish_recommendation_conv_steam_meat_probe_turkey_breast": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_recommendation_conv_steam_meat_probe_turkey_breast%]",
"cooking_oven_program_dish_recommendation_conv_steam_part_cooked_bread_rolls_or_baguette": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_recommendation_conv_steam_part_cooked_bread_rolls_or_baguette%]",
"cooking_oven_program_dish_recommendation_full_steam_broccoli": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_recommendation_full_steam_broccoli%]",
"cooking_oven_program_dish_subsequent_cooking": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_subsequent_cooking%]",
"cooking_oven_program_heating_mode_2_d_hot_air": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_2_d_hot_air%]",
"cooking_oven_program_heating_mode_3_d_hot_air": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_3_d_hot_air%]",
"cooking_oven_program_heating_mode_air_fry": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_air_fry%]",
"cooking_oven_program_heating_mode_bottom_heating": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_bottom_heating%]",
@@ -268,6 +314,7 @@
"cooking_oven_program_heating_mode_desiccation": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_desiccation%]",
"cooking_oven_program_heating_mode_dough_proving": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_dough_proving%]",
"cooking_oven_program_heating_mode_frozen_heatup_special": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_frozen_heatup_special%]",
"cooking_oven_program_heating_mode_full_surface_grill": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_full_surface_grill%]",
"cooking_oven_program_heating_mode_grill_large_area": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_grill_large_area%]",
"cooking_oven_program_heating_mode_grill_small_area": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_grill_small_area%]",
"cooking_oven_program_heating_mode_hot_air": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air%]",
@@ -280,6 +327,7 @@
"cooking_oven_program_heating_mode_hot_air_grilling": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_grilling%]",
"cooking_oven_program_heating_mode_intensive_heat": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_intensive_heat%]",
"cooking_oven_program_heating_mode_keep_warm": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_keep_warm%]",
"cooking_oven_program_heating_mode_let_rest": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_let_rest%]",
"cooking_oven_program_heating_mode_pizza_setting": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_pizza_setting%]",
"cooking_oven_program_heating_mode_pre_heating": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_pre_heating%]",
"cooking_oven_program_heating_mode_preheat_ovenware": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_preheat_ovenware%]",
@@ -297,6 +345,13 @@
"cooking_oven_program_microwave_900_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_900_watt%]",
"cooking_oven_program_microwave_90_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_90_watt%]",
"cooking_oven_program_microwave_max": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_max%]",
"cooking_oven_program_steam_modes_dough_proving": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_steam_modes_dough_proving%]",
"cooking_oven_program_steam_modes_reheat": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_steam_modes_reheat%]",
"cooking_oven_program_steam_modes_steam": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_steam_modes_steam%]",
"cooking_oven_program_subsequent_mode_continue_cooking": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_subsequent_mode_continue_cooking%]",
"cooking_oven_program_subsequent_mode_keep_warm": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_subsequent_mode_keep_warm%]",
"cooking_oven_program_subsequent_mode_leave_to_rest": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_subsequent_mode_leave_to_rest%]",
"cooking_oven_program_subsequent_mode_microwave": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_subsequent_mode_microwave%]",
"dishcare_dishwasher_program_auto_1": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_auto_1%]",
"dishcare_dishwasher_program_auto_2": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_auto_2%]",
"dishcare_dishwasher_program_auto_3": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_auto_3%]",
@@ -305,6 +360,7 @@
"dishcare_dishwasher_program_express_sparkle_65": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_express_sparkle_65%]",
"dishcare_dishwasher_program_glas_40": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_glas_40%]",
"dishcare_dishwasher_program_glass_care": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_glass_care%]",
"dishcare_dishwasher_program_glass_shine": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_glass_shine%]",
"dishcare_dishwasher_program_intensiv_45": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_45%]",
"dishcare_dishwasher_program_intensiv_70": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_70%]",
"dishcare_dishwasher_program_intensiv_power": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_power%]",
@@ -313,6 +369,7 @@
"dishcare_dishwasher_program_learning_dishwasher": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_learning_dishwasher%]",
"dishcare_dishwasher_program_machine_care": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_machine_care%]",
"dishcare_dishwasher_program_magic_daily": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_magic_daily%]",
"dishcare_dishwasher_program_max_efficient": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_max_efficient%]",
"dishcare_dishwasher_program_maximum_cleaning": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_maximum_cleaning%]",
"dishcare_dishwasher_program_mixed_load": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_mixed_load%]",
"dishcare_dishwasher_program_night_wash": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_night_wash%]",
@@ -321,6 +378,7 @@
"dishcare_dishwasher_program_pre_rinse": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_pre_rinse%]",
"dishcare_dishwasher_program_quick_45": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_quick_45%]",
"dishcare_dishwasher_program_quick_65": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_quick_65%]",
"dishcare_dishwasher_program_quick_d": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_quick_d%]",
"dishcare_dishwasher_program_steam_fresh": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_steam_fresh%]",
"dishcare_dishwasher_program_super_60": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_super_60%]",
"heating_ventilation_air_conditioning_air_conditioner_program_active_clean": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_active_clean%]",
@@ -330,24 +388,39 @@
"heating_ventilation_air_conditioning_air_conditioner_program_fan": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_fan%]",
"heating_ventilation_air_conditioning_air_conditioner_program_heat": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_heat%]",
"laundry_care_dryer_program_anti_shrink": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_anti_shrink%]",
"laundry_care_dryer_program_bedlinens": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_bedlinens%]",
"laundry_care_dryer_program_blankets": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_blankets%]",
"laundry_care_dryer_program_business_shirts": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_business_shirts%]",
"laundry_care_dryer_program_business_shirts_easy_iron": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_business_shirts_easy_iron%]",
"laundry_care_dryer_program_cold_refresh_1_piece": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_cold_refresh_1_piece%]",
"laundry_care_dryer_program_cold_refresh_5_piece": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_cold_refresh_5_piece%]",
"laundry_care_dryer_program_cold_refresh_business": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_cold_refresh_business%]",
"laundry_care_dryer_program_cold_refresh_cold_refresh_cold_refresh": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_cold_refresh_cold_refresh_cold_refresh%]",
"laundry_care_dryer_program_connected_dry": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_connected_dry%]",
"laundry_care_dryer_program_cotton": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_cotton%]",
"laundry_care_dryer_program_cotton_cotton_eco": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_cotton_cotton_eco%]",
"laundry_care_dryer_program_cotton_eco_4060": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_cotton_eco_4060%]",
"laundry_care_dryer_program_delicates": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_delicates%]",
"laundry_care_dryer_program_dessous": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_dessous%]",
"laundry_care_dryer_program_down_feathers": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_down_feathers%]",
"laundry_care_dryer_program_easy_care_easy_care": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_easy_care_easy_care%]",
"laundry_care_dryer_program_hygiene": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_hygiene%]",
"laundry_care_dryer_program_in_basket": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_in_basket%]",
"laundry_care_dryer_program_in_basket_wool_basket": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_in_basket_wool_basket%]",
"laundry_care_dryer_program_jeans": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_jeans%]",
"laundry_care_dryer_program_maintenance_care_1_maintenance_care_1_quick_care": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_maintenance_care_1_maintenance_care_1_quick_care%]",
"laundry_care_dryer_program_maintenance_care_2_maintenance_care_2_depth_care": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_maintenance_care_2_maintenance_care_2_depth_care%]",
"laundry_care_dryer_program_mix": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_mix%]",
"laundry_care_dryer_program_my_time_my_drying_time": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_my_time_my_drying_time%]",
"laundry_care_dryer_program_outdoor": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_outdoor%]",
"laundry_care_dryer_program_outdoor_sportswear": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_outdoor_sportswear%]",
"laundry_care_dryer_program_pillow": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_pillow%]",
"laundry_care_dryer_program_shirts_15": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_shirts_15%]",
"laundry_care_dryer_program_super_40": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_super_40%]",
"laundry_care_dryer_program_synthetic": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_synthetic%]",
"laundry_care_dryer_program_synthetic_refresh": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_synthetic_refresh%]",
"laundry_care_dryer_program_time_cold": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_time_cold%]",
"laundry_care_dryer_program_time_cold_air_fluff": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_time_cold_air_fluff%]",
"laundry_care_dryer_program_time_cold_fix_time_cold_20": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_time_cold_fix_time_cold_20%]",
"laundry_care_dryer_program_time_cold_fix_time_cold_30": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_time_cold_fix_time_cold_30%]",
"laundry_care_dryer_program_time_cold_fix_time_cold_60": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_time_cold_fix_time_cold_60%]",
@@ -356,12 +429,19 @@
"laundry_care_dryer_program_time_warm_fix_time_warm_40": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_time_warm_fix_time_warm_40%]",
"laundry_care_dryer_program_time_warm_fix_time_warm_60": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_time_warm_fix_time_warm_60%]",
"laundry_care_dryer_program_towels": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_towels%]",
"laundry_care_dryer_program_wool_finish": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_wool_finish%]",
"laundry_care_washer_dryer_program_cotton": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_cotton%]",
"laundry_care_washer_dryer_program_cotton_eco_4060": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_cotton_eco_4060%]",
"laundry_care_washer_dryer_program_delicates_silk": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_delicates_silk%]",
"laundry_care_washer_dryer_program_drum_clean_and_dry_drum_care": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_drum_clean_and_dry_drum_care%]",
"laundry_care_washer_dryer_program_easy_care": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_easy_care%]",
"laundry_care_washer_dryer_program_mix": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_mix%]",
"laundry_care_washer_dryer_program_rinse": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_rinse%]",
"laundry_care_washer_dryer_program_sensitive": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_sensitive%]",
"laundry_care_washer_dryer_program_spin": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_spin%]",
"laundry_care_washer_dryer_program_wash_and_dry_60": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_wash_and_dry_60%]",
"laundry_care_washer_dryer_program_wash_and_dry_90": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_wash_and_dry_90%]",
"laundry_care_washer_dryer_program_wool": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_wool%]",
"laundry_care_washer_program_auto_30": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_auto_30%]",
"laundry_care_washer_program_auto_40": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_auto_40%]",
"laundry_care_washer_program_auto_60": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_auto_60%]",
@@ -381,6 +461,7 @@
"laundry_care_washer_program_mix": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_mix%]",
"laundry_care_washer_program_mix_night_wash": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_mix_night_wash%]",
"laundry_care_washer_program_monsoon": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_monsoon%]",
"laundry_care_washer_program_my_time": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_my_time%]",
"laundry_care_washer_program_outdoor": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_outdoor%]",
"laundry_care_washer_program_plush_toy": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_plush_toy%]",
"laundry_care_washer_program_power_speed_59": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_power_speed_59%]",
@@ -390,9 +471,14 @@
"laundry_care_washer_program_shirts_blouses": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_shirts_blouses%]",
"laundry_care_washer_program_spin_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_spin_spin_drain%]",
"laundry_care_washer_program_sport_fitness": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_sport_fitness%]",
"laundry_care_washer_program_sport_shoes": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_sport_shoes%]",
"laundry_care_washer_program_steaming_steaming": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_steaming_steaming%]",
"laundry_care_washer_program_super_153045_super_15": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_super_153045_super_15%]",
"laundry_care_washer_program_super_153045_super_1530": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_super_153045_super_1530%]",
"laundry_care_washer_program_super_153045_super_30": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_super_153045_super_30%]",
"laundry_care_washer_program_towels": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_towels%]",
"laundry_care_washer_program_wash_and_dry_60": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_wash_and_dry_60%]",
"laundry_care_washer_program_wash_and_dry_90": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_wash_and_dry_90%]",
"laundry_care_washer_program_water_proof": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_water_proof%]",
"laundry_care_washer_program_wool": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_wool%]"
}
@@ -589,6 +675,7 @@
"consumer_products_coffee_maker_program_beverage_caffe_latte": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_caffe_latte%]",
"consumer_products_coffee_maker_program_beverage_cappuccino": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_cappuccino%]",
"consumer_products_coffee_maker_program_beverage_coffee": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_coffee%]",
"consumer_products_coffee_maker_program_beverage_coffee_pot": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_coffee_pot%]",
"consumer_products_coffee_maker_program_beverage_espresso": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_espresso%]",
"consumer_products_coffee_maker_program_beverage_espresso_doppio": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_espresso_doppio%]",
"consumer_products_coffee_maker_program_beverage_espresso_macchiato": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_espresso_macchiato%]",
@@ -598,11 +685,27 @@
"consumer_products_coffee_maker_program_beverage_ristretto": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_ristretto%]",
"consumer_products_coffee_maker_program_beverage_warm_milk": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_warm_milk%]",
"consumer_products_coffee_maker_program_beverage_x_l_coffee": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_beverage_x_l_coffee%]",
"consumer_products_coffee_maker_program_cleaning_modes_appliance_off_rinsing": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_appliance_off_rinsing%]",
"consumer_products_coffee_maker_program_cleaning_modes_appliance_on_rinsing": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_appliance_on_rinsing%]",
"consumer_products_coffee_maker_program_cleaning_modes_auto_clean": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_auto_clean%]",
"consumer_products_coffee_maker_program_cleaning_modes_auto_descale": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_auto_descale%]",
"consumer_products_coffee_maker_program_cleaning_modes_calc_n_clean": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_calc_n_clean%]",
"consumer_products_coffee_maker_program_cleaning_modes_clean": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_clean%]",
"consumer_products_coffee_maker_program_cleaning_modes_clean_brewing_unit_manually": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_clean_brewing_unit_manually%]",
"consumer_products_coffee_maker_program_cleaning_modes_clean_brewing_unit_manually_detailed": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_clean_brewing_unit_manually_detailed%]",
"consumer_products_coffee_maker_program_cleaning_modes_clean_outlet_manually": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_clean_outlet_manually%]",
"consumer_products_coffee_maker_program_cleaning_modes_descale": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_descale%]",
"consumer_products_coffee_maker_program_cleaning_modes_frost_protection": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_frost_protection%]",
"consumer_products_coffee_maker_program_cleaning_modes_remove_water_filter": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_remove_water_filter%]",
"consumer_products_coffee_maker_program_cleaning_modes_replace_water_filter": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_replace_water_filter%]",
"consumer_products_coffee_maker_program_cleaning_modes_rinse_milk_system": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_cleaning_modes_rinse_milk_system%]",
"consumer_products_coffee_maker_program_coffee_world_americano": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_americano%]",
"consumer_products_coffee_maker_program_coffee_world_black_eye": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_black_eye%]",
"consumer_products_coffee_maker_program_coffee_world_cafe_au_lait": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_cafe_au_lait%]",
"consumer_products_coffee_maker_program_coffee_world_cafe_con_leche": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_cafe_con_leche%]",
"consumer_products_coffee_maker_program_coffee_world_cafe_cortado": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_cafe_cortado%]",
"consumer_products_coffee_maker_program_coffee_world_cold_brew": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_cold_brew%]",
"consumer_products_coffee_maker_program_coffee_world_cold_brew_macchiato": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_cold_brew_macchiato%]",
"consumer_products_coffee_maker_program_coffee_world_cortado": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_cortado%]",
"consumer_products_coffee_maker_program_coffee_world_dead_eye": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_dead_eye%]",
"consumer_products_coffee_maker_program_coffee_world_doppio": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_doppio%]",
@@ -614,12 +717,41 @@
"consumer_products_coffee_maker_program_coffee_world_kleiner_brauner": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_kleiner_brauner%]",
"consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd%]",
"consumer_products_coffee_maker_program_coffee_world_red_eye": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_red_eye%]",
"consumer_products_coffee_maker_program_coffee_world_slow_brew": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_slow_brew%]",
"consumer_products_coffee_maker_program_coffee_world_verlaengerter": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_verlaengerter%]",
"consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun%]",
"consumer_products_coffee_maker_program_coffee_world_wiener_melange": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_coffee_world_wiener_melange%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_1": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_1%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_2": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_2%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_3": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_3%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_4": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_4%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_5": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_5%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_6": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_6%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_7": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_7%]",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_8": "[%key:component::home_connect::selector::programs::options::consumer_products_coffee_maker_program_my_coffee_my_coffee_8%]",
"cooking_common_program_hood_automatic": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_automatic%]",
"cooking_common_program_hood_delayed_shut_off": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_delayed_shut_off%]",
"cooking_common_program_hood_interval": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_interval%]",
"cooking_common_program_hood_venting": "[%key:component::home_connect::selector::programs::options::cooking_common_program_hood_venting%]",
"cooking_hob_program_frying_sensor_mode": "[%key:component::home_connect::selector::programs::options::cooking_hob_program_frying_sensor_mode%]",
"cooking_hob_program_power_level_mode": "[%key:component::home_connect::selector::programs::options::cooking_hob_program_power_level_mode%]",
"cooking_hob_program_power_move_mode": "[%key:component::home_connect::selector::programs::options::cooking_hob_program_power_move_mode%]",
"cooking_oven_program_cleaning_draining": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_cleaning_draining%]",
"cooking_oven_program_cleaning_drying": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_cleaning_drying%]",
"cooking_oven_program_cleaning_ecolysis": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_cleaning_ecolysis%]",
"cooking_oven_program_cleaning_modes_auto_steam_calibration": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_cleaning_modes_auto_steam_calibration%]",
"cooking_oven_program_cleaning_pyrolysis": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_cleaning_pyrolysis%]",
"cooking_oven_program_dish_automatic_conv_steam_boneless_pork_neck_joint": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_automatic_conv_steam_boneless_pork_neck_joint%]",
"cooking_oven_program_dish_automatic_microwave_gefluegelteile_auftauen": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_automatic_microwave_gefluegelteile_auftauen%]",
"cooking_oven_program_dish_automatic_microwave_haehnchenteile": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_automatic_microwave_haehnchenteile%]",
"cooking_oven_program_dish_automatic_microwave_pommes_frites": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_automatic_microwave_pommes_frites%]",
"cooking_oven_program_dish_recommendation_conv_meat_probe_goose_breast": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_recommendation_conv_meat_probe_goose_breast%]",
"cooking_oven_program_dish_recommendation_conv_steam_meat_probe_boneless_pork_neck_joint": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_recommendation_conv_steam_meat_probe_boneless_pork_neck_joint%]",
"cooking_oven_program_dish_recommendation_conv_steam_meat_probe_turkey_breast": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_recommendation_conv_steam_meat_probe_turkey_breast%]",
"cooking_oven_program_dish_recommendation_conv_steam_part_cooked_bread_rolls_or_baguette": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_recommendation_conv_steam_part_cooked_bread_rolls_or_baguette%]",
"cooking_oven_program_dish_recommendation_full_steam_broccoli": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_recommendation_full_steam_broccoli%]",
"cooking_oven_program_dish_subsequent_cooking": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_dish_subsequent_cooking%]",
"cooking_oven_program_heating_mode_2_d_hot_air": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_2_d_hot_air%]",
"cooking_oven_program_heating_mode_3_d_hot_air": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_3_d_hot_air%]",
"cooking_oven_program_heating_mode_air_fry": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_air_fry%]",
"cooking_oven_program_heating_mode_bottom_heating": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_bottom_heating%]",
@@ -628,6 +760,7 @@
"cooking_oven_program_heating_mode_desiccation": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_desiccation%]",
"cooking_oven_program_heating_mode_dough_proving": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_dough_proving%]",
"cooking_oven_program_heating_mode_frozen_heatup_special": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_frozen_heatup_special%]",
"cooking_oven_program_heating_mode_full_surface_grill": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_full_surface_grill%]",
"cooking_oven_program_heating_mode_grill_large_area": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_grill_large_area%]",
"cooking_oven_program_heating_mode_grill_small_area": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_grill_small_area%]",
"cooking_oven_program_heating_mode_hot_air": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air%]",
@@ -640,6 +773,7 @@
"cooking_oven_program_heating_mode_hot_air_grilling": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_hot_air_grilling%]",
"cooking_oven_program_heating_mode_intensive_heat": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_intensive_heat%]",
"cooking_oven_program_heating_mode_keep_warm": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_keep_warm%]",
"cooking_oven_program_heating_mode_let_rest": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_let_rest%]",
"cooking_oven_program_heating_mode_pizza_setting": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_pizza_setting%]",
"cooking_oven_program_heating_mode_pre_heating": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_pre_heating%]",
"cooking_oven_program_heating_mode_preheat_ovenware": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_heating_mode_preheat_ovenware%]",
@@ -657,7 +791,13 @@
"cooking_oven_program_microwave_900_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_900_watt%]",
"cooking_oven_program_microwave_90_watt": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_90_watt%]",
"cooking_oven_program_microwave_max": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_microwave_max%]",
"cooking_oven_program_steam_modes_dough_proving": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_steam_modes_dough_proving%]",
"cooking_oven_program_steam_modes_reheat": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_steam_modes_reheat%]",
"cooking_oven_program_steam_modes_steam": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_steam_modes_steam%]",
"cooking_oven_program_subsequent_mode_continue_cooking": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_subsequent_mode_continue_cooking%]",
"cooking_oven_program_subsequent_mode_keep_warm": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_subsequent_mode_keep_warm%]",
"cooking_oven_program_subsequent_mode_leave_to_rest": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_subsequent_mode_leave_to_rest%]",
"cooking_oven_program_subsequent_mode_microwave": "[%key:component::home_connect::selector::programs::options::cooking_oven_program_subsequent_mode_microwave%]",
"dishcare_dishwasher_program_auto_1": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_auto_1%]",
"dishcare_dishwasher_program_auto_2": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_auto_2%]",
"dishcare_dishwasher_program_auto_3": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_auto_3%]",
@@ -666,6 +806,7 @@
"dishcare_dishwasher_program_express_sparkle_65": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_express_sparkle_65%]",
"dishcare_dishwasher_program_glas_40": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_glas_40%]",
"dishcare_dishwasher_program_glass_care": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_glass_care%]",
"dishcare_dishwasher_program_glass_shine": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_glass_shine%]",
"dishcare_dishwasher_program_intensiv_45": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_45%]",
"dishcare_dishwasher_program_intensiv_70": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_70%]",
"dishcare_dishwasher_program_intensiv_power": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_intensiv_power%]",
@@ -674,6 +815,7 @@
"dishcare_dishwasher_program_learning_dishwasher": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_learning_dishwasher%]",
"dishcare_dishwasher_program_machine_care": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_machine_care%]",
"dishcare_dishwasher_program_magic_daily": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_magic_daily%]",
"dishcare_dishwasher_program_max_efficient": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_max_efficient%]",
"dishcare_dishwasher_program_maximum_cleaning": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_maximum_cleaning%]",
"dishcare_dishwasher_program_mixed_load": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_mixed_load%]",
"dishcare_dishwasher_program_night_wash": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_night_wash%]",
@@ -682,6 +824,7 @@
"dishcare_dishwasher_program_pre_rinse": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_pre_rinse%]",
"dishcare_dishwasher_program_quick_45": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_quick_45%]",
"dishcare_dishwasher_program_quick_65": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_quick_65%]",
"dishcare_dishwasher_program_quick_d": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_quick_d%]",
"dishcare_dishwasher_program_steam_fresh": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_steam_fresh%]",
"dishcare_dishwasher_program_super_60": "[%key:component::home_connect::selector::programs::options::dishcare_dishwasher_program_super_60%]",
"heating_ventilation_air_conditioning_air_conditioner_program_active_clean": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_active_clean%]",
@@ -691,24 +834,39 @@
"heating_ventilation_air_conditioning_air_conditioner_program_fan": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_fan%]",
"heating_ventilation_air_conditioning_air_conditioner_program_heat": "[%key:component::home_connect::selector::programs::options::heating_ventilation_air_conditioning_air_conditioner_program_heat%]",
"laundry_care_dryer_program_anti_shrink": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_anti_shrink%]",
"laundry_care_dryer_program_bedlinens": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_bedlinens%]",
"laundry_care_dryer_program_blankets": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_blankets%]",
"laundry_care_dryer_program_business_shirts": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_business_shirts%]",
"laundry_care_dryer_program_business_shirts_easy_iron": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_business_shirts_easy_iron%]",
"laundry_care_dryer_program_cold_refresh_1_piece": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_cold_refresh_1_piece%]",
"laundry_care_dryer_program_cold_refresh_5_piece": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_cold_refresh_5_piece%]",
"laundry_care_dryer_program_cold_refresh_business": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_cold_refresh_business%]",
"laundry_care_dryer_program_cold_refresh_cold_refresh_cold_refresh": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_cold_refresh_cold_refresh_cold_refresh%]",
"laundry_care_dryer_program_connected_dry": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_connected_dry%]",
"laundry_care_dryer_program_cotton": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_cotton%]",
"laundry_care_dryer_program_cotton_cotton_eco": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_cotton_cotton_eco%]",
"laundry_care_dryer_program_cotton_eco_4060": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_cotton_eco_4060%]",
"laundry_care_dryer_program_delicates": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_delicates%]",
"laundry_care_dryer_program_dessous": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_dessous%]",
"laundry_care_dryer_program_down_feathers": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_down_feathers%]",
"laundry_care_dryer_program_easy_care_easy_care": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_easy_care_easy_care%]",
"laundry_care_dryer_program_hygiene": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_hygiene%]",
"laundry_care_dryer_program_in_basket": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_in_basket%]",
"laundry_care_dryer_program_in_basket_wool_basket": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_in_basket_wool_basket%]",
"laundry_care_dryer_program_jeans": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_jeans%]",
"laundry_care_dryer_program_maintenance_care_1_maintenance_care_1_quick_care": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_maintenance_care_1_maintenance_care_1_quick_care%]",
"laundry_care_dryer_program_maintenance_care_2_maintenance_care_2_depth_care": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_maintenance_care_2_maintenance_care_2_depth_care%]",
"laundry_care_dryer_program_mix": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_mix%]",
"laundry_care_dryer_program_my_time_my_drying_time": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_my_time_my_drying_time%]",
"laundry_care_dryer_program_outdoor": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_outdoor%]",
"laundry_care_dryer_program_outdoor_sportswear": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_outdoor_sportswear%]",
"laundry_care_dryer_program_pillow": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_pillow%]",
"laundry_care_dryer_program_shirts_15": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_shirts_15%]",
"laundry_care_dryer_program_super_40": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_super_40%]",
"laundry_care_dryer_program_synthetic": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_synthetic%]",
"laundry_care_dryer_program_synthetic_refresh": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_synthetic_refresh%]",
"laundry_care_dryer_program_time_cold": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_time_cold%]",
"laundry_care_dryer_program_time_cold_air_fluff": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_time_cold_air_fluff%]",
"laundry_care_dryer_program_time_cold_fix_time_cold_20": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_time_cold_fix_time_cold_20%]",
"laundry_care_dryer_program_time_cold_fix_time_cold_30": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_time_cold_fix_time_cold_30%]",
"laundry_care_dryer_program_time_cold_fix_time_cold_60": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_time_cold_fix_time_cold_60%]",
@@ -717,12 +875,19 @@
"laundry_care_dryer_program_time_warm_fix_time_warm_40": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_time_warm_fix_time_warm_40%]",
"laundry_care_dryer_program_time_warm_fix_time_warm_60": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_time_warm_fix_time_warm_60%]",
"laundry_care_dryer_program_towels": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_towels%]",
"laundry_care_dryer_program_wool_finish": "[%key:component::home_connect::selector::programs::options::laundry_care_dryer_program_wool_finish%]",
"laundry_care_washer_dryer_program_cotton": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_cotton%]",
"laundry_care_washer_dryer_program_cotton_eco_4060": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_cotton_eco_4060%]",
"laundry_care_washer_dryer_program_delicates_silk": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_delicates_silk%]",
"laundry_care_washer_dryer_program_drum_clean_and_dry_drum_care": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_drum_clean_and_dry_drum_care%]",
"laundry_care_washer_dryer_program_easy_care": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_easy_care%]",
"laundry_care_washer_dryer_program_mix": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_mix%]",
"laundry_care_washer_dryer_program_rinse": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_rinse%]",
"laundry_care_washer_dryer_program_sensitive": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_sensitive%]",
"laundry_care_washer_dryer_program_spin": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_spin%]",
"laundry_care_washer_dryer_program_wash_and_dry_60": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_wash_and_dry_60%]",
"laundry_care_washer_dryer_program_wash_and_dry_90": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_wash_and_dry_90%]",
"laundry_care_washer_dryer_program_wool": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_dryer_program_wool%]",
"laundry_care_washer_program_auto_30": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_auto_30%]",
"laundry_care_washer_program_auto_40": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_auto_40%]",
"laundry_care_washer_program_auto_60": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_auto_60%]",
@@ -742,6 +907,7 @@
"laundry_care_washer_program_mix": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_mix%]",
"laundry_care_washer_program_mix_night_wash": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_mix_night_wash%]",
"laundry_care_washer_program_monsoon": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_monsoon%]",
"laundry_care_washer_program_my_time": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_my_time%]",
"laundry_care_washer_program_outdoor": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_outdoor%]",
"laundry_care_washer_program_plush_toy": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_plush_toy%]",
"laundry_care_washer_program_power_speed_59": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_power_speed_59%]",
@@ -751,9 +917,14 @@
"laundry_care_washer_program_shirts_blouses": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_shirts_blouses%]",
"laundry_care_washer_program_spin_spin_drain": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_spin_spin_drain%]",
"laundry_care_washer_program_sport_fitness": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_sport_fitness%]",
"laundry_care_washer_program_sport_shoes": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_sport_shoes%]",
"laundry_care_washer_program_steaming_steaming": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_steaming_steaming%]",
"laundry_care_washer_program_super_153045_super_15": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_super_153045_super_15%]",
"laundry_care_washer_program_super_153045_super_1530": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_super_153045_super_1530%]",
"laundry_care_washer_program_super_153045_super_30": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_super_153045_super_30%]",
"laundry_care_washer_program_towels": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_towels%]",
"laundry_care_washer_program_wash_and_dry_60": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_wash_and_dry_60%]",
"laundry_care_washer_program_wash_and_dry_90": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_wash_and_dry_90%]",
"laundry_care_washer_program_water_proof": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_water_proof%]",
"laundry_care_washer_program_wool": "[%key:component::home_connect::selector::programs::options::laundry_care_washer_program_wool%]"
}
@@ -1598,6 +1769,7 @@
"consumer_products_coffee_maker_program_beverage_caffe_latte": "Caffe latte",
"consumer_products_coffee_maker_program_beverage_cappuccino": "Cappuccino",
"consumer_products_coffee_maker_program_beverage_coffee": "Coffee",
"consumer_products_coffee_maker_program_beverage_coffee_pot": "Coffee pot",
"consumer_products_coffee_maker_program_beverage_espresso": "Espresso",
"consumer_products_coffee_maker_program_beverage_espresso_doppio": "Espresso doppio",
"consumer_products_coffee_maker_program_beverage_espresso_macchiato": "Espresso macchiato",
@@ -1607,11 +1779,27 @@
"consumer_products_coffee_maker_program_beverage_ristretto": "Ristretto",
"consumer_products_coffee_maker_program_beverage_warm_milk": "Warm milk",
"consumer_products_coffee_maker_program_beverage_x_l_coffee": "XL coffee",
"consumer_products_coffee_maker_program_cleaning_modes_appliance_off_rinsing": "Appliance off rinsing",
"consumer_products_coffee_maker_program_cleaning_modes_appliance_on_rinsing": "Appliance on rinsing",
"consumer_products_coffee_maker_program_cleaning_modes_auto_clean": "Auto clean",
"consumer_products_coffee_maker_program_cleaning_modes_auto_descale": "Auto descale",
"consumer_products_coffee_maker_program_cleaning_modes_calc_n_clean": "Calc'N'Clean",
"consumer_products_coffee_maker_program_cleaning_modes_clean": "Clean",
"consumer_products_coffee_maker_program_cleaning_modes_clean_brewing_unit_manually": "Clean brewing unit manually",
"consumer_products_coffee_maker_program_cleaning_modes_clean_brewing_unit_manually_detailed": "Clean brewing unit manually detailed",
"consumer_products_coffee_maker_program_cleaning_modes_clean_outlet_manually": "Clean outlet manually",
"consumer_products_coffee_maker_program_cleaning_modes_descale": "Descale",
"consumer_products_coffee_maker_program_cleaning_modes_frost_protection": "Frost protection",
"consumer_products_coffee_maker_program_cleaning_modes_remove_water_filter": "Remove water filter",
"consumer_products_coffee_maker_program_cleaning_modes_replace_water_filter": "Replace water filter",
"consumer_products_coffee_maker_program_cleaning_modes_rinse_milk_system": "Rinse milk system",
"consumer_products_coffee_maker_program_coffee_world_americano": "Americano",
"consumer_products_coffee_maker_program_coffee_world_black_eye": "Black eye",
"consumer_products_coffee_maker_program_coffee_world_cafe_au_lait": "Cafe au lait",
"consumer_products_coffee_maker_program_coffee_world_cafe_con_leche": "Cafe con leche",
"consumer_products_coffee_maker_program_coffee_world_cafe_cortado": "Cafe cortado",
"consumer_products_coffee_maker_program_coffee_world_cold_brew": "Cold brew",
"consumer_products_coffee_maker_program_coffee_world_cold_brew_macchiato": "Cold brew macchiato",
"consumer_products_coffee_maker_program_coffee_world_cortado": "Cortado",
"consumer_products_coffee_maker_program_coffee_world_dead_eye": "Dead eye",
"consumer_products_coffee_maker_program_coffee_world_doppio": "Doppio",
@@ -1623,12 +1811,41 @@
"consumer_products_coffee_maker_program_coffee_world_kleiner_brauner": "Kleiner Brauner",
"consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd": "Koffie verkeerd",
"consumer_products_coffee_maker_program_coffee_world_red_eye": "Red eye",
"consumer_products_coffee_maker_program_coffee_world_slow_brew": "Slow brew",
"consumer_products_coffee_maker_program_coffee_world_verlaengerter": "Verlaengerter",
"consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun": "Verlaengerter braun",
"consumer_products_coffee_maker_program_coffee_world_wiener_melange": "Wiener Melange",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_1": "My coffee 1",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_2": "My coffee 2",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_3": "My coffee 3",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_4": "My coffee 4",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_5": "My coffee 5",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_6": "My coffee 6",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_7": "My coffee 7",
"consumer_products_coffee_maker_program_my_coffee_my_coffee_8": "My coffee 8",
"cooking_common_program_hood_automatic": "Automatic",
"cooking_common_program_hood_delayed_shut_off": "Delayed shut off",
"cooking_common_program_hood_interval": "Interval",
"cooking_common_program_hood_venting": "Venting",
"cooking_hob_program_frying_sensor_mode": "Frying sensor mode",
"cooking_hob_program_power_level_mode": "Power level mode",
"cooking_hob_program_power_move_mode": "Power move mode",
"cooking_oven_program_cleaning_draining": "Draining",
"cooking_oven_program_cleaning_drying": "Drying",
"cooking_oven_program_cleaning_ecolysis": "Ecolysis",
"cooking_oven_program_cleaning_modes_auto_steam_calibration": "Auto steam calibration",
"cooking_oven_program_cleaning_pyrolysis": "Pyrolysis",
"cooking_oven_program_dish_automatic_conv_steam_boneless_pork_neck_joint": "Boneless pork neck joint",
"cooking_oven_program_dish_automatic_microwave_gefluegelteile_auftauen": "Poultry defrost",
"cooking_oven_program_dish_automatic_microwave_haehnchenteile": "Chicken pieces",
"cooking_oven_program_dish_automatic_microwave_pommes_frites": "Pommes frites",
"cooking_oven_program_dish_recommendation_conv_meat_probe_goose_breast": "Goose breast",
"cooking_oven_program_dish_recommendation_conv_steam_meat_probe_boneless_pork_neck_joint": "Boneless pork neck joint",
"cooking_oven_program_dish_recommendation_conv_steam_meat_probe_turkey_breast": "Turkey breast",
"cooking_oven_program_dish_recommendation_conv_steam_part_cooked_bread_rolls_or_baguette": "Part cooked bread rolls or baguette",
"cooking_oven_program_dish_recommendation_full_steam_broccoli": "Broccoli",
"cooking_oven_program_dish_subsequent_cooking": "Subsequent cooking",
"cooking_oven_program_heating_mode_2_d_hot_air": "2D hot air",
"cooking_oven_program_heating_mode_3_d_hot_air": "3D hot air",
"cooking_oven_program_heating_mode_air_fry": "Air fry",
"cooking_oven_program_heating_mode_bottom_heating": "Bottom heating",
@@ -1637,6 +1854,7 @@
"cooking_oven_program_heating_mode_desiccation": "Desiccation",
"cooking_oven_program_heating_mode_dough_proving": "Dough proving",
"cooking_oven_program_heating_mode_frozen_heatup_special": "Special heat-up for frozen products",
"cooking_oven_program_heating_mode_full_surface_grill": "Full surface grill",
"cooking_oven_program_heating_mode_grill_large_area": "Grill (large area)",
"cooking_oven_program_heating_mode_grill_small_area": "Grill (small area)",
"cooking_oven_program_heating_mode_hot_air": "Hot air",
@@ -1649,6 +1867,7 @@
"cooking_oven_program_heating_mode_hot_air_grilling": "Hot air grilling",
"cooking_oven_program_heating_mode_intensive_heat": "Intensive heat",
"cooking_oven_program_heating_mode_keep_warm": "Keep warm",
"cooking_oven_program_heating_mode_let_rest": "Let rest",
"cooking_oven_program_heating_mode_pizza_setting": "Pizza setting",
"cooking_oven_program_heating_mode_pre_heating": "Pre-heating",
"cooking_oven_program_heating_mode_preheat_ovenware": "Preheat ovenware",
@@ -1666,7 +1885,13 @@
"cooking_oven_program_microwave_900_watt": "900 Watt",
"cooking_oven_program_microwave_90_watt": "90 Watt",
"cooking_oven_program_microwave_max": "Max",
"cooking_oven_program_steam_modes_dough_proving": "Dough proving",
"cooking_oven_program_steam_modes_reheat": "Reheat",
"cooking_oven_program_steam_modes_steam": "Steam mode",
"cooking_oven_program_subsequent_mode_continue_cooking": "Continue cooking",
"cooking_oven_program_subsequent_mode_keep_warm": "Keep warm",
"cooking_oven_program_subsequent_mode_leave_to_rest": "Leave to rest",
"cooking_oven_program_subsequent_mode_microwave": "Microwave",
"dishcare_dishwasher_program_auto_1": "Auto 1",
"dishcare_dishwasher_program_auto_2": "Auto 2",
"dishcare_dishwasher_program_auto_3": "Auto 3",
@@ -1675,6 +1900,7 @@
"dishcare_dishwasher_program_express_sparkle_65": "Express sparkle 65ºC",
"dishcare_dishwasher_program_glas_40": "Glass 40ºC",
"dishcare_dishwasher_program_glass_care": "Glass care",
"dishcare_dishwasher_program_glass_shine": "Glass shine",
"dishcare_dishwasher_program_intensiv_45": "Intensive 45ºC",
"dishcare_dishwasher_program_intensiv_70": "Intensive 70ºC",
"dishcare_dishwasher_program_intensiv_power": "Intensive power",
@@ -1683,6 +1909,7 @@
"dishcare_dishwasher_program_learning_dishwasher": "Intelligent",
"dishcare_dishwasher_program_machine_care": "Machine care",
"dishcare_dishwasher_program_magic_daily": "Magic daily",
"dishcare_dishwasher_program_max_efficient": "Max efficient",
"dishcare_dishwasher_program_maximum_cleaning": "Maximum cleaning",
"dishcare_dishwasher_program_mixed_load": "Mixed load",
"dishcare_dishwasher_program_night_wash": "Night wash",
@@ -1691,6 +1918,7 @@
"dishcare_dishwasher_program_pre_rinse": "Pre-rinse",
"dishcare_dishwasher_program_quick_45": "Quick 45ºC",
"dishcare_dishwasher_program_quick_65": "Quick 65ºC",
"dishcare_dishwasher_program_quick_d": "Quick d",
"dishcare_dishwasher_program_steam_fresh": "Steam fresh",
"dishcare_dishwasher_program_super_60": "Super 60ºC",
"heating_ventilation_air_conditioning_air_conditioner_program_active_clean": "Active clean",
@@ -1700,24 +1928,39 @@
"heating_ventilation_air_conditioning_air_conditioner_program_fan": "Fan",
"heating_ventilation_air_conditioning_air_conditioner_program_heat": "Heat",
"laundry_care_dryer_program_anti_shrink": "Anti shrink",
"laundry_care_dryer_program_bedlinens": "Bed linens",
"laundry_care_dryer_program_blankets": "Blankets",
"laundry_care_dryer_program_business_shirts": "Business shirts",
"laundry_care_dryer_program_business_shirts_easy_iron": "Business shirts - Easy iron",
"laundry_care_dryer_program_cold_refresh_1_piece": "Cold refresh - 1 piece",
"laundry_care_dryer_program_cold_refresh_5_piece": "Cold refresh - 5 piece",
"laundry_care_dryer_program_cold_refresh_business": "Cold refresh - Business",
"laundry_care_dryer_program_cold_refresh_cold_refresh_cold_refresh": "Cold refresh",
"laundry_care_dryer_program_connected_dry": "Connected dry",
"laundry_care_dryer_program_cotton": "Cotton",
"laundry_care_dryer_program_cotton_cotton_eco": "Cotton eco",
"laundry_care_dryer_program_cotton_eco_4060": "Cotton eco 40/60ºC",
"laundry_care_dryer_program_delicates": "Delicates",
"laundry_care_dryer_program_dessous": "Dessous",
"laundry_care_dryer_program_down_feathers": "Down feathers",
"laundry_care_dryer_program_easy_care_easy_care": "Easy care",
"laundry_care_dryer_program_hygiene": "Hygiene",
"laundry_care_dryer_program_in_basket": "In basket",
"laundry_care_dryer_program_in_basket_wool_basket": "Wool basket",
"laundry_care_dryer_program_jeans": "Jeans",
"laundry_care_dryer_program_maintenance_care_1_maintenance_care_1_quick_care": "Quick care",
"laundry_care_dryer_program_maintenance_care_2_maintenance_care_2_depth_care": "Depth care",
"laundry_care_dryer_program_mix": "Mix",
"laundry_care_dryer_program_my_time_my_drying_time": "My drying time",
"laundry_care_dryer_program_outdoor": "Outdoor",
"laundry_care_dryer_program_outdoor_sportswear": "Sportswear",
"laundry_care_dryer_program_pillow": "Pillow",
"laundry_care_dryer_program_shirts_15": "Shirts 15ºC",
"laundry_care_dryer_program_super_40": "Super 40ºC",
"laundry_care_dryer_program_synthetic": "Synthetic",
"laundry_care_dryer_program_synthetic_refresh": "Synthetic refresh",
"laundry_care_dryer_program_time_cold": "Cold (variable time)",
"laundry_care_dryer_program_time_cold_air_fluff": "Air fluff",
"laundry_care_dryer_program_time_cold_fix_time_cold_20": "Cold (20 min)",
"laundry_care_dryer_program_time_cold_fix_time_cold_30": "Cold (30 min)",
"laundry_care_dryer_program_time_cold_fix_time_cold_60": "Cold (60 min)",
@@ -1726,12 +1969,19 @@
"laundry_care_dryer_program_time_warm_fix_time_warm_40": "Warm (40 min)",
"laundry_care_dryer_program_time_warm_fix_time_warm_60": "Warm (60 min)",
"laundry_care_dryer_program_towels": "Towels",
"laundry_care_dryer_program_wool_finish": "Wool finish",
"laundry_care_washer_dryer_program_cotton": "Cotton",
"laundry_care_washer_dryer_program_cotton_eco_4060": "Cotton eco 40/60ºC",
"laundry_care_washer_dryer_program_delicates_silk": "Delicates silk",
"laundry_care_washer_dryer_program_drum_clean_and_dry_drum_care": "Drum care",
"laundry_care_washer_dryer_program_easy_care": "Easy care",
"laundry_care_washer_dryer_program_mix": "Mix",
"laundry_care_washer_dryer_program_rinse": "Rinse",
"laundry_care_washer_dryer_program_sensitive": "Sensitive",
"laundry_care_washer_dryer_program_spin": "Spin",
"laundry_care_washer_dryer_program_wash_and_dry_60": "Wash and dry (60 min)",
"laundry_care_washer_dryer_program_wash_and_dry_90": "Wash and dry (90 min)",
"laundry_care_washer_dryer_program_wool": "Wool",
"laundry_care_washer_program_auto_30": "Auto 30ºC",
"laundry_care_washer_program_auto_40": "Auto 40ºC",
"laundry_care_washer_program_auto_60": "Auto 60ºC",
@@ -1751,6 +2001,7 @@
"laundry_care_washer_program_mix": "Mix",
"laundry_care_washer_program_mix_night_wash": "Mix night wash",
"laundry_care_washer_program_monsoon": "Monsoon",
"laundry_care_washer_program_my_time": "My time",
"laundry_care_washer_program_outdoor": "Outdoor",
"laundry_care_washer_program_plush_toy": "Plush toy",
"laundry_care_washer_program_power_speed_59": "Power speed <59 min",
@@ -1760,9 +2011,14 @@
"laundry_care_washer_program_shirts_blouses": "Shirts/blouses",
"laundry_care_washer_program_spin_spin_drain": "Spin/drain",
"laundry_care_washer_program_sport_fitness": "Sport/fitness",
"laundry_care_washer_program_sport_shoes": "Sport shoes",
"laundry_care_washer_program_steaming_steaming": "Steaming",
"laundry_care_washer_program_super_153045_super_15": "Super 15 min",
"laundry_care_washer_program_super_153045_super_1530": "Super 15/30 min",
"laundry_care_washer_program_super_153045_super_30": "Super 30 min",
"laundry_care_washer_program_towels": "Towels",
"laundry_care_washer_program_wash_and_dry_60": "Wash and dry 60 min",
"laundry_care_washer_program_wash_and_dry_90": "Wash and dry 90 min",
"laundry_care_washer_program_water_proof": "Water proof",
"laundry_care_washer_program_wool": "Wool"
}
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["homematicip"],
"requirements": ["homematicip==2.13.4"]
"requirements": ["homematicip==2.14.0"]
}
@@ -132,8 +132,8 @@ class HortimaxCoordinator(DataUpdateCoordinator[dict[str, HortimaxDeviceData]]):
"""
registry = dr.async_get(self.hass)
for key, name in device_data.source_names.items():
device = registry.async_get_device(
identifiers={(DOMAIN, f"{device_id}::{key}")}
device = registry.async_get_device_by_identifier(
(DOMAIN, f"{device_id}::{key}"), self.config_entry.entry_id
)
if device is not None and device.name != name:
registry.async_update_device(device.id, name=name)
@@ -74,10 +74,10 @@ class AutomowerCalendarEntity(AutomowerBaseEntity, CalendarEntity):
if not program_event:
return None
work_area_name = None
if self.mower_attributes.work_area_dict and program_event.work_area_id:
work_area_name = self.mower_attributes.work_area_dict[
program_event.work_area_id
]
if (work_area_dict := self.mower_attributes.work_area_dict) and (
work_area_id := program_event.work_area_id
) is not None:
work_area_name = work_area_dict.get(work_area_id)
name_str = make_name_string(work_area_name, program_event.schedule_no)
return CalendarEvent(
summary=f"{self.device_name} {name_str}",
@@ -104,10 +104,10 @@ class AutomowerCalendarEntity(AutomowerBaseEntity, CalendarEntity):
calendar_events = []
for program_event in cursor:
work_area_name = None
if self.mower_attributes.work_area_dict and program_event.work_area_id:
work_area_name = self.mower_attributes.work_area_dict[
program_event.work_area_id
]
if (work_area_dict := self.mower_attributes.work_area_dict) and (
work_area_id := program_event.work_area_id
) is not None:
work_area_name = work_area_dict.get(work_area_id)
name_str = make_name_string(work_area_name, program_event.schedule_no)
calendar_events.append(
CalendarEvent(
@@ -249,6 +249,7 @@ class AutomowerDataUpdateCoordinator(DataUpdateCoordinator[MowerDictionary]):
for mower_id, mower_data in self.data.items()
if mower_data.capabilities.stay_out_zones
and mower_data.stay_out_zones is not None
and mower_data.stay_out_zones.zones is not None
}
entity_registry = er.async_get(self.hass)
@@ -4,7 +4,7 @@ import asyncio
from collections.abc import Callable, Coroutine
import functools
import logging
from typing import TYPE_CHECKING, Any, Concatenate, overload, override
from typing import Any, Concatenate, overload, override
from aioautomower.exceptions import ApiError
from aioautomower.model import MowerActivities, MowerAttributes, MowerStates, WorkArea
@@ -170,22 +170,26 @@ class WorkAreaAvailableEntity(AutomowerControlEntity):
self.work_area_id = work_area_id
@property
def work_areas(self) -> dict[int, WorkArea]:
def work_areas(self) -> dict[int, WorkArea] | None:
"""Get the work areas from the mower attributes."""
if TYPE_CHECKING:
assert self.mower_attributes.work_areas is not None
return self.mower_attributes.work_areas
@property
def work_area_attributes(self) -> WorkArea:
def work_area_attributes(self) -> WorkArea | None:
"""Get the work area attributes of the current work area."""
return self.work_areas[self.work_area_id]
if (work_areas := self.work_areas) is None:
return None
return work_areas.get(self.work_area_id)
@property
@override
def available(self) -> bool:
"""Return True if the work area is available and the mower has no errors."""
return super().available and self.work_area_id in self.work_areas
return (
super().available
and self.work_areas is not None
and self.work_area_id in self.work_areas
)
class WorkAreaControlEntity(WorkAreaAvailableEntity, AutomowerControlEntity):
@@ -1,7 +1,7 @@
"""Husqvarna Automower lawn mower entity."""
from datetime import timedelta
from typing import TYPE_CHECKING, override
from typing import override
from aioautomower.model import MowerActivities, MowerStates, WorkArea
@@ -141,9 +141,7 @@ class AutomowerLawnMowerEntity(AutomowerBaseEntity, LawnMowerEntity):
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="work_areas_not_supported"
)
if TYPE_CHECKING:
assert self.work_areas is not None
if work_area_id not in self.work_areas:
if (work_areas := self.work_areas) is None or work_area_id not in work_areas:
raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="work_area_not_existing"
)
@@ -201,6 +201,9 @@ class WorkAreaNumberEntity(WorkAreaControlEntity, NumberEntity):
super().__init__(mower_id, coordinator, work_area_id)
self.entity_description = description
self._attr_unique_id = f"{mower_id}_{work_area_id}_{description.key}"
if TYPE_CHECKING:
# Work area does not get created if it is None
assert self.work_area_attributes is not None
self._attr_translation_placeholders = {
"work_area": self.work_area_attributes.name
}
@@ -215,9 +218,11 @@ class WorkAreaNumberEntity(WorkAreaControlEntity, NumberEntity):
@property
@override
def native_value(self) -> float:
def native_value(self) -> float | None:
"""Return the state of the number."""
return self.entity_description.value_fn(self.work_area_attributes)
if (work_area := self.work_area_attributes) is None:
return None
return self.entity_description.value_fn(work_area)
@handle_sending_exception(poll_after_sending=True)
@override
@@ -98,12 +98,7 @@ def _get_restricted_reason(data: MowerAttributes) -> str:
@callback
def _get_work_area_names(data: MowerAttributes) -> list[str]:
"""Return a list with all work area names."""
if TYPE_CHECKING:
# Sensor does not get created if it is None
assert data.work_areas is not None
work_area_list = [
data.work_areas[work_area_id].name for work_area_id in data.work_areas
]
work_area_list = [work_area.name for work_area in (data.work_areas or {}).values()]
work_area_list.append(STATE_NO_WORK_AREA_ACTIVE)
return work_area_list
@@ -111,14 +106,12 @@ def _get_work_area_names(data: MowerAttributes) -> list[str]:
@callback
def _get_current_work_area_name(data: MowerAttributes) -> str:
"""Return the name of the current work area."""
if TYPE_CHECKING:
# Sensor does not get created if values are None
assert data.work_areas is not None
work_areas = data.work_areas or {}
if (
data.mower.work_area_id is not None
and data.mower.work_area_id in data.work_areas
and (work_area := work_areas.get(data.mower.work_area_id)) is not None
):
return data.work_areas[data.mower.work_area_id].name
return work_area.name
return STATE_NO_WORK_AREA_ACTIVE
@@ -133,10 +126,9 @@ def _get_remaining_charging_time(data: MowerAttributes) -> int | None:
@callback
def _get_current_work_area_dict(data: MowerAttributes) -> Mapping[str, Any]:
"""Return the name of the current work area."""
if TYPE_CHECKING:
# Sensor does not get created if it is None
assert data.work_areas is not None
return {ATTR_WORK_AREA_ID_ASSIGNMENT: data.work_area_dict}
return {
ATTR_WORK_AREA_ID_ASSIGNMENT: data.work_area_dict if data.work_areas else {}
}
@callback
@@ -383,8 +375,8 @@ async def async_setup_entry(
mower_id, coordinator, description, work_area_id
)
for description in WORK_AREA_SENSOR_TYPES
for work_area_id in _work_areas
if description.exists_fn(_work_areas[work_area_id])
for work_area_id, work_area in _work_areas.items()
if description.exists_fn(work_area)
)
entities.extend(
AutomowerSensorEntity(mower_id, coordinator, description)
@@ -394,16 +386,15 @@ async def async_setup_entry(
async_add_entities(entities)
def _async_add_new_work_areas(mower_id: str, work_area_ids: set[int]) -> None:
mower_data = coordinator.data[mower_id]
if mower_data.work_areas is None:
if (work_areas := coordinator.data[mower_id].work_areas) is None:
return
async_add_entities(
WorkAreaSensorEntity(mower_id, coordinator, description, work_area_id)
for description in WORK_AREA_SENSOR_TYPES
for work_area_id in work_area_ids
if work_area_id in mower_data.work_areas
and description.exists_fn(mower_data.work_areas[work_area_id])
if (work_area := work_areas.get(work_area_id)) is not None
and description.exists_fn(work_area)
)
def _async_add_new_devices(mower_ids: set[str]) -> None:
@@ -464,7 +455,11 @@ class AutomowerSensorEntity(AutomowerBaseEntity, SensorEntity):
@override
def available(self) -> bool:
"""Return the available attribute of the entity."""
return super().available and self.native_value is not None
return (
super().available
and self.entity_description.exists_fn(self.mower_attributes)
and self.native_value is not None
)
class WorkAreaSensorEntity(WorkAreaAvailableEntity, SensorEntity):
@@ -483,15 +478,19 @@ class WorkAreaSensorEntity(WorkAreaAvailableEntity, SensorEntity):
super().__init__(mower_id, coordinator, work_area_id)
self.entity_description = description
self._attr_unique_id = f"{mower_id}_{work_area_id}_{description.key}"
if TYPE_CHECKING:
assert self.work_area_attributes is not None
self._attr_translation_placeholders = {
"work_area": self.work_area_attributes.name
}
@property
@override
def native_value(self) -> StateType | datetime:
def native_value(self) -> StateType | datetime | None:
"""Return the state of the sensor."""
return self.entity_description.value_fn(self.work_area_attributes)
if (work_area := self.work_area_attributes) is None:
return None
return self.entity_description.value_fn(work_area)
@property
@override
@@ -38,7 +38,7 @@ async def async_setup_entry(
for mower_id in coordinator.data:
if coordinator.data[mower_id].capabilities.stay_out_zones:
_stay_out_zones = coordinator.data[mower_id].stay_out_zones
if _stay_out_zones is not None:
if _stay_out_zones is not None and _stay_out_zones.zones is not None:
entities.extend(
StayOutZoneSwitchEntity(coordinator, mower_id, stay_out_zone_uid)
for stay_out_zone_uid in _stay_out_zones.zones
@@ -140,31 +140,41 @@ class StayOutZoneSwitchEntity(AutomowerControlEntity, SwitchEntity):
self._attr_unique_id = (
f"{self.mower_id}_{stay_out_zone_uid}_{self._attr_translation_key}"
)
if TYPE_CHECKING:
assert self.stay_out_zone is not None
self._attr_translation_placeholders = {"stay_out_zone": self.stay_out_zone.name}
@property
def stay_out_zones(self) -> StayOutZones:
def stay_out_zones(self) -> StayOutZones | None:
"""Return all stay out zones."""
if TYPE_CHECKING:
assert self.mower_attributes.stay_out_zones is not None
return self.mower_attributes.stay_out_zones
@property
def stay_out_zone(self) -> Zone:
def stay_out_zone(self) -> Zone | None:
"""Return the specific stay out zone."""
return self.stay_out_zones.zones[self.stay_out_zone_uid]
if (stay_out_zones := self.stay_out_zones) is None:
return None
return stay_out_zones.zones.get(self.stay_out_zone_uid)
@property
@override
def is_on(self) -> bool:
def is_on(self) -> bool | None:
"""Return the state of the switch."""
return self.stay_out_zone.enabled
if (stay_out_zone := self.stay_out_zone) is None:
return None
return stay_out_zone.enabled
@property
@override
def available(self) -> bool:
"""Return True if the device is available and the zones are not `dirty`."""
return super().available and not self.stay_out_zones.dirty
if not super().available:
return False
stay_out_zones = self.stay_out_zones
if stay_out_zones is None or stay_out_zones.zones is None:
return False
if self.stay_out_zone_uid not in stay_out_zones.zones:
return False
return not stay_out_zones.dirty
@handle_sending_exception(poll_after_sending=True)
@override
@@ -197,6 +207,8 @@ class WorkAreaSwitchEntity(WorkAreaControlEntity, SwitchEntity):
key = "work_area"
self._attr_translation_key = _work_area_translation_key(work_area_id, key)
self._attr_unique_id = f"{mower_id}_{work_area_id}_{key}"
if TYPE_CHECKING:
assert self.work_area_attributes is not None
if self.work_area_attributes.name == "my_lawn":
self._attr_translation_placeholders = {
"work_area": self.work_area_attributes.name
@@ -206,9 +218,11 @@ class WorkAreaSwitchEntity(WorkAreaControlEntity, SwitchEntity):
@property
@override
def is_on(self) -> bool:
def is_on(self) -> bool | None:
"""Return the state of the switch."""
return self.work_area_attributes.enabled
if (work_area := self.work_area_attributes) is None:
return None
return work_area.enabled
@handle_sending_exception(poll_after_sending=True)
@override
@@ -9,5 +9,5 @@
"iot_class": "local_polling",
"loggers": ["aioimmich"],
"quality_scale": "platinum",
"requirements": ["aioimmich==0.16.1"]
"requirements": ["aioimmich==0.17.0"]
}
+36 -19
View File
@@ -140,33 +140,33 @@ class ImmichMediaSource(MediaSource):
if item.identifier:
can_search = bool(ImmichMediaSourceIdentifier(item.identifier).unique_id)
title, children = await self._async_build_immich(item, entries)
return BrowseMediaSource(
domain=DOMAIN,
identifier=item.identifier,
media_class=MediaClass.DIRECTORY,
media_content_type=MediaClass.IMAGE,
title="Immich",
title=title,
can_play=False,
can_expand=True,
can_search=can_search,
search_media_classes=[MediaClass.IMAGE, MediaClass.VIDEO],
children_media_class=MediaClass.DIRECTORY,
children=[
*await self._async_build_immich(item, entries),
],
children=children,
)
async def _async_build_immich(
self, item: MediaSourceItem, entries: list[ConfigEntry]
) -> list[BrowseMediaSource]:
"""Handle browsing different immich instances."""
) -> tuple[str, list[BrowseMediaSource]]:
"""Return the title and the children of the browsed item."""
# --------------------------------------------------------
# root level, render immich instances
# --------------------------------------------------------
if not item.identifier:
LOGGER.debug("Render all Immich instances")
return [
return "Immich", [
BrowseMediaSource(
domain=DOMAIN,
identifier=entry.unique_id,
@@ -193,7 +193,7 @@ class ImmichMediaSource(MediaSource):
if identifier.collection is None:
LOGGER.debug("Render all collections for %s", entry.title)
return [
return entry.title, [
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{identifier.unique_id}|{collection}",
@@ -221,9 +221,9 @@ class ImmichMediaSource(MediaSource):
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
return identifier.collection, []
return [
return identifier.collection, [
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{identifier.unique_id}|albums|{album.album_id}",
@@ -248,9 +248,9 @@ class ImmichMediaSource(MediaSource):
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
return identifier.collection, []
return [
return identifier.collection, [
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{identifier.unique_id}|tags|{tag.tag_id}",
@@ -274,9 +274,9 @@ class ImmichMediaSource(MediaSource):
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
return identifier.collection, []
return [
return identifier.collection, [
BrowseMediaSource(
domain=DOMAIN,
identifier=f"{identifier.unique_id}|people|{person.person_id}",
@@ -295,6 +295,7 @@ class ImmichMediaSource(MediaSource):
# --------------------------------------------------------
assert identifier.collection_id is not None
assets: list[ImmichAsset] = []
title = identifier.collection
if identifier.collection == "albums":
LOGGER.debug(
"Render all assets of album %s for %s",
@@ -302,6 +303,9 @@ class ImmichMediaSource(MediaSource):
entry.title,
)
try:
album = await immich_api.albums.async_get_album_info(
identifier.collection_id
)
assets = await immich_api.search.async_get_all_by_album_ids(
[identifier.collection_id]
)
@@ -312,7 +316,9 @@ class ImmichMediaSource(MediaSource):
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
return title, []
title = album.album_name
elif identifier.collection == "tags":
LOGGER.debug(
@@ -320,6 +326,9 @@ class ImmichMediaSource(MediaSource):
identifier.collection_id,
)
try:
tag = await immich_api.tags.async_get_tag_by_id(
identifier.collection_id
)
assets = await immich_api.search.async_get_all_by_tag_ids(
[identifier.collection_id]
)
@@ -330,7 +339,9 @@ class ImmichMediaSource(MediaSource):
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
return title, []
title = tag.name
elif identifier.collection == "people":
LOGGER.debug(
@@ -338,6 +349,9 @@ class ImmichMediaSource(MediaSource):
identifier.collection_id,
)
try:
person = await immich_api.people.async_get_person_by_id(
identifier.collection_id
)
assets = await immich_api.search.async_get_all_by_person_ids(
[identifier.collection_id]
)
@@ -348,7 +362,10 @@ class ImmichMediaSource(MediaSource):
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
return title, []
title = person.name
elif identifier.collection == "favorites":
LOGGER.debug("Render all assets for favorites collection")
try:
@@ -360,9 +377,9 @@ class ImmichMediaSource(MediaSource):
translation_placeholders={"msg": str(err)},
) from err
except ImmichError:
return []
return title, []
return _parse_assets(assets, identifier)
return title, _parse_assets(assets, identifier)
@override
async def async_resolve_media(self, item: MediaSourceItem) -> PlayMedia:
+1 -1
View File
@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "bronze",
"requirements": ["pyimouapi==1.3.0"]
"requirements": ["pyimouapi==1.3.4"]
}
@@ -8,6 +8,6 @@
"integration_type": "device",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["indevolt-api==1.8.6"],
"requirements": ["indevolt-api==1.8.7"],
"zeroconf": [{ "name": "igen_fw*", "type": "_http._tcp.local." }]
}
+10
View File
@@ -12,6 +12,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceEntry
from homeassistant.helpers.reload import async_integration_yaml_config
from homeassistant.helpers.storage import STORAGE_DIR
@@ -286,6 +287,15 @@ async def async_remove_config_entry_device(
):
# can not remove interface device
return False
ui_unique_ids = knx_module.config_store.get_entity_uids()
entity_registry = er.async_get(hass)
if any(
entity.config_entry_id == config_entry.entry_id
and entity.unique_id not in ui_unique_ids
for entity in er.async_entries_for_device(entity_registry, device_entry.id)
):
# device still has YAML-configured KNX entities; it would be recreated after reload
return False
for entity in knx_module.config_store.get_entity_entries():
if entity.device_id == device_entry.id:
await knx_module.config_store.delete_entity(entity.entity_id)
+5
View File
@@ -19,6 +19,11 @@ if TYPE_CHECKING:
DOMAIN: Final = "knx"
KNX_MODULE_KEY: HassKey[KNXModule] = HassKey(DOMAIN)
# Prefix of device identifiers created via the `knx/create_device` websocket
# command (see websocket.py). A YAML `device.id` matching this prefix is
# assumed to reference such a device verbatim and is not slugified.
UI_DEVICE_ID_PREFIX: Final = "knx_vdev_"
# Address is used for configuration and services by the
# same functions so the key has to match
KNX_ADDRESS: Final = "address"
+20 -3
View File
@@ -6,7 +6,13 @@ from typing import TYPE_CHECKING, Any, override
from xknx.devices import Device as XknxDevice
from xknx.telegram.address import DeviceGroupAddress, GroupAddress
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, EntityCategory
from homeassistant.const import (
CONF_DEVICE,
CONF_ENTITY_CATEGORY,
CONF_ID,
CONF_NAME,
EntityCategory,
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.device_registry import DeviceInfo
@@ -115,6 +121,7 @@ class KnxUiEntityPlatformController(PlatformControllerBase):
class _KnxEntityBase(Entity):
"""Representation of a KNX entity."""
_attr_has_entity_name = True
_attr_should_poll = False
_attr_unique_id: str
@@ -195,6 +202,18 @@ class KnxYamlEntity(_KnxEntityBase):
self._attr_unique_id = new_unique_id
self._attr_entity_category = entity_config.get(CONF_ENTITY_CATEGORY)
if device := entity_config.get(CONF_DEVICE):
# Entities sharing the same `device` `id` are grouped into one
# device. `id` is normalized in the schema (`_device_id`), which
# also lets YAML entities join a UI-created device by referencing
# its identifier verbatim.
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device[CONF_ID])},
manufacturer="KNX",
)
if device_name := device.get(CONF_NAME):
self._attr_device_info["name"] = device_name
default_entity_id: str | None
if (default_entity_id := entity_config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
self.entity_id = default_entity_id
@@ -203,8 +222,6 @@ class KnxYamlEntity(_KnxEntityBase):
class KnxUiEntity(_KnxEntityBase):
"""Representation of a KNX UI entity."""
_attr_has_entity_name = True
def __init__(
self, knx_module: KNXModule, unique_id: str, entity_config: dict[str, Any]
) -> None:
+1 -1
View File
@@ -11,7 +11,7 @@
"loggers": ["xknx", "xknxproject"],
"quality_scale": "platinum",
"requirements": [
"xknx==3.18.0",
"xknx==3.19.0",
"xknxproject==3.10.0",
"knx-frontend==2026.8.2.133643",
"knx-telegram-store[sqlite,postgres]==0.11.2"
@@ -20,10 +20,7 @@ rules:
docs-triggers: done
entity-event-setup: done
entity-unique-id: done
has-entity-name:
status: exempt
comment: |
YAML entities don't support devices. UI entities do and use `has_entity_name`.
has-entity-name: done
runtime-data:
status: exempt
comment: |
@@ -63,10 +60,7 @@ rules:
Integration has no authentication.
test-coverage: done
# Gold
devices:
status: exempt
comment: |
YAML entities don't support devices. UI entities support user-defined devices.
devices: done
diagnostics: done
discovery-update-info:
status: exempt
+27
View File
@@ -32,10 +32,12 @@ from homeassistant.components.switch import (
)
from homeassistant.components.text import TextMode
from homeassistant.const import (
CONF_DEVICE,
CONF_DEVICE_CLASS,
CONF_ENTITY_CATEGORY,
CONF_ENTITY_ID,
CONF_EVENT,
CONF_ID,
CONF_MODE,
CONF_NAME,
CONF_PAYLOAD,
@@ -46,6 +48,7 @@ from homeassistant.const import (
)
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.entity import ENTITY_CATEGORIES_SCHEMA
from homeassistant.util import slugify
from .const import (
CONF_CONTEXT_TIMEOUT,
@@ -60,6 +63,7 @@ from .const import (
CONF_SYNC_STATE,
CONF_VALUE,
KNX_ADDRESS,
UI_DEVICE_ID_PREFIX,
ClimateConf,
ColorTempModes,
CoverConf,
@@ -204,11 +208,34 @@ class KNXPlatformSchema(ABC):
}
def _device_id(value: str) -> str:
"""Normalize a YAML device id.
A value matching the identifier of a device created in the UI (see
`UI_DEVICE_ID_PREFIX`) is passed through verbatim, so it keeps linking to
that device. Any other value is slugified so ids that only differ in
case or whitespace resolve to the same device instead of silently
creating a separate one.
"""
value = value.strip()
if value.startswith(UI_DEVICE_ID_PREFIX):
return value
return slugify(value)
def _entity_base_schema(platform: Platform) -> vol.Schema:
"""Return a base schema for KNX entities."""
return vol.Schema(
{
vol.Optional(CONF_NAME, default=""): cv.string,
vol.Optional(CONF_DEVICE): vol.Schema(
{
vol.Required(CONF_ID): vol.All(
cv.string, _device_id, vol.Length(min=1)
),
vol.Optional(CONF_NAME): cv.string,
}
),
vol.Optional(CONF_DEFAULT_ENTITY_ID): vol.All(
cv.entity_id, cv.entity_domain(platform)
),
@@ -181,12 +181,14 @@ class KNXConfigStore:
entity_registry.async_remove(entity_id)
await self._store.async_save(self.data)
def get_entity_uids(self) -> set[str]:
"""Return unique_ids of all UI configured entities."""
return {uid for platform in self.data["entities"].values() for uid in platform}
def get_entity_entries(self) -> list[er.RegistryEntry]:
"""Get entity_ids of all UI configured entities."""
entity_registry = er.async_get(self.hass)
unique_ids = {
uid for platform in self.data["entities"].values() for uid in platform
}
unique_ids = self.get_entity_uids()
return [
registry_entry
for registry_entry in er.async_entries_for_config_entry(
+14 -10
View File
@@ -48,14 +48,18 @@ _LOGGER = logging.getLogger(__name__)
EVICT_EXPIRED_HOUR = 3
# Interval at which buffered telegram writes are flushed to the database.
# Websocket queries flush on demand (``flush_first=True``), so the only telegrams
# at risk from a longer interval are those buffered during an ungraceful shutdown.
FLUSH_INTERVAL_SECONDS = 600
# Postgres flushes far more often than sqlite: push-based consumers, unlike
# on-demand-flushed (``flush_first=True``) queries, only see a telegram once
# it is actually written.
FLUSH_INTERVAL_SECONDS_SQLITE = 600
FLUSH_INTERVAL_SECONDS_POSTGRES = 1
# The buffer drops the oldest telegrams when full. Size it to cover a full
# flush interval at ~50 telegrams/s, the maximum rate of a KNX TP line, so
# nothing is dropped while the database is healthy.
MAX_BUFFER_TELEGRAMS = FLUSH_INTERVAL_SECONDS * 50
# flush interval at ~50 telegrams/s, the maximum rate of a single KNX TP line,
# times 4 for headroom - routing can record multiple TP lines concurrently -
# so nothing is dropped while the database is healthy.
MAX_BUFFER_TELEGRAMS_SQLITE = FLUSH_INTERVAL_SECONDS_SQLITE * 50 * 4
MAX_BUFFER_TELEGRAMS_POSTGRES = FLUSH_INTERVAL_SECONDS_POSTGRES * 50 * 4
# Timeout for the migration probe and store initialization, so an unreachable
# database cannot block KNX setup until the driver/OS connection timeout expires.
@@ -127,8 +131,8 @@ class Telegrams:
self._uninitialized_store = BufferedPostgresStore(
self.dsn,
retention_days=self.retention_days,
flush_interval=FLUSH_INTERVAL_SECONDS,
max_buffer_size=MAX_BUFFER_TELEGRAMS,
flush_interval=FLUSH_INTERVAL_SECONDS_POSTGRES,
max_buffer_size=MAX_BUFFER_TELEGRAMS_POSTGRES,
)
else:
full_path = hass.config.path(STORAGE_DIR, KNX_TELEGRAM_DB_PATH_SQLITE)
@@ -136,8 +140,8 @@ class Telegrams:
self._uninitialized_store = BufferedSqliteStore(
full_path,
retention_days=self.retention_days,
flush_interval=FLUSH_INTERVAL_SECONDS,
max_buffer_size=MAX_BUFFER_TELEGRAMS,
flush_interval=FLUSH_INTERVAL_SECONDS_SQLITE,
max_buffer_size=MAX_BUFFER_TELEGRAMS_SQLITE,
)
self._xknx_telegram_cb_handle = (
+3 -1
View File
@@ -36,6 +36,7 @@ from .const import (
SIGNAL_KNX_DATA_SECURE_ISSUE_TELEGRAM,
SIGNAL_KNX_TELEGRAM,
SUPPORTED_PLATFORMS_UI,
UI_DEVICE_ID_PREFIX,
)
from .dpt import get_supported_dpts
from .storage.config_store import ConfigStoreException
@@ -103,6 +104,7 @@ async def register_panel(hass: HomeAssistant) -> None:
module_url=f"{URL_BASE}/{knx_panel.entrypoint_js}",
embed_iframe=True,
require_admin=True,
handle_safe_area=True,
)
@@ -699,7 +701,7 @@ def ws_create_device(
msg: dict,
) -> None:
"""Create a new KNX device."""
identifier = f"knx_vdev_{ulid_now()}"
identifier = f"{UI_DEVICE_ID_PREFIX}{ulid_now()}"
device_registry = dr.async_get(hass)
_device = device_registry.async_get_or_create(
config_entry_id=knx.entry.entry_id,
+7 -8
View File
@@ -184,14 +184,13 @@ class LiebherrSelectEntity(LiebherrEntity, SelectEntity):
self._attr_options = description.options_fn(control)
# Add zone suffix only for multi-zone devices
if has_multiple_zones:
temp_controls = coordinator.data.get_temperature_controls()
if (
(tc := temp_controls.get(zone_id))
and isinstance(tc.zone_position, ZonePosition)
and (zone_key := ZONE_POSITION_MAP.get(tc.zone_position))
):
self._attr_translation_key = f"{description.translation_key}_{zone_key}"
if (
has_multiple_zones
and control is not None
and isinstance(control.zone_position, ZonePosition)
and (zone_key := ZONE_POSITION_MAP.get(control.zone_position))
):
self._attr_translation_key = f"{description.translation_key}_{zone_key}"
@property
def _select_control(self) -> SelectControl | None:
@@ -16,5 +16,5 @@
"iot_class": "cloud_push",
"loggers": ["pylitterbot"],
"quality_scale": "platinum",
"requirements": ["pylitterbot==2025.5.0"]
"requirements": ["pylitterbot==2025.6.4"]
}
@@ -7,5 +7,5 @@
"documentation": "https://www.home-assistant.io/integrations/local_calendar",
"iot_class": "local_polling",
"loggers": ["ical"],
"requirements": ["ical==14.0.1"]
"requirements": ["ical==14.1.0"]
}
@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/local_todo",
"iot_class": "local_polling",
"requirements": ["ical==14.0.1"]
"requirements": ["ical==14.1.0"]
}
@@ -9,7 +9,7 @@
"iot_class": "local_push",
"loggers": ["lyngdorf", "async_upnp_client"],
"quality_scale": "silver",
"requirements": ["lyngdorf==1.4.0"],
"requirements": ["lyngdorf==1.4.8"],
"ssdp": [
{
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:2",
+1 -1
View File
@@ -22,5 +22,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"loggers": ["aiolyric"],
"requirements": ["aiolyric==2.1.1"]
"requirements": ["aiolyric==2.1.2"]
}
+7
View File
@@ -27,6 +27,12 @@ from .models import MatterDiscoverySchema
# The MASK used for extracting bits 0 to 1 of the byte.
OPERATIONAL_STATUS_MASK = 0b11
# Cover state is derived from both the operational status and the current
# position, which some devices report as separate attribute updates shortly
# after each other (e.g. stopped before the final position). Debounce state
# writes to avoid writing intermittent states.
STATE_WRITE_DEBOUNCE_COOLDOWN = 0.1
# map Matter window cover types to HA device class
TYPE_MAP = {
clusters.WindowCovering.Enums.Type.kRollerShade: CoverDeviceClass.SHADE,
@@ -69,6 +75,7 @@ class MatterCoverEntityDescription(CoverEntityDescription, MatterEntityDescripti
class MatterCover(MatterEntity, CoverEntity):
"""Representation of a Matter Cover."""
_write_state_debounce_cooldown = STATE_WRITE_DEBOUNCE_COOLDOWN
entity_description: MatterCoverEntityDescription
@property
+27 -1
View File
@@ -19,6 +19,7 @@ from propcache.api import cached_property
from homeassistant.core import callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.typing import UndefinedType
@@ -94,6 +95,11 @@ class MatterEntity(Entity):
_attr_should_poll = False
_name_postfix: str | None = None
_platform_translation_key: str | None = None
# Cooldown in seconds to debounce state writes on updates from the device.
# Platforms which derive their state from multiple attributes can set this
# to coalesce attribute updates which arrive as separate events.
_write_state_debounce_cooldown: float | None = None
_write_state_debouncer: Debouncer[None] | None = None
def __init__(
self,
@@ -189,6 +195,15 @@ class MatterEntity(Entity):
"""Handle being added to Home Assistant."""
await super().async_added_to_hass()
if self._write_state_debounce_cooldown is not None:
self._write_state_debouncer = Debouncer(
self.hass,
LOGGER,
cooldown=self._write_state_debounce_cooldown,
immediate=False,
function=self.async_write_ha_state,
)
# Subscribe to attribute updates.
sub_paths: list[str] = []
for attr_cls in self._entity_info.attributes_to_watch:
@@ -305,6 +320,14 @@ class MatterEntity(Entity):
return True
return bool(reachable)
@override
async def async_will_remove_from_hass(self) -> None:
"""Handle being removed from Home Assistant."""
await super().async_will_remove_from_hass()
if self._write_state_debouncer is not None:
self._write_state_debouncer.async_shutdown()
self._write_state_debouncer = None
@callback
def _on_matter_event(self, event: EventType, data: Any = None) -> None:
"""Call on update from the device."""
@@ -312,7 +335,10 @@ class MatterEntity(Entity):
self._endpoint.node.available and self._get_bridged_reachable()
)
self._update_from_device()
self.async_write_ha_state()
if self._write_state_debouncer is not None:
self._write_state_debouncer.async_schedule_call()
else:
self.async_write_ha_state()
@callback
def _on_featuremap_update(self, event: EventType, data: int | None) -> None:
+1 -1
View File
@@ -16,7 +16,7 @@
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
"ingress_url": "Ingress URLs are only used for accessing the Mealie UI. Use your Home Assistant IP address and the network port within the configuration tab of the Mealie app.",
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
"mealie_version": "Minimum required version is v1.0.0. Please upgrade Mealie and then retry.",
"mealie_version": "Minimum required version is v2.0.0. Please upgrade Mealie and then retry.",
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
+1 -2
View File
@@ -5,8 +5,7 @@ from typing import Any, cast, override
from midealocal.const import DeviceType
from midealocal.devices.ac import DeviceAttributes as ACAttributes, MideaACDevice
from midealocal.devices.c3 import MideaC3Device
from midealocal.devices.c3.const import DeviceAttributes as C3Attributes
from midealocal.devices.c3 import DeviceAttributes as C3Attributes, MideaC3Device
from midealocal.devices.cc import DeviceAttributes as CCAttributes, MideaCCDevice
from midealocal.devices.cf import DeviceAttributes as CFAttributes, MideaCFDevice
from midealocal.devices.fb import DeviceAttributes as FBAttributes, MideaFBDevice
+1 -1
View File
@@ -8,5 +8,5 @@
"iot_class": "local_polling",
"loggers": ["midealocal"],
"quality_scale": "bronze",
"requirements": ["midea-local==6.11.1"]
"requirements": ["midea-local==7.0.0"]
}
@@ -1,7 +1,10 @@
"""Constants used in the Mikrotik components."""
import logging
from typing import Final
LOGGER = logging.getLogger(__package__)
DOMAIN: Final = "mikrotik"
DEFAULT_NAME: Final = "Mikrotik"
DEFAULT_API_PORT: Final = 8728
@@ -1,7 +1,6 @@
"""The Mikrotik router class."""
from datetime import timedelta
import logging
import ssl
from typing import Any, override
@@ -37,6 +36,7 @@ from .const import (
IS_WIFI,
IS_WIFIWAVE2,
IS_WIRELESS,
LOGGER,
MIKROTIK_SERVICES,
NAME,
POE,
@@ -49,9 +49,7 @@ from .const import (
)
from .device import Device
from .errors import CannotConnect, LoginError
from .utils import mikrotik_config_entry_errors
_LOGGER = logging.getLogger(__name__)
from .utils import calculate_uptime, mikrotik_config_entry_errors, percentage
type MikrotikConfigEntry = ConfigEntry[MikrotikDataUpdateCoordinator]
@@ -95,6 +93,43 @@ class MikrotikData:
or [{}]
)[0]
def _get_health_details(self) -> None:
"""Retrieve health details from Mikrotik API."""
health_data = (
self.command(MIKROTIK_SERVICES[HEALTH], suppress_errors=True) or []
)
self.sensors[HEALTH] = {
entry["name"]: entry["value"]
for entry in health_data
if "name" in entry and "value" in entry
}
def _get_resource_details(self) -> None:
"""Retrieve resource details from Mikrotik API."""
resource_data = (
self.command(MIKROTIK_SERVICES[RESOURCE], suppress_errors=True) or [{}]
)[0]
self.sensors[RESOURCE] = (
{
"cpu-load": resource_data.get("cpu-load"),
"memory-usage": percentage(
resource_data.get("total-memory", 0),
resource_data.get("free-memory", 0),
),
"disk-usage": percentage(
resource_data.get("total-hdd-space", 0),
resource_data.get("free-hdd-space", 0),
),
"uptime": (
calculate_uptime(resource_data["uptime"])
if resource_data.get("uptime")
else None
),
}
if resource_data
else {}
)
def _get_interfaces_details(self) -> None:
"""Get interfaces details."""
all_interfs = self.command(MIKROTIK_SERVICES[INTERFACE])
@@ -204,24 +239,24 @@ class MikrotikData:
# Retrieve data
self.all_devices = self.get_list_from_interface(DHCP)
if self.support_capsman:
_LOGGER.debug("Hub is a CAPSman manager")
LOGGER.debug("Hub is a CAPSman manager")
device_list = wireless_devices = self.get_list_from_interface(CAPSMAN)
elif self.support_wireless:
_LOGGER.debug("Hub supports wireless Interface")
LOGGER.debug("Hub supports wireless Interface")
device_list = wireless_devices = self.get_list_from_interface(WIRELESS)
elif self.support_wifiwave2:
_LOGGER.debug("Hub supports wifiwave2 Interface")
LOGGER.debug("Hub supports wifiwave2 Interface")
device_list = wireless_devices = self.get_list_from_interface(WIFIWAVE2)
elif self.support_wifi:
_LOGGER.debug("Hub supports wifi Interface")
LOGGER.debug("Hub supports wifi Interface")
device_list = wireless_devices = self.get_list_from_interface(WIFI)
if not device_list or self.force_dhcp:
device_list = self.all_devices
_LOGGER.debug("Falling back to DHCP for scanning devices")
LOGGER.debug("Falling back to DHCP for scanning devices")
if self.arp_enabled:
_LOGGER.debug("Using arp-ping to check devices")
LOGGER.debug("Using arp-ping to check devices")
arp_devices = self.get_list_from_interface(ARP)
# get hub details and system info
@@ -231,13 +266,8 @@ class MikrotikData:
self.command(MIKROTIK_SERVICES[UPDATE], suppress_errors=True) or [{}]
)[0]
self.sensors[HEALTH] = (
self.command(MIKROTIK_SERVICES[HEALTH], suppress_errors=True) or []
)
self.sensors[RESOURCE] = (
self.command(MIKROTIK_SERVICES[RESOURCE], suppress_errors=True) or []
)
self._get_health_details()
self._get_resource_details()
self._get_interfaces_details()
if not device_list:
@@ -270,7 +300,7 @@ class MikrotikData:
def do_arp_ping(self, ip_address: str, interface: str) -> bool:
"""Attempt to arp ping MAC address via interface."""
_LOGGER.debug("pinging - %s", ip_address)
LOGGER.debug("pinging - %s", ip_address)
params = {
"arp-ping": "yes",
"interval": "100ms",
@@ -286,7 +316,7 @@ class MikrotikData:
if "status" in result:
status += 1
if status == len(data):
_LOGGER.debug(
LOGGER.debug(
"Mikrotik %s - %s arp_ping timed out", ip_address, interface
)
return False
@@ -300,7 +330,7 @@ class MikrotikData:
during_setup: bool = False,
) -> list[dict[str, Any]]:
"""Retrieve data from Mikrotik API."""
_LOGGER.debug("Running command %s", cmd)
LOGGER.debug("Running command %s", cmd)
with mikrotik_config_entry_errors(
suppress_errors=suppress_errors, during_setup=during_setup
):
@@ -324,7 +354,7 @@ class MikrotikDataUpdateCoordinator(DataUpdateCoordinator[None]):
self._mk_data = MikrotikData(hass, config_entry, api)
super().__init__(
hass,
_LOGGER,
LOGGER,
config_entry=config_entry,
name=f"{DOMAIN} - {config_entry.data[CONF_HOST]}",
update_interval=timedelta(seconds=10),
@@ -377,7 +407,7 @@ class MikrotikDataUpdateCoordinator(DataUpdateCoordinator[None]):
def get_api(entry: dict[str, Any]) -> librouteros.Api:
"""Connect to Mikrotik hub."""
_LOGGER.debug("Connecting to Mikrotik hub [%s]", entry[CONF_HOST])
LOGGER.debug("Connecting to Mikrotik hub [%s]", entry[CONF_HOST])
kwargs = {"port": entry["port"], "encoding": "utf8"}
@@ -408,10 +438,10 @@ def get_api(entry: dict[str, Any]) -> librouteros.Api:
_error = api_error
if _error is not None:
_LOGGER.debug("Mikrotik %s error: %s", entry[CONF_HOST], _error)
LOGGER.debug("Mikrotik %s error: %s", entry[CONF_HOST], _error)
if "invalid user name or password" in str(_error):
raise LoginError from _error
raise CannotConnect from _error
_LOGGER.debug("Connected to %s successfully", entry[CONF_HOST])
LOGGER.debug("Connected to %s successfully", entry[CONF_HOST])
return api
@@ -1,6 +1,6 @@
{
"domain": "mikrotik",
"name": "Mikrotik",
"name": "MikroTik",
"codeowners": ["@engrbm87", "@chemelli74"],
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/mikrotik",
+7 -67
View File
@@ -1,9 +1,8 @@
"""Support for Mikrotik routers sensors."""
from collections.abc import Callable
from dataclasses import dataclass
from datetime import datetime, timedelta
from typing import Any, Final, override
from datetime import datetime
from typing import Final, cast, override
from homeassistant.components.sensor import (
SensorDeviceClass,
@@ -20,10 +19,9 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util.dt import utcnow
from .const import HEALTH, RESOURCE
from .coordinator import _LOGGER, MikrotikConfigEntry
from .coordinator import MikrotikConfigEntry
from .entity import MikrotikEntity
PARALLEL_UPDATES = 0
@@ -33,44 +31,7 @@ PARALLEL_UPDATES = 0
class MikrotikSensorEntityDescription(SensorEntityDescription):
"""Shared Mikrotik Sensors entity description."""
value: Callable[[dict[str, Any]], StateType | datetime]
type: str
index: int
def _calculate_uptime(data: dict[str, Any]) -> datetime | None:
"""Calculate uptime."""
# e.g. 1d3h39m30s
uptime_string = data["uptime"]
total = 0
num = 0
for ch in uptime_string.strip():
if ch.isdigit():
num = num * 10 + int(ch)
else:
if ch == "w":
total += num * (60 * 60 * 24 * 7)
elif ch == "d":
total += num * (60 * 60 * 24)
elif ch == "h":
total += num * (60 * 60)
elif ch == "m":
total += num * 60
elif ch == "s":
total += num
else:
_LOGGER.warning("Unknown uptime format: %s", uptime_string)
return None
num = 0
if num != 0:
_LOGGER.warning("Unknown uptime format: %s", uptime_string)
return None
return utcnow() - timedelta(seconds=total)
SENSORS: Final = (
@@ -79,18 +40,14 @@ SENSORS: Final = (
device_class=SensorDeviceClass.TEMPERATURE,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value=lambda _data: _data["value"],
type=HEALTH,
index=1,
),
MikrotikSensorEntityDescription(
key="voltage",
device_class=SensorDeviceClass.VOLTAGE,
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfElectricPotential.VOLT,
value=lambda _data: _data["value"],
type=HEALTH,
index=0,
),
MikrotikSensorEntityDescription(
key="cpu-load",
@@ -99,9 +56,7 @@ SENSORS: Final = (
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
suggested_display_precision=2,
value=lambda _data: _data["cpu-load"],
type=RESOURCE,
index=0,
),
MikrotikSensorEntityDescription(
key="memory-usage",
@@ -110,13 +65,7 @@ SENSORS: Final = (
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
suggested_display_precision=2,
value=lambda _data: (
None
if (total := _data.get("total-memory", 0)) == 0
else (total - _data.get("free-memory", 0)) / total * 100
),
type=RESOURCE,
index=0,
),
MikrotikSensorEntityDescription(
key="disk-usage",
@@ -125,20 +74,12 @@ SENSORS: Final = (
entity_category=EntityCategory.DIAGNOSTIC,
native_unit_of_measurement=UnitOfRatio.PERCENTAGE,
suggested_display_precision=2,
value=lambda _data: (
None
if (total := _data.get("total-hdd-space", 0)) == 0
else (total - _data.get("free-hdd-space", 0)) / total * 100
),
type=RESOURCE,
index=0,
),
MikrotikSensorEntityDescription(
key="uptime",
device_class=SensorDeviceClass.UPTIME,
value=_calculate_uptime,
type=RESOURCE,
index=0,
),
)
@@ -155,8 +96,8 @@ async def async_setup_entry(
sensors_list = [
MikrotikSensorEntity(coordinator, sensor_desc)
for sensor_desc in SENSORS
if len(coordinator.api.sensors.get(sensor_desc.type, []))
>= (sensor_desc.index + 1)
if coordinator.api.sensors.get(sensor_desc.type, {}).get(sensor_desc.key)
is not None
]
async_add_entities(sensors_list)
@@ -171,7 +112,6 @@ class MikrotikSensorEntity(MikrotikEntity, SensorEntity):
@override
def native_value(self) -> StateType | datetime:
"""Return the state of the sensor."""
data_list = self.coordinator.api.sensors[self.entity_description.type]
data_entry = data_list[self.entity_description.index]
data = self.coordinator.api.sensors[self.entity_description.type]
return self.entity_description.value(data_entry)
return cast(StateType | datetime, data.get(self.entity_description.key))
@@ -15,7 +15,7 @@
"password": "[%key:common::config_flow::data::password%]"
},
"data_description": {
"password": "[%key:component::vodafone_station::config::step::user::data_description::password%]"
"password": "The password for your Mikrotik device."
},
"description": "The password for {username} is invalid.",
"title": "[%key:common::config_flow::title::reauth%]"
+42 -1
View File
@@ -2,6 +2,7 @@
from collections.abc import Generator
from contextlib import contextmanager
from datetime import datetime, timedelta
from librouteros.exceptions import ConnectionClosed, LibRouterosError
@@ -11,11 +12,51 @@ from homeassistant.exceptions import (
HomeAssistantError,
)
from homeassistant.helpers.update_coordinator import UpdateFailed
from homeassistant.util.dt import utcnow
from .const import DOMAIN
from .const import DOMAIN, LOGGER
from .errors import CannotConnect, LoginError
def percentage(total: float, free: float) -> float | None:
"""Return the used percentage for a total/free pair, or None if total is zero."""
if total == 0:
return None
return (total - free) / total * 100
def calculate_uptime(uptime_string: str) -> datetime | None:
"""Calculate uptime from a RouterOS duration string, e.g. "1d3h39m30s"."""
total = 0
num = 0
for ch in uptime_string.strip():
if ch.isdigit():
num = num * 10 + int(ch)
else:
if ch == "w":
total += num * (60 * 60 * 24 * 7)
elif ch == "d":
total += num * (60 * 60 * 24)
elif ch == "h":
total += num * (60 * 60)
elif ch == "m":
total += num * 60
elif ch == "s":
total += num
else:
LOGGER.warning("Unknown uptime format: %s", uptime_string)
return None
num = 0
if num != 0:
LOGGER.warning("Unknown uptime format: %s", uptime_string)
return None
return utcnow() - timedelta(seconds=total)
@contextmanager
def mikrotik_config_entry_errors(
suppress_errors: bool = False, during_setup: bool = False
@@ -8,5 +8,5 @@
"integration_type": "hub",
"iot_class": "local_polling",
"quality_scale": "bronze",
"requirements": ["mitsubishi-comfort==0.5.1"]
"requirements": ["mitsubishi-comfort==0.5.2"]
}
@@ -1,5 +1,4 @@
"""Live Activity webhook handlers."""
# pylint: disable=home-assistant-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern
from typing import Any
@@ -199,7 +199,6 @@ async def async_register_webhook(
try:
await entry.runtime_data.auth.async_addwebhook(webhook_url)
_LOGGER.debug("Register Netatmo webhook: %s", webhook_url)
# pylint: disable-next=home-assistant-action-swallowed-exception
except pyatmo.ApiError as err:
webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID])
_LOGGER.error("Error during webhook registration - %s", err)
@@ -66,7 +66,6 @@ class OpenRGBCoordinator(DataUpdateCoordinator[dict[str, Device]]):
DEFAULT_CLIENT_NAME,
)
except CONNECTION_ERRORS as err:
# pylint: disable-next=home-assistant-exception-translation-key-missing
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="cannot_connect",
@@ -1,5 +1,6 @@
"""Config flow for the Ouman EH-800 integration."""
from collections.abc import Mapping
import logging
from typing import Any, override
@@ -14,6 +15,11 @@ from yarl import URL
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.selector import (
TextSelector,
TextSelectorConfig,
TextSelectorType,
)
from .const import DOMAIN
@@ -21,9 +27,22 @@ _LOGGER = logging.getLogger(__name__)
STEP_USER_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_URL): str,
vol.Required(CONF_USERNAME): str,
vol.Required(CONF_PASSWORD): str,
vol.Required(CONF_URL): TextSelector(
TextSelectorConfig(type=TextSelectorType.URL)
),
vol.Required(CONF_USERNAME): TextSelector(),
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
}
)
STEP_REAUTH_DATA_SCHEMA = vol.Schema(
{
vol.Required(CONF_USERNAME): TextSelector(),
vol.Required(CONF_PASSWORD): TextSelector(
TextSelectorConfig(type=TextSelectorType.PASSWORD)
),
}
)
@@ -38,22 +57,16 @@ class OumanEh800ConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
async def _async_validate_input(self, user_input: dict[str, Any]) -> dict[str, str]:
"""Normalize the URL, check for duplicates and test the connection.
async def _async_try_login(self, data: Mapping[str, Any]) -> dict[str, str]:
"""Test the connection by logging in to the device.
Mutates user_input to hold the normalized URL. Returns form errors,
empty if validation succeeded.
Returns form errors, empty if the login succeeded.
"""
try:
user_input[CONF_URL] = _normalize_url(user_input[CONF_URL])
except ValueError:
return {CONF_URL: "invalid_url"}
self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]})
client = OumanEh800Client(
session=async_get_clientsession(self.hass),
username=user_input[CONF_USERNAME],
password=user_input[CONF_PASSWORD],
address=user_input[CONF_URL],
username=data[CONF_USERNAME],
password=data[CONF_PASSWORD],
address=data[CONF_URL],
)
try:
await client.login()
@@ -66,6 +79,19 @@ class OumanEh800ConfigFlow(ConfigFlow, domain=DOMAIN):
return {"base": "unknown"}
return {}
async def _async_validate_input(self, user_input: dict[str, Any]) -> dict[str, str]:
"""Normalize the URL, check for duplicates and test the connection.
Mutates user_input to hold the normalized URL. Returns form errors,
empty if validation succeeded.
"""
try:
user_input[CONF_URL] = _normalize_url(user_input[CONF_URL])
except ValueError:
return {CONF_URL: "invalid_url"}
self._async_abort_entries_match({CONF_URL: user_input[CONF_URL]})
return await self._async_try_login(user_input)
@override
async def async_step_user(
self, user_input: dict[str, Any] | None = None
@@ -84,6 +110,36 @@ class OumanEh800ConfigFlow(ConfigFlow, domain=DOMAIN):
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle initiation of re-authentication."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle re-authentication with the device."""
reauth_entry = self._get_reauth_entry()
errors: dict[str, str] = {}
if user_input is not None:
if not (
errors := await self._async_try_login(
{CONF_URL: reauth_entry.data[CONF_URL], **user_input}
)
):
return self.async_update_reload_and_abort(
reauth_entry, data_updates=user_input
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=self.add_suggested_values_to_schema(
STEP_REAUTH_DATA_SCHEMA, user_input or reauth_entry.data
),
errors=errors,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -20,7 +20,7 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import (
ConfigEntryError,
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
)
@@ -103,7 +103,7 @@ class OumanEh800Coordinator(DataUpdateCoordinator[dict[OumanEndpoint, OumanValue
await self.client.login()
self._registry_set = await self.client.get_active_registries()
except OumanClientAuthenticationError as err:
raise ConfigEntryError("Invalid credentials") from err
raise ConfigEntryAuthFailed("Invalid credentials") from err
except OumanClientCommunicationError as err:
raise ConfigEntryNotReady("Error communicating with API") from err
@@ -122,6 +122,9 @@ class OumanEh800Coordinator(DataUpdateCoordinator[dict[OumanEndpoint, OumanValue
try:
result = await self.client.set_endpoint_value(endpoint, value)
except OumanClientAuthenticationError as err:
# Reload the config entry; setup re-validates the credentials and
# starts a reauth flow if they are no longer valid.
self.hass.config_entries.async_schedule_reload(self.config_entry.entry_id)
raise HomeAssistantError("Authentication failed") from err
except OumanClientCommunicationError as err:
raise HomeAssistantError("Error communicating with API") from err
@@ -42,7 +42,7 @@ rules:
integration-owner: done
log-when-unavailable: done
parallel-updates: done
reauthentication-flow: todo
reauthentication-flow: done
test-coverage: todo
# Gold
@@ -2,6 +2,7 @@
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
},
"error": {
@@ -11,6 +12,17 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
"username": "[%key:common::config_flow::data::username%]"
},
"data_description": {
"password": "[%key:component::ouman_eh_800::config::step::user::data_description::password%]",
"username": "[%key:component::ouman_eh_800::config::step::user::data_description::username%]"
},
"description": "Re-enter the credentials for the Ouman EH-800 web interface."
},
"reconfigure": {
"data": {
"password": "[%key:common::config_flow::data::password%]",
@@ -23,9 +23,11 @@ CONF_EMBED_IFRAME = "embed_iframe"
CONF_TRUST_EXTERNAL_SCRIPT = "trust_external_script"
CONF_URL_EXCLUSIVE_GROUP = "url_exclusive_group"
CONF_REQUIRE_ADMIN = "require_admin"
CONF_HANDLE_SAFE_AREA = "handle_safe_area"
DEFAULT_EMBED_IFRAME = False
DEFAULT_TRUST_EXTERNAL = False
DEFAULT_HANDLE_SAFE_AREA = False
DEFAULT_ICON = "mdi:bookmark"
LEGACY_URL = "/api/panel_custom/{}"
@@ -59,6 +61,9 @@ CONFIG_SCHEMA = vol.Schema(
default=DEFAULT_TRUST_EXTERNAL,
): cv.boolean,
vol.Optional(CONF_REQUIRE_ADMIN, default=False): cv.boolean,
vol.Optional(
CONF_HANDLE_SAFE_AREA, default=DEFAULT_HANDLE_SAFE_AREA
): cv.boolean,
}
),
],
@@ -92,6 +97,9 @@ async def async_register_panel(
# If your panel is used to configure an integration,
# needs the domain of the integration
config_panel_domain: str | None = None,
# If your panel handles the safe area insets itself, opting out of the
# padding Home Assistant would otherwise add around it
handle_safe_area: bool = DEFAULT_HANDLE_SAFE_AREA,
) -> None:
"""Register a new custom panel."""
if js_url is None and module_url is None:
@@ -103,6 +111,7 @@ async def async_register_panel(
"name": webcomponent_name,
"embed_iframe": embed_iframe,
"trust_external": trust_external,
"handle_safe_area": handle_safe_area,
}
if js_url is not None:
@@ -148,6 +157,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"trust_external": panel[CONF_TRUST_EXTERNAL_SCRIPT],
"embed_iframe": panel[CONF_EMBED_IFRAME],
"require_admin": panel[CONF_REQUIRE_ADMIN],
"handle_safe_area": panel[CONF_HANDLE_SAFE_AREA],
}
if CONF_JS_URL in panel:
@@ -192,7 +192,7 @@ class PicnicConfigFlow(ConfigFlow, domain=DOMAIN):
CONF_ACCESS_TOKEN: auth_token,
CONF_COUNTRY_CODE: user_input[CONF_COUNTRY_CODE],
}
existing_entry = await self.async_set_unique_id(user_data["user_id"])
existing_entry = await self.async_set_unique_id(user_data.user_id)
# Abort if we're adding a new config and the unique id
# is already in use, else create the entry
+76 -58
View File
@@ -2,12 +2,13 @@
import asyncio
from contextlib import suppress
import copy
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import override
from python_picnic_api2 import PicnicAPI
from python_picnic_api2.models import Cart, DeliverySummary, Slot
from python_picnic_api2.session import PicnicAuthError
from homeassistant.config_entries import ConfigEntry
@@ -32,6 +33,25 @@ from .const import (
type PicnicConfigEntry = ConfigEntry[PicnicUpdateCoordinator]
@dataclass
class NextDeliveryData:
"""The next (current, undelivered) delivery, with its live ETA."""
delivery: DeliverySummary | None = None
eta_start: str | None = None
eta_end: str | None = None
estimated_arrival: int | None = None
@dataclass
class LastOrderData:
"""The most recent delivery, with its total price."""
delivery: DeliverySummary | None = None
total_price: int = 0
delivery_time_start: str | None = None
class PicnicUpdateCoordinator(DataUpdateCoordinator):
"""The coordinator to fetch data from the Picnic API at a set interval."""
@@ -86,21 +106,20 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator):
return data
@staticmethod
def _get_update_interval(next_delivery: dict | None) -> timedelta:
def _get_update_interval(next_delivery: NextDeliveryData | None) -> timedelta:
"""Poll faster around the delivery so the live ETA is picked up in time."""
if not next_delivery:
if next_delivery is None or next_delivery.delivery is None:
return DEFAULT_UPDATE_INTERVAL
eta = next_delivery.get("eta")
slot = next_delivery.get("slot")
slot = next_delivery.delivery.slot
start = end = None
if eta:
start = dt_util.parse_datetime(str(eta.get("start")))
end = dt_util.parse_datetime(str(eta.get("end")))
if next_delivery.eta_start and next_delivery.eta_end:
start = dt_util.parse_datetime(next_delivery.eta_start)
end = dt_util.parse_datetime(next_delivery.eta_end)
if (start is None or end is None) and slot:
start = dt_util.parse_datetime(str(slot.get("window_start")))
end = dt_util.parse_datetime(str(slot.get("window_end")))
start = dt_util.parse_datetime(str(slot.window_start))
end = dt_util.parse_datetime(str(slot.window_end))
if start is None or end is None:
return DEFAULT_UPDATE_INTERVAL
@@ -129,12 +148,11 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator):
raise UpdateFailed("API response doesn't contain expected data.")
next_delivery, last_order = self._get_order_data()
slot_data = self._get_slot_data(cart)
return {
ADDRESS: self._get_address(),
CART_DATA: cart,
SLOT_DATA: slot_data,
SLOT_DATA: self._get_slot_data(cart),
NEXT_DELIVERY_DATA: next_delivery,
LAST_ORDER_DATA: last_order,
}
@@ -142,85 +160,85 @@ class PicnicUpdateCoordinator(DataUpdateCoordinator):
def _get_address(self):
"""Get the address that identifies the Picnic service."""
if self._user_address is None:
address = self.picnic_api_client.get_user()["address"]
address = self.picnic_api_client.get_user().address
self._user_address = (
f"{address['street']} "
f"{address['house_number']}{address['house_number_ext']}"
f"{address.street} "
f"{address.house_number}{address.house_number_ext or ''}"
)
return self._user_address
@staticmethod
def _get_slot_data(cart: dict) -> dict:
def _get_slot_data(cart: Cart) -> Slot | None:
"""Get the selected slot, if it's explicitly selected."""
selected_slot = cart.get("selected_slot", {})
available_slots = cart.get("delivery_slots", [])
selected_slot = cart.selected_slot
if selected_slot.get("state") == "EXPLICIT":
slot_data = filter(
lambda slot: slot.get("slot_id") == selected_slot.get("slot_id"),
available_slots,
)
if slot_data:
return next(slot_data)
if selected_slot and selected_slot.state == "EXPLICIT":
for slot in cart.delivery_slots:
if slot.slot_id == selected_slot.slot_id:
return slot
return {}
return None
def _get_order_data(self) -> tuple[dict, dict]:
@staticmethod
def _delivery_time(delivery: DeliverySummary) -> dict | None:
"""Return the raw delivery-time window; not a field the library models."""
return delivery.raw.get("delivery_time") if delivery.raw else None
def _get_order_data(self) -> tuple[NextDeliveryData, LastOrderData]:
"""Get data of the last order from the list of deliveries."""
# Get the deliveries
deliveries = self.picnic_api_client.get_deliveries(summary=True)
# Determine the last order and return an empty dict if there is none
# Determine the last order and return empty data if there is none
try:
# Filter on status CURRENT and select the last
# on the list which is the first one to be delivered
# Make a deepcopy because some references are local
next_deliveries = list(
filter(lambda d: d["status"] == "CURRENT", deliveries)
)
next_delivery = (
copy.deepcopy(next_deliveries[-1]) if next_deliveries else {}
)
last_order = copy.deepcopy(deliveries[0]) if deliveries else {}
except KeyError, TypeError:
# A KeyError or TypeError indicate that the
next_deliveries = [d for d in deliveries if d.status == "CURRENT"]
next_delivery = next_deliveries[-1] if next_deliveries else None
last_order = deliveries[0] if deliveries else None
except AttributeError, TypeError:
# An AttributeError or TypeError indicate that the
# response contains unexpected data
return {}, {}
return NextDeliveryData(), LastOrderData()
if last_order is None:
return NextDeliveryData(), LastOrderData()
# Get the next order's position details if there is an undelivered order
delivery_position = {}
if next_delivery and not next_delivery.get("delivery_time"):
if next_delivery and not self._delivery_time(next_delivery):
# ValueError: If no information yet can mean an empty response
with suppress(ValueError):
delivery_position = self.picnic_api_client.get_delivery_position(
next_delivery["delivery_id"]
next_delivery.delivery_id
)
# Determine the ETA, if available, the one from the
# delivery position API is more precise
# but, it's only available shortly before the actual delivery.
next_delivery["eta"] = delivery_position.get(
"eta_window", next_delivery.get("eta2", {})
eta_window = delivery_position.get("eta_window") or {}
eta2 = next_delivery.eta2 if next_delivery else None
next_delivery_data = NextDeliveryData(
delivery=next_delivery,
eta_start=eta_window.get("start") or (eta2.start if eta2 else None),
eta_end=eta_window.get("end") or (eta2.end if eta2 else None),
# The position response's eta (unix timestamp in milliseconds) feeds
# the estimated arrival sensor; the API only serves it shortly before
# the delivery, so that sensor is unknown outside that window
estimated_arrival=delivery_position.get("eta"),
)
if "eta2" in next_delivery:
del next_delivery["eta2"]
# The position response's eta (unix timestamp in milliseconds) feeds
# the estimated arrival sensor; the API only serves it shortly before
# the delivery, so that sensor is unknown outside that window
next_delivery["estimated_arrival"] = delivery_position.get("eta")
# Determine the total price by adding up the total price of all sub-orders
total_price = 0
for order in last_order.get("orders", []):
total_price += order.get("total_price", 0)
last_order["total_price"] = total_price
total_price = sum(order.total_price or 0 for order in last_order.orders)
delivery_time = self._delivery_time(last_order)
last_order_data = LastOrderData(
delivery=last_order,
total_price=total_price,
delivery_time_start=delivery_time.get("start") if delivery_time else None,
)
# Make sure delivery_time is a dict
last_order.setdefault("delivery_time", {})
return next_delivery, last_order
return next_delivery_data, last_order_data
@callback
def _update_auth_token(self):
@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["python_picnic_api2"],
"requirements": ["python-picnic-api2==1.3.4"]
"requirements": ["python-picnic-api2==2.0.1"]
}
+60 -30
View File
@@ -39,7 +39,17 @@ from .const import (
SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE,
SENSOR_SELECTED_SLOT_START,
)
from .coordinator import PicnicConfigEntry, PicnicUpdateCoordinator
from .coordinator import (
LastOrderData,
NextDeliveryData,
PicnicConfigEntry,
PicnicUpdateCoordinator,
)
_EMPTY_DATA_FACTORIES: dict[str, Callable[[], Any]] = {
"next_delivery_data": NextDeliveryData,
"last_order_data": LastOrderData,
}
@dataclass(frozen=True, kw_only=True)
@@ -59,35 +69,41 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
key=SENSOR_CART_ITEMS_COUNT,
translation_key=SENSOR_CART_ITEMS_COUNT,
data_type="cart_data",
value_fn=lambda cart: cart.get("total_count", 0),
value_fn=lambda cart: (cart.total_count or 0) if cart else 0,
),
PicnicSensorEntityDescription(
key=SENSOR_CART_TOTAL_PRICE,
translation_key=SENSOR_CART_TOTAL_PRICE,
native_unit_of_measurement=CURRENCY_EURO,
data_type="cart_data",
value_fn=lambda cart: cart.get("total_price", 0) / 100,
value_fn=lambda cart: ((cart.total_price or 0) if cart else 0) / 100,
),
PicnicSensorEntityDescription(
key=SENSOR_SELECTED_SLOT_START,
translation_key=SENSOR_SELECTED_SLOT_START,
device_class=SensorDeviceClass.TIMESTAMP,
data_type="slot_data",
value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("window_start"))),
value_fn=lambda slot: (
dt_util.parse_datetime(str(slot.window_start)) if slot else None
),
),
PicnicSensorEntityDescription(
key=SENSOR_SELECTED_SLOT_END,
translation_key=SENSOR_SELECTED_SLOT_END,
device_class=SensorDeviceClass.TIMESTAMP,
data_type="slot_data",
value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("window_end"))),
value_fn=lambda slot: (
dt_util.parse_datetime(str(slot.window_end)) if slot else None
),
),
PicnicSensorEntityDescription(
key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME,
translation_key=SENSOR_SELECTED_SLOT_MAX_ORDER_TIME,
device_class=SensorDeviceClass.TIMESTAMP,
data_type="slot_data",
value_fn=lambda slot: dt_util.parse_datetime(str(slot.get("cut_off_time"))),
value_fn=lambda slot: (
dt_util.parse_datetime(str(slot.cut_off_time)) if slot else None
),
),
PicnicSensorEntityDescription(
key=SENSOR_SELECTED_SLOT_MIN_ORDER_VALUE,
@@ -95,8 +111,8 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
native_unit_of_measurement=CURRENCY_EURO,
data_type="slot_data",
value_fn=lambda slot: (
slot["minimum_order_value"] / 100
if slot.get("minimum_order_value")
slot.minimum_order_value / 100
if slot and slot.minimum_order_value
else None
),
),
@@ -105,8 +121,10 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
translation_key=SENSOR_LAST_ORDER_SLOT_START,
device_class=SensorDeviceClass.TIMESTAMP,
data_type="last_order_data",
value_fn=lambda last_order: dt_util.parse_datetime(
str(last_order.get("slot", {}).get("window_start"))
value_fn=lambda last_order: (
dt_util.parse_datetime(str(last_order.delivery.slot.window_start))
if last_order.delivery and last_order.delivery.slot
else None
),
),
PicnicSensorEntityDescription(
@@ -114,23 +132,29 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
translation_key=SENSOR_LAST_ORDER_SLOT_END,
device_class=SensorDeviceClass.TIMESTAMP,
data_type="last_order_data",
value_fn=lambda last_order: dt_util.parse_datetime(
str(last_order.get("slot", {}).get("window_end"))
value_fn=lambda last_order: (
dt_util.parse_datetime(str(last_order.delivery.slot.window_end))
if last_order.delivery and last_order.delivery.slot
else None
),
),
PicnicSensorEntityDescription(
key=SENSOR_LAST_ORDER_STATUS,
translation_key=SENSOR_LAST_ORDER_STATUS,
data_type="last_order_data",
value_fn=lambda last_order: last_order.get("status"),
value_fn=lambda last_order: (
last_order.delivery.status if last_order.delivery else None
),
),
PicnicSensorEntityDescription(
key=SENSOR_LAST_ORDER_MAX_ORDER_TIME,
translation_key=SENSOR_LAST_ORDER_MAX_ORDER_TIME,
device_class=SensorDeviceClass.TIMESTAMP,
data_type="last_order_data",
value_fn=lambda last_order: dt_util.parse_datetime(
str(last_order.get("slot", {}).get("cut_off_time"))
value_fn=lambda last_order: (
dt_util.parse_datetime(str(last_order.delivery.slot.cut_off_time))
if last_order.delivery and last_order.delivery.slot
else None
),
),
PicnicSensorEntityDescription(
@@ -139,7 +163,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.TIMESTAMP,
data_type="last_order_data",
value_fn=lambda last_order: dt_util.parse_datetime(
str(last_order.get("delivery_time", {}).get("start"))
str(last_order.delivery_time_start)
),
),
PicnicSensorEntityDescription(
@@ -147,7 +171,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
translation_key=SENSOR_LAST_ORDER_TOTAL_PRICE,
native_unit_of_measurement=CURRENCY_EURO,
data_type="last_order_data",
value_fn=lambda last_order: last_order.get("total_price", 0) / 100,
value_fn=lambda last_order: last_order.total_price / 100,
),
PicnicSensorEntityDescription(
key=SENSOR_NEXT_DELIVERY_ETA_START,
@@ -155,7 +179,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.TIMESTAMP,
data_type="next_delivery_data",
value_fn=lambda next_delivery: dt_util.parse_datetime(
str(next_delivery.get("eta", {}).get("start"))
str(next_delivery.eta_start)
),
),
PicnicSensorEntityDescription(
@@ -164,7 +188,7 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.TIMESTAMP,
data_type="next_delivery_data",
value_fn=lambda next_delivery: dt_util.parse_datetime(
str(next_delivery.get("eta", {}).get("end"))
str(next_delivery.eta_end)
),
),
PicnicSensorEntityDescription(
@@ -173,8 +197,8 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
device_class=SensorDeviceClass.TIMESTAMP,
data_type="next_delivery_data",
value_fn=lambda next_delivery: (
dt_util.utc_from_timestamp(next_delivery["estimated_arrival"] / 1000)
if next_delivery.get("estimated_arrival")
dt_util.utc_from_timestamp(next_delivery.estimated_arrival / 1000)
if next_delivery.estimated_arrival
else None
),
),
@@ -183,8 +207,10 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
translation_key=SENSOR_NEXT_DELIVERY_SLOT_START,
device_class=SensorDeviceClass.TIMESTAMP,
data_type="next_delivery_data",
value_fn=lambda next_delivery: dt_util.parse_datetime(
str(next_delivery.get("slot", {}).get("window_start"))
value_fn=lambda next_delivery: (
dt_util.parse_datetime(str(next_delivery.delivery.slot.window_start))
if next_delivery.delivery and next_delivery.delivery.slot
else None
),
),
PicnicSensorEntityDescription(
@@ -192,8 +218,10 @@ SENSOR_TYPES: tuple[PicnicSensorEntityDescription, ...] = (
translation_key=SENSOR_NEXT_DELIVERY_SLOT_END,
device_class=SensorDeviceClass.TIMESTAMP,
data_type="next_delivery_data",
value_fn=lambda next_delivery: dt_util.parse_datetime(
str(next_delivery.get("slot", {}).get("window_end"))
value_fn=lambda next_delivery: (
dt_util.parse_datetime(str(next_delivery.delivery.slot.window_end))
if next_delivery.delivery and next_delivery.delivery.slot
else None
),
),
)
@@ -243,9 +271,11 @@ class PicnicSensor(SensorEntity, CoordinatorEntity[PicnicUpdateCoordinator]):
@override
def native_value(self) -> StateType | datetime:
"""Return the value reported by the sensor."""
data_set = (
self.coordinator.data.get(self.entity_description.data_type, {})
if self.coordinator.data is not None
else {}
)
data = self.coordinator.data or {}
data_type = self.entity_description.data_type
if data_type in data:
data_set = data[data_type]
else:
factory = _EMPTY_DATA_FACTORIES.get(data_type)
data_set = factory() if factory else None
return self.entity_description.value_fn(data_set)
+4 -4
View File
@@ -80,12 +80,12 @@ def product_search(api_client: PicnicAPI, product_name: str | None) -> str | Non
search_result = api_client.search(product_name)
if not search_result or "items" not in search_result[0]:
if not search_result or not search_result.items:
return None
# Return the first valid result
for item in search_result[0]["items"]:
if "name" in item:
return str(item["id"])
for item in search_result.items:
if item.name:
return str(item.id)
return None
+7 -6
View File
@@ -15,7 +15,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .const import CART_DATA, DOMAIN
from .coordinator import PicnicConfigEntry, PicnicUpdateCoordinator
from .services import product_search
@@ -62,17 +62,18 @@ class PicnicCart(TodoListEntity, CoordinatorEntity[PicnicUpdateCoordinator]):
if self.coordinator.data is None:
return None
_LOGGER.debug(self.coordinator.data["cart_data"]["items"])
cart = self.coordinator.data[CART_DATA]
_LOGGER.debug(cart.items)
return [
TodoItem(
summary=f"{article['name']} ({article['unit_quantity']})",
uid=f"{item['id']}-{article['id']}",
summary=f"{article.name} ({article.unit_quantity})",
uid=f"{line.id}-{article.id}",
# We set 'NEEDS_ACTION' so they count as state
status=TodoItemStatus.NEEDS_ACTION,
)
for item in self.coordinator.data["cart_data"]["items"]
for article in item["items"]
for line in cart.items
for article in line.items
]
@override
+2 -2
View File
@@ -45,8 +45,8 @@ ZEROCONF_MAP: Final[dict[str, str]] = {
}
type NumberType = Literal[
"maximum_boiler_temperature",
"max_dhw_temperature",
"boiler_temperature",
"dhw_temperature",
"temperature_offset",
]
@@ -8,6 +8,6 @@
"iot_class": "local_polling",
"loggers": ["plugwise"],
"quality_scale": "platinum",
"requirements": ["plugwise==1.14.4"],
"requirements": ["plugwise==1.14.5"],
"zeroconf": ["_plugwise._tcp.local."]
}

Some files were not shown because too many files have changed in this diff Show More