Merge branch 'dev' into flussButton

This commit is contained in:
Marcello
2025-02-17 08:09:36 +02:00
committed by GitHub
182 changed files with 5752 additions and 1073 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 65 KiB

After

Width:  |  Height:  |  Size: 99 KiB

+4
View File
@@ -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"
}
}
}
}
+48
View File
@@ -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)
+56
View File
@@ -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
+1 -1
View File
@@ -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"])
+5 -1
View File
@@ -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"]
}
+6 -8
View File
@@ -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
+1 -1
View File
@@ -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)
+272 -2
View File
@@ -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:
+6 -1
View File
@@ -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
+110
View File
@@ -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"
+24 -28
View File
@@ -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
+14 -1
View File
@@ -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.
+55 -31
View File
@@ -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
+6 -6
View File
@@ -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",
+17 -2
View File
@@ -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] = []
+11 -2
View File
@@ -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:
+5 -21
View File
@@ -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)
+5 -21
View File
@@ -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 -21
View File
@@ -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)
+66
View File
@@ -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
+9 -11
View File
@@ -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
+3 -3
View File
@@ -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."]
}
+103 -5
View File
@@ -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()
+1 -1
View File
@@ -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": {
+5 -5
View File
@@ -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."
}
}
}
+1 -1
View File
@@ -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."
}
}
}
}
}
+11 -15
View File
@@ -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
)
+4 -7
View File
@@ -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)
+4 -7
View File
@@ -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
)
+4 -7
View File
@@ -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
)
+4 -7
View File
@@ -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