mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Merge branch 'dev' into media-player-group-members-without-grouping
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}"
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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." }]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -22,5 +22,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aiolyric"],
|
||||
"requirements": ["aiolyric==2.1.1"]
|
||||
"requirements": ["aiolyric==2.1.2"]
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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%]"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"]
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user