mirror of
https://github.com/home-assistant/core.git
synced 2026-05-05 12:24:48 +02:00
Merge branch 'dev' into flussButton
This commit is contained in:
Binary file not shown.
|
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 99 KiB |
@@ -150,6 +150,10 @@ LOGGING_AND_HTTP_DEPS_INTEGRATIONS = {
|
||||
"isal",
|
||||
# Set log levels
|
||||
"logger",
|
||||
# Ensure network config is available
|
||||
# before hassio or any other integration is
|
||||
# loaded that might create an aiohttp client session
|
||||
"network",
|
||||
# Error logging
|
||||
"system_log",
|
||||
"sentry",
|
||||
|
||||
@@ -90,7 +90,7 @@
|
||||
},
|
||||
"alarm_arm_home": {
|
||||
"name": "Arm home",
|
||||
"description": "Sets the alarm to: _armed, but someone is home_.",
|
||||
"description": "Arms the alarm in the home mode.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
|
||||
@@ -100,7 +100,7 @@
|
||||
},
|
||||
"alarm_arm_away": {
|
||||
"name": "Arm away",
|
||||
"description": "Sets the alarm to: _armed, no one home_.",
|
||||
"description": "Arms the alarm in the away mode.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
|
||||
@@ -110,7 +110,7 @@
|
||||
},
|
||||
"alarm_arm_night": {
|
||||
"name": "Arm night",
|
||||
"description": "Sets the alarm to: _armed for the night_.",
|
||||
"description": "Arms the alarm in the night mode.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
|
||||
@@ -120,7 +120,7 @@
|
||||
},
|
||||
"alarm_arm_vacation": {
|
||||
"name": "Arm vacation",
|
||||
"description": "Sets the alarm to: _armed for vacation_.",
|
||||
"description": "Arms the alarm in the vacation mode.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
|
||||
@@ -130,7 +130,7 @@
|
||||
},
|
||||
"alarm_trigger": {
|
||||
"name": "Trigger",
|
||||
"description": "Trigger the alarm manually.",
|
||||
"description": "Triggers the alarm manually.",
|
||||
"fields": {
|
||||
"code": {
|
||||
"name": "[%key:component::alarm_control_panel::services::alarm_disarm::fields::code::name%]",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/arcam_fmj",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["arcam"],
|
||||
"requirements": ["arcam-fmj==1.5.2"],
|
||||
"requirements": ["arcam-fmj==1.8.0"],
|
||||
"ssdp": [
|
||||
{
|
||||
"deviceType": "urn:schemas-upnp-org:device:MediaRenderer:1",
|
||||
|
||||
@@ -19,6 +19,7 @@ from .const import (
|
||||
DOMAIN,
|
||||
AssistSatelliteEntityFeature,
|
||||
)
|
||||
from .entity import AssistSatelliteConfiguration
|
||||
|
||||
CONNECTION_TEST_TIMEOUT = 30
|
||||
|
||||
@@ -91,7 +92,16 @@ def websocket_get_configuration(
|
||||
)
|
||||
return
|
||||
|
||||
config_dict = asdict(satellite.async_get_configuration())
|
||||
try:
|
||||
config_dict = asdict(satellite.async_get_configuration())
|
||||
except NotImplementedError:
|
||||
# Stub configuration
|
||||
config_dict = asdict(
|
||||
AssistSatelliteConfiguration(
|
||||
available_wake_words=[], active_wake_words=[], max_active_wake_words=1
|
||||
)
|
||||
)
|
||||
|
||||
config_dict["pipeline_entity_id"] = satellite.pipeline_entity_id
|
||||
config_dict["vad_entity_id"] = satellite.vad_sensitivity_entity_id
|
||||
|
||||
|
||||
@@ -24,6 +24,8 @@ PLATFORMS = [
|
||||
Platform.FAN,
|
||||
Platform.LIGHT,
|
||||
Platform.SELECT,
|
||||
Platform.SWITCH,
|
||||
Platform.TIME,
|
||||
]
|
||||
|
||||
|
||||
|
||||
@@ -14,5 +14,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/balboa",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["pybalboa"],
|
||||
"requirements": ["pybalboa==1.1.2"]
|
||||
"requirements": ["pybalboa==1.1.3"]
|
||||
}
|
||||
|
||||
@@ -78,6 +78,19 @@
|
||||
"high": "High"
|
||||
}
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"filter_cycle_2_enabled": {
|
||||
"name": "Filter cycle 2 enabled"
|
||||
}
|
||||
},
|
||||
"time": {
|
||||
"filter_cycle_start": {
|
||||
"name": "Filter cycle {index} start"
|
||||
},
|
||||
"filter_cycle_end": {
|
||||
"name": "Filter cycle {index} end"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
"""Support for Balboa switches."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pybalboa import SpaClient
|
||||
|
||||
from homeassistant.components.switch import SwitchEntity
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BalboaConfigEntry
|
||||
from .entity import BalboaEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: BalboaConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the spa's switches."""
|
||||
spa = entry.runtime_data
|
||||
async_add_entities([BalboaSwitchEntity(spa)])
|
||||
|
||||
|
||||
class BalboaSwitchEntity(BalboaEntity, SwitchEntity):
|
||||
"""Representation of a Balboa switch entity."""
|
||||
|
||||
def __init__(self, spa: SpaClient) -> None:
|
||||
"""Initialize a Balboa switch entity."""
|
||||
super().__init__(spa, "filter_cycle_2_enabled")
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
self._attr_translation_key = "filter_cycle_2_enabled"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return True if entity is on."""
|
||||
return self._client.filter_cycle_2_enabled
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity on."""
|
||||
await self._client.configure_filter_cycle(2, enabled=True)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the entity off."""
|
||||
await self._client.configure_filter_cycle(2, enabled=False)
|
||||
@@ -0,0 +1,56 @@
|
||||
"""Support for Balboa times."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import time
|
||||
import itertools
|
||||
from typing import Any
|
||||
|
||||
from pybalboa import SpaClient
|
||||
|
||||
from homeassistant.components.time import TimeEntity
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import BalboaConfigEntry
|
||||
from .entity import BalboaEntity
|
||||
|
||||
FILTER_CYCLE = "filter_cycle_"
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: BalboaConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the spa's times."""
|
||||
spa = entry.runtime_data
|
||||
async_add_entities(
|
||||
BalboaTimeEntity(spa, index, period)
|
||||
for index, period in itertools.product((1, 2), ("start", "end"))
|
||||
)
|
||||
|
||||
|
||||
class BalboaTimeEntity(BalboaEntity, TimeEntity):
|
||||
"""Representation of a Balboa time entity."""
|
||||
|
||||
entity_category = EntityCategory.CONFIG
|
||||
|
||||
def __init__(self, spa: SpaClient, index: int, period: str) -> None:
|
||||
"""Initialize a Balboa time entity."""
|
||||
super().__init__(spa, f"{FILTER_CYCLE}{index}_{period}")
|
||||
self.index = index
|
||||
self.period = period
|
||||
self._attr_translation_key = f"{FILTER_CYCLE}{period}"
|
||||
self._attr_translation_placeholders = {"index": str(index)}
|
||||
|
||||
@property
|
||||
def native_value(self) -> time | None:
|
||||
"""Return the value reported by the time."""
|
||||
return getattr(self._client, f"{FILTER_CYCLE}{self.index}_{self.period}")
|
||||
|
||||
async def async_set_value(self, value: time) -> None:
|
||||
"""Change the time."""
|
||||
args: dict[str, Any] = {self.period: value}
|
||||
await self._client.configure_filter_cycle(self.index, **args)
|
||||
@@ -4,12 +4,13 @@ from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from homeassistant.components.event import DOMAIN as EVENT_DOMAIN
|
||||
from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
|
||||
from . import BangOlufsenConfigEntry
|
||||
from .const import DOMAIN
|
||||
from .const import DEVICE_BUTTONS, DOMAIN
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
@@ -25,8 +26,9 @@ async def async_get_config_entry_diagnostics(
|
||||
if TYPE_CHECKING:
|
||||
assert config_entry.unique_id
|
||||
|
||||
# Add media_player entity's state
|
||||
entity_registry = er.async_get(hass)
|
||||
|
||||
# Add media_player entity's state
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
MEDIA_PLAYER_DOMAIN, DOMAIN, config_entry.unique_id
|
||||
):
|
||||
@@ -37,4 +39,16 @@ async def async_get_config_entry_diagnostics(
|
||||
state_dict.pop("context")
|
||||
data["media_player"] = state_dict
|
||||
|
||||
# Add button Event entity states (if enabled)
|
||||
for device_button in DEVICE_BUTTONS:
|
||||
if entity_id := entity_registry.async_get_entity_id(
|
||||
EVENT_DOMAIN, DOMAIN, f"{config_entry.unique_id}_{device_button}"
|
||||
):
|
||||
if state := hass.states.get(entity_id):
|
||||
state_dict = dict(state.as_dict())
|
||||
|
||||
# Remove context as it is not relevant
|
||||
state_dict.pop("context")
|
||||
data[f"{device_button}_event"] = state_dict
|
||||
|
||||
return data
|
||||
|
||||
@@ -7,5 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["bring_api"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["bring-api==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ rules:
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions: done
|
||||
docs-high-level-description: todo
|
||||
docs-installation-instructions: todo
|
||||
docs-removal-instructions: todo
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: The integration registers no events
|
||||
@@ -26,8 +26,10 @@ rules:
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: Integration has no configuration parameters
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable:
|
||||
@@ -46,13 +48,15 @@ rules:
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: Integration is a service and has no devices.
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
docs-data-update: done
|
||||
docs-examples: done
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices:
|
||||
status: exempt
|
||||
comment: Integration is a service and has no devices.
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices: done
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
|
||||
@@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.91.0"],
|
||||
"requirements": ["hass-nabucasa==0.92.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -53,6 +53,7 @@ from homeassistant.helpers import (
|
||||
)
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import async_track_state_added_domain
|
||||
from homeassistant.util import language as language_util
|
||||
from homeassistant.util.json import JsonObjectType, json_loads_object
|
||||
|
||||
from .chat_log import AssistantContent, async_get_chat_log
|
||||
@@ -914,26 +915,20 @@ class DefaultAgent(ConversationEntity):
|
||||
def _load_intents(self, language: str) -> LanguageIntents | None:
|
||||
"""Load all intents for language (run inside executor)."""
|
||||
intents_dict: dict[str, Any] = {}
|
||||
language_variant: str | None = None
|
||||
supported_langs = set(get_languages())
|
||||
|
||||
# Choose a language variant upfront and commit to it for custom
|
||||
# sentences, etc.
|
||||
all_language_variants = {lang.lower(): lang for lang in supported_langs}
|
||||
lang_matches = language_util.matches(language, supported_langs)
|
||||
|
||||
# en-US, en_US, en, ...
|
||||
for maybe_variant in _get_language_variations(language):
|
||||
matching_variant = all_language_variants.get(maybe_variant.lower())
|
||||
if matching_variant:
|
||||
language_variant = matching_variant
|
||||
break
|
||||
|
||||
if not language_variant:
|
||||
if not lang_matches:
|
||||
_LOGGER.warning(
|
||||
"Unable to find supported language variant for %s", language
|
||||
)
|
||||
return None
|
||||
|
||||
language_variant = lang_matches[0]
|
||||
|
||||
# Load intents for this language variant
|
||||
lang_variant_intents = get_intents(language_variant, json_load=json_load)
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/econet",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["paho_mqtt", "pyeconet"],
|
||||
"requirements": ["pyeconet==0.1.27"]
|
||||
"requirements": ["pyeconet==0.1.28"]
|
||||
}
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
"message": "Params are required for the command: {command}"
|
||||
},
|
||||
"vacuum_raw_get_positions_not_supported": {
|
||||
"message": "Getting the positions of the chargers and the device itself is not supported"
|
||||
"message": "Retrieving the positions of the chargers and the device itself is not supported"
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
@@ -264,7 +264,7 @@
|
||||
"services": {
|
||||
"raw_get_positions": {
|
||||
"name": "Get raw positions",
|
||||
"description": "Get the raw response for the positions of the chargers and the device itself."
|
||||
"description": "Retrieves a raw response containing the positions of the chargers and the device itself."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["sense_energy"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["sense-energy==0.13.4"]
|
||||
"requirements": ["sense-energy==0.13.5"]
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol", "bleak_esphome"],
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==29.0.0",
|
||||
"aioesphomeapi==29.0.2",
|
||||
"esphome-dashboard-api==1.2.3",
|
||||
"bleak-esphome==2.7.1"
|
||||
],
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/flexit_bacnet",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["flexit_bacnet==2.2.3"]
|
||||
}
|
||||
|
||||
@@ -0,0 +1,88 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Integration does not define custom 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 use any actions.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: |
|
||||
Entities don't subscribe to events explicitly
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup:
|
||||
status: done
|
||||
comment: |
|
||||
Done implicitly with `await coordinator.async_config_entry_first_refresh()`.
|
||||
unique-config-entry: done
|
||||
# Silver
|
||||
action-exceptions: todo
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters:
|
||||
status: exempt
|
||||
comment: |
|
||||
Integration does not use options flow.
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable:
|
||||
status: done
|
||||
comment: |
|
||||
Done implicitly with coordinator.
|
||||
integration-owner: done
|
||||
log-when-unavailable:
|
||||
status: done
|
||||
comment: |
|
||||
Done implicitly with coordinator.
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: todo
|
||||
# Gold
|
||||
entity-translations: done
|
||||
entity-device-class: done
|
||||
devices: done
|
||||
entity-category: todo
|
||||
entity-disabled-by-default: todo
|
||||
discovery: todo
|
||||
stale-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Device type integration.
|
||||
diagnostics: todo
|
||||
exception-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: |
|
||||
Device type integration.
|
||||
discovery-update-info: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: |
|
||||
This is not applicable for this integration.
|
||||
docs-use-cases: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-data-update: done
|
||||
docs-known-limitations: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-examples: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: todo
|
||||
inject-websession: todo
|
||||
strict-typing: done
|
||||
@@ -5,6 +5,10 @@
|
||||
"data": {
|
||||
"ip_address": "[%key:common::config_flow::data::ip%]",
|
||||
"device_id": "[%key:common::config_flow::data::device%]"
|
||||
},
|
||||
"data_description": {
|
||||
"ip_address": "The IP address of the Flexit Nordic device",
|
||||
"device_id": "The device ID of the Flexit Nordic device"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -36,11 +36,11 @@
|
||||
"issues": {
|
||||
"import_failed_not_allowed_path": {
|
||||
"title": "The Folder Watcher YAML configuration could not be imported",
|
||||
"description": "Configuring Folder Watcher using YAML is being removed but your configuration could not be imported as the folder {path} is not in the configured allowlist.\n\nPlease add it to `{config_variable}` in config.yaml and restart Home Assistant to import it and fix this issue."
|
||||
"description": "Configuring Folder Watcher using YAML is being removed but your configuration could not be imported as the folder {path} is not in the configured allowlist.\n\nPlease add it to `{config_variable}` in configuration.yaml and restart Home Assistant to import it and fix this issue."
|
||||
},
|
||||
"setup_not_allowed_path": {
|
||||
"title": "The Folder Watcher configuration for {path} could not start",
|
||||
"description": "The path {path} is not accessible or not allowed to be accessed.\n\nPlease check the path is accessible and add it to `{config_variable}` in config.yaml and restart Home Assistant to fix this issue."
|
||||
"description": "The path {path} is not accessible or not allowed to be accessed.\n\nPlease check the path is accessible and add it to `{config_variable}` in configuration.yaml and restart Home Assistant to fix this issue."
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
|
||||
@@ -196,6 +196,7 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
self.hass = hass
|
||||
self.host = host
|
||||
self.mesh_role = MeshRoles.NONE
|
||||
self.mesh_wifi_uplink = False
|
||||
self.device_conn_type: str | None = None
|
||||
self.device_is_router: bool = False
|
||||
self.password = password
|
||||
@@ -610,6 +611,12 @@ class FritzBoxTools(DataUpdateCoordinator[UpdateCoordinatorDataType]):
|
||||
ssid=interf.get("ssid", ""),
|
||||
type=interf["type"],
|
||||
)
|
||||
|
||||
if interf["type"].lower() == "wlan" and interf[
|
||||
"name"
|
||||
].lower().startswith("uplink"):
|
||||
self.mesh_wifi_uplink = True
|
||||
|
||||
if dr.format_mac(int_mac) == self.mac:
|
||||
self.mesh_role = MeshRoles(node["mesh_role"])
|
||||
|
||||
|
||||
@@ -207,8 +207,9 @@ async def async_all_entities_list(
|
||||
local_ip: str,
|
||||
) -> list[Entity]:
|
||||
"""Get a list of all entities."""
|
||||
|
||||
if avm_wrapper.mesh_role == MeshRoles.SLAVE:
|
||||
if not avm_wrapper.mesh_wifi_uplink:
|
||||
return [*await _async_wifi_entities_list(avm_wrapper, device_friendly_name)]
|
||||
return []
|
||||
|
||||
return [
|
||||
@@ -565,6 +566,9 @@ class FritzBoxWifiSwitch(FritzBoxBaseSwitch):
|
||||
|
||||
self._attributes = {}
|
||||
self._attr_entity_category = EntityCategory.CONFIG
|
||||
self._attr_entity_registry_enabled_default = (
|
||||
avm_wrapper.mesh_role is not MeshRoles.SLAVE
|
||||
)
|
||||
self._network_num = network_num
|
||||
|
||||
switch_info = SwitchInfo(
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250210.0"]
|
||||
"requirements": ["home-assistant-frontend==20250214.0"]
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ class HabiticaImage(HabiticaBase, ImageEntity):
|
||||
translation_key=HabiticaImageEntity.AVATAR,
|
||||
)
|
||||
_attr_content_type = "image/png"
|
||||
_current_appearance: Avatar | None = None
|
||||
_avatar: Avatar | None = None
|
||||
_cache: bytes | None = None
|
||||
|
||||
def __init__(
|
||||
@@ -55,13 +55,13 @@ class HabiticaImage(HabiticaBase, ImageEntity):
|
||||
super().__init__(coordinator, self.entity_description)
|
||||
ImageEntity.__init__(self, hass)
|
||||
self._attr_image_last_updated = dt_util.utcnow()
|
||||
self._avatar = extract_avatar(self.coordinator.data.user)
|
||||
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Check if equipped gear and other things have changed since last avatar image generation."""
|
||||
new_appearance = extract_avatar(self.coordinator.data.user)
|
||||
|
||||
if self._current_appearance != new_appearance:
|
||||
self._current_appearance = new_appearance
|
||||
if self._avatar != self.coordinator.data.user:
|
||||
self._avatar = extract_avatar(self.coordinator.data.user)
|
||||
self._attr_image_last_updated = dt_util.utcnow()
|
||||
self._cache = None
|
||||
|
||||
@@ -69,8 +69,6 @@ class HabiticaImage(HabiticaBase, ImageEntity):
|
||||
|
||||
async def async_image(self) -> bytes | None:
|
||||
"""Return cached bytes, otherwise generate new avatar."""
|
||||
if not self._cache and self._current_appearance:
|
||||
self._cache = await self.coordinator.generate_avatar(
|
||||
self._current_appearance
|
||||
)
|
||||
if not self._cache and self._avatar:
|
||||
self._cache = await self.coordinator.generate_avatar(self._avatar)
|
||||
return self._cache
|
||||
|
||||
@@ -6,5 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/habitica",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["habiticalib"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["habiticalib==0.3.7"]
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ rules:
|
||||
status: exempt
|
||||
comment: No supportable devices.
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
|
||||
@@ -9,5 +9,5 @@
|
||||
},
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["apyhiveapi"],
|
||||
"requirements": ["pyhive-integration==1.0.1"]
|
||||
"requirements": ["pyhive-integration==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -2,11 +2,19 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohomeconnect.client import Client as HomeConnectClient
|
||||
from aiohomeconnect.model import CommandKey, Option, OptionKey, ProgramKey, SettingKey
|
||||
from aiohomeconnect.model import (
|
||||
ArrayOfOptions,
|
||||
CommandKey,
|
||||
Option,
|
||||
OptionKey,
|
||||
ProgramKey,
|
||||
SettingKey,
|
||||
)
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -19,34 +27,74 @@ from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
)
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .api import AsyncConfigEntryAuth
|
||||
from .const import (
|
||||
AFFECTS_TO_ACTIVE_PROGRAM,
|
||||
AFFECTS_TO_SELECTED_PROGRAM,
|
||||
ATTR_AFFECTS_TO,
|
||||
ATTR_KEY,
|
||||
ATTR_PROGRAM,
|
||||
ATTR_UNIT,
|
||||
ATTR_VALUE,
|
||||
DOMAIN,
|
||||
OLD_NEW_UNIQUE_ID_SUFFIX_MAP,
|
||||
PROGRAM_ENUM_OPTIONS,
|
||||
SERVICE_OPTION_ACTIVE,
|
||||
SERVICE_OPTION_SELECTED,
|
||||
SERVICE_PAUSE_PROGRAM,
|
||||
SERVICE_RESUME_PROGRAM,
|
||||
SERVICE_SELECT_PROGRAM,
|
||||
SERVICE_SET_PROGRAM_AND_OPTIONS,
|
||||
SERVICE_SETTING,
|
||||
SERVICE_START_PROGRAM,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY,
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE,
|
||||
TRANSLATION_KEYS_PROGRAMS_MAP,
|
||||
)
|
||||
from .coordinator import HomeConnectConfigEntry, HomeConnectCoordinator
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
from .utils import bsh_key_to_translation_key, get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
PROGRAM_OPTIONS = {
|
||||
bsh_key_to_translation_key(key): (
|
||||
key,
|
||||
value,
|
||||
)
|
||||
for key, value in {
|
||||
OptionKey.BSH_COMMON_DURATION: int,
|
||||
OptionKey.BSH_COMMON_START_IN_RELATIVE: int,
|
||||
OptionKey.BSH_COMMON_FINISH_IN_RELATIVE: int,
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_FILL_QUANTITY: int,
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_MULTIPLE_BEVERAGES: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_INTENSIV_ZONE: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_BRILLIANCE_DRY: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_VARIO_SPEED_PLUS: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_SILENCE_ON_DEMAND: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_HALF_LOAD: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_EXTRA_DRY: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_HYGIENE_PLUS: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_ECO_DRY: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_ZEOLITE_DRY: bool,
|
||||
OptionKey.COOKING_OVEN_SETPOINT_TEMPERATURE: int,
|
||||
OptionKey.COOKING_OVEN_FAST_PRE_HEAT: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_I_DOS_1_ACTIVE: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_I_DOS_2_ACTIVE: bool,
|
||||
}.items()
|
||||
}
|
||||
|
||||
|
||||
SERVICE_SETTING_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): str,
|
||||
@@ -58,6 +106,7 @@ SERVICE_SETTING_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
# DEPRECATED: Remove in 2025.9.0
|
||||
SERVICE_OPTION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): str,
|
||||
@@ -70,6 +119,7 @@ SERVICE_OPTION_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
# DEPRECATED: Remove in 2025.9.0
|
||||
SERVICE_PROGRAM_SCHEMA = vol.Any(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): str,
|
||||
@@ -93,6 +143,46 @@ SERVICE_PROGRAM_SCHEMA = vol.Any(
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _require_program_or_at_least_one_option(data: dict) -> dict:
|
||||
if ATTR_PROGRAM not in data and not any(
|
||||
option_key in data for option_key in (PROGRAM_ENUM_OPTIONS | PROGRAM_OPTIONS)
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="required_program_or_one_option_at_least",
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
SERVICE_PROGRAM_AND_OPTIONS_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): str,
|
||||
vol.Required(ATTR_AFFECTS_TO): vol.In(
|
||||
[AFFECTS_TO_ACTIVE_PROGRAM, AFFECTS_TO_SELECTED_PROGRAM]
|
||||
),
|
||||
vol.Optional(ATTR_PROGRAM): vol.In(TRANSLATION_KEYS_PROGRAMS_MAP.keys()),
|
||||
}
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
vol.Optional(translation_key): vol.In(allowed_values.keys())
|
||||
for translation_key, (
|
||||
key,
|
||||
allowed_values,
|
||||
) in PROGRAM_ENUM_OPTIONS.items()
|
||||
}
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
vol.Optional(translation_key): schema
|
||||
for translation_key, (key, schema) in PROGRAM_OPTIONS.items()
|
||||
}
|
||||
),
|
||||
_require_program_or_at_least_one_option,
|
||||
)
|
||||
|
||||
SERVICE_COMMAND_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_ID): str})
|
||||
|
||||
PLATFORMS = [
|
||||
@@ -144,7 +234,7 @@ async def _get_client_and_ha_id(
|
||||
return entry.runtime_data.client, ha_id
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
|
||||
"""Set up Home Connect component."""
|
||||
|
||||
async def _async_service_program(call: ServiceCall, start: bool):
|
||||
@@ -165,6 +255,57 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
else None
|
||||
)
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_set_program_and_option_actions",
|
||||
breaks_in_ha_version="2025.9.0",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_set_program_and_option_actions",
|
||||
translation_placeholders={
|
||||
"new_action_key": SERVICE_SET_PROGRAM_AND_OPTIONS,
|
||||
"remove_release": "2025.9.0",
|
||||
"deprecated_action_yaml": "\n".join(
|
||||
[
|
||||
"```yaml",
|
||||
f"action: {DOMAIN}.{SERVICE_START_PROGRAM if start else SERVICE_SELECT_PROGRAM}",
|
||||
"data:",
|
||||
f" {ATTR_DEVICE_ID}: DEVICE_ID",
|
||||
f" {ATTR_PROGRAM}: {program}",
|
||||
*([f" {ATTR_KEY}: {options[0].key}"] if options else []),
|
||||
*([f" {ATTR_VALUE}: {options[0].value}"] if options else []),
|
||||
*(
|
||||
[f" {ATTR_UNIT}: {options[0].unit}"]
|
||||
if options and options[0].unit
|
||||
else []
|
||||
),
|
||||
"```",
|
||||
]
|
||||
),
|
||||
"new_action_yaml": "\n ".join(
|
||||
[
|
||||
"```yaml",
|
||||
f"action: {DOMAIN}.{SERVICE_SET_PROGRAM_AND_OPTIONS}",
|
||||
"data:",
|
||||
f" {ATTR_DEVICE_ID}: DEVICE_ID",
|
||||
f" {ATTR_AFFECTS_TO}: {AFFECTS_TO_ACTIVE_PROGRAM if start else AFFECTS_TO_SELECTED_PROGRAM}",
|
||||
f" {ATTR_PROGRAM}: {bsh_key_to_translation_key(program.value)}",
|
||||
*(
|
||||
[
|
||||
f" {bsh_key_to_translation_key(options[0].key)}: {options[0].value}"
|
||||
]
|
||||
if options
|
||||
else []
|
||||
),
|
||||
"```",
|
||||
]
|
||||
),
|
||||
"repo_link": "[aiohomeconnect](https://github.com/MartinHjelmare/aiohomeconnect)",
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
if start:
|
||||
await client.start_program(ha_id, program_key=program, options=options)
|
||||
@@ -189,6 +330,44 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
unit = call.data.get(ATTR_UNIT)
|
||||
client, ha_id = await _get_client_and_ha_id(hass, call.data[ATTR_DEVICE_ID])
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_set_program_and_option_actions",
|
||||
breaks_in_ha_version="2025.9.0",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_set_program_and_option_actions",
|
||||
translation_placeholders={
|
||||
"new_action_key": SERVICE_SET_PROGRAM_AND_OPTIONS,
|
||||
"remove_release": "2025.9.0",
|
||||
"deprecated_action_yaml": "\n".join(
|
||||
[
|
||||
"```yaml",
|
||||
f"action: {DOMAIN}.{SERVICE_OPTION_ACTIVE if active else SERVICE_OPTION_SELECTED}",
|
||||
"data:",
|
||||
f" {ATTR_DEVICE_ID}: DEVICE_ID",
|
||||
f" {ATTR_KEY}: {option_key}",
|
||||
f" {ATTR_VALUE}: {value}",
|
||||
*([f" {ATTR_UNIT}: {unit}"] if unit else []),
|
||||
"```",
|
||||
]
|
||||
),
|
||||
"new_action_yaml": "\n ".join(
|
||||
[
|
||||
"```yaml",
|
||||
f"action: {DOMAIN}.{SERVICE_SET_PROGRAM_AND_OPTIONS}",
|
||||
"data:",
|
||||
f" {ATTR_DEVICE_ID}: DEVICE_ID",
|
||||
f" {ATTR_AFFECTS_TO}: {AFFECTS_TO_ACTIVE_PROGRAM if active else AFFECTS_TO_SELECTED_PROGRAM}",
|
||||
f" {bsh_key_to_translation_key(option_key)}: {value}",
|
||||
"```",
|
||||
]
|
||||
),
|
||||
"repo_link": "[aiohomeconnect](https://github.com/MartinHjelmare/aiohomeconnect)",
|
||||
},
|
||||
)
|
||||
try:
|
||||
if active:
|
||||
await client.set_active_program_option(
|
||||
@@ -272,6 +451,76 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Service for selecting a program."""
|
||||
await _async_service_program(call, False)
|
||||
|
||||
async def async_service_set_program_and_options(call: ServiceCall):
|
||||
"""Service for setting a program and options."""
|
||||
data = dict(call.data)
|
||||
program = data.pop(ATTR_PROGRAM, None)
|
||||
affects_to = data.pop(ATTR_AFFECTS_TO)
|
||||
client, ha_id = await _get_client_and_ha_id(hass, data.pop(ATTR_DEVICE_ID))
|
||||
|
||||
options: list[Option] = []
|
||||
|
||||
for option, value in data.items():
|
||||
if option in PROGRAM_ENUM_OPTIONS:
|
||||
options.append(
|
||||
Option(
|
||||
PROGRAM_ENUM_OPTIONS[option][0],
|
||||
PROGRAM_ENUM_OPTIONS[option][1][value],
|
||||
)
|
||||
)
|
||||
elif option in PROGRAM_OPTIONS:
|
||||
option_key = PROGRAM_OPTIONS[option][0]
|
||||
options.append(Option(option_key, value))
|
||||
|
||||
method_call: Awaitable[Any]
|
||||
exception_translation_key: str
|
||||
if program:
|
||||
program = (
|
||||
program
|
||||
if isinstance(program, ProgramKey)
|
||||
else TRANSLATION_KEYS_PROGRAMS_MAP[program]
|
||||
)
|
||||
|
||||
if affects_to == AFFECTS_TO_ACTIVE_PROGRAM:
|
||||
method_call = client.start_program(
|
||||
ha_id, program_key=program, options=options
|
||||
)
|
||||
exception_translation_key = "start_program"
|
||||
elif affects_to == AFFECTS_TO_SELECTED_PROGRAM:
|
||||
method_call = client.set_selected_program(
|
||||
ha_id, program_key=program, options=options
|
||||
)
|
||||
exception_translation_key = "select_program"
|
||||
else:
|
||||
array_of_options = ArrayOfOptions(options)
|
||||
if affects_to == AFFECTS_TO_ACTIVE_PROGRAM:
|
||||
method_call = client.set_active_program_options(
|
||||
ha_id, array_of_options=array_of_options
|
||||
)
|
||||
exception_translation_key = "set_options_active_program"
|
||||
else:
|
||||
# affects_to is AFFECTS_TO_SELECTED_PROGRAM
|
||||
method_call = client.set_selected_program_options(
|
||||
ha_id, array_of_options=array_of_options
|
||||
)
|
||||
exception_translation_key = "set_options_selected_program"
|
||||
|
||||
try:
|
||||
await method_call
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=exception_translation_key,
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
**(
|
||||
{SVE_TRANSLATION_PLACEHOLDER_PROGRAM: program}
|
||||
if program
|
||||
else {}
|
||||
),
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_service_start_program(call: ServiceCall):
|
||||
"""Service for starting a program."""
|
||||
await _async_service_program(call, True)
|
||||
@@ -315,6 +564,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async_service_start_program,
|
||||
schema=SERVICE_PROGRAM_SCHEMA,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_PROGRAM_AND_OPTIONS,
|
||||
async_service_set_program_and_options,
|
||||
schema=SERVICE_PROGRAM_AND_OPTIONS_SCHEMA,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@@ -349,6 +604,7 @@ async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: HomeConnectConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
async_delete_issue(hass, DOMAIN, "deprecated_set_program_and_option_actions")
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
"""Constants for the Home Connect integration."""
|
||||
|
||||
from aiohomeconnect.model import EventKey, SettingKey, StatusKey
|
||||
from typing import cast
|
||||
|
||||
from aiohomeconnect.model import EventKey, OptionKey, ProgramKey, SettingKey, StatusKey
|
||||
|
||||
from .utils import bsh_key_to_translation_key
|
||||
|
||||
DOMAIN = "home_connect"
|
||||
|
||||
@@ -52,15 +56,18 @@ SERVICE_OPTION_SELECTED = "set_option_selected"
|
||||
SERVICE_PAUSE_PROGRAM = "pause_program"
|
||||
SERVICE_RESUME_PROGRAM = "resume_program"
|
||||
SERVICE_SELECT_PROGRAM = "select_program"
|
||||
SERVICE_SET_PROGRAM_AND_OPTIONS = "set_program_and_options"
|
||||
SERVICE_SETTING = "change_setting"
|
||||
SERVICE_START_PROGRAM = "start_program"
|
||||
|
||||
|
||||
ATTR_AFFECTS_TO = "affects_to"
|
||||
ATTR_KEY = "key"
|
||||
ATTR_PROGRAM = "program"
|
||||
ATTR_UNIT = "unit"
|
||||
ATTR_VALUE = "value"
|
||||
|
||||
AFFECTS_TO_ACTIVE_PROGRAM = "active_program"
|
||||
AFFECTS_TO_SELECTED_PROGRAM = "selected_program"
|
||||
|
||||
SVE_TRANSLATION_KEY_SET_SETTING = "set_setting_entity"
|
||||
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME = "appliance_name"
|
||||
@@ -70,6 +77,269 @@ SVE_TRANSLATION_PLACEHOLDER_KEY = "key"
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE = "value"
|
||||
|
||||
|
||||
TRANSLATION_KEYS_PROGRAMS_MAP = {
|
||||
bsh_key_to_translation_key(program.value): cast(ProgramKey, program)
|
||||
for program in ProgramKey
|
||||
if program != ProgramKey.UNKNOWN
|
||||
}
|
||||
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP = {
|
||||
value: key for key, value in TRANSLATION_KEYS_PROGRAMS_MAP.items()
|
||||
}
|
||||
|
||||
REFERENCE_MAP_ID_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CleaningRobot.EnumType.AvailableMaps.TempMap",
|
||||
"ConsumerProducts.CleaningRobot.EnumType.AvailableMaps.Map1",
|
||||
"ConsumerProducts.CleaningRobot.EnumType.AvailableMaps.Map2",
|
||||
"ConsumerProducts.CleaningRobot.EnumType.AvailableMaps.Map3",
|
||||
)
|
||||
}
|
||||
|
||||
CLEANING_MODE_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CleaningRobot.EnumType.CleaningModes.Silent",
|
||||
"ConsumerProducts.CleaningRobot.EnumType.CleaningModes.Standard",
|
||||
"ConsumerProducts.CleaningRobot.EnumType.CleaningModes.Power",
|
||||
)
|
||||
}
|
||||
|
||||
BEAN_AMOUNT_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.VeryMild",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.Mild",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.MildPlus",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.Normal",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.NormalPlus",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.Strong",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.StrongPlus",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.VeryStrong",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.VeryStrongPlus",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.ExtraStrong",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.DoubleShot",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.DoubleShotPlus",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.DoubleShotPlusPlus",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.TripleShot",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.TripleShotPlus",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.CoffeeGround",
|
||||
)
|
||||
}
|
||||
|
||||
COFFEE_TEMPERATURE_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.88C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.90C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.92C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.94C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.95C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.96C",
|
||||
)
|
||||
}
|
||||
|
||||
BEAN_CONTAINER_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanContainerSelection.Right",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanContainerSelection.Left",
|
||||
)
|
||||
}
|
||||
|
||||
FLOW_RATE_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.FlowRate.Normal",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.FlowRate.Intense",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.FlowRate.IntensePlus",
|
||||
)
|
||||
}
|
||||
|
||||
COFFEE_MILK_RATIO_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.10Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.20Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.25Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.30Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.40Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.50Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.55Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.60Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.65Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.67Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.70Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.75Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.80Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.85Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.90Percent",
|
||||
)
|
||||
}
|
||||
|
||||
HOT_WATER_TEMPERATURE_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.WhiteTea",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.GreenTea",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.BlackTea",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.50C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.55C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.60C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.65C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.70C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.75C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.80C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.85C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.90C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.95C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.97C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.122F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.131F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.140F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.149F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.158F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.167F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.176F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.185F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.194F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.203F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.Max",
|
||||
)
|
||||
}
|
||||
|
||||
DRYING_TARGET_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"LaundryCare.Dryer.EnumType.DryingTarget.IronDry",
|
||||
"LaundryCare.Dryer.EnumType.DryingTarget.GentleDry",
|
||||
"LaundryCare.Dryer.EnumType.DryingTarget.CupboardDry",
|
||||
"LaundryCare.Dryer.EnumType.DryingTarget.CupboardDryPlus",
|
||||
"LaundryCare.Dryer.EnumType.DryingTarget.ExtraDry",
|
||||
)
|
||||
}
|
||||
|
||||
VENTING_LEVEL_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"Cooking.Hood.EnumType.Stage.FanOff",
|
||||
"Cooking.Hood.EnumType.Stage.FanStage01",
|
||||
"Cooking.Hood.EnumType.Stage.FanStage02",
|
||||
"Cooking.Hood.EnumType.Stage.FanStage03",
|
||||
"Cooking.Hood.EnumType.Stage.FanStage04",
|
||||
"Cooking.Hood.EnumType.Stage.FanStage05",
|
||||
)
|
||||
}
|
||||
|
||||
INTENSIVE_LEVEL_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"Cooking.Hood.EnumType.IntensiveStage.IntensiveStageOff",
|
||||
"Cooking.Hood.EnumType.IntensiveStage.IntensiveStage1",
|
||||
"Cooking.Hood.EnumType.IntensiveStage.IntensiveStage2",
|
||||
)
|
||||
}
|
||||
|
||||
WARMING_LEVEL_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"Cooking.Oven.EnumType.WarmingLevel.Low",
|
||||
"Cooking.Oven.EnumType.WarmingLevel.Medium",
|
||||
"Cooking.Oven.EnumType.WarmingLevel.High",
|
||||
)
|
||||
}
|
||||
|
||||
TEMPERATURE_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"LaundryCare.Washer.EnumType.Temperature.Cold",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC20",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC30",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC40",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC50",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC60",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC70",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC80",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC90",
|
||||
"LaundryCare.Washer.EnumType.Temperature.UlCold",
|
||||
"LaundryCare.Washer.EnumType.Temperature.UlWarm",
|
||||
"LaundryCare.Washer.EnumType.Temperature.UlHot",
|
||||
"LaundryCare.Washer.EnumType.Temperature.UlExtraHot",
|
||||
)
|
||||
}
|
||||
|
||||
SPIN_SPEED_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.Off",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.RPM400",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.RPM600",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.RPM800",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1000",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1200",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1400",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1600",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.UlOff",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.UlLow",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.UlMedium",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.UlHigh",
|
||||
)
|
||||
}
|
||||
|
||||
VARIO_PERFECT_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"LaundryCare.Common.EnumType.VarioPerfect.Off",
|
||||
"LaundryCare.Common.EnumType.VarioPerfect.EcoPerfect",
|
||||
"LaundryCare.Common.EnumType.VarioPerfect.SpeedPerfect",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
PROGRAM_ENUM_OPTIONS = {
|
||||
bsh_key_to_translation_key(option_key): (
|
||||
option_key,
|
||||
options,
|
||||
)
|
||||
for option_key, options in (
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_REFERENCE_MAP_ID,
|
||||
REFERENCE_MAP_ID_OPTIONS,
|
||||
),
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_CLEANING_MODE,
|
||||
CLEANING_MODE_OPTIONS,
|
||||
),
|
||||
(OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEAN_AMOUNT, BEAN_AMOUNT_OPTIONS),
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_COFFEE_TEMPERATURE,
|
||||
COFFEE_TEMPERATURE_OPTIONS,
|
||||
),
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEAN_CONTAINER_SELECTION,
|
||||
BEAN_CONTAINER_OPTIONS,
|
||||
),
|
||||
(OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_FLOW_RATE, FLOW_RATE_OPTIONS),
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_COFFEE_MILK_RATIO,
|
||||
COFFEE_MILK_RATIO_OPTIONS,
|
||||
),
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_HOT_WATER_TEMPERATURE,
|
||||
HOT_WATER_TEMPERATURE_OPTIONS,
|
||||
),
|
||||
(OptionKey.LAUNDRY_CARE_DRYER_DRYING_TARGET, DRYING_TARGET_OPTIONS),
|
||||
(OptionKey.COOKING_COMMON_HOOD_VENTING_LEVEL, VENTING_LEVEL_OPTIONS),
|
||||
(OptionKey.COOKING_COMMON_HOOD_INTENSIVE_LEVEL, INTENSIVE_LEVEL_OPTIONS),
|
||||
(OptionKey.COOKING_OVEN_WARMING_LEVEL, WARMING_LEVEL_OPTIONS),
|
||||
(OptionKey.LAUNDRY_CARE_WASHER_TEMPERATURE, TEMPERATURE_OPTIONS),
|
||||
(OptionKey.LAUNDRY_CARE_WASHER_SPIN_SPEED, SPIN_SPEED_OPTIONS),
|
||||
(OptionKey.LAUNDRY_CARE_COMMON_VARIO_PERFECT, VARIO_PERFECT_OPTIONS),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
OLD_NEW_UNIQUE_ID_SUFFIX_MAP = {
|
||||
"ChildLock": SettingKey.BSH_COMMON_CHILD_LOCK,
|
||||
"Operation State": StatusKey.BSH_COMMON_OPERATION_STATE,
|
||||
|
||||
@@ -18,6 +18,9 @@
|
||||
"set_option_selected": {
|
||||
"service": "mdi:gesture-tap"
|
||||
},
|
||||
"set_program_and_options": {
|
||||
"service": "mdi:form-select"
|
||||
},
|
||||
"change_setting": {
|
||||
"service": "mdi:cog"
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Home Connect",
|
||||
"codeowners": ["@DavidMStraub", "@Diegorro98", "@MartinHjelmare"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"dependencies": ["application_credentials", "repairs"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
|
||||
@@ -15,24 +15,20 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .common import setup_home_connect_entry
|
||||
from .const import APPLIANCES_WITH_PROGRAMS, DOMAIN, SVE_TRANSLATION_PLACEHOLDER_PROGRAM
|
||||
from .const import (
|
||||
APPLIANCES_WITH_PROGRAMS,
|
||||
DOMAIN,
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP,
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM,
|
||||
TRANSLATION_KEYS_PROGRAMS_MAP,
|
||||
)
|
||||
from .coordinator import (
|
||||
HomeConnectApplianceData,
|
||||
HomeConnectConfigEntry,
|
||||
HomeConnectCoordinator,
|
||||
)
|
||||
from .entity import HomeConnectEntity
|
||||
from .utils import bsh_key_to_translation_key, get_dict_from_home_connect_error
|
||||
|
||||
TRANSLATION_KEYS_PROGRAMS_MAP = {
|
||||
bsh_key_to_translation_key(program.value): cast(ProgramKey, program)
|
||||
for program in ProgramKey
|
||||
if program != ProgramKey.UNKNOWN
|
||||
}
|
||||
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP = {
|
||||
value: key for key, value in TRANSLATION_KEYS_PROGRAMS_MAP.items()
|
||||
}
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
|
||||
@@ -46,6 +46,558 @@ select_program:
|
||||
example: "seconds"
|
||||
selector:
|
||||
text:
|
||||
set_program_and_options:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: home_connect
|
||||
affects_to:
|
||||
example: active_program
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
translation_key: affects_to
|
||||
options:
|
||||
- active_program
|
||||
- selected_program
|
||||
program:
|
||||
example: dishcare_dishwasher_program_auto2
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
custom_value: false
|
||||
translation_key: programs
|
||||
options:
|
||||
- consumer_products_cleaning_robot_program_cleaning_clean_all
|
||||
- consumer_products_cleaning_robot_program_cleaning_clean_map
|
||||
- consumer_products_cleaning_robot_program_basic_go_home
|
||||
- consumer_products_coffee_maker_program_beverage_ristretto
|
||||
- consumer_products_coffee_maker_program_beverage_espresso
|
||||
- consumer_products_coffee_maker_program_beverage_espresso_doppio
|
||||
- consumer_products_coffee_maker_program_beverage_coffee
|
||||
- consumer_products_coffee_maker_program_beverage_x_l_coffee
|
||||
- consumer_products_coffee_maker_program_beverage_caffe_grande
|
||||
- consumer_products_coffee_maker_program_beverage_espresso_macchiato
|
||||
- consumer_products_coffee_maker_program_beverage_cappuccino
|
||||
- consumer_products_coffee_maker_program_beverage_latte_macchiato
|
||||
- consumer_products_coffee_maker_program_beverage_caffe_latte
|
||||
- consumer_products_coffee_maker_program_beverage_milk_froth
|
||||
- consumer_products_coffee_maker_program_beverage_warm_milk
|
||||
- consumer_products_coffee_maker_program_coffee_world_kleiner_brauner
|
||||
- consumer_products_coffee_maker_program_coffee_world_grosser_brauner
|
||||
- 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_coffee_world_flat_white
|
||||
- consumer_products_coffee_maker_program_coffee_world_cortado
|
||||
- consumer_products_coffee_maker_program_coffee_world_cafe_cortado
|
||||
- consumer_products_coffee_maker_program_coffee_world_cafe_con_leche
|
||||
- consumer_products_coffee_maker_program_coffee_world_cafe_au_lait
|
||||
- consumer_products_coffee_maker_program_coffee_world_doppio
|
||||
- consumer_products_coffee_maker_program_coffee_world_kaapi
|
||||
- consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd
|
||||
- consumer_products_coffee_maker_program_coffee_world_galao
|
||||
- consumer_products_coffee_maker_program_coffee_world_garoto
|
||||
- consumer_products_coffee_maker_program_coffee_world_americano
|
||||
- consumer_products_coffee_maker_program_coffee_world_red_eye
|
||||
- consumer_products_coffee_maker_program_coffee_world_black_eye
|
||||
- consumer_products_coffee_maker_program_coffee_world_dead_eye
|
||||
- consumer_products_coffee_maker_program_beverage_hot_water
|
||||
- dishcare_dishwasher_program_pre_rinse
|
||||
- dishcare_dishwasher_program_auto_1
|
||||
- dishcare_dishwasher_program_auto_2
|
||||
- dishcare_dishwasher_program_auto_3
|
||||
- dishcare_dishwasher_program_eco_50
|
||||
- dishcare_dishwasher_program_quick_45
|
||||
- dishcare_dishwasher_program_intensiv_70
|
||||
- dishcare_dishwasher_program_normal_65
|
||||
- dishcare_dishwasher_program_glas_40
|
||||
- dishcare_dishwasher_program_glass_care
|
||||
- dishcare_dishwasher_program_night_wash
|
||||
- dishcare_dishwasher_program_quick_65
|
||||
- dishcare_dishwasher_program_normal_45
|
||||
- dishcare_dishwasher_program_intensiv_45
|
||||
- dishcare_dishwasher_program_auto_half_load
|
||||
- dishcare_dishwasher_program_intensiv_power
|
||||
- dishcare_dishwasher_program_magic_daily
|
||||
- dishcare_dishwasher_program_super_60
|
||||
- dishcare_dishwasher_program_kurz_60
|
||||
- dishcare_dishwasher_program_express_sparkle_65
|
||||
- dishcare_dishwasher_program_machine_care
|
||||
- dishcare_dishwasher_program_steam_fresh
|
||||
- dishcare_dishwasher_program_maximum_cleaning
|
||||
- dishcare_dishwasher_program_mixed_load
|
||||
- laundry_care_dryer_program_cotton
|
||||
- laundry_care_dryer_program_synthetic
|
||||
- laundry_care_dryer_program_mix
|
||||
- laundry_care_dryer_program_blankets
|
||||
- laundry_care_dryer_program_business_shirts
|
||||
- laundry_care_dryer_program_down_feathers
|
||||
- laundry_care_dryer_program_hygiene
|
||||
- laundry_care_dryer_program_jeans
|
||||
- laundry_care_dryer_program_outdoor
|
||||
- laundry_care_dryer_program_synthetic_refresh
|
||||
- laundry_care_dryer_program_towels
|
||||
- laundry_care_dryer_program_delicates
|
||||
- laundry_care_dryer_program_super_40
|
||||
- laundry_care_dryer_program_shirts_15
|
||||
- laundry_care_dryer_program_pillow
|
||||
- laundry_care_dryer_program_anti_shrink
|
||||
- laundry_care_dryer_program_my_time_my_drying_time
|
||||
- laundry_care_dryer_program_time_cold
|
||||
- laundry_care_dryer_program_time_warm
|
||||
- laundry_care_dryer_program_in_basket
|
||||
- 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
|
||||
- laundry_care_dryer_program_time_warm_fix_time_warm_30
|
||||
- laundry_care_dryer_program_time_warm_fix_time_warm_40
|
||||
- laundry_care_dryer_program_time_warm_fix_time_warm_60
|
||||
- laundry_care_dryer_program_dessous
|
||||
- cooking_common_program_hood_automatic
|
||||
- cooking_common_program_hood_venting
|
||||
- cooking_common_program_hood_delayed_shut_off
|
||||
- cooking_oven_program_heating_mode_pre_heating
|
||||
- cooking_oven_program_heating_mode_hot_air
|
||||
- cooking_oven_program_heating_mode_hot_air_eco
|
||||
- cooking_oven_program_heating_mode_hot_air_grilling
|
||||
- cooking_oven_program_heating_mode_top_bottom_heating
|
||||
- cooking_oven_program_heating_mode_top_bottom_heating_eco
|
||||
- cooking_oven_program_heating_mode_bottom_heating
|
||||
- cooking_oven_program_heating_mode_pizza_setting
|
||||
- cooking_oven_program_heating_mode_slow_cook
|
||||
- cooking_oven_program_heating_mode_intensive_heat
|
||||
- cooking_oven_program_heating_mode_keep_warm
|
||||
- cooking_oven_program_heating_mode_preheat_ovenware
|
||||
- cooking_oven_program_heating_mode_frozen_heatup_special
|
||||
- cooking_oven_program_heating_mode_desiccation
|
||||
- cooking_oven_program_heating_mode_defrost
|
||||
- cooking_oven_program_heating_mode_proof
|
||||
- cooking_oven_program_heating_mode_hot_air_30_steam
|
||||
- cooking_oven_program_heating_mode_hot_air_60_steam
|
||||
- cooking_oven_program_heating_mode_hot_air_80_steam
|
||||
- cooking_oven_program_heating_mode_hot_air_100_steam
|
||||
- cooking_oven_program_heating_mode_sabbath_programme
|
||||
- cooking_oven_program_microwave_90_watt
|
||||
- cooking_oven_program_microwave_180_watt
|
||||
- cooking_oven_program_microwave_360_watt
|
||||
- cooking_oven_program_microwave_600_watt
|
||||
- cooking_oven_program_microwave_900_watt
|
||||
- cooking_oven_program_microwave_1000_watt
|
||||
- cooking_oven_program_microwave_max
|
||||
- cooking_oven_program_heating_mode_warming_drawer
|
||||
- laundry_care_washer_program_cotton
|
||||
- laundry_care_washer_program_cotton_cotton_eco
|
||||
- laundry_care_washer_program_cotton_eco_4060
|
||||
- laundry_care_washer_program_cotton_colour
|
||||
- laundry_care_washer_program_easy_care
|
||||
- laundry_care_washer_program_mix
|
||||
- laundry_care_washer_program_mix_night_wash
|
||||
- laundry_care_washer_program_delicates_silk
|
||||
- laundry_care_washer_program_wool
|
||||
- laundry_care_washer_program_sensitive
|
||||
- laundry_care_washer_program_auto_30
|
||||
- laundry_care_washer_program_auto_40
|
||||
- laundry_care_washer_program_auto_60
|
||||
- laundry_care_washer_program_chiffon
|
||||
- laundry_care_washer_program_curtains
|
||||
- laundry_care_washer_program_dark_wash
|
||||
- laundry_care_washer_program_dessous
|
||||
- laundry_care_washer_program_monsoon
|
||||
- laundry_care_washer_program_outdoor
|
||||
- laundry_care_washer_program_plush_toy
|
||||
- laundry_care_washer_program_shirts_blouses
|
||||
- laundry_care_washer_program_sport_fitness
|
||||
- laundry_care_washer_program_towels
|
||||
- laundry_care_washer_program_water_proof
|
||||
- laundry_care_washer_program_power_speed_59
|
||||
- laundry_care_washer_program_super_153045_super_15
|
||||
- laundry_care_washer_program_super_153045_super_1530
|
||||
- laundry_care_washer_program_down_duvet_duvet
|
||||
- laundry_care_washer_program_rinse_rinse_spin_drain
|
||||
- laundry_care_washer_program_drum_clean
|
||||
- laundry_care_washer_dryer_program_cotton
|
||||
- laundry_care_washer_dryer_program_cotton_eco_4060
|
||||
- laundry_care_washer_dryer_program_mix
|
||||
- laundry_care_washer_dryer_program_easy_care
|
||||
- laundry_care_washer_dryer_program_wash_and_dry_60
|
||||
- laundry_care_washer_dryer_program_wash_and_dry_90
|
||||
cleaning_robot_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
consumer_products_cleaning_robot_option_reference_map_id:
|
||||
example: consumer_products_cleaning_robot_enum_type_available_maps_map1
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: available_maps
|
||||
options:
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_temp_map
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_map1
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_map2
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_map3
|
||||
consumer_products_cleaning_robot_option_cleaning_mode:
|
||||
example: consumer_products_cleaning_robot_enum_type_cleaning_modes_standard
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: cleaning_mode
|
||||
options:
|
||||
- consumer_products_cleaning_robot_enum_type_cleaning_modes_silent
|
||||
- consumer_products_cleaning_robot_enum_type_cleaning_modes_standard
|
||||
- consumer_products_cleaning_robot_enum_type_cleaning_modes_power
|
||||
coffee_maker_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
consumer_products_coffee_maker_option_bean_amount:
|
||||
example: consumer_products_coffee_maker_enum_type_bean_amount_normal
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: bean_amount
|
||||
options:
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_very_mild
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_mild
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_mild_plus
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_normal
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_normal_plus
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_strong
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_strong_plus
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_very_strong
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_very_strong_plus
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_extra_strong
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_double_shot
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_double_shot_plus
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_double_shot_plus_plus
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_triple_shot
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_triple_shot_plus
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_coffee_ground
|
||||
consumer_products_coffee_maker_option_fill_quantity:
|
||||
example: 60
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
step: 1
|
||||
mode: box
|
||||
unit_of_measurement: ml
|
||||
consumer_products_coffee_maker_option_coffee_temperature:
|
||||
example: consumer_products_coffee_maker_enum_type_coffee_temperature_88_c
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: coffee_temperature
|
||||
options:
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_88_c
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_90_c
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_92_c
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_94_c
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_95_c
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_96_c
|
||||
consumer_products_coffee_maker_option_bean_container:
|
||||
example: consumer_products_coffee_maker_enum_type_bean_container_selection_right
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: bean_container
|
||||
options:
|
||||
- consumer_products_coffee_maker_enum_type_bean_container_selection_right
|
||||
- consumer_products_coffee_maker_enum_type_bean_container_selection_left
|
||||
consumer_products_coffee_maker_option_flow_rate:
|
||||
example: consumer_products_coffee_maker_enum_type_flow_rate_normal
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: flow_rate
|
||||
options:
|
||||
- consumer_products_coffee_maker_enum_type_flow_rate_normal
|
||||
- consumer_products_coffee_maker_enum_type_flow_rate_intense
|
||||
- consumer_products_coffee_maker_enum_type_flow_rate_intense_plus
|
||||
consumer_products_coffee_maker_option_multiple_beverages:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
consumer_products_coffee_maker_option_coffee_milk_ratio:
|
||||
example: consumer_products_coffee_maker_enum_type_coffee_milk_ratio_50_percent
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: coffee_milk_ratio
|
||||
options:
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_10_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_20_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_25_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_30_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_40_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_50_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_55_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_60_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_65_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_67_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_70_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_75_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_80_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_85_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_90_percent
|
||||
consumer_products_coffee_maker_option_hot_water_temperature:
|
||||
example: consumer_products_coffee_maker_enum_type_hot_water_temperature_50_c
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: hot_water_temperature
|
||||
options:
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_white_tea
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_green_tea
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_black_tea
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_50_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_55_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_60_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_65_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_70_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_75_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_80_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_85_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_90_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_95_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_97_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_122_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_131_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_140_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_149_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_158_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_167_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_176_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_185_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_194_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_203_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_max
|
||||
dish_washer_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
b_s_h_common_option_start_in_relative:
|
||||
example: 3600
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
step: 1
|
||||
mode: box
|
||||
unit_of_measurement: s
|
||||
dishcare_dishwasher_option_intensiv_zone:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_brilliance_dry:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_vario_speed_plus:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_silence_on_demand:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_half_load:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_extra_dry:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_hygiene_plus:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_eco_dry:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_zeolite_dry:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dryer_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
laundry_care_dryer_option_drying_target:
|
||||
example: laundry_care_dryer_enum_type_drying_target_iron_dry
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: drying_target
|
||||
options:
|
||||
- laundry_care_dryer_enum_type_drying_target_iron_dry
|
||||
- laundry_care_dryer_enum_type_drying_target_gentle_dry
|
||||
- laundry_care_dryer_enum_type_drying_target_cupboard_dry
|
||||
- laundry_care_dryer_enum_type_drying_target_cupboard_dry_plus
|
||||
- laundry_care_dryer_enum_type_drying_target_extra_dry
|
||||
hood_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
cooking_hood_option_venting_level:
|
||||
example: cooking_hood_enum_type_stage_fan_stage01
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: venting_level
|
||||
options:
|
||||
- cooking_hood_enum_type_stage_fan_off
|
||||
- cooking_hood_enum_type_stage_fan_stage01
|
||||
- cooking_hood_enum_type_stage_fan_stage02
|
||||
- cooking_hood_enum_type_stage_fan_stage03
|
||||
- cooking_hood_enum_type_stage_fan_stage04
|
||||
- cooking_hood_enum_type_stage_fan_stage05
|
||||
cooking_hood_option_intensive_level:
|
||||
example: cooking_hood_enum_type_intensive_stage_intensive_stage1
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: intensive_level
|
||||
options:
|
||||
- cooking_hood_enum_type_intensive_stage_intensive_stage_off
|
||||
- cooking_hood_enum_type_intensive_stage_intensive_stage1
|
||||
- cooking_hood_enum_type_intensive_stage_intensive_stage2
|
||||
oven_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
cooking_oven_option_setpoint_temperature:
|
||||
example: 180
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
step: 1
|
||||
mode: box
|
||||
unit_of_measurement: °C/°F
|
||||
b_s_h_common_option_duration:
|
||||
example: 900
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
step: 1
|
||||
mode: box
|
||||
unit_of_measurement: s
|
||||
cooking_oven_option_fast_pre_heat:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
warming_drawer_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
cooking_oven_option_warming_level:
|
||||
example: cooking_oven_enum_type_warming_level_medium
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: warming_level
|
||||
options:
|
||||
- cooking_oven_enum_type_warming_level_low
|
||||
- cooking_oven_enum_type_warming_level_medium
|
||||
- cooking_oven_enum_type_warming_level_high
|
||||
washer_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
laundry_care_washer_option_temperature:
|
||||
example: laundry_care_washer_enum_type_temperature_g_c40
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: washer_temperature
|
||||
options:
|
||||
- laundry_care_washer_enum_type_temperature_cold
|
||||
- laundry_care_washer_enum_type_temperature_g_c20
|
||||
- laundry_care_washer_enum_type_temperature_g_c30
|
||||
- laundry_care_washer_enum_type_temperature_g_c40
|
||||
- laundry_care_washer_enum_type_temperature_g_c50
|
||||
- laundry_care_washer_enum_type_temperature_g_c60
|
||||
- laundry_care_washer_enum_type_temperature_g_c70
|
||||
- laundry_care_washer_enum_type_temperature_g_c80
|
||||
- laundry_care_washer_enum_type_temperature_g_c90
|
||||
- laundry_care_washer_enum_type_temperature_ul_cold
|
||||
- laundry_care_washer_enum_type_temperature_ul_warm
|
||||
- laundry_care_washer_enum_type_temperature_ul_hot
|
||||
- laundry_care_washer_enum_type_temperature_ul_extra_hot
|
||||
laundry_care_washer_option_spin_speed:
|
||||
example: laundry_care_washer_enum_type_spin_speed_r_p_m800
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: spin_speed
|
||||
options:
|
||||
- laundry_care_washer_enum_type_spin_speed_off
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m400
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m600
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m800
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m1000
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m1200
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m1400
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m1600
|
||||
- laundry_care_washer_enum_type_spin_speed_ul_off
|
||||
- laundry_care_washer_enum_type_spin_speed_ul_low
|
||||
- laundry_care_washer_enum_type_spin_speed_ul_medium
|
||||
- laundry_care_washer_enum_type_spin_speed_ul_high
|
||||
b_s_h_common_option_finish_in_relative:
|
||||
example: 3600
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
step: 1
|
||||
mode: box
|
||||
unit_of_measurement: s
|
||||
laundry_care_washer_option_i_dos1_active:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
laundry_care_washer_option_i_dos2_active:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
laundry_care_washer_option_vario_perfect:
|
||||
example: laundry_care_common_enum_type_vario_perfect_eco_perfect
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: vario_perfect
|
||||
options:
|
||||
- laundry_care_common_enum_type_vario_perfect_off
|
||||
- laundry_care_common_enum_type_vario_perfect_eco_perfect
|
||||
- laundry_care_common_enum_type_vario_perfect_speed_perfect
|
||||
pause_program:
|
||||
fields:
|
||||
device_id:
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -23,8 +23,10 @@ import voluptuous as vol
|
||||
from homeassistant.components import onboarding
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import instance_id
|
||||
from homeassistant.helpers.selector import TextSelector
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
@@ -88,7 +90,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# Tell device we want a token, user must now press the button within 30 seconds
|
||||
# The first attempt will always fail, but this opens the window to press the button
|
||||
token = await async_request_token(self.ip_address)
|
||||
token = await async_request_token(self.hass, self.ip_address)
|
||||
errors: dict[str, str] | None = None
|
||||
|
||||
if token is None:
|
||||
@@ -250,7 +252,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
errors: dict[str, str] | None = None
|
||||
|
||||
token = await async_request_token(self.ip_address)
|
||||
token = await async_request_token(self.hass, self.ip_address)
|
||||
|
||||
if user_input is not None:
|
||||
if token is None:
|
||||
@@ -353,7 +355,7 @@ async def async_try_connect(ip_address: str, token: str | None = None) -> Device
|
||||
await energy_api.close()
|
||||
|
||||
|
||||
async def async_request_token(ip_address: str) -> str | None:
|
||||
async def async_request_token(hass: HomeAssistant, ip_address: str) -> str | None:
|
||||
"""Try to request a token from the device.
|
||||
|
||||
This method is used to request a token from the device,
|
||||
@@ -362,8 +364,12 @@ async def async_request_token(ip_address: str) -> str | None:
|
||||
|
||||
api = HomeWizardEnergyV2(ip_address)
|
||||
|
||||
# Get a part of the unique id to make the token unique
|
||||
# This is to prevent token conflicts when multiple HA instances are used
|
||||
uuid = await instance_id.async_get(hass)
|
||||
|
||||
try:
|
||||
return await api.get_token("home-assistant")
|
||||
return await api.get_token(f"home-assistant#{uuid[:6]}")
|
||||
except DisabledError:
|
||||
return None
|
||||
finally:
|
||||
|
||||
@@ -47,7 +47,7 @@ class MigrateToV2ApiRepairFlow(RepairsFlow):
|
||||
|
||||
# Tell device we want a token, user must now press the button within 30 seconds
|
||||
# The first attempt will always fail, but this opens the window to press the button
|
||||
token = await async_request_token(ip_address)
|
||||
token = await async_request_token(self.hass, ip_address)
|
||||
errors: dict[str, str] | None = None
|
||||
|
||||
if token is None:
|
||||
|
||||
@@ -22,7 +22,12 @@ from .const import (
|
||||
)
|
||||
from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.SWITCH, Platform.TIME]
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.BINARY_SENSOR,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.TIME,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LetPotConfigEntry) -> bool:
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Support for LetPot binary sensor entities."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from letpot.models import DeviceFeature, LetPotDeviceStatus
|
||||
|
||||
from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator
|
||||
from .entity import LetPotEntity, LetPotEntityDescription
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class LetPotBinarySensorEntityDescription(
|
||||
LetPotEntityDescription, BinarySensorEntityDescription
|
||||
):
|
||||
"""Describes a LetPot binary sensor entity."""
|
||||
|
||||
is_on_fn: Callable[[LetPotDeviceStatus], bool]
|
||||
|
||||
|
||||
BINARY_SENSORS: tuple[LetPotBinarySensorEntityDescription, ...] = (
|
||||
LetPotBinarySensorEntityDescription(
|
||||
key="low_nutrients",
|
||||
translation_key="low_nutrients",
|
||||
is_on_fn=lambda status: bool(status.errors.low_nutrients),
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.data.errors.low_nutrients is not None
|
||||
),
|
||||
),
|
||||
LetPotBinarySensorEntityDescription(
|
||||
key="low_water",
|
||||
translation_key="low_water",
|
||||
is_on_fn=lambda status: bool(status.errors.low_water),
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
supported_fn=lambda coordinator: coordinator.data.errors.low_water is not None,
|
||||
),
|
||||
LetPotBinarySensorEntityDescription(
|
||||
key="pump",
|
||||
translation_key="pump",
|
||||
is_on_fn=lambda status: status.pump_status == 1,
|
||||
device_class=BinarySensorDeviceClass.RUNNING,
|
||||
supported_fn=(
|
||||
lambda coordinator: DeviceFeature.PUMP_STATUS
|
||||
in coordinator.device_client.device_features
|
||||
),
|
||||
),
|
||||
LetPotBinarySensorEntityDescription(
|
||||
key="pump_error",
|
||||
translation_key="pump_error",
|
||||
is_on_fn=lambda status: bool(status.errors.pump_malfunction),
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.data.errors.pump_malfunction is not None
|
||||
),
|
||||
),
|
||||
LetPotBinarySensorEntityDescription(
|
||||
key="refill_error",
|
||||
translation_key="refill_error",
|
||||
is_on_fn=lambda status: bool(status.errors.refill_error),
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
device_class=BinarySensorDeviceClass.PROBLEM,
|
||||
supported_fn=(
|
||||
lambda coordinator: coordinator.data.errors.refill_error is not None
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LetPotConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up LetPot binary sensor entities based on a config entry and device status/features."""
|
||||
coordinators = entry.runtime_data
|
||||
async_add_entities(
|
||||
LetPotBinarySensorEntity(coordinator, description)
|
||||
for description in BINARY_SENSORS
|
||||
for coordinator in coordinators
|
||||
if description.supported_fn(coordinator)
|
||||
)
|
||||
|
||||
|
||||
class LetPotBinarySensorEntity(LetPotEntity, BinarySensorEntity):
|
||||
"""Defines a LetPot binary sensor entity."""
|
||||
|
||||
entity_description: LetPotBinarySensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LetPotDeviceCoordinator,
|
||||
description: LetPotBinarySensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize LetPot binary sensor entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{coordinator.device.serial_number}_{description.key}"
|
||||
|
||||
@property
|
||||
def is_on(self) -> bool:
|
||||
"""Return if the binary sensor is on."""
|
||||
return self.entity_description.is_on_fn(self.coordinator.data)
|
||||
@@ -1,18 +1,27 @@
|
||||
"""Base class for LetPot entities."""
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Concatenate
|
||||
|
||||
from letpot.exceptions import LetPotConnectionException, LetPotException
|
||||
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import EntityDescription
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LetPotDeviceCoordinator
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class LetPotEntityDescription(EntityDescription):
|
||||
"""Description for all LetPot entities."""
|
||||
|
||||
supported_fn: Callable[[LetPotDeviceCoordinator], bool] = lambda _: True
|
||||
|
||||
|
||||
class LetPotEntity(CoordinatorEntity[LetPotDeviceCoordinator]):
|
||||
"""Defines a base LetPot entity."""
|
||||
|
||||
|
||||
@@ -1,5 +1,30 @@
|
||||
{
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"low_nutrients": {
|
||||
"default": "mdi:beaker-alert",
|
||||
"state": {
|
||||
"off": "mdi:beaker"
|
||||
}
|
||||
},
|
||||
"low_water": {
|
||||
"default": "mdi:water-percent-alert",
|
||||
"state": {
|
||||
"off": "mdi:water-percent"
|
||||
}
|
||||
},
|
||||
"pump": {
|
||||
"default": "mdi:pump",
|
||||
"state": {
|
||||
"off": "mdi:pump-off"
|
||||
}
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"water_level": {
|
||||
"default": "mdi:water-percent"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"alarm_sound": {
|
||||
"default": "mdi:bell-ring",
|
||||
|
||||
@@ -59,9 +59,9 @@ rules:
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: done
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
"""Support for LetPot sensor entities."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from letpot.models import DeviceFeature, LetPotDeviceStatus, TemperatureUnit
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorDeviceClass,
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import PERCENTAGE, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
|
||||
from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator
|
||||
from .entity import LetPotEntity, LetPotEntityDescription
|
||||
|
||||
# Coordinator is used to centralize the data updates
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
LETPOT_TEMPERATURE_UNIT_HA_UNIT = {
|
||||
TemperatureUnit.CELSIUS: UnitOfTemperature.CELSIUS,
|
||||
TemperatureUnit.FAHRENHEIT: UnitOfTemperature.FAHRENHEIT,
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class LetPotSensorEntityDescription(LetPotEntityDescription, SensorEntityDescription):
|
||||
"""Describes a LetPot sensor entity."""
|
||||
|
||||
native_unit_of_measurement_fn: Callable[[LetPotDeviceStatus], str | None]
|
||||
value_fn: Callable[[LetPotDeviceStatus], StateType]
|
||||
|
||||
|
||||
SENSORS: tuple[LetPotSensorEntityDescription, ...] = (
|
||||
LetPotSensorEntityDescription(
|
||||
key="temperature",
|
||||
value_fn=lambda status: status.temperature_value,
|
||||
native_unit_of_measurement_fn=(
|
||||
lambda status: LETPOT_TEMPERATURE_UNIT_HA_UNIT[
|
||||
status.temperature_unit or TemperatureUnit.CELSIUS
|
||||
]
|
||||
),
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
supported_fn=(
|
||||
lambda coordinator: DeviceFeature.TEMPERATURE
|
||||
in coordinator.device_client.device_features
|
||||
),
|
||||
),
|
||||
LetPotSensorEntityDescription(
|
||||
key="water_level",
|
||||
translation_key="water_level",
|
||||
value_fn=lambda status: status.water_level,
|
||||
native_unit_of_measurement_fn=lambda _: PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
supported_fn=(
|
||||
lambda coordinator: DeviceFeature.WATER_LEVEL
|
||||
in coordinator.device_client.device_features
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LetPotConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up LetPot sensor entities based on a device features."""
|
||||
coordinators = entry.runtime_data
|
||||
async_add_entities(
|
||||
LetPotSensorEntity(coordinator, description)
|
||||
for description in SENSORS
|
||||
for coordinator in coordinators
|
||||
if description.supported_fn(coordinator)
|
||||
)
|
||||
|
||||
|
||||
class LetPotSensorEntity(LetPotEntity, SensorEntity):
|
||||
"""Defines a LetPot sensor entity."""
|
||||
|
||||
entity_description: LetPotSensorEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LetPotDeviceCoordinator,
|
||||
description: LetPotSensorEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize LetPot sensor entity."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.config_entry.unique_id}_{coordinator.device.serial_number}_{description.key}"
|
||||
|
||||
@property
|
||||
def native_unit_of_measurement(self) -> str | None:
|
||||
"""Return the native unit of measurement."""
|
||||
return self.entity_description.native_unit_of_measurement_fn(
|
||||
self.coordinator.data
|
||||
)
|
||||
|
||||
@property
|
||||
def native_value(self) -> StateType:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
@@ -32,6 +32,28 @@
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"binary_sensor": {
|
||||
"low_nutrients": {
|
||||
"name": "Low nutrients"
|
||||
},
|
||||
"low_water": {
|
||||
"name": "Low water"
|
||||
},
|
||||
"pump": {
|
||||
"name": "Pump"
|
||||
},
|
||||
"pump_error": {
|
||||
"name": "Pump error"
|
||||
},
|
||||
"refill_error": {
|
||||
"name": "Refill error"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
"water_level": {
|
||||
"name": "Water level"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
"alarm_sound": {
|
||||
"name": "Alarm sound"
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .coordinator import LetPotConfigEntry, LetPotDeviceCoordinator
|
||||
from .entity import LetPotEntity, exception_handler
|
||||
from .entity import LetPotEntity, LetPotEntityDescription, exception_handler
|
||||
|
||||
# Each change pushes a 'full' device status with the change. The library will cache
|
||||
# pending changes to avoid overwriting, but try to avoid a lot of parallelism.
|
||||
@@ -21,14 +21,33 @@ PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class LetPotSwitchEntityDescription(SwitchEntityDescription):
|
||||
class LetPotSwitchEntityDescription(LetPotEntityDescription, SwitchEntityDescription):
|
||||
"""Describes a LetPot switch entity."""
|
||||
|
||||
value_fn: Callable[[LetPotDeviceStatus], bool | None]
|
||||
set_value_fn: Callable[[LetPotDeviceClient, bool], Coroutine[Any, Any, None]]
|
||||
|
||||
|
||||
BASE_SWITCHES: tuple[LetPotSwitchEntityDescription, ...] = (
|
||||
SWITCHES: tuple[LetPotSwitchEntityDescription, ...] = (
|
||||
LetPotSwitchEntityDescription(
|
||||
key="alarm_sound",
|
||||
translation_key="alarm_sound",
|
||||
value_fn=lambda status: status.system_sound,
|
||||
set_value_fn=lambda device_client, value: device_client.set_sound(value),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported_fn=lambda coordinator: coordinator.data.system_sound is not None,
|
||||
),
|
||||
LetPotSwitchEntityDescription(
|
||||
key="auto_mode",
|
||||
translation_key="auto_mode",
|
||||
value_fn=lambda status: status.water_mode == 1,
|
||||
set_value_fn=lambda device_client, value: device_client.set_water_mode(value),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
supported_fn=(
|
||||
lambda coordinator: DeviceFeature.PUMP_AUTO
|
||||
in coordinator.device_client.device_features
|
||||
),
|
||||
),
|
||||
LetPotSwitchEntityDescription(
|
||||
key="power",
|
||||
translation_key="power",
|
||||
@@ -44,20 +63,6 @@ BASE_SWITCHES: tuple[LetPotSwitchEntityDescription, ...] = (
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
)
|
||||
ALARM_SWITCH: LetPotSwitchEntityDescription = LetPotSwitchEntityDescription(
|
||||
key="alarm_sound",
|
||||
translation_key="alarm_sound",
|
||||
value_fn=lambda status: status.system_sound,
|
||||
set_value_fn=lambda device_client, value: device_client.set_sound(value),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
AUTO_MODE_SWITCH: LetPotSwitchEntityDescription = LetPotSwitchEntityDescription(
|
||||
key="auto_mode",
|
||||
translation_key="auto_mode",
|
||||
value_fn=lambda status: status.water_mode == 1,
|
||||
set_value_fn=lambda device_client, value: device_client.set_water_mode(value),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -69,19 +74,10 @@ async def async_setup_entry(
|
||||
coordinators = entry.runtime_data
|
||||
entities: list[SwitchEntity] = [
|
||||
LetPotSwitchEntity(coordinator, description)
|
||||
for description in BASE_SWITCHES
|
||||
for description in SWITCHES
|
||||
for coordinator in coordinators
|
||||
if description.supported_fn(coordinator)
|
||||
]
|
||||
entities.extend(
|
||||
LetPotSwitchEntity(coordinator, ALARM_SWITCH)
|
||||
for coordinator in coordinators
|
||||
if coordinator.data.system_sound is not None
|
||||
)
|
||||
entities.extend(
|
||||
LetPotSwitchEntity(coordinator, AUTO_MODE_SWITCH)
|
||||
for coordinator in coordinators
|
||||
if DeviceFeature.PUMP_AUTO in coordinator.device_client.device_features
|
||||
)
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -31,6 +32,8 @@ class MotionMountMovingSensor(MotionMountEntity, BinarySensorEntity):
|
||||
|
||||
_attr_device_class = BinarySensorDeviceClass.MOVING
|
||||
_attr_translation_key = "motionmount_is_moving"
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_entity_registry_enabled_default = False
|
||||
|
||||
def __init__(
|
||||
self, mm: motionmount.MotionMount, config_entry: MotionMountConfigEntry
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"motionmount_error_status": {
|
||||
"default": "mdi:alert-circle-outline",
|
||||
"state": {
|
||||
"none": "mdi:check-circle-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -56,12 +56,12 @@ rules:
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: Single device per config entry
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
|
||||
@@ -6,6 +6,7 @@ import motionmount
|
||||
from motionmount import MotionMountSystemError
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@@ -47,6 +48,8 @@ class MotionMountErrorStatusSensor(MotionMountEntity, SensorEntity):
|
||||
"internal",
|
||||
]
|
||||
_attr_translation_key = "motionmount_error_status"
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_entity_registry_enabled_default = False
|
||||
|
||||
def __init__(
|
||||
self, mm: motionmount.MotionMount, config_entry: MotionMountConfigEntry
|
||||
|
||||
@@ -6,7 +6,14 @@ from functools import lru_cache
|
||||
from types import TracebackType
|
||||
from typing import Self
|
||||
|
||||
from paho.mqtt.client import Client as MQTTClient
|
||||
from paho.mqtt.client import (
|
||||
CallbackOnConnect_v2,
|
||||
CallbackOnDisconnect_v2,
|
||||
CallbackOnPublish_v2,
|
||||
CallbackOnSubscribe_v2,
|
||||
CallbackOnUnsubscribe_v2,
|
||||
Client as MQTTClient,
|
||||
)
|
||||
|
||||
_MQTT_LOCK_COUNT = 7
|
||||
|
||||
@@ -44,6 +51,12 @@ class AsyncMQTTClient(MQTTClient):
|
||||
that is not needed since we are running in an async event loop.
|
||||
"""
|
||||
|
||||
on_connect: CallbackOnConnect_v2
|
||||
on_disconnect: CallbackOnDisconnect_v2
|
||||
on_publish: CallbackOnPublish_v2
|
||||
on_subscribe: CallbackOnSubscribe_v2
|
||||
on_unsubscribe: CallbackOnUnsubscribe_v2
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Set up the client.
|
||||
|
||||
|
||||
@@ -311,8 +311,8 @@ class MqttClientSetup:
|
||||
client_id = None
|
||||
transport: str = config.get(CONF_TRANSPORT, DEFAULT_TRANSPORT)
|
||||
self._client = AsyncMQTTClient(
|
||||
mqtt.CallbackAPIVersion.VERSION1,
|
||||
client_id,
|
||||
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
|
||||
client_id=client_id,
|
||||
protocol=proto,
|
||||
transport=transport, # type: ignore[arg-type]
|
||||
reconnect_on_failure=False,
|
||||
@@ -476,9 +476,9 @@ class MQTT:
|
||||
mqttc.on_connect = self._async_mqtt_on_connect
|
||||
mqttc.on_disconnect = self._async_mqtt_on_disconnect
|
||||
mqttc.on_message = self._async_mqtt_on_message
|
||||
mqttc.on_publish = self._async_mqtt_on_callback
|
||||
mqttc.on_subscribe = self._async_mqtt_on_callback
|
||||
mqttc.on_unsubscribe = self._async_mqtt_on_callback
|
||||
mqttc.on_publish = self._async_mqtt_on_publish
|
||||
mqttc.on_subscribe = self._async_mqtt_on_subscribe_unsubscribe
|
||||
mqttc.on_unsubscribe = self._async_mqtt_on_subscribe_unsubscribe
|
||||
|
||||
# suppress exceptions at callback
|
||||
mqttc.suppress_exceptions = True
|
||||
@@ -498,7 +498,7 @@ class MQTT:
|
||||
def _async_reader_callback(self, client: mqtt.Client) -> None:
|
||||
"""Handle reading data from the socket."""
|
||||
if (status := client.loop_read(MAX_PACKETS_TO_READ)) != 0:
|
||||
self._async_on_disconnect(status)
|
||||
self._async_handle_callback_exception(status)
|
||||
|
||||
@callback
|
||||
def _async_start_misc_periodic(self) -> None:
|
||||
@@ -593,7 +593,7 @@ class MQTT:
|
||||
def _async_writer_callback(self, client: mqtt.Client) -> None:
|
||||
"""Handle writing data to the socket."""
|
||||
if (status := client.loop_write()) != 0:
|
||||
self._async_on_disconnect(status)
|
||||
self._async_handle_callback_exception(status)
|
||||
|
||||
def _on_socket_register_write(
|
||||
self, client: mqtt.Client, userdata: Any, sock: SocketType
|
||||
@@ -983,9 +983,9 @@ class MQTT:
|
||||
self,
|
||||
_mqttc: mqtt.Client,
|
||||
_userdata: None,
|
||||
_flags: dict[str, int],
|
||||
result_code: int,
|
||||
properties: mqtt.Properties | None = None,
|
||||
_connect_flags: mqtt.ConnectFlags,
|
||||
reason_code: mqtt.ReasonCode,
|
||||
_properties: mqtt.Properties | None = None,
|
||||
) -> None:
|
||||
"""On connect callback.
|
||||
|
||||
@@ -993,19 +993,20 @@ class MQTT:
|
||||
message.
|
||||
"""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
if result_code != mqtt.CONNACK_ACCEPTED:
|
||||
if result_code in (
|
||||
mqtt.CONNACK_REFUSED_BAD_USERNAME_PASSWORD,
|
||||
mqtt.CONNACK_REFUSED_NOT_AUTHORIZED,
|
||||
):
|
||||
if reason_code.is_failure:
|
||||
# 24: Continue authentication
|
||||
# 25: Re-authenticate
|
||||
# 134: Bad user name or password
|
||||
# 135: Not authorized
|
||||
# 140: Bad authentication method
|
||||
if reason_code.value in (24, 25, 134, 135, 140):
|
||||
self._should_reconnect = False
|
||||
self.hass.async_create_task(self.async_disconnect())
|
||||
self.config_entry.async_start_reauth(self.hass)
|
||||
_LOGGER.error(
|
||||
"Unable to connect to the MQTT broker: %s",
|
||||
mqtt.connack_string(result_code),
|
||||
reason_code.getName(), # type: ignore[no-untyped-call]
|
||||
)
|
||||
self._async_connection_result(False)
|
||||
return
|
||||
@@ -1016,7 +1017,7 @@ class MQTT:
|
||||
"Connected to MQTT server %s:%s (%s)",
|
||||
self.conf[CONF_BROKER],
|
||||
self.conf.get(CONF_PORT, DEFAULT_PORT),
|
||||
result_code,
|
||||
reason_code,
|
||||
)
|
||||
|
||||
birth: dict[str, Any]
|
||||
@@ -1153,18 +1154,32 @@ class MQTT:
|
||||
self._mqtt_data.state_write_requests.process_write_state_requests(msg)
|
||||
|
||||
@callback
|
||||
def _async_mqtt_on_callback(
|
||||
def _async_mqtt_on_publish(
|
||||
self,
|
||||
_mqttc: mqtt.Client,
|
||||
_userdata: None,
|
||||
mid: int,
|
||||
_granted_qos_reason: tuple[int, ...] | mqtt.ReasonCodes | None = None,
|
||||
_properties_reason: mqtt.ReasonCodes | None = None,
|
||||
_reason_code: mqtt.ReasonCode,
|
||||
_properties: mqtt.Properties | None,
|
||||
) -> None:
|
||||
"""Publish callback."""
|
||||
self._async_mqtt_on_callback(mid)
|
||||
|
||||
@callback
|
||||
def _async_mqtt_on_subscribe_unsubscribe(
|
||||
self,
|
||||
_mqttc: mqtt.Client,
|
||||
_userdata: None,
|
||||
mid: int,
|
||||
_reason_code: list[mqtt.ReasonCode],
|
||||
_properties: mqtt.Properties | None,
|
||||
) -> None:
|
||||
"""Subscribe / Unsubscribe callback."""
|
||||
self._async_mqtt_on_callback(mid)
|
||||
|
||||
@callback
|
||||
def _async_mqtt_on_callback(self, mid: int) -> None:
|
||||
"""Publish / Subscribe / Unsubscribe callback."""
|
||||
# The callback signature for on_unsubscribe is different from on_subscribe
|
||||
# see https://github.com/eclipse/paho.mqtt.python/issues/687
|
||||
# properties and reason codes are not used in Home Assistant
|
||||
future = self._async_get_mid_future(mid)
|
||||
if future.done() and (future.cancelled() or future.exception()):
|
||||
# Timed out or cancelled
|
||||
@@ -1180,19 +1195,28 @@ class MQTT:
|
||||
self._pending_operations[mid] = future
|
||||
return future
|
||||
|
||||
@callback
|
||||
def _async_handle_callback_exception(self, status: mqtt.MQTTErrorCode) -> None:
|
||||
"""Handle a callback exception."""
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
|
||||
|
||||
_LOGGER.warning(
|
||||
"Error returned from MQTT server: %s",
|
||||
mqtt.error_string(status),
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_mqtt_on_disconnect(
|
||||
self,
|
||||
_mqttc: mqtt.Client,
|
||||
_userdata: None,
|
||||
result_code: int,
|
||||
_disconnect_flags: mqtt.DisconnectFlags,
|
||||
reason_code: mqtt.ReasonCode,
|
||||
properties: mqtt.Properties | None = None,
|
||||
) -> None:
|
||||
"""Disconnected callback."""
|
||||
self._async_on_disconnect(result_code)
|
||||
|
||||
@callback
|
||||
def _async_on_disconnect(self, result_code: int) -> None:
|
||||
if not self.connected:
|
||||
# This function is re-entrant and may be called multiple times
|
||||
# when there is a broken pipe error.
|
||||
@@ -1203,11 +1227,11 @@ class MQTT:
|
||||
self.connected = False
|
||||
async_dispatcher_send(self.hass, MQTT_CONNECTION_STATE, False)
|
||||
_LOGGER.log(
|
||||
logging.INFO if result_code == 0 else logging.DEBUG,
|
||||
logging.INFO if reason_code == 0 else logging.DEBUG,
|
||||
"Disconnected from MQTT server %s:%s (%s)",
|
||||
self.conf[CONF_BROKER],
|
||||
self.conf.get(CONF_PORT, DEFAULT_PORT),
|
||||
result_code,
|
||||
reason_code,
|
||||
)
|
||||
|
||||
@callback
|
||||
|
||||
@@ -1023,14 +1023,14 @@ def try_connection(
|
||||
result: queue.Queue[bool] = queue.Queue(maxsize=1)
|
||||
|
||||
def on_connect(
|
||||
client_: mqtt.Client,
|
||||
userdata: None,
|
||||
flags: dict[str, Any],
|
||||
result_code: int,
|
||||
properties: mqtt.Properties | None = None,
|
||||
_mqttc: mqtt.Client,
|
||||
_userdata: None,
|
||||
_connect_flags: mqtt.ConnectFlags,
|
||||
reason_code: mqtt.ReasonCode,
|
||||
_properties: mqtt.Properties | None = None,
|
||||
) -> None:
|
||||
"""Handle connection result."""
|
||||
result.put(result_code == mqtt.CONNACK_ACCEPTED)
|
||||
result.put(not reason_code.is_failure)
|
||||
|
||||
client.on_connect = on_connect
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@ MODELS_V2 = [
|
||||
"RBR",
|
||||
"RBS",
|
||||
"RBW",
|
||||
"RS",
|
||||
"LBK",
|
||||
"LBR",
|
||||
"CBK",
|
||||
|
||||
@@ -20,7 +20,7 @@ from .const import (
|
||||
PUBLIC_TARGET_IP,
|
||||
)
|
||||
from .models import Adapter
|
||||
from .network import Network, async_get_network
|
||||
from .network import Network, async_get_loaded_network, async_get_network
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -34,6 +34,12 @@ async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]:
|
||||
return network.adapters
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_loaded_adapters(hass: HomeAssistant) -> list[Adapter]:
|
||||
"""Get the network adapter configuration."""
|
||||
return async_get_loaded_network(hass).adapters
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_source_ip(
|
||||
hass: HomeAssistant, target_ip: str | UndefinedType = UNDEFINED
|
||||
@@ -74,7 +80,14 @@ async def async_get_enabled_source_ips(
|
||||
hass: HomeAssistant,
|
||||
) -> list[IPv4Address | IPv6Address]:
|
||||
"""Build the list of enabled source ips."""
|
||||
adapters = await async_get_adapters(hass)
|
||||
return async_get_enabled_source_ips_from_adapters(await async_get_adapters(hass))
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_enabled_source_ips_from_adapters(
|
||||
adapters: list[Adapter],
|
||||
) -> list[IPv4Address | IPv6Address]:
|
||||
"""Build the list of enabled source ips."""
|
||||
sources: list[IPv4Address | IPv6Address] = []
|
||||
for adapter in adapters:
|
||||
if not adapter["enabled"]:
|
||||
@@ -151,5 +164,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async_register_websocket_commands,
|
||||
)
|
||||
|
||||
await async_get_network(hass)
|
||||
|
||||
async_register_websocket_commands(hass)
|
||||
return True
|
||||
|
||||
@@ -12,8 +12,6 @@ DOMAIN: Final = "network"
|
||||
STORAGE_KEY: Final = "core.network"
|
||||
STORAGE_VERSION: Final = 1
|
||||
|
||||
DATA_NETWORK: Final = "network"
|
||||
|
||||
ATTR_ADAPTERS: Final = "adapters"
|
||||
ATTR_CONFIGURED_ADAPTERS: Final = "configured_adapters"
|
||||
DEFAULT_CONFIGURED_ADAPTERS: list[str] = []
|
||||
|
||||
@@ -9,11 +9,12 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import (
|
||||
ATTR_CONFIGURED_ADAPTERS,
|
||||
DATA_NETWORK,
|
||||
DEFAULT_CONFIGURED_ADAPTERS,
|
||||
DOMAIN,
|
||||
STORAGE_KEY,
|
||||
STORAGE_VERSION,
|
||||
)
|
||||
@@ -22,8 +23,16 @@ from .util import async_load_adapters, enable_adapters, enable_auto_detected_ada
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_NETWORK: HassKey[Network] = HassKey(DOMAIN)
|
||||
|
||||
@singleton(DATA_NETWORK)
|
||||
|
||||
@callback
|
||||
def async_get_loaded_network(hass: HomeAssistant) -> Network:
|
||||
"""Get network singleton."""
|
||||
return hass.data[DATA_NETWORK]
|
||||
|
||||
|
||||
@singleton(DOMAIN)
|
||||
async def async_get_network(hass: HomeAssistant) -> Network:
|
||||
"""Get network singleton."""
|
||||
network = Network(hass)
|
||||
|
||||
@@ -153,7 +153,7 @@ class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
|
||||
)
|
||||
try:
|
||||
if datetime.now().timestamp() >= expiry_time:
|
||||
await self._update_refresh_token()
|
||||
await self.update_refresh_token()
|
||||
else:
|
||||
await self.api.authenticate_refresh(
|
||||
self.refresh_token, async_get_clientsession(self.hass)
|
||||
@@ -178,7 +178,7 @@ class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
|
||||
else:
|
||||
self.async_set_updated_data(devices)
|
||||
|
||||
async def _update_refresh_token(self) -> None:
|
||||
async def update_refresh_token(self) -> None:
|
||||
"""Update the refresh token with Nice G.O. API."""
|
||||
_LOGGER.debug("Updating the refresh token with Nice G.O. API")
|
||||
try:
|
||||
|
||||
@@ -2,21 +2,17 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError
|
||||
from nice_go import ApiError
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import NiceGOConfigEntry
|
||||
from .entity import NiceGOEntity
|
||||
from .util import retry
|
||||
|
||||
DEVICE_CLASSES = {
|
||||
"WallStation": CoverDeviceClass.GARAGE,
|
||||
@@ -71,30 +67,18 @@ class NiceGOCoverEntity(NiceGOEntity, CoverEntity):
|
||||
"""Return if cover is closing."""
|
||||
return self.data.barrier_status == "closing"
|
||||
|
||||
@retry("close_cover_error")
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the garage door."""
|
||||
if self.is_closed:
|
||||
return
|
||||
|
||||
try:
|
||||
await self.coordinator.api.close_barrier(self._device_id)
|
||||
except (ApiError, ClientError) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="close_cover_error",
|
||||
translation_placeholders={"exception": str(err)},
|
||||
) from err
|
||||
await self.coordinator.api.close_barrier(self._device_id)
|
||||
|
||||
@retry("open_cover_error")
|
||||
async def async_open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the garage door."""
|
||||
if self.is_opened:
|
||||
return
|
||||
|
||||
try:
|
||||
await self.coordinator.api.open_barrier(self._device_id)
|
||||
except (ApiError, ClientError) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="open_cover_error",
|
||||
translation_placeholders={"exception": str(err)},
|
||||
) from err
|
||||
await self.coordinator.api.open_barrier(self._device_id)
|
||||
|
||||
@@ -3,23 +3,19 @@
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiohttp import ClientError
|
||||
from nice_go import ApiError
|
||||
|
||||
from homeassistant.components.light import ColorMode, LightEntity
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
KNOWN_UNSUPPORTED_DEVICE_TYPES,
|
||||
SUPPORTED_DEVICE_TYPES,
|
||||
UNSUPPORTED_DEVICE_WARNING,
|
||||
)
|
||||
from .coordinator import NiceGOConfigEntry
|
||||
from .entity import NiceGOEntity
|
||||
from .util import retry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -63,26 +59,14 @@ class NiceGOLightEntity(NiceGOEntity, LightEntity):
|
||||
assert self.data.light_status is not None
|
||||
return self.data.light_status
|
||||
|
||||
@retry("light_on_error")
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on the light."""
|
||||
|
||||
try:
|
||||
await self.coordinator.api.light_on(self._device_id)
|
||||
except (ApiError, ClientError) as error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="light_on_error",
|
||||
translation_placeholders={"exception": str(error)},
|
||||
) from error
|
||||
await self.coordinator.api.light_on(self._device_id)
|
||||
|
||||
@retry("light_off_error")
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off the light."""
|
||||
|
||||
try:
|
||||
await self.coordinator.api.light_off(self._device_id)
|
||||
except (ApiError, ClientError) as error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="light_off_error",
|
||||
translation_placeholders={"exception": str(error)},
|
||||
) from error
|
||||
await self.coordinator.api.light_off(self._device_id)
|
||||
|
||||
@@ -5,23 +5,19 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any
|
||||
|
||||
from aiohttp import ClientError
|
||||
from nice_go import ApiError
|
||||
|
||||
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
KNOWN_UNSUPPORTED_DEVICE_TYPES,
|
||||
SUPPORTED_DEVICE_TYPES,
|
||||
UNSUPPORTED_DEVICE_WARNING,
|
||||
)
|
||||
from .coordinator import NiceGOConfigEntry
|
||||
from .entity import NiceGOEntity
|
||||
from .util import retry
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -65,26 +61,14 @@ class NiceGOSwitchEntity(NiceGOEntity, SwitchEntity):
|
||||
assert self.data.vacation_mode is not None
|
||||
return self.data.vacation_mode
|
||||
|
||||
@retry("switch_on_error")
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch on."""
|
||||
|
||||
try:
|
||||
await self.coordinator.api.vacation_mode_on(self.data.id)
|
||||
except (ApiError, ClientError) as error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="switch_on_error",
|
||||
translation_placeholders={"exception": str(error)},
|
||||
) from error
|
||||
await self.coordinator.api.vacation_mode_on(self.data.id)
|
||||
|
||||
@retry("switch_off_error")
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the switch off."""
|
||||
|
||||
try:
|
||||
await self.coordinator.api.vacation_mode_off(self.data.id)
|
||||
except (ApiError, ClientError) as error:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="switch_off_error",
|
||||
translation_placeholders={"exception": str(error)},
|
||||
) from error
|
||||
await self.coordinator.api.vacation_mode_off(self.data.id)
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
"""Utilities for Nice G.O."""
|
||||
|
||||
from collections.abc import Callable, Coroutine
|
||||
from functools import wraps
|
||||
from typing import Any, Protocol, runtime_checkable
|
||||
|
||||
from aiohttp import ClientError
|
||||
from nice_go import ApiError, AuthFailedError
|
||||
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, HomeAssistantError
|
||||
from homeassistant.helpers.update_coordinator import UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
|
||||
@runtime_checkable
|
||||
class _ArgsProtocol(Protocol):
|
||||
coordinator: Any
|
||||
hass: Any
|
||||
|
||||
|
||||
def retry[_R, **P](
|
||||
translation_key: str,
|
||||
) -> Callable[
|
||||
[Callable[P, Coroutine[Any, Any, _R]]], Callable[P, Coroutine[Any, Any, _R]]
|
||||
]:
|
||||
"""Retry decorator to handle API errors."""
|
||||
|
||||
def decorator(
|
||||
func: Callable[P, Coroutine[Any, Any, _R]],
|
||||
) -> Callable[P, Coroutine[Any, Any, _R]]:
|
||||
@wraps(func)
|
||||
async def wrapper(*args: P.args, **kwargs: P.kwargs):
|
||||
instance = args[0]
|
||||
if not isinstance(instance, _ArgsProtocol):
|
||||
raise TypeError("First argument must have correct attributes")
|
||||
try:
|
||||
return await func(*args, **kwargs)
|
||||
except (ApiError, ClientError) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders={"exception": str(err)},
|
||||
) from err
|
||||
except AuthFailedError:
|
||||
# Try refreshing token and retry
|
||||
try:
|
||||
await instance.coordinator.update_refresh_token()
|
||||
return await func(*args, **kwargs)
|
||||
except (ApiError, ClientError, UpdateFailed) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders={"exception": str(err)},
|
||||
) from err
|
||||
except (AuthFailedError, ConfigEntryAuthFailed) as err:
|
||||
instance.coordinator.config_entry.async_start_reauth(instance.hass)
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=translation_key,
|
||||
translation_placeholders={"exception": str(err)},
|
||||
) from err
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
@@ -33,7 +33,6 @@ from homeassistant.helpers.hassio import is_hassio
|
||||
from homeassistant.helpers.system_info import async_get_system_info
|
||||
from homeassistant.helpers.translation import async_get_translations
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import OnboardingData, OnboardingStorage, OnboardingStoreData
|
||||
@@ -235,22 +234,21 @@ class CoreConfigOnboardingView(_BaseOnboardingView):
|
||||
):
|
||||
onboard_integrations.append("rpi_power")
|
||||
|
||||
coros: list[Coroutine[Any, Any, Any]] = [
|
||||
hass.config_entries.flow.async_init(
|
||||
domain, context={"source": "onboarding"}
|
||||
for domain in onboard_integrations:
|
||||
# Create tasks so onboarding isn't affected
|
||||
# by errors in these integrations.
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
domain, context={"source": "onboarding"}
|
||||
),
|
||||
f"onboarding_setup_{domain}",
|
||||
)
|
||||
for domain in onboard_integrations
|
||||
]
|
||||
|
||||
if "analytics" not in hass.config.components:
|
||||
# If by some chance that analytics has not finished
|
||||
# setting up, wait for it here so its ready for the
|
||||
# next step.
|
||||
coros.append(async_setup_component(hass, "analytics", {}))
|
||||
|
||||
# Set up integrations after onboarding and ensure
|
||||
# analytics is ready for the next step.
|
||||
await asyncio.gather(*(create_eager_task(coro) for coro in coros))
|
||||
await async_setup_component(hass, "analytics", {})
|
||||
|
||||
return self.json({})
|
||||
|
||||
|
||||
@@ -8,12 +8,14 @@ from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from onedrive_personal_sdk import OneDriveClient
|
||||
from onedrive_personal_sdk.const import DriveState
|
||||
from onedrive_personal_sdk.exceptions import AuthenticationError, OneDriveException
|
||||
from onedrive_personal_sdk.models.items import Drive
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -67,4 +69,27 @@ class OneDriveUpdateCoordinator(DataUpdateCoordinator[Drive]):
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN, translation_key="update_failed"
|
||||
) from err
|
||||
|
||||
# create an issue if the drive is almost full
|
||||
if drive.quota and (state := drive.quota.state) in (
|
||||
DriveState.CRITICAL,
|
||||
DriveState.EXCEEDED,
|
||||
):
|
||||
key = "drive_full" if state is DriveState.EXCEEDED else "drive_almost_full"
|
||||
ir.async_create_issue(
|
||||
self.hass,
|
||||
DOMAIN,
|
||||
key,
|
||||
is_fixable=False,
|
||||
severity=(
|
||||
ir.IssueSeverity.ERROR
|
||||
if state is DriveState.EXCEEDED
|
||||
else ir.IssueSeverity.WARNING
|
||||
),
|
||||
translation_key=key,
|
||||
translation_placeholders={
|
||||
"total": str(drive.quota.total),
|
||||
"used": str(drive.quota.used),
|
||||
},
|
||||
)
|
||||
return drive
|
||||
|
||||
@@ -36,7 +36,7 @@ DRIVE_STATE_ENTITIES: tuple[OneDriveSensorEntityDescription, ...] = (
|
||||
key="total_size",
|
||||
value_fn=lambda quota: quota.total,
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.GIGABYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES,
|
||||
suggested_display_precision=0,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
@@ -46,7 +46,7 @@ DRIVE_STATE_ENTITIES: tuple[OneDriveSensorEntityDescription, ...] = (
|
||||
key="used_size",
|
||||
value_fn=lambda quota: quota.used,
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.GIGABYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES,
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
@@ -55,7 +55,7 @@ DRIVE_STATE_ENTITIES: tuple[OneDriveSensorEntityDescription, ...] = (
|
||||
key="remaining_size",
|
||||
value_fn=lambda quota: quota.remaining,
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.GIGABYTES,
|
||||
suggested_unit_of_measurement=UnitOfInformation.GIBIBYTES,
|
||||
suggested_display_precision=2,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
|
||||
@@ -29,6 +29,16 @@
|
||||
"default": "[%key:common::config_flow::create_entry::authenticated%]"
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"drive_full": {
|
||||
"title": "OneDrive data cap exceeded",
|
||||
"description": "Your OneDrive has exceeded your quota limit. This means your next backup will fail. Please free up some space or upgrade your OneDrive plan. Currently using {used} GiB of {total} GiB."
|
||||
},
|
||||
"drive_almost_full": {
|
||||
"title": "OneDrive near data cap",
|
||||
"description": "Your OneDrive is near your quota limit. If you go over this limit your drive will be temporarily frozen and your backups will start failing. Please free up some space or upgrade your OneDrive plan. Currently using {used} GiB of {total} GiB."
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"authentication_failed": {
|
||||
"message": "Authentication failed"
|
||||
|
||||
@@ -385,7 +385,7 @@
|
||||
},
|
||||
"set_central_heating_ovrd": {
|
||||
"name": "Set central heating override",
|
||||
"description": "Sets the central heating override option on the gateway. When overriding the control setpoint (via a set_control_setpoint action with a value other than 0), the gateway automatically enables the central heating override to start heating. This action can then be used to control the central heating override status. To return control of the central heating to the thermostat, use the set_control_setpoint action with temperature value 0. You will only need this if you are writing your own software thermostat.",
|
||||
"description": "Sets the central heating override option on the gateway. When overriding the control setpoint (via a 'Set control set point' action with a value other than 0), the gateway automatically enables the central heating override to start heating. This action can then be used to control the central heating override status. To return control of the central heating to the thermostat, use the 'Set control set point' action with temperature value 0. You will only need this if you are writing your own software thermostat.",
|
||||
"fields": {
|
||||
"gateway_id": {
|
||||
"name": "[%key:component::opentherm_gw::services::reset_gateway::fields::gateway_id::name%]",
|
||||
@@ -393,7 +393,7 @@
|
||||
},
|
||||
"ch_override": {
|
||||
"name": "Central heating override",
|
||||
"description": "The desired boolean value for the central heating override."
|
||||
"description": "Whether to enable or disable the override."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.8.9"]
|
||||
"requirements": ["opower==0.9.0"]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["plugwise"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["plugwise==1.7.1"],
|
||||
"requirements": ["plugwise==1.7.2"],
|
||||
"zeroconf": ["_plugwise._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_IDLE, UnitOfDataRate
|
||||
from homeassistant.const import STATE_IDLE, UnitOfDataRate, UnitOfInformation
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@@ -27,8 +27,14 @@ from .coordinator import QBittorrentDataCoordinator
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_TYPE_CURRENT_STATUS = "current_status"
|
||||
SENSOR_TYPE_CONNECTION_STATUS = "connection_status"
|
||||
SENSOR_TYPE_DOWNLOAD_SPEED = "download_speed"
|
||||
SENSOR_TYPE_UPLOAD_SPEED = "upload_speed"
|
||||
SENSOR_TYPE_DOWNLOAD_SPEED_LIMIT = "download_speed_limit"
|
||||
SENSOR_TYPE_UPLOAD_SPEED_LIMIT = "upload_speed_limit"
|
||||
SENSOR_TYPE_ALLTIME_DOWNLOAD = "alltime_download"
|
||||
SENSOR_TYPE_ALLTIME_UPLOAD = "alltime_upload"
|
||||
SENSOR_TYPE_GLOBAL_RATIO = "global_ratio"
|
||||
SENSOR_TYPE_ALL_TORRENTS = "all_torrents"
|
||||
SENSOR_TYPE_PAUSED_TORRENTS = "paused_torrents"
|
||||
SENSOR_TYPE_ACTIVE_TORRENTS = "active_torrents"
|
||||
@@ -50,18 +56,54 @@ def get_state(coordinator: QBittorrentDataCoordinator) -> str:
|
||||
return STATE_IDLE
|
||||
|
||||
|
||||
def get_dl(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
def get_connection_status(coordinator: QBittorrentDataCoordinator) -> str:
|
||||
"""Get current download/upload state."""
|
||||
server_state = cast(Mapping, coordinator.data.get("server_state"))
|
||||
return cast(str, server_state.get("connection_status"))
|
||||
|
||||
|
||||
def get_download_speed(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
"""Get current download speed."""
|
||||
server_state = cast(Mapping, coordinator.data.get("server_state"))
|
||||
return cast(int, server_state.get("dl_info_speed"))
|
||||
|
||||
|
||||
def get_up(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
def get_upload_speed(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
"""Get current upload speed."""
|
||||
server_state = cast(Mapping[str, Any], coordinator.data.get("server_state"))
|
||||
return cast(int, server_state.get("up_info_speed"))
|
||||
|
||||
|
||||
def get_download_speed_limit(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
"""Get current download speed."""
|
||||
server_state = cast(Mapping, coordinator.data.get("server_state"))
|
||||
return cast(int, server_state.get("dl_rate_limit"))
|
||||
|
||||
|
||||
def get_upload_speed_limit(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
"""Get current upload speed."""
|
||||
server_state = cast(Mapping[str, Any], coordinator.data.get("server_state"))
|
||||
return cast(int, server_state.get("up_rate_limit"))
|
||||
|
||||
|
||||
def get_alltime_download(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
"""Get current download speed."""
|
||||
server_state = cast(Mapping, coordinator.data.get("server_state"))
|
||||
return cast(int, server_state.get("alltime_dl"))
|
||||
|
||||
|
||||
def get_alltime_upload(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
"""Get current download speed."""
|
||||
server_state = cast(Mapping, coordinator.data.get("server_state"))
|
||||
return cast(int, server_state.get("alltime_ul"))
|
||||
|
||||
|
||||
def get_global_ratio(coordinator: QBittorrentDataCoordinator) -> float:
|
||||
"""Get current download speed."""
|
||||
server_state = cast(Mapping, coordinator.data.get("server_state"))
|
||||
return cast(float, server_state.get("global_ratio"))
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class QBittorrentSensorEntityDescription(SensorEntityDescription):
|
||||
"""Entity description class for qBittorent sensors."""
|
||||
@@ -77,6 +119,13 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
|
||||
options=[STATE_IDLE, STATE_UP_DOWN, STATE_SEEDING, STATE_DOWNLOADING],
|
||||
value_fn=get_state,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_CONNECTION_STATUS,
|
||||
translation_key="connection_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["connected", "firewalled", "disconnected"],
|
||||
value_fn=get_connection_status,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_DOWNLOAD_SPEED,
|
||||
translation_key="download_speed",
|
||||
@@ -85,7 +134,7 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
|
||||
suggested_display_precision=2,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
|
||||
value_fn=get_dl,
|
||||
value_fn=get_download_speed,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_UPLOAD_SPEED,
|
||||
@@ -95,7 +144,56 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
|
||||
suggested_display_precision=2,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
|
||||
value_fn=get_up,
|
||||
value_fn=get_upload_speed,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_DOWNLOAD_SPEED_LIMIT,
|
||||
translation_key="download_speed_limit",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
|
||||
suggested_display_precision=2,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
|
||||
value_fn=get_download_speed_limit,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_UPLOAD_SPEED_LIMIT,
|
||||
translation_key="upload_speed_limit",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
|
||||
suggested_display_precision=2,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
|
||||
value_fn=get_upload_speed_limit,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_ALLTIME_DOWNLOAD,
|
||||
translation_key="alltime_download",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
suggested_display_precision=2,
|
||||
suggested_unit_of_measurement=UnitOfInformation.TEBIBYTES,
|
||||
value_fn=get_alltime_download,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_ALLTIME_UPLOAD,
|
||||
translation_key="alltime_upload",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
native_unit_of_measurement="B",
|
||||
suggested_display_precision=2,
|
||||
suggested_unit_of_measurement="TiB",
|
||||
value_fn=get_alltime_upload,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_GLOBAL_RATIO,
|
||||
translation_key="global_ratio",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=get_global_ratio,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_ALL_TORRENTS,
|
||||
|
||||
@@ -26,6 +26,21 @@
|
||||
"upload_speed": {
|
||||
"name": "Upload speed"
|
||||
},
|
||||
"download_speed_limit": {
|
||||
"name": "Download speed limit"
|
||||
},
|
||||
"upload_speed_limit": {
|
||||
"name": "Upload speed limit"
|
||||
},
|
||||
"alltime_download": {
|
||||
"name": "Alltime download"
|
||||
},
|
||||
"alltime_upload": {
|
||||
"name": "Alltime upload"
|
||||
},
|
||||
"global_ratio": {
|
||||
"name": "Global ratio"
|
||||
},
|
||||
"current_status": {
|
||||
"name": "Status",
|
||||
"state": {
|
||||
@@ -35,6 +50,14 @@
|
||||
"downloading": "Downloading"
|
||||
}
|
||||
},
|
||||
"connection_status": {
|
||||
"name": "Connection status",
|
||||
"state": {
|
||||
"connected": "Conencted",
|
||||
"firewalled": "Firewalled",
|
||||
"disconnected": "Disconnected"
|
||||
}
|
||||
},
|
||||
"active_torrents": {
|
||||
"name": "Active torrents",
|
||||
"unit_of_measurement": "torrents"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from rtmapi import Rtm
|
||||
import voluptuous as vol
|
||||
@@ -160,56 +160,64 @@ class RememberTheMilkConfiguration:
|
||||
This class stores the authentication token it get from the backend.
|
||||
"""
|
||||
|
||||
def __init__(self, hass):
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
"""Create new instance of configuration."""
|
||||
self._config_file_path = hass.config.path(CONFIG_FILE_NAME)
|
||||
if not os.path.isfile(self._config_file_path):
|
||||
self._config = {}
|
||||
return
|
||||
self._config = {}
|
||||
_LOGGER.debug("Loading configuration from file: %s", self._config_file_path)
|
||||
try:
|
||||
_LOGGER.debug("Loading configuration from file: %s", self._config_file_path)
|
||||
with open(self._config_file_path, encoding="utf8") as config_file:
|
||||
self._config = json.load(config_file)
|
||||
self._config = json.loads(
|
||||
Path(self._config_file_path).read_text(encoding="utf8")
|
||||
)
|
||||
except FileNotFoundError:
|
||||
_LOGGER.debug("Missing configuration file: %s", self._config_file_path)
|
||||
except OSError:
|
||||
_LOGGER.debug(
|
||||
"Failed to read from configuration file, %s, using empty configuration",
|
||||
self._config_file_path,
|
||||
)
|
||||
except ValueError:
|
||||
_LOGGER.error(
|
||||
"Failed to load configuration file, creating a new one: %s",
|
||||
"Failed to parse configuration file, %s, using empty configuration",
|
||||
self._config_file_path,
|
||||
)
|
||||
self._config = {}
|
||||
|
||||
def save_config(self):
|
||||
def _save_config(self) -> None:
|
||||
"""Write the configuration to a file."""
|
||||
with open(self._config_file_path, "w", encoding="utf8") as config_file:
|
||||
json.dump(self._config, config_file)
|
||||
Path(self._config_file_path).write_text(
|
||||
json.dumps(self._config), encoding="utf8"
|
||||
)
|
||||
|
||||
def get_token(self, profile_name):
|
||||
def get_token(self, profile_name: str) -> str | None:
|
||||
"""Get the server token for a profile."""
|
||||
if profile_name in self._config:
|
||||
return self._config[profile_name][CONF_TOKEN]
|
||||
return None
|
||||
|
||||
def set_token(self, profile_name, token):
|
||||
def set_token(self, profile_name: str, token: str) -> None:
|
||||
"""Store a new server token for a profile."""
|
||||
self._initialize_profile(profile_name)
|
||||
self._config[profile_name][CONF_TOKEN] = token
|
||||
self.save_config()
|
||||
self._save_config()
|
||||
|
||||
def delete_token(self, profile_name):
|
||||
def delete_token(self, profile_name: str) -> None:
|
||||
"""Delete a token for a profile.
|
||||
|
||||
Usually called when the token has expired.
|
||||
"""
|
||||
self._config.pop(profile_name, None)
|
||||
self.save_config()
|
||||
self._save_config()
|
||||
|
||||
def _initialize_profile(self, profile_name):
|
||||
def _initialize_profile(self, profile_name: str) -> None:
|
||||
"""Initialize the data structures for a profile."""
|
||||
if profile_name not in self._config:
|
||||
self._config[profile_name] = {}
|
||||
if CONF_ID_MAP not in self._config[profile_name]:
|
||||
self._config[profile_name][CONF_ID_MAP] = {}
|
||||
|
||||
def get_rtm_id(self, profile_name, hass_id):
|
||||
def get_rtm_id(
|
||||
self, profile_name: str, hass_id: str
|
||||
) -> tuple[str, str, str] | None:
|
||||
"""Get the RTM ids for a Home Assistant task ID.
|
||||
|
||||
The id of a RTM tasks consists of the tuple:
|
||||
@@ -221,7 +229,14 @@ class RememberTheMilkConfiguration:
|
||||
return None
|
||||
return ids[CONF_LIST_ID], ids[CONF_TIMESERIES_ID], ids[CONF_TASK_ID]
|
||||
|
||||
def set_rtm_id(self, profile_name, hass_id, list_id, time_series_id, rtm_task_id):
|
||||
def set_rtm_id(
|
||||
self,
|
||||
profile_name: str,
|
||||
hass_id: str,
|
||||
list_id: str,
|
||||
time_series_id: str,
|
||||
rtm_task_id: str,
|
||||
) -> None:
|
||||
"""Add/Update the RTM task ID for a Home Assistant task IS."""
|
||||
self._initialize_profile(profile_name)
|
||||
id_tuple = {
|
||||
@@ -230,11 +245,11 @@ class RememberTheMilkConfiguration:
|
||||
CONF_TASK_ID: rtm_task_id,
|
||||
}
|
||||
self._config[profile_name][CONF_ID_MAP][hass_id] = id_tuple
|
||||
self.save_config()
|
||||
self._save_config()
|
||||
|
||||
def delete_rtm_id(self, profile_name, hass_id):
|
||||
def delete_rtm_id(self, profile_name: str, hass_id: str) -> None:
|
||||
"""Delete a key mapping."""
|
||||
self._initialize_profile(profile_name)
|
||||
if hass_id in self._config[profile_name][CONF_ID_MAP]:
|
||||
del self._config[profile_name][CONF_ID_MAP][hass_id]
|
||||
self.save_config()
|
||||
self._save_config()
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/sense",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["sense_energy"],
|
||||
"requirements": ["sense-energy==0.13.4"]
|
||||
"requirements": ["sense-energy==0.13.5"]
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"delivered": {
|
||||
"default": "mdi:package"
|
||||
},
|
||||
"returned": {
|
||||
"alert": {
|
||||
"default": "mdi:package"
|
||||
},
|
||||
"package": {
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyseventeentrack"],
|
||||
"requirements": ["pyseventeentrack==1.0.1"]
|
||||
"requirements": ["pyseventeentrack==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ get_packages:
|
||||
- "ready_to_be_picked_up"
|
||||
- "undelivered"
|
||||
- "delivered"
|
||||
- "returned"
|
||||
- "alert"
|
||||
translation_key: package_state
|
||||
config_entry_id:
|
||||
required: true
|
||||
|
||||
@@ -57,8 +57,8 @@
|
||||
"delivered": {
|
||||
"name": "Delivered"
|
||||
},
|
||||
"returned": {
|
||||
"name": "Returned"
|
||||
"alert": {
|
||||
"name": "Alert"
|
||||
},
|
||||
"package": {
|
||||
"name": "Package {name}"
|
||||
@@ -104,7 +104,7 @@
|
||||
"ready_to_be_picked_up": "[%key:component::seventeentrack::entity::sensor::ready_to_be_picked_up::name%]",
|
||||
"undelivered": "[%key:component::seventeentrack::entity::sensor::undelivered::name%]",
|
||||
"delivered": "[%key:component::seventeentrack::entity::sensor::delivered::name%]",
|
||||
"returned": "[%key:component::seventeentrack::entity::sensor::returned::name%]"
|
||||
"alert": "[%key:component::seventeentrack::entity::sensor::alert::name%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pymodbus", "pysmarty2"],
|
||||
"requirements": ["pysmarty2==0.10.1"]
|
||||
"requirements": ["pysmarty2==0.10.2"]
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"description": "Set up SMLIGHT Zigbee Integration",
|
||||
"description": "Set up SMLIGHT Zigbee integration",
|
||||
"data": {
|
||||
"host": "[%key:common::config_flow::data::host%]"
|
||||
},
|
||||
@@ -111,7 +111,7 @@
|
||||
"name": "Zigbee flash mode"
|
||||
},
|
||||
"reconnect_zigbee_router": {
|
||||
"name": "Reconnect zigbee router"
|
||||
"name": "Reconnect Zigbee router"
|
||||
}
|
||||
},
|
||||
"switch": {
|
||||
|
||||
@@ -27,25 +27,25 @@
|
||||
"services": {
|
||||
"transition_on": {
|
||||
"name": "Transition on",
|
||||
"description": "Transitions to a target volume level over time.",
|
||||
"description": "Transitions the volume level over a specified duration. If the device is powered off, the transition will start at the lowest volume level.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"name": "Transition duration",
|
||||
"description": "Time it takes to reach the target volume level."
|
||||
"description": "Time to transition to the target volume."
|
||||
},
|
||||
"volume": {
|
||||
"name": "Target volume",
|
||||
"description": "If not specified, the volume level is read from the device."
|
||||
"description": "Relative volume level. If not specified, the setting on the device is used."
|
||||
}
|
||||
}
|
||||
},
|
||||
"transition_off": {
|
||||
"name": "Transition off",
|
||||
"description": "Transitions volume off over time.",
|
||||
"description": "Transitions the volume level to the lowest setting over a specified duration, then powers off the device.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"name": "[%key:component::snooz::services::transition_on::fields::duration::name%]",
|
||||
"description": "Time it takes to turn off."
|
||||
"description": "Time to complete the transition."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
"dependencies": ["ssdp"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["soco"],
|
||||
"loggers": ["soco", "sonos_websocket"],
|
||||
"requirements": ["soco==0.30.8", "sonos-websocket==0.1.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
|
||||
@@ -129,10 +129,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: SqueezeboxConfigEntry) -
|
||||
|
||||
server_coordinator = LMSStatusDataUpdateCoordinator(hass, entry, lms)
|
||||
|
||||
entry.runtime_data = SqueezeboxData(
|
||||
coordinator=server_coordinator,
|
||||
server=lms,
|
||||
)
|
||||
entry.runtime_data = SqueezeboxData(coordinator=server_coordinator, server=lms)
|
||||
|
||||
# set up player discovery
|
||||
known_servers = hass.data.setdefault(DOMAIN, {}).setdefault(KNOWN_SERVERS, {})
|
||||
|
||||
@@ -81,11 +81,12 @@ CONTENT_TYPE_TO_CHILD_TYPE = {
|
||||
"New Music": MediaType.ALBUM,
|
||||
}
|
||||
|
||||
BROWSE_LIMIT = 1000
|
||||
|
||||
|
||||
async def build_item_response(
|
||||
entity: MediaPlayerEntity, player: Player, payload: dict[str, str | None]
|
||||
entity: MediaPlayerEntity,
|
||||
player: Player,
|
||||
payload: dict[str, str | None],
|
||||
browse_limit: int,
|
||||
) -> BrowseMedia:
|
||||
"""Create response payload for search described by payload."""
|
||||
|
||||
@@ -107,7 +108,7 @@ async def build_item_response(
|
||||
|
||||
result = await player.async_browse(
|
||||
MEDIA_TYPE_TO_SQUEEZEBOX[search_type],
|
||||
limit=BROWSE_LIMIT,
|
||||
limit=browse_limit,
|
||||
browse_id=browse_id,
|
||||
)
|
||||
|
||||
@@ -237,7 +238,11 @@ def media_source_content_filter(item: BrowseMedia) -> bool:
|
||||
return item.media_content_type.startswith("audio/")
|
||||
|
||||
|
||||
async def generate_playlist(player: Player, payload: dict[str, str]) -> list | None:
|
||||
async def generate_playlist(
|
||||
player: Player,
|
||||
payload: dict[str, str],
|
||||
browse_limit: int,
|
||||
) -> list | None:
|
||||
"""Generate playlist from browsing payload."""
|
||||
media_type = payload["search_type"]
|
||||
media_id = payload["search_id"]
|
||||
@@ -247,7 +252,7 @@ async def generate_playlist(player: Player, payload: dict[str, str]) -> list | N
|
||||
|
||||
browse_id = (SQUEEZEBOX_ID_BY_TYPE[media_type], media_id)
|
||||
result = await player.async_browse(
|
||||
"titles", limit=BROWSE_LIMIT, browse_id=browse_id
|
||||
"titles", limit=browse_limit, browse_id=browse_id
|
||||
)
|
||||
if result and "items" in result:
|
||||
items: list = result["items"]
|
||||
|
||||
@@ -11,15 +11,34 @@ from pysqueezebox import Server, async_discover
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.media_player import DOMAIN as MP_DOMAIN
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.config_entries import (
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
ConfigFlowResult,
|
||||
OptionsFlow,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_PORT, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.selector import (
|
||||
NumberSelector,
|
||||
NumberSelectorConfig,
|
||||
NumberSelectorMode,
|
||||
)
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
|
||||
from .const import CONF_HTTPS, DEFAULT_PORT, DOMAIN
|
||||
from .const import (
|
||||
CONF_BROWSE_LIMIT,
|
||||
CONF_HTTPS,
|
||||
CONF_VOLUME_STEP,
|
||||
DEFAULT_BROWSE_LIMIT,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_VOLUME_STEP,
|
||||
DOMAIN,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -77,6 +96,12 @@ class SqueezeboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
self.data_schema = _base_schema()
|
||||
self.discovery_info: dict[str, Any] | None = None
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry: ConfigEntry) -> OptionsFlowHandler:
|
||||
"""Get the options flow for this handler."""
|
||||
return OptionsFlowHandler()
|
||||
|
||||
async def _discover(self, uuid: str | None = None) -> None:
|
||||
"""Discover an unconfigured LMS server."""
|
||||
self.discovery_info = None
|
||||
@@ -222,3 +247,48 @@ class SqueezeboxConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# if the player is unknown, then we likely need to configure its server
|
||||
return await self.async_step_user()
|
||||
|
||||
|
||||
OPTIONS_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_BROWSE_LIMIT): vol.All(
|
||||
NumberSelector(
|
||||
NumberSelectorConfig(min=1, max=65534, mode=NumberSelectorMode.BOX)
|
||||
),
|
||||
vol.Coerce(int),
|
||||
),
|
||||
vol.Required(CONF_VOLUME_STEP): vol.All(
|
||||
NumberSelector(
|
||||
NumberSelectorConfig(min=1, max=20, mode=NumberSelectorMode.SLIDER)
|
||||
),
|
||||
vol.Coerce(int),
|
||||
),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class OptionsFlowHandler(OptionsFlow):
|
||||
"""Options Flow Handler."""
|
||||
|
||||
async def async_step_init(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Options Flow Steps."""
|
||||
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
OPTIONS_SCHEMA,
|
||||
{
|
||||
CONF_BROWSE_LIMIT: self.config_entry.options.get(
|
||||
CONF_BROWSE_LIMIT, DEFAULT_BROWSE_LIMIT
|
||||
),
|
||||
CONF_VOLUME_STEP: self.config_entry.options.get(
|
||||
CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP
|
||||
),
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
@@ -32,3 +32,7 @@ SIGNAL_PLAYER_DISCOVERED = "squeezebox_player_discovered"
|
||||
SIGNAL_PLAYER_REDISCOVERED = "squeezebox_player_rediscovered"
|
||||
DISCOVERY_INTERVAL = 60
|
||||
PLAYER_UPDATE_INTERVAL = 5
|
||||
CONF_BROWSE_LIMIT = "browse_limit"
|
||||
CONF_VOLUME_STEP = "volume_step"
|
||||
DEFAULT_BROWSE_LIMIT = 1000
|
||||
DEFAULT_VOLUME_STEP = 5
|
||||
|
||||
@@ -12,5 +12,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/squeezebox",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pysqueezebox"],
|
||||
"requirements": ["pysqueezebox==0.11.1"]
|
||||
"requirements": ["pysqueezebox==0.12.0"]
|
||||
}
|
||||
|
||||
@@ -52,6 +52,10 @@ from .browse_media import (
|
||||
media_source_content_filter,
|
||||
)
|
||||
from .const import (
|
||||
CONF_BROWSE_LIMIT,
|
||||
CONF_VOLUME_STEP,
|
||||
DEFAULT_BROWSE_LIMIT,
|
||||
DEFAULT_VOLUME_STEP,
|
||||
DISCOVERY_TASK,
|
||||
DOMAIN,
|
||||
KNOWN_PLAYERS,
|
||||
@@ -166,6 +170,7 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
| MediaPlayerEntityFeature.PAUSE
|
||||
| MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
| MediaPlayerEntityFeature.VOLUME_STEP
|
||||
| MediaPlayerEntityFeature.PREVIOUS_TRACK
|
||||
| MediaPlayerEntityFeature.NEXT_TRACK
|
||||
| MediaPlayerEntityFeature.SEEK
|
||||
@@ -184,10 +189,7 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
_attr_name = None
|
||||
_last_update: datetime | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: SqueezeBoxPlayerUpdateCoordinator,
|
||||
) -> None:
|
||||
def __init__(self, coordinator: SqueezeBoxPlayerUpdateCoordinator) -> None:
|
||||
"""Initialize the SqueezeBox device."""
|
||||
super().__init__(coordinator)
|
||||
player = coordinator.player
|
||||
@@ -223,6 +225,23 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
self._last_update = utcnow()
|
||||
self.async_write_ha_state()
|
||||
|
||||
@property
|
||||
def volume_step(self) -> float:
|
||||
"""Return the step to be used for volume up down."""
|
||||
return float(
|
||||
self.coordinator.config_entry.options.get(
|
||||
CONF_VOLUME_STEP, DEFAULT_VOLUME_STEP
|
||||
)
|
||||
/ 100
|
||||
)
|
||||
|
||||
@property
|
||||
def browse_limit(self) -> int:
|
||||
"""Return the step to be used for volume up down."""
|
||||
return self.coordinator.config_entry.options.get(
|
||||
CONF_BROWSE_LIMIT, DEFAULT_BROWSE_LIMIT
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
@@ -366,16 +385,6 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
await self._player.async_set_power(False)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_volume_up(self) -> None:
|
||||
"""Volume up media player."""
|
||||
await self._player.async_set_volume("+5")
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_volume_down(self) -> None:
|
||||
"""Volume down media player."""
|
||||
await self._player.async_set_volume("-5")
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_set_volume_level(self, volume: float) -> None:
|
||||
"""Set volume level, range 0..1."""
|
||||
volume_percent = str(int(volume * 100))
|
||||
@@ -466,7 +475,11 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
"search_id": media_id,
|
||||
"search_type": MediaType.PLAYLIST,
|
||||
}
|
||||
playlist = await generate_playlist(self._player, payload)
|
||||
playlist = await generate_playlist(
|
||||
self._player,
|
||||
payload,
|
||||
self.browse_limit,
|
||||
)
|
||||
except BrowseError:
|
||||
# a list of urls
|
||||
content = json.loads(media_id)
|
||||
@@ -477,7 +490,11 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
"search_id": media_id,
|
||||
"search_type": media_type,
|
||||
}
|
||||
playlist = await generate_playlist(self._player, payload)
|
||||
playlist = await generate_playlist(
|
||||
self._player,
|
||||
payload,
|
||||
self.browse_limit,
|
||||
)
|
||||
|
||||
_LOGGER.debug("Generated playlist: %s", playlist)
|
||||
|
||||
@@ -587,7 +604,12 @@ class SqueezeBoxMediaPlayerEntity(
|
||||
"search_id": media_content_id,
|
||||
}
|
||||
|
||||
return await build_item_response(self, self._player, payload)
|
||||
return await build_item_response(
|
||||
self,
|
||||
self._player,
|
||||
payload,
|
||||
self.browse_limit,
|
||||
)
|
||||
|
||||
async def async_get_browse_image(
|
||||
self,
|
||||
|
||||
@@ -103,5 +103,20 @@
|
||||
"unit_of_measurement": "[%key:component::squeezebox::entity::sensor::player_count::unit_of_measurement%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "LMS Configuration",
|
||||
"data": {
|
||||
"browse_limit": "Browse limit",
|
||||
"volume_step": "Volume step"
|
||||
},
|
||||
"data_description": {
|
||||
"browse_limit": "Maximum number of items when browsing or in a playlist.",
|
||||
"volume_step": "Amount to adjust the volume when turning volume up or down."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StarlinkUpdateCoordinator
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkUpdateCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
@@ -19,21 +17,19 @@ PLATFORMS = [
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: StarlinkConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Starlink from a config entry."""
|
||||
coordinator = StarlinkUpdateCoordinator(hass, entry)
|
||||
config_entry.runtime_data = StarlinkUpdateCoordinator(hass, config_entry)
|
||||
await config_entry.runtime_data.async_config_entry_first_refresh()
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: StarlinkConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
|
||||
@@ -10,26 +10,22 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StarlinkData
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkData
|
||||
from .entity import StarlinkEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
config_entry: StarlinkConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up all binary sensors for this entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
StarlinkBinarySensorEntity(coordinator, description)
|
||||
StarlinkBinarySensorEntity(config_entry.runtime_data, description)
|
||||
for description in BINARY_SENSORS
|
||||
)
|
||||
|
||||
|
||||
@@ -10,26 +10,23 @@ from homeassistant.components.button import (
|
||||
ButtonEntity,
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StarlinkUpdateCoordinator
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkUpdateCoordinator
|
||||
from .entity import StarlinkEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
config_entry: StarlinkConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up all binary sensors for this entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
StarlinkButtonEntity(coordinator, description) for description in BUTTONS
|
||||
StarlinkButtonEntity(config_entry.runtime_data, description)
|
||||
for description in BUTTONS
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -34,6 +34,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type StarlinkConfigEntry = ConfigEntry[StarlinkUpdateCoordinator]
|
||||
|
||||
|
||||
@dataclass
|
||||
class StarlinkData:
|
||||
@@ -51,9 +53,9 @@ class StarlinkData:
|
||||
class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]):
|
||||
"""Coordinates updates between all Starlink sensors defined in this file."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: StarlinkConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
def __init__(self, hass: HomeAssistant, config_entry: StarlinkConfigEntry) -> None:
|
||||
"""Initialize an UpdateCoordinator for a group of sensors."""
|
||||
self.channel_context = ChannelContext(target=config_entry.data[CONF_IP_ADDRESS])
|
||||
self.history_stats_start = None
|
||||
|
||||
@@ -8,25 +8,22 @@ from homeassistant.components.device_tracker import (
|
||||
TrackerEntity,
|
||||
TrackerEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import ATTR_ALTITUDE, DOMAIN
|
||||
from .coordinator import StarlinkData
|
||||
from .const import ATTR_ALTITUDE
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkData
|
||||
from .entity import StarlinkEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
config_entry: StarlinkConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up all binary sensors for this entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
StarlinkDeviceTrackerEntity(coordinator, description)
|
||||
StarlinkDeviceTrackerEntity(config_entry.runtime_data, description)
|
||||
for description in DEVICE_TRACKERS
|
||||
)
|
||||
|
||||
|
||||
@@ -4,18 +4,15 @@ from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StarlinkUpdateCoordinator
|
||||
from .coordinator import StarlinkConfigEntry
|
||||
|
||||
TO_REDACT = {"id", "latitude", "longitude", "altitude"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
hass: HomeAssistant, config_entry: StarlinkConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for Starlink config entries."""
|
||||
coordinator: StarlinkUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
return async_redact_data(asdict(coordinator.data), TO_REDACT)
|
||||
return async_redact_data(asdict(config_entry.runtime_data.data), TO_REDACT)
|
||||
|
||||
@@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
DEGREE,
|
||||
PERCENTAGE,
|
||||
@@ -28,21 +27,19 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util.dt import now
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StarlinkData
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkData
|
||||
from .entity import StarlinkEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
config_entry: StarlinkConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up all sensors for this entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
StarlinkSensorEntity(coordinator, description) for description in SENSORS
|
||||
StarlinkSensorEntity(config_entry.runtime_data, description)
|
||||
for description in SENSORS
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -11,25 +11,22 @@ from homeassistant.components.switch import (
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StarlinkData, StarlinkUpdateCoordinator
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkData, StarlinkUpdateCoordinator
|
||||
from .entity import StarlinkEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
config_entry: StarlinkConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up all binary sensors for this entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
StarlinkSwitchEntity(coordinator, description) for description in SWITCHES
|
||||
StarlinkSwitchEntity(config_entry.runtime_data, description)
|
||||
for description in SWITCHES
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -8,26 +8,23 @@ from datetime import UTC, datetime, time, tzinfo
|
||||
import math
|
||||
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StarlinkData, StarlinkUpdateCoordinator
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkData, StarlinkUpdateCoordinator
|
||||
from .entity import StarlinkEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
config_entry: StarlinkConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up all time entities for this entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
StarlinkTimeEntity(coordinator, description) for description in TIMES
|
||||
StarlinkTimeEntity(config_entry.runtime_data, description)
|
||||
for description in TIMES
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/stookwijzer",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["stookwijzer==1.5.2"]
|
||||
"requirements": ["stookwijzer==1.5.4"]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ from synology_dsm.api.surveillance_station.camera import SynoCamera
|
||||
from synology_dsm.exceptions import SynologyDSMNotLoggedInException
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MAC, CONF_VERIFY_SSL
|
||||
from homeassistant.const import CONF_MAC, CONF_SCAN_INTERVAL, CONF_VERIFY_SSL
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
@@ -68,6 +68,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
entry,
|
||||
options={**entry.options, CONF_BACKUP_SHARE: None, CONF_BACKUP_PATH: None},
|
||||
)
|
||||
if CONF_SCAN_INTERVAL in entry.options:
|
||||
current_options = {**entry.options}
|
||||
current_options.pop(CONF_SCAN_INTERVAL)
|
||||
hass.config_entries.async_update_entry(entry, options=current_options)
|
||||
|
||||
# Continue setup
|
||||
api = SynoApi(hass, entry)
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user