mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 21:27:38 +00:00
Merge branch 'dev' into aranet-threshold-level
This commit is contained in:
commit
a5a163a23f
BIN
.github/assets/screenshot-integrations.png
vendored
BIN
.github/assets/screenshot-integrations.png
vendored
Binary file not shown.
Before Width: | Height: | Size: 65 KiB After Width: | Height: | Size: 99 KiB |
@ -134,14 +134,12 @@ DATA_REGISTRIES_LOADED: HassKey[None] = HassKey("bootstrap_registries_loaded")
|
||||
LOG_SLOW_STARTUP_INTERVAL = 60
|
||||
SLOW_STARTUP_CHECK_INTERVAL = 1
|
||||
|
||||
STAGE_0_SUBSTAGE_TIMEOUT = 60
|
||||
STAGE_1_TIMEOUT = 120
|
||||
STAGE_2_TIMEOUT = 300
|
||||
WRAP_UP_TIMEOUT = 300
|
||||
COOLDOWN_TIME = 60
|
||||
|
||||
|
||||
DEBUGGER_INTEGRATIONS = {"debugpy"}
|
||||
|
||||
# Core integrations are unconditionally loaded
|
||||
CORE_INTEGRATIONS = {"homeassistant", "persistent_notification"}
|
||||
|
||||
@ -152,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",
|
||||
@ -172,12 +174,27 @@ FRONTEND_INTEGRATIONS = {
|
||||
# add it here.
|
||||
"backup",
|
||||
}
|
||||
RECORDER_INTEGRATIONS = {
|
||||
# Setup after frontend
|
||||
# To record data
|
||||
"recorder",
|
||||
}
|
||||
DISCOVERY_INTEGRATIONS = ("bluetooth", "dhcp", "ssdp", "usb", "zeroconf")
|
||||
# Stage 0 is divided into substages. Each substage has a name, a set of integrations and a timeout.
|
||||
# The substage containing recorder should have no timeout, as it could cancel a database migration.
|
||||
# Recorder freezes "recorder" timeout during a migration, but it does not freeze other timeouts.
|
||||
# The substages preceding it should also have no timeout, until we ensure that the recorder
|
||||
# is not accidentally promoted as a dependency of any of the integrations in them.
|
||||
# If we add timeouts to the frontend substages, we should make sure they don't apply in recovery mode.
|
||||
STAGE_0_INTEGRATIONS = (
|
||||
# Load logging and http deps as soon as possible
|
||||
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS, None),
|
||||
# Setup frontend
|
||||
("frontend", FRONTEND_INTEGRATIONS, None),
|
||||
# Setup recorder
|
||||
("recorder", {"recorder"}, None),
|
||||
# Start up debuggers. Start these first in case they want to wait.
|
||||
("debugger", {"debugpy"}, STAGE_0_SUBSTAGE_TIMEOUT),
|
||||
# Zeroconf is used for mdns resolution in aiohttp client helper.
|
||||
("zeroconf", {"zeroconf"}, STAGE_0_SUBSTAGE_TIMEOUT),
|
||||
)
|
||||
|
||||
DISCOVERY_INTEGRATIONS = ("bluetooth", "dhcp", "ssdp", "usb")
|
||||
# Stage 1 integrations are not to be preimported in bootstrap.
|
||||
STAGE_1_INTEGRATIONS = {
|
||||
# We need to make sure discovery integrations
|
||||
# update their deps before stage 2 integrations
|
||||
@ -189,9 +206,8 @@ STAGE_1_INTEGRATIONS = {
|
||||
"mqtt_eventstream",
|
||||
# To provide account link implementations
|
||||
"cloud",
|
||||
# Ensure supervisor is available
|
||||
"hassio",
|
||||
}
|
||||
|
||||
DEFAULT_INTEGRATIONS = {
|
||||
# These integrations are set up unless recovery mode is activated.
|
||||
#
|
||||
@ -232,22 +248,12 @@ DEFAULT_INTEGRATIONS_SUPERVISOR = {
|
||||
# These integrations are set up if using the Supervisor
|
||||
"hassio",
|
||||
}
|
||||
|
||||
CRITICAL_INTEGRATIONS = {
|
||||
# Recovery mode is activated if these integrations fail to set up
|
||||
"frontend",
|
||||
}
|
||||
|
||||
SETUP_ORDER = (
|
||||
# Load logging and http deps as soon as possible
|
||||
("logging, http deps", LOGGING_AND_HTTP_DEPS_INTEGRATIONS),
|
||||
# Setup frontend
|
||||
("frontend", FRONTEND_INTEGRATIONS),
|
||||
# Setup recorder
|
||||
("recorder", RECORDER_INTEGRATIONS),
|
||||
# Start up debuggers. Start these first in case they want to wait.
|
||||
("debugger", DEBUGGER_INTEGRATIONS),
|
||||
)
|
||||
|
||||
#
|
||||
# Storage keys we are likely to load during startup
|
||||
# in order of when we expect to load them.
|
||||
@ -694,7 +700,6 @@ async def async_mount_local_lib_path(config_dir: str) -> str:
|
||||
return deps_dir
|
||||
|
||||
|
||||
@core.callback
|
||||
def _get_domains(hass: core.HomeAssistant, config: dict[str, Any]) -> set[str]:
|
||||
"""Get domains of components to set up."""
|
||||
# Filter out the repeating and common config section [homeassistant]
|
||||
@ -890,42 +895,25 @@ async def _async_set_up_integrations(
|
||||
domains_to_setup, integration_cache = await _async_resolve_domains_to_setup(
|
||||
hass, config
|
||||
)
|
||||
stage_2_domains = domains_to_setup.copy()
|
||||
|
||||
# Initialize recorder
|
||||
if "recorder" in domains_to_setup:
|
||||
recorder.async_initialize_recorder(hass)
|
||||
|
||||
pre_stage_domains = [
|
||||
(name, domains_to_setup & domain_group) for name, domain_group in SETUP_ORDER
|
||||
stage_0_and_1_domains: list[tuple[str, set[str], int | None]] = [
|
||||
*(
|
||||
(name, domain_group & domains_to_setup, timeout)
|
||||
for name, domain_group, timeout in STAGE_0_INTEGRATIONS
|
||||
),
|
||||
("stage 1", STAGE_1_INTEGRATIONS & domains_to_setup, STAGE_1_TIMEOUT),
|
||||
]
|
||||
|
||||
# calculate what components to setup in what stage
|
||||
stage_1_domains: set[str] = set()
|
||||
|
||||
# Find all dependencies of any dependency of any stage 1 integration that
|
||||
# we plan on loading and promote them to stage 1. This is done only to not
|
||||
# get misleading log messages
|
||||
deps_promotion: set[str] = STAGE_1_INTEGRATIONS
|
||||
while deps_promotion:
|
||||
old_deps_promotion = deps_promotion
|
||||
deps_promotion = set()
|
||||
|
||||
for domain in old_deps_promotion:
|
||||
if domain not in domains_to_setup or domain in stage_1_domains:
|
||||
_LOGGER.info("Setting up stage 0 and 1")
|
||||
for name, domain_group, timeout in stage_0_and_1_domains:
|
||||
if not domain_group:
|
||||
continue
|
||||
|
||||
stage_1_domains.add(domain)
|
||||
|
||||
if (dep_itg := integration_cache.get(domain)) is None:
|
||||
continue
|
||||
|
||||
deps_promotion.update(dep_itg.all_dependencies)
|
||||
|
||||
stage_2_domains = domains_to_setup - stage_1_domains
|
||||
|
||||
for name, domain_group in pre_stage_domains:
|
||||
if domain_group:
|
||||
stage_2_domains -= domain_group
|
||||
_LOGGER.info("Setting up %s: %s", name, domain_group)
|
||||
to_be_loaded = domain_group.copy()
|
||||
to_be_loaded.update(
|
||||
@ -935,22 +923,18 @@ async def _async_set_up_integrations(
|
||||
for dep in integration.all_dependencies
|
||||
)
|
||||
async_set_domains_to_be_loaded(hass, to_be_loaded)
|
||||
stage_2_domains -= to_be_loaded
|
||||
|
||||
if timeout is None:
|
||||
await _async_setup_multi_components(hass, domain_group, config)
|
||||
|
||||
# Enables after dependencies when setting up stage 1 domains
|
||||
async_set_domains_to_be_loaded(hass, stage_1_domains)
|
||||
|
||||
# Start setup
|
||||
if stage_1_domains:
|
||||
_LOGGER.info("Setting up stage 1: %s", stage_1_domains)
|
||||
else:
|
||||
try:
|
||||
async with hass.timeout.async_timeout(
|
||||
STAGE_1_TIMEOUT, cool_down=COOLDOWN_TIME
|
||||
):
|
||||
await _async_setup_multi_components(hass, stage_1_domains, config)
|
||||
async with hass.timeout.async_timeout(timeout, cool_down=COOLDOWN_TIME):
|
||||
await _async_setup_multi_components(hass, domain_group, config)
|
||||
except TimeoutError:
|
||||
_LOGGER.warning(
|
||||
"Setup timed out for stage 1 waiting on %s - moving forward",
|
||||
"Setup timed out for %s waiting on %s - moving forward",
|
||||
name,
|
||||
hass._active_tasks, # noqa: SLF001
|
||||
)
|
||||
|
||||
|
@ -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
|
||||
|
||||
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
homeassistant/components/balboa/switch.py
Normal file
48
homeassistant/components/balboa/switch.py
Normal 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
homeassistant/components/balboa/time.py
Normal file
56
homeassistant/components/balboa/time.py
Normal 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
|
||||
|
@ -13,6 +13,6 @@
|
||||
"integration_type": "system",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["acme", "hass_nabucasa", "snitun"],
|
||||
"requirements": ["hass-nabucasa==0.90.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."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -18,7 +18,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import EnphaseConfigEntry, EnphaseUpdateCoordinator
|
||||
from .entity import EnvoyBaseEntity
|
||||
from .entity import EnvoyBaseEntity, exception_handler
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
@ -192,6 +192,7 @@ class EnvoyRelaySelectEntity(EnvoyBaseEntity, SelectEntity):
|
||||
"""Return the state of the Enpower switch."""
|
||||
return self.entity_description.value_fn(self.relay)
|
||||
|
||||
@exception_handler
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Update the relay."""
|
||||
await self.entity_description.update_fn(self.envoy, self.relay, option)
|
||||
@ -243,6 +244,7 @@ class EnvoyStorageSettingsSelectEntity(EnvoyBaseEntity, SelectEntity):
|
||||
assert self.data.tariff.storage_settings is not None
|
||||
return self.entity_description.value_fn(self.data.tariff.storage_settings)
|
||||
|
||||
@exception_handler
|
||||
async def async_select_option(self, option: str) -> None:
|
||||
"""Update the relay."""
|
||||
await self.entity_description.update_fn(self.envoy, option)
|
||||
|
@ -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"
|
||||
],
|
||||
|
@ -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": {
|
||||
|
@ -21,5 +21,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250210.0"]
|
||||
"requirements": ["home-assistant-frontend==20250214.0"]
|
||||
}
|
||||
|
@ -43,7 +43,7 @@ class HabiticaImage(HabiticaBase, ImageEntity):
|
||||
translation_key=HabiticaImageEntity.AVATAR,
|
||||
)
|
||||
_attr_content_type = "image/png"
|
||||
_current_appearance: Avatar | None = None
|
||||
_avatar: Avatar | None = None
|
||||
_cache: bytes | None = None
|
||||
|
||||
def __init__(
|
||||
@ -55,13 +55,13 @@ class HabiticaImage(HabiticaBase, ImageEntity):
|
||||
super().__init__(coordinator, self.entity_description)
|
||||
ImageEntity.__init__(self, hass)
|
||||
self._attr_image_last_updated = dt_util.utcnow()
|
||||
self._avatar = extract_avatar(self.coordinator.data.user)
|
||||
|
||||
def _handle_coordinator_update(self) -> None:
|
||||
"""Check if equipped gear and other things have changed since last avatar image generation."""
|
||||
new_appearance = extract_avatar(self.coordinator.data.user)
|
||||
|
||||
if self._current_appearance != new_appearance:
|
||||
self._current_appearance = new_appearance
|
||||
if self._avatar != self.coordinator.data.user:
|
||||
self._avatar = extract_avatar(self.coordinator.data.user)
|
||||
self._attr_image_last_updated = dt_util.utcnow()
|
||||
self._cache = None
|
||||
|
||||
@ -69,8 +69,6 @@ class HabiticaImage(HabiticaBase, ImageEntity):
|
||||
|
||||
async def async_image(self) -> bytes | None:
|
||||
"""Return cached bytes, otherwise generate new avatar."""
|
||||
if not self._cache and self._current_appearance:
|
||||
self._cache = await self.coordinator.generate_avatar(
|
||||
self._current_appearance
|
||||
)
|
||||
if not self._cache and self._avatar:
|
||||
self._cache = await self.coordinator.generate_avatar(self._avatar)
|
||||
return self._cache
|
||||
|
@ -6,5 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/habitica",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["habiticalib"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["habiticalib==0.3.7"]
|
||||
}
|
||||
|
@ -51,7 +51,7 @@ rules:
|
||||
status: exempt
|
||||
comment: No supportable devices.
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: todo
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: done
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
|
@ -9,5 +9,5 @@
|
||||
},
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["apyhiveapi"],
|
||||
"requirements": ["pyhive-integration==1.0.1"]
|
||||
"requirements": ["pyhive-integration==1.0.2"]
|
||||
}
|
||||
|
@ -2,11 +2,19 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable
|
||||
import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from aiohomeconnect.client import Client as HomeConnectClient
|
||||
from aiohomeconnect.model import CommandKey, Option, OptionKey, ProgramKey, SettingKey
|
||||
from aiohomeconnect.model import (
|
||||
ArrayOfOptions,
|
||||
CommandKey,
|
||||
Option,
|
||||
OptionKey,
|
||||
ProgramKey,
|
||||
SettingKey,
|
||||
)
|
||||
from aiohomeconnect.model.error import HomeConnectError
|
||||
import voluptuous as vol
|
||||
|
||||
@ -19,34 +27,74 @@ from homeassistant.helpers import (
|
||||
device_registry as dr,
|
||||
)
|
||||
from homeassistant.helpers.entity_registry import RegistryEntry, async_migrate_entries
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .api import AsyncConfigEntryAuth
|
||||
from .const import (
|
||||
AFFECTS_TO_ACTIVE_PROGRAM,
|
||||
AFFECTS_TO_SELECTED_PROGRAM,
|
||||
ATTR_AFFECTS_TO,
|
||||
ATTR_KEY,
|
||||
ATTR_PROGRAM,
|
||||
ATTR_UNIT,
|
||||
ATTR_VALUE,
|
||||
DOMAIN,
|
||||
OLD_NEW_UNIQUE_ID_SUFFIX_MAP,
|
||||
PROGRAM_ENUM_OPTIONS,
|
||||
SERVICE_OPTION_ACTIVE,
|
||||
SERVICE_OPTION_SELECTED,
|
||||
SERVICE_PAUSE_PROGRAM,
|
||||
SERVICE_RESUME_PROGRAM,
|
||||
SERVICE_SELECT_PROGRAM,
|
||||
SERVICE_SET_PROGRAM_AND_OPTIONS,
|
||||
SERVICE_SETTING,
|
||||
SERVICE_START_PROGRAM,
|
||||
SVE_TRANSLATION_PLACEHOLDER_KEY,
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM,
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE,
|
||||
TRANSLATION_KEYS_PROGRAMS_MAP,
|
||||
)
|
||||
from .coordinator import HomeConnectConfigEntry, HomeConnectCoordinator
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
from .utils import bsh_key_to_translation_key, get_dict_from_home_connect_error
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
|
||||
|
||||
PROGRAM_OPTIONS = {
|
||||
bsh_key_to_translation_key(key): (
|
||||
key,
|
||||
value,
|
||||
)
|
||||
for key, value in {
|
||||
OptionKey.BSH_COMMON_DURATION: int,
|
||||
OptionKey.BSH_COMMON_START_IN_RELATIVE: int,
|
||||
OptionKey.BSH_COMMON_FINISH_IN_RELATIVE: int,
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_FILL_QUANTITY: int,
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_MULTIPLE_BEVERAGES: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_INTENSIV_ZONE: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_BRILLIANCE_DRY: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_VARIO_SPEED_PLUS: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_SILENCE_ON_DEMAND: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_HALF_LOAD: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_EXTRA_DRY: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_HYGIENE_PLUS: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_ECO_DRY: bool,
|
||||
OptionKey.DISHCARE_DISHWASHER_ZEOLITE_DRY: bool,
|
||||
OptionKey.COOKING_OVEN_SETPOINT_TEMPERATURE: int,
|
||||
OptionKey.COOKING_OVEN_FAST_PRE_HEAT: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_I_DOS_1_ACTIVE: bool,
|
||||
OptionKey.LAUNDRY_CARE_WASHER_I_DOS_2_ACTIVE: bool,
|
||||
}.items()
|
||||
}
|
||||
|
||||
|
||||
SERVICE_SETTING_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): str,
|
||||
@ -58,6 +106,7 @@ SERVICE_SETTING_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
# DEPRECATED: Remove in 2025.9.0
|
||||
SERVICE_OPTION_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): str,
|
||||
@ -70,6 +119,7 @@ SERVICE_OPTION_SCHEMA = vol.Schema(
|
||||
}
|
||||
)
|
||||
|
||||
# DEPRECATED: Remove in 2025.9.0
|
||||
SERVICE_PROGRAM_SCHEMA = vol.Any(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): str,
|
||||
@ -93,6 +143,46 @@ SERVICE_PROGRAM_SCHEMA = vol.Any(
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
def _require_program_or_at_least_one_option(data: dict) -> dict:
|
||||
if ATTR_PROGRAM not in data and not any(
|
||||
option_key in data for option_key in (PROGRAM_ENUM_OPTIONS | PROGRAM_OPTIONS)
|
||||
):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="required_program_or_one_option_at_least",
|
||||
)
|
||||
return data
|
||||
|
||||
|
||||
SERVICE_PROGRAM_AND_OPTIONS_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_DEVICE_ID): str,
|
||||
vol.Required(ATTR_AFFECTS_TO): vol.In(
|
||||
[AFFECTS_TO_ACTIVE_PROGRAM, AFFECTS_TO_SELECTED_PROGRAM]
|
||||
),
|
||||
vol.Optional(ATTR_PROGRAM): vol.In(TRANSLATION_KEYS_PROGRAMS_MAP.keys()),
|
||||
}
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
vol.Optional(translation_key): vol.In(allowed_values.keys())
|
||||
for translation_key, (
|
||||
key,
|
||||
allowed_values,
|
||||
) in PROGRAM_ENUM_OPTIONS.items()
|
||||
}
|
||||
)
|
||||
.extend(
|
||||
{
|
||||
vol.Optional(translation_key): schema
|
||||
for translation_key, (key, schema) in PROGRAM_OPTIONS.items()
|
||||
}
|
||||
),
|
||||
_require_program_or_at_least_one_option,
|
||||
)
|
||||
|
||||
SERVICE_COMMAND_SCHEMA = vol.Schema({vol.Required(ATTR_DEVICE_ID): str})
|
||||
|
||||
PLATFORMS = [
|
||||
@ -144,7 +234,7 @@ async def _get_client_and_ha_id(
|
||||
return entry.runtime_data.client, ha_id
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
|
||||
"""Set up Home Connect component."""
|
||||
|
||||
async def _async_service_program(call: ServiceCall, start: bool):
|
||||
@ -165,6 +255,57 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
else None
|
||||
)
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_set_program_and_option_actions",
|
||||
breaks_in_ha_version="2025.9.0",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_set_program_and_option_actions",
|
||||
translation_placeholders={
|
||||
"new_action_key": SERVICE_SET_PROGRAM_AND_OPTIONS,
|
||||
"remove_release": "2025.9.0",
|
||||
"deprecated_action_yaml": "\n".join(
|
||||
[
|
||||
"```yaml",
|
||||
f"action: {DOMAIN}.{SERVICE_START_PROGRAM if start else SERVICE_SELECT_PROGRAM}",
|
||||
"data:",
|
||||
f" {ATTR_DEVICE_ID}: DEVICE_ID",
|
||||
f" {ATTR_PROGRAM}: {program}",
|
||||
*([f" {ATTR_KEY}: {options[0].key}"] if options else []),
|
||||
*([f" {ATTR_VALUE}: {options[0].value}"] if options else []),
|
||||
*(
|
||||
[f" {ATTR_UNIT}: {options[0].unit}"]
|
||||
if options and options[0].unit
|
||||
else []
|
||||
),
|
||||
"```",
|
||||
]
|
||||
),
|
||||
"new_action_yaml": "\n ".join(
|
||||
[
|
||||
"```yaml",
|
||||
f"action: {DOMAIN}.{SERVICE_SET_PROGRAM_AND_OPTIONS}",
|
||||
"data:",
|
||||
f" {ATTR_DEVICE_ID}: DEVICE_ID",
|
||||
f" {ATTR_AFFECTS_TO}: {AFFECTS_TO_ACTIVE_PROGRAM if start else AFFECTS_TO_SELECTED_PROGRAM}",
|
||||
f" {ATTR_PROGRAM}: {bsh_key_to_translation_key(program.value)}",
|
||||
*(
|
||||
[
|
||||
f" {bsh_key_to_translation_key(options[0].key)}: {options[0].value}"
|
||||
]
|
||||
if options
|
||||
else []
|
||||
),
|
||||
"```",
|
||||
]
|
||||
),
|
||||
"repo_link": "[aiohomeconnect](https://github.com/MartinHjelmare/aiohomeconnect)",
|
||||
},
|
||||
)
|
||||
|
||||
try:
|
||||
if start:
|
||||
await client.start_program(ha_id, program_key=program, options=options)
|
||||
@ -189,6 +330,44 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
unit = call.data.get(ATTR_UNIT)
|
||||
client, ha_id = await _get_client_and_ha_id(hass, call.data[ATTR_DEVICE_ID])
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_set_program_and_option_actions",
|
||||
breaks_in_ha_version="2025.9.0",
|
||||
is_fixable=True,
|
||||
is_persistent=True,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_set_program_and_option_actions",
|
||||
translation_placeholders={
|
||||
"new_action_key": SERVICE_SET_PROGRAM_AND_OPTIONS,
|
||||
"remove_release": "2025.9.0",
|
||||
"deprecated_action_yaml": "\n".join(
|
||||
[
|
||||
"```yaml",
|
||||
f"action: {DOMAIN}.{SERVICE_OPTION_ACTIVE if active else SERVICE_OPTION_SELECTED}",
|
||||
"data:",
|
||||
f" {ATTR_DEVICE_ID}: DEVICE_ID",
|
||||
f" {ATTR_KEY}: {option_key}",
|
||||
f" {ATTR_VALUE}: {value}",
|
||||
*([f" {ATTR_UNIT}: {unit}"] if unit else []),
|
||||
"```",
|
||||
]
|
||||
),
|
||||
"new_action_yaml": "\n ".join(
|
||||
[
|
||||
"```yaml",
|
||||
f"action: {DOMAIN}.{SERVICE_SET_PROGRAM_AND_OPTIONS}",
|
||||
"data:",
|
||||
f" {ATTR_DEVICE_ID}: DEVICE_ID",
|
||||
f" {ATTR_AFFECTS_TO}: {AFFECTS_TO_ACTIVE_PROGRAM if active else AFFECTS_TO_SELECTED_PROGRAM}",
|
||||
f" {bsh_key_to_translation_key(option_key)}: {value}",
|
||||
"```",
|
||||
]
|
||||
),
|
||||
"repo_link": "[aiohomeconnect](https://github.com/MartinHjelmare/aiohomeconnect)",
|
||||
},
|
||||
)
|
||||
try:
|
||||
if active:
|
||||
await client.set_active_program_option(
|
||||
@ -272,6 +451,76 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Service for selecting a program."""
|
||||
await _async_service_program(call, False)
|
||||
|
||||
async def async_service_set_program_and_options(call: ServiceCall):
|
||||
"""Service for setting a program and options."""
|
||||
data = dict(call.data)
|
||||
program = data.pop(ATTR_PROGRAM, None)
|
||||
affects_to = data.pop(ATTR_AFFECTS_TO)
|
||||
client, ha_id = await _get_client_and_ha_id(hass, data.pop(ATTR_DEVICE_ID))
|
||||
|
||||
options: list[Option] = []
|
||||
|
||||
for option, value in data.items():
|
||||
if option in PROGRAM_ENUM_OPTIONS:
|
||||
options.append(
|
||||
Option(
|
||||
PROGRAM_ENUM_OPTIONS[option][0],
|
||||
PROGRAM_ENUM_OPTIONS[option][1][value],
|
||||
)
|
||||
)
|
||||
elif option in PROGRAM_OPTIONS:
|
||||
option_key = PROGRAM_OPTIONS[option][0]
|
||||
options.append(Option(option_key, value))
|
||||
|
||||
method_call: Awaitable[Any]
|
||||
exception_translation_key: str
|
||||
if program:
|
||||
program = (
|
||||
program
|
||||
if isinstance(program, ProgramKey)
|
||||
else TRANSLATION_KEYS_PROGRAMS_MAP[program]
|
||||
)
|
||||
|
||||
if affects_to == AFFECTS_TO_ACTIVE_PROGRAM:
|
||||
method_call = client.start_program(
|
||||
ha_id, program_key=program, options=options
|
||||
)
|
||||
exception_translation_key = "start_program"
|
||||
elif affects_to == AFFECTS_TO_SELECTED_PROGRAM:
|
||||
method_call = client.set_selected_program(
|
||||
ha_id, program_key=program, options=options
|
||||
)
|
||||
exception_translation_key = "select_program"
|
||||
else:
|
||||
array_of_options = ArrayOfOptions(options)
|
||||
if affects_to == AFFECTS_TO_ACTIVE_PROGRAM:
|
||||
method_call = client.set_active_program_options(
|
||||
ha_id, array_of_options=array_of_options
|
||||
)
|
||||
exception_translation_key = "set_options_active_program"
|
||||
else:
|
||||
# affects_to is AFFECTS_TO_SELECTED_PROGRAM
|
||||
method_call = client.set_selected_program_options(
|
||||
ha_id, array_of_options=array_of_options
|
||||
)
|
||||
exception_translation_key = "set_options_selected_program"
|
||||
|
||||
try:
|
||||
await method_call
|
||||
except HomeConnectError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key=exception_translation_key,
|
||||
translation_placeholders={
|
||||
**get_dict_from_home_connect_error(err),
|
||||
**(
|
||||
{SVE_TRANSLATION_PLACEHOLDER_PROGRAM: program}
|
||||
if program
|
||||
else {}
|
||||
),
|
||||
},
|
||||
) from err
|
||||
|
||||
async def async_service_start_program(call: ServiceCall):
|
||||
"""Service for starting a program."""
|
||||
await _async_service_program(call, True)
|
||||
@ -315,6 +564,12 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async_service_start_program,
|
||||
schema=SERVICE_PROGRAM_SCHEMA,
|
||||
)
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_PROGRAM_AND_OPTIONS,
|
||||
async_service_set_program_and_options,
|
||||
schema=SERVICE_PROGRAM_AND_OPTIONS_SCHEMA,
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
@ -349,6 +604,7 @@ async def async_unload_entry(
|
||||
hass: HomeAssistant, entry: HomeConnectConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
async_delete_issue(hass, DOMAIN, "deprecated_set_program_and_option_actions")
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
|
||||
|
||||
|
@ -1,6 +1,10 @@
|
||||
"""Constants for the Home Connect integration."""
|
||||
|
||||
from aiohomeconnect.model import EventKey, SettingKey, StatusKey
|
||||
from typing import cast
|
||||
|
||||
from aiohomeconnect.model import EventKey, OptionKey, ProgramKey, SettingKey, StatusKey
|
||||
|
||||
from .utils import bsh_key_to_translation_key
|
||||
|
||||
DOMAIN = "home_connect"
|
||||
|
||||
@ -52,15 +56,18 @@ SERVICE_OPTION_SELECTED = "set_option_selected"
|
||||
SERVICE_PAUSE_PROGRAM = "pause_program"
|
||||
SERVICE_RESUME_PROGRAM = "resume_program"
|
||||
SERVICE_SELECT_PROGRAM = "select_program"
|
||||
SERVICE_SET_PROGRAM_AND_OPTIONS = "set_program_and_options"
|
||||
SERVICE_SETTING = "change_setting"
|
||||
SERVICE_START_PROGRAM = "start_program"
|
||||
|
||||
|
||||
ATTR_AFFECTS_TO = "affects_to"
|
||||
ATTR_KEY = "key"
|
||||
ATTR_PROGRAM = "program"
|
||||
ATTR_UNIT = "unit"
|
||||
ATTR_VALUE = "value"
|
||||
|
||||
AFFECTS_TO_ACTIVE_PROGRAM = "active_program"
|
||||
AFFECTS_TO_SELECTED_PROGRAM = "selected_program"
|
||||
|
||||
SVE_TRANSLATION_KEY_SET_SETTING = "set_setting_entity"
|
||||
SVE_TRANSLATION_PLACEHOLDER_APPLIANCE_NAME = "appliance_name"
|
||||
@ -70,6 +77,269 @@ SVE_TRANSLATION_PLACEHOLDER_KEY = "key"
|
||||
SVE_TRANSLATION_PLACEHOLDER_VALUE = "value"
|
||||
|
||||
|
||||
TRANSLATION_KEYS_PROGRAMS_MAP = {
|
||||
bsh_key_to_translation_key(program.value): cast(ProgramKey, program)
|
||||
for program in ProgramKey
|
||||
if program != ProgramKey.UNKNOWN
|
||||
}
|
||||
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP = {
|
||||
value: key for key, value in TRANSLATION_KEYS_PROGRAMS_MAP.items()
|
||||
}
|
||||
|
||||
REFERENCE_MAP_ID_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CleaningRobot.EnumType.AvailableMaps.TempMap",
|
||||
"ConsumerProducts.CleaningRobot.EnumType.AvailableMaps.Map1",
|
||||
"ConsumerProducts.CleaningRobot.EnumType.AvailableMaps.Map2",
|
||||
"ConsumerProducts.CleaningRobot.EnumType.AvailableMaps.Map3",
|
||||
)
|
||||
}
|
||||
|
||||
CLEANING_MODE_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CleaningRobot.EnumType.CleaningModes.Silent",
|
||||
"ConsumerProducts.CleaningRobot.EnumType.CleaningModes.Standard",
|
||||
"ConsumerProducts.CleaningRobot.EnumType.CleaningModes.Power",
|
||||
)
|
||||
}
|
||||
|
||||
BEAN_AMOUNT_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.VeryMild",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.Mild",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.MildPlus",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.Normal",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.NormalPlus",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.Strong",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.StrongPlus",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.VeryStrong",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.VeryStrongPlus",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.ExtraStrong",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.DoubleShot",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.DoubleShotPlus",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.DoubleShotPlusPlus",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.TripleShot",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.TripleShotPlus",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanAmount.CoffeeGround",
|
||||
)
|
||||
}
|
||||
|
||||
COFFEE_TEMPERATURE_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.88C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.90C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.92C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.94C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.95C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeTemperature.96C",
|
||||
)
|
||||
}
|
||||
|
||||
BEAN_CONTAINER_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanContainerSelection.Right",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.BeanContainerSelection.Left",
|
||||
)
|
||||
}
|
||||
|
||||
FLOW_RATE_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.FlowRate.Normal",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.FlowRate.Intense",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.FlowRate.IntensePlus",
|
||||
)
|
||||
}
|
||||
|
||||
COFFEE_MILK_RATIO_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.10Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.20Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.25Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.30Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.40Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.50Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.55Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.60Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.65Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.67Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.70Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.75Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.80Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.85Percent",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.CoffeeMilkRatio.90Percent",
|
||||
)
|
||||
}
|
||||
|
||||
HOT_WATER_TEMPERATURE_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.WhiteTea",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.GreenTea",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.BlackTea",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.50C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.55C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.60C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.65C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.70C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.75C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.80C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.85C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.90C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.95C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.97C",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.122F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.131F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.140F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.149F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.158F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.167F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.176F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.185F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.194F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.203F",
|
||||
"ConsumerProducts.CoffeeMaker.EnumType.HotWaterTemperature.Max",
|
||||
)
|
||||
}
|
||||
|
||||
DRYING_TARGET_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"LaundryCare.Dryer.EnumType.DryingTarget.IronDry",
|
||||
"LaundryCare.Dryer.EnumType.DryingTarget.GentleDry",
|
||||
"LaundryCare.Dryer.EnumType.DryingTarget.CupboardDry",
|
||||
"LaundryCare.Dryer.EnumType.DryingTarget.CupboardDryPlus",
|
||||
"LaundryCare.Dryer.EnumType.DryingTarget.ExtraDry",
|
||||
)
|
||||
}
|
||||
|
||||
VENTING_LEVEL_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"Cooking.Hood.EnumType.Stage.FanOff",
|
||||
"Cooking.Hood.EnumType.Stage.FanStage01",
|
||||
"Cooking.Hood.EnumType.Stage.FanStage02",
|
||||
"Cooking.Hood.EnumType.Stage.FanStage03",
|
||||
"Cooking.Hood.EnumType.Stage.FanStage04",
|
||||
"Cooking.Hood.EnumType.Stage.FanStage05",
|
||||
)
|
||||
}
|
||||
|
||||
INTENSIVE_LEVEL_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"Cooking.Hood.EnumType.IntensiveStage.IntensiveStageOff",
|
||||
"Cooking.Hood.EnumType.IntensiveStage.IntensiveStage1",
|
||||
"Cooking.Hood.EnumType.IntensiveStage.IntensiveStage2",
|
||||
)
|
||||
}
|
||||
|
||||
WARMING_LEVEL_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"Cooking.Oven.EnumType.WarmingLevel.Low",
|
||||
"Cooking.Oven.EnumType.WarmingLevel.Medium",
|
||||
"Cooking.Oven.EnumType.WarmingLevel.High",
|
||||
)
|
||||
}
|
||||
|
||||
TEMPERATURE_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"LaundryCare.Washer.EnumType.Temperature.Cold",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC20",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC30",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC40",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC50",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC60",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC70",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC80",
|
||||
"LaundryCare.Washer.EnumType.Temperature.GC90",
|
||||
"LaundryCare.Washer.EnumType.Temperature.UlCold",
|
||||
"LaundryCare.Washer.EnumType.Temperature.UlWarm",
|
||||
"LaundryCare.Washer.EnumType.Temperature.UlHot",
|
||||
"LaundryCare.Washer.EnumType.Temperature.UlExtraHot",
|
||||
)
|
||||
}
|
||||
|
||||
SPIN_SPEED_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.Off",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.RPM400",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.RPM600",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.RPM800",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1000",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1200",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1400",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.RPM1600",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.UlOff",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.UlLow",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.UlMedium",
|
||||
"LaundryCare.Washer.EnumType.SpinSpeed.UlHigh",
|
||||
)
|
||||
}
|
||||
|
||||
VARIO_PERFECT_OPTIONS = {
|
||||
bsh_key_to_translation_key(option): option
|
||||
for option in (
|
||||
"LaundryCare.Common.EnumType.VarioPerfect.Off",
|
||||
"LaundryCare.Common.EnumType.VarioPerfect.EcoPerfect",
|
||||
"LaundryCare.Common.EnumType.VarioPerfect.SpeedPerfect",
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
PROGRAM_ENUM_OPTIONS = {
|
||||
bsh_key_to_translation_key(option_key): (
|
||||
option_key,
|
||||
options,
|
||||
)
|
||||
for option_key, options in (
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_REFERENCE_MAP_ID,
|
||||
REFERENCE_MAP_ID_OPTIONS,
|
||||
),
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_CLEANING_ROBOT_CLEANING_MODE,
|
||||
CLEANING_MODE_OPTIONS,
|
||||
),
|
||||
(OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEAN_AMOUNT, BEAN_AMOUNT_OPTIONS),
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_COFFEE_TEMPERATURE,
|
||||
COFFEE_TEMPERATURE_OPTIONS,
|
||||
),
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_BEAN_CONTAINER_SELECTION,
|
||||
BEAN_CONTAINER_OPTIONS,
|
||||
),
|
||||
(OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_FLOW_RATE, FLOW_RATE_OPTIONS),
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_COFFEE_MILK_RATIO,
|
||||
COFFEE_MILK_RATIO_OPTIONS,
|
||||
),
|
||||
(
|
||||
OptionKey.CONSUMER_PRODUCTS_COFFEE_MAKER_HOT_WATER_TEMPERATURE,
|
||||
HOT_WATER_TEMPERATURE_OPTIONS,
|
||||
),
|
||||
(OptionKey.LAUNDRY_CARE_DRYER_DRYING_TARGET, DRYING_TARGET_OPTIONS),
|
||||
(OptionKey.COOKING_COMMON_HOOD_VENTING_LEVEL, VENTING_LEVEL_OPTIONS),
|
||||
(OptionKey.COOKING_COMMON_HOOD_INTENSIVE_LEVEL, INTENSIVE_LEVEL_OPTIONS),
|
||||
(OptionKey.COOKING_OVEN_WARMING_LEVEL, WARMING_LEVEL_OPTIONS),
|
||||
(OptionKey.LAUNDRY_CARE_WASHER_TEMPERATURE, TEMPERATURE_OPTIONS),
|
||||
(OptionKey.LAUNDRY_CARE_WASHER_SPIN_SPEED, SPIN_SPEED_OPTIONS),
|
||||
(OptionKey.LAUNDRY_CARE_COMMON_VARIO_PERFECT, VARIO_PERFECT_OPTIONS),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
OLD_NEW_UNIQUE_ID_SUFFIX_MAP = {
|
||||
"ChildLock": SettingKey.BSH_COMMON_CHILD_LOCK,
|
||||
"Operation State": StatusKey.BSH_COMMON_OPERATION_STATE,
|
||||
|
@ -18,6 +18,9 @@
|
||||
"set_option_selected": {
|
||||
"service": "mdi:gesture-tap"
|
||||
},
|
||||
"set_program_and_options": {
|
||||
"service": "mdi:form-select"
|
||||
},
|
||||
"change_setting": {
|
||||
"service": "mdi:cog"
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
"name": "Home Connect",
|
||||
"codeowners": ["@DavidMStraub", "@Diegorro98", "@MartinHjelmare"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["application_credentials"],
|
||||
"dependencies": ["application_credentials", "repairs"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/home_connect",
|
||||
"iot_class": "cloud_push",
|
||||
"loggers": ["aiohomeconnect"],
|
||||
|
@ -15,24 +15,20 @@ from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .common import setup_home_connect_entry
|
||||
from .const import APPLIANCES_WITH_PROGRAMS, DOMAIN, SVE_TRANSLATION_PLACEHOLDER_PROGRAM
|
||||
from .const import (
|
||||
APPLIANCES_WITH_PROGRAMS,
|
||||
DOMAIN,
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP,
|
||||
SVE_TRANSLATION_PLACEHOLDER_PROGRAM,
|
||||
TRANSLATION_KEYS_PROGRAMS_MAP,
|
||||
)
|
||||
from .coordinator import (
|
||||
HomeConnectApplianceData,
|
||||
HomeConnectConfigEntry,
|
||||
HomeConnectCoordinator,
|
||||
)
|
||||
from .entity import HomeConnectEntity
|
||||
from .utils import bsh_key_to_translation_key, get_dict_from_home_connect_error
|
||||
|
||||
TRANSLATION_KEYS_PROGRAMS_MAP = {
|
||||
bsh_key_to_translation_key(program.value): cast(ProgramKey, program)
|
||||
for program in ProgramKey
|
||||
if program != ProgramKey.UNKNOWN
|
||||
}
|
||||
|
||||
PROGRAMS_TRANSLATION_KEYS_MAP = {
|
||||
value: key for key, value in TRANSLATION_KEYS_PROGRAMS_MAP.items()
|
||||
}
|
||||
from .utils import get_dict_from_home_connect_error
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
|
@ -46,6 +46,558 @@ select_program:
|
||||
example: "seconds"
|
||||
selector:
|
||||
text:
|
||||
set_program_and_options:
|
||||
fields:
|
||||
device_id:
|
||||
required: true
|
||||
selector:
|
||||
device:
|
||||
integration: home_connect
|
||||
affects_to:
|
||||
example: active_program
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
translation_key: affects_to
|
||||
options:
|
||||
- active_program
|
||||
- selected_program
|
||||
program:
|
||||
example: dishcare_dishwasher_program_auto2
|
||||
required: true
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
custom_value: false
|
||||
translation_key: programs
|
||||
options:
|
||||
- consumer_products_cleaning_robot_program_cleaning_clean_all
|
||||
- consumer_products_cleaning_robot_program_cleaning_clean_map
|
||||
- consumer_products_cleaning_robot_program_basic_go_home
|
||||
- consumer_products_coffee_maker_program_beverage_ristretto
|
||||
- consumer_products_coffee_maker_program_beverage_espresso
|
||||
- consumer_products_coffee_maker_program_beverage_espresso_doppio
|
||||
- consumer_products_coffee_maker_program_beverage_coffee
|
||||
- consumer_products_coffee_maker_program_beverage_x_l_coffee
|
||||
- consumer_products_coffee_maker_program_beverage_caffe_grande
|
||||
- consumer_products_coffee_maker_program_beverage_espresso_macchiato
|
||||
- consumer_products_coffee_maker_program_beverage_cappuccino
|
||||
- consumer_products_coffee_maker_program_beverage_latte_macchiato
|
||||
- consumer_products_coffee_maker_program_beverage_caffe_latte
|
||||
- consumer_products_coffee_maker_program_beverage_milk_froth
|
||||
- consumer_products_coffee_maker_program_beverage_warm_milk
|
||||
- consumer_products_coffee_maker_program_coffee_world_kleiner_brauner
|
||||
- consumer_products_coffee_maker_program_coffee_world_grosser_brauner
|
||||
- consumer_products_coffee_maker_program_coffee_world_verlaengerter
|
||||
- consumer_products_coffee_maker_program_coffee_world_verlaengerter_braun
|
||||
- consumer_products_coffee_maker_program_coffee_world_wiener_melange
|
||||
- consumer_products_coffee_maker_program_coffee_world_flat_white
|
||||
- consumer_products_coffee_maker_program_coffee_world_cortado
|
||||
- consumer_products_coffee_maker_program_coffee_world_cafe_cortado
|
||||
- consumer_products_coffee_maker_program_coffee_world_cafe_con_leche
|
||||
- consumer_products_coffee_maker_program_coffee_world_cafe_au_lait
|
||||
- consumer_products_coffee_maker_program_coffee_world_doppio
|
||||
- consumer_products_coffee_maker_program_coffee_world_kaapi
|
||||
- consumer_products_coffee_maker_program_coffee_world_koffie_verkeerd
|
||||
- consumer_products_coffee_maker_program_coffee_world_galao
|
||||
- consumer_products_coffee_maker_program_coffee_world_garoto
|
||||
- consumer_products_coffee_maker_program_coffee_world_americano
|
||||
- consumer_products_coffee_maker_program_coffee_world_red_eye
|
||||
- consumer_products_coffee_maker_program_coffee_world_black_eye
|
||||
- consumer_products_coffee_maker_program_coffee_world_dead_eye
|
||||
- consumer_products_coffee_maker_program_beverage_hot_water
|
||||
- dishcare_dishwasher_program_pre_rinse
|
||||
- dishcare_dishwasher_program_auto_1
|
||||
- dishcare_dishwasher_program_auto_2
|
||||
- dishcare_dishwasher_program_auto_3
|
||||
- dishcare_dishwasher_program_eco_50
|
||||
- dishcare_dishwasher_program_quick_45
|
||||
- dishcare_dishwasher_program_intensiv_70
|
||||
- dishcare_dishwasher_program_normal_65
|
||||
- dishcare_dishwasher_program_glas_40
|
||||
- dishcare_dishwasher_program_glass_care
|
||||
- dishcare_dishwasher_program_night_wash
|
||||
- dishcare_dishwasher_program_quick_65
|
||||
- dishcare_dishwasher_program_normal_45
|
||||
- dishcare_dishwasher_program_intensiv_45
|
||||
- dishcare_dishwasher_program_auto_half_load
|
||||
- dishcare_dishwasher_program_intensiv_power
|
||||
- dishcare_dishwasher_program_magic_daily
|
||||
- dishcare_dishwasher_program_super_60
|
||||
- dishcare_dishwasher_program_kurz_60
|
||||
- dishcare_dishwasher_program_express_sparkle_65
|
||||
- dishcare_dishwasher_program_machine_care
|
||||
- dishcare_dishwasher_program_steam_fresh
|
||||
- dishcare_dishwasher_program_maximum_cleaning
|
||||
- dishcare_dishwasher_program_mixed_load
|
||||
- laundry_care_dryer_program_cotton
|
||||
- laundry_care_dryer_program_synthetic
|
||||
- laundry_care_dryer_program_mix
|
||||
- laundry_care_dryer_program_blankets
|
||||
- laundry_care_dryer_program_business_shirts
|
||||
- laundry_care_dryer_program_down_feathers
|
||||
- laundry_care_dryer_program_hygiene
|
||||
- laundry_care_dryer_program_jeans
|
||||
- laundry_care_dryer_program_outdoor
|
||||
- laundry_care_dryer_program_synthetic_refresh
|
||||
- laundry_care_dryer_program_towels
|
||||
- laundry_care_dryer_program_delicates
|
||||
- laundry_care_dryer_program_super_40
|
||||
- laundry_care_dryer_program_shirts_15
|
||||
- laundry_care_dryer_program_pillow
|
||||
- laundry_care_dryer_program_anti_shrink
|
||||
- laundry_care_dryer_program_my_time_my_drying_time
|
||||
- laundry_care_dryer_program_time_cold
|
||||
- laundry_care_dryer_program_time_warm
|
||||
- laundry_care_dryer_program_in_basket
|
||||
- laundry_care_dryer_program_time_cold_fix_time_cold_20
|
||||
- laundry_care_dryer_program_time_cold_fix_time_cold_30
|
||||
- laundry_care_dryer_program_time_cold_fix_time_cold_60
|
||||
- laundry_care_dryer_program_time_warm_fix_time_warm_30
|
||||
- laundry_care_dryer_program_time_warm_fix_time_warm_40
|
||||
- laundry_care_dryer_program_time_warm_fix_time_warm_60
|
||||
- laundry_care_dryer_program_dessous
|
||||
- cooking_common_program_hood_automatic
|
||||
- cooking_common_program_hood_venting
|
||||
- cooking_common_program_hood_delayed_shut_off
|
||||
- cooking_oven_program_heating_mode_pre_heating
|
||||
- cooking_oven_program_heating_mode_hot_air
|
||||
- cooking_oven_program_heating_mode_hot_air_eco
|
||||
- cooking_oven_program_heating_mode_hot_air_grilling
|
||||
- cooking_oven_program_heating_mode_top_bottom_heating
|
||||
- cooking_oven_program_heating_mode_top_bottom_heating_eco
|
||||
- cooking_oven_program_heating_mode_bottom_heating
|
||||
- cooking_oven_program_heating_mode_pizza_setting
|
||||
- cooking_oven_program_heating_mode_slow_cook
|
||||
- cooking_oven_program_heating_mode_intensive_heat
|
||||
- cooking_oven_program_heating_mode_keep_warm
|
||||
- cooking_oven_program_heating_mode_preheat_ovenware
|
||||
- cooking_oven_program_heating_mode_frozen_heatup_special
|
||||
- cooking_oven_program_heating_mode_desiccation
|
||||
- cooking_oven_program_heating_mode_defrost
|
||||
- cooking_oven_program_heating_mode_proof
|
||||
- cooking_oven_program_heating_mode_hot_air_30_steam
|
||||
- cooking_oven_program_heating_mode_hot_air_60_steam
|
||||
- cooking_oven_program_heating_mode_hot_air_80_steam
|
||||
- cooking_oven_program_heating_mode_hot_air_100_steam
|
||||
- cooking_oven_program_heating_mode_sabbath_programme
|
||||
- cooking_oven_program_microwave_90_watt
|
||||
- cooking_oven_program_microwave_180_watt
|
||||
- cooking_oven_program_microwave_360_watt
|
||||
- cooking_oven_program_microwave_600_watt
|
||||
- cooking_oven_program_microwave_900_watt
|
||||
- cooking_oven_program_microwave_1000_watt
|
||||
- cooking_oven_program_microwave_max
|
||||
- cooking_oven_program_heating_mode_warming_drawer
|
||||
- laundry_care_washer_program_cotton
|
||||
- laundry_care_washer_program_cotton_cotton_eco
|
||||
- laundry_care_washer_program_cotton_eco_4060
|
||||
- laundry_care_washer_program_cotton_colour
|
||||
- laundry_care_washer_program_easy_care
|
||||
- laundry_care_washer_program_mix
|
||||
- laundry_care_washer_program_mix_night_wash
|
||||
- laundry_care_washer_program_delicates_silk
|
||||
- laundry_care_washer_program_wool
|
||||
- laundry_care_washer_program_sensitive
|
||||
- laundry_care_washer_program_auto_30
|
||||
- laundry_care_washer_program_auto_40
|
||||
- laundry_care_washer_program_auto_60
|
||||
- laundry_care_washer_program_chiffon
|
||||
- laundry_care_washer_program_curtains
|
||||
- laundry_care_washer_program_dark_wash
|
||||
- laundry_care_washer_program_dessous
|
||||
- laundry_care_washer_program_monsoon
|
||||
- laundry_care_washer_program_outdoor
|
||||
- laundry_care_washer_program_plush_toy
|
||||
- laundry_care_washer_program_shirts_blouses
|
||||
- laundry_care_washer_program_sport_fitness
|
||||
- laundry_care_washer_program_towels
|
||||
- laundry_care_washer_program_water_proof
|
||||
- laundry_care_washer_program_power_speed_59
|
||||
- laundry_care_washer_program_super_153045_super_15
|
||||
- laundry_care_washer_program_super_153045_super_1530
|
||||
- laundry_care_washer_program_down_duvet_duvet
|
||||
- laundry_care_washer_program_rinse_rinse_spin_drain
|
||||
- laundry_care_washer_program_drum_clean
|
||||
- laundry_care_washer_dryer_program_cotton
|
||||
- laundry_care_washer_dryer_program_cotton_eco_4060
|
||||
- laundry_care_washer_dryer_program_mix
|
||||
- laundry_care_washer_dryer_program_easy_care
|
||||
- laundry_care_washer_dryer_program_wash_and_dry_60
|
||||
- laundry_care_washer_dryer_program_wash_and_dry_90
|
||||
cleaning_robot_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
consumer_products_cleaning_robot_option_reference_map_id:
|
||||
example: consumer_products_cleaning_robot_enum_type_available_maps_map1
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: available_maps
|
||||
options:
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_temp_map
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_map1
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_map2
|
||||
- consumer_products_cleaning_robot_enum_type_available_maps_map3
|
||||
consumer_products_cleaning_robot_option_cleaning_mode:
|
||||
example: consumer_products_cleaning_robot_enum_type_cleaning_modes_standard
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: cleaning_mode
|
||||
options:
|
||||
- consumer_products_cleaning_robot_enum_type_cleaning_modes_silent
|
||||
- consumer_products_cleaning_robot_enum_type_cleaning_modes_standard
|
||||
- consumer_products_cleaning_robot_enum_type_cleaning_modes_power
|
||||
coffee_maker_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
consumer_products_coffee_maker_option_bean_amount:
|
||||
example: consumer_products_coffee_maker_enum_type_bean_amount_normal
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: bean_amount
|
||||
options:
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_very_mild
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_mild
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_mild_plus
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_normal
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_normal_plus
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_strong
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_strong_plus
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_very_strong
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_very_strong_plus
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_extra_strong
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_double_shot
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_double_shot_plus
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_double_shot_plus_plus
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_triple_shot
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_triple_shot_plus
|
||||
- consumer_products_coffee_maker_enum_type_bean_amount_coffee_ground
|
||||
consumer_products_coffee_maker_option_fill_quantity:
|
||||
example: 60
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
step: 1
|
||||
mode: box
|
||||
unit_of_measurement: ml
|
||||
consumer_products_coffee_maker_option_coffee_temperature:
|
||||
example: consumer_products_coffee_maker_enum_type_coffee_temperature_88_c
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: coffee_temperature
|
||||
options:
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_88_c
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_90_c
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_92_c
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_94_c
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_95_c
|
||||
- consumer_products_coffee_maker_enum_type_coffee_temperature_96_c
|
||||
consumer_products_coffee_maker_option_bean_container:
|
||||
example: consumer_products_coffee_maker_enum_type_bean_container_selection_right
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: bean_container
|
||||
options:
|
||||
- consumer_products_coffee_maker_enum_type_bean_container_selection_right
|
||||
- consumer_products_coffee_maker_enum_type_bean_container_selection_left
|
||||
consumer_products_coffee_maker_option_flow_rate:
|
||||
example: consumer_products_coffee_maker_enum_type_flow_rate_normal
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: flow_rate
|
||||
options:
|
||||
- consumer_products_coffee_maker_enum_type_flow_rate_normal
|
||||
- consumer_products_coffee_maker_enum_type_flow_rate_intense
|
||||
- consumer_products_coffee_maker_enum_type_flow_rate_intense_plus
|
||||
consumer_products_coffee_maker_option_multiple_beverages:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
consumer_products_coffee_maker_option_coffee_milk_ratio:
|
||||
example: consumer_products_coffee_maker_enum_type_coffee_milk_ratio_50_percent
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: coffee_milk_ratio
|
||||
options:
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_10_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_20_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_25_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_30_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_40_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_50_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_55_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_60_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_65_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_67_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_70_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_75_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_80_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_85_percent
|
||||
- consumer_products_coffee_maker_enum_type_coffee_milk_ratio_90_percent
|
||||
consumer_products_coffee_maker_option_hot_water_temperature:
|
||||
example: consumer_products_coffee_maker_enum_type_hot_water_temperature_50_c
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: hot_water_temperature
|
||||
options:
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_white_tea
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_green_tea
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_black_tea
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_50_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_55_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_60_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_65_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_70_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_75_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_80_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_85_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_90_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_95_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_97_c
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_122_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_131_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_140_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_149_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_158_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_167_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_176_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_185_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_194_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_203_f
|
||||
- consumer_products_coffee_maker_enum_type_hot_water_temperature_max
|
||||
dish_washer_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
b_s_h_common_option_start_in_relative:
|
||||
example: 3600
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
step: 1
|
||||
mode: box
|
||||
unit_of_measurement: s
|
||||
dishcare_dishwasher_option_intensiv_zone:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_brilliance_dry:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_vario_speed_plus:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_silence_on_demand:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_half_load:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_extra_dry:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_hygiene_plus:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_eco_dry:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dishcare_dishwasher_option_zeolite_dry:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
dryer_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
laundry_care_dryer_option_drying_target:
|
||||
example: laundry_care_dryer_enum_type_drying_target_iron_dry
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: drying_target
|
||||
options:
|
||||
- laundry_care_dryer_enum_type_drying_target_iron_dry
|
||||
- laundry_care_dryer_enum_type_drying_target_gentle_dry
|
||||
- laundry_care_dryer_enum_type_drying_target_cupboard_dry
|
||||
- laundry_care_dryer_enum_type_drying_target_cupboard_dry_plus
|
||||
- laundry_care_dryer_enum_type_drying_target_extra_dry
|
||||
hood_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
cooking_hood_option_venting_level:
|
||||
example: cooking_hood_enum_type_stage_fan_stage01
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: venting_level
|
||||
options:
|
||||
- cooking_hood_enum_type_stage_fan_off
|
||||
- cooking_hood_enum_type_stage_fan_stage01
|
||||
- cooking_hood_enum_type_stage_fan_stage02
|
||||
- cooking_hood_enum_type_stage_fan_stage03
|
||||
- cooking_hood_enum_type_stage_fan_stage04
|
||||
- cooking_hood_enum_type_stage_fan_stage05
|
||||
cooking_hood_option_intensive_level:
|
||||
example: cooking_hood_enum_type_intensive_stage_intensive_stage1
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: intensive_level
|
||||
options:
|
||||
- cooking_hood_enum_type_intensive_stage_intensive_stage_off
|
||||
- cooking_hood_enum_type_intensive_stage_intensive_stage1
|
||||
- cooking_hood_enum_type_intensive_stage_intensive_stage2
|
||||
oven_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
cooking_oven_option_setpoint_temperature:
|
||||
example: 180
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
step: 1
|
||||
mode: box
|
||||
unit_of_measurement: °C/°F
|
||||
b_s_h_common_option_duration:
|
||||
example: 900
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
step: 1
|
||||
mode: box
|
||||
unit_of_measurement: s
|
||||
cooking_oven_option_fast_pre_heat:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
warming_drawer_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
cooking_oven_option_warming_level:
|
||||
example: cooking_oven_enum_type_warming_level_medium
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: warming_level
|
||||
options:
|
||||
- cooking_oven_enum_type_warming_level_low
|
||||
- cooking_oven_enum_type_warming_level_medium
|
||||
- cooking_oven_enum_type_warming_level_high
|
||||
washer_options:
|
||||
collapsed: true
|
||||
fields:
|
||||
laundry_care_washer_option_temperature:
|
||||
example: laundry_care_washer_enum_type_temperature_g_c40
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: washer_temperature
|
||||
options:
|
||||
- laundry_care_washer_enum_type_temperature_cold
|
||||
- laundry_care_washer_enum_type_temperature_g_c20
|
||||
- laundry_care_washer_enum_type_temperature_g_c30
|
||||
- laundry_care_washer_enum_type_temperature_g_c40
|
||||
- laundry_care_washer_enum_type_temperature_g_c50
|
||||
- laundry_care_washer_enum_type_temperature_g_c60
|
||||
- laundry_care_washer_enum_type_temperature_g_c70
|
||||
- laundry_care_washer_enum_type_temperature_g_c80
|
||||
- laundry_care_washer_enum_type_temperature_g_c90
|
||||
- laundry_care_washer_enum_type_temperature_ul_cold
|
||||
- laundry_care_washer_enum_type_temperature_ul_warm
|
||||
- laundry_care_washer_enum_type_temperature_ul_hot
|
||||
- laundry_care_washer_enum_type_temperature_ul_extra_hot
|
||||
laundry_care_washer_option_spin_speed:
|
||||
example: laundry_care_washer_enum_type_spin_speed_r_p_m800
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: spin_speed
|
||||
options:
|
||||
- laundry_care_washer_enum_type_spin_speed_off
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m400
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m600
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m800
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m1000
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m1200
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m1400
|
||||
- laundry_care_washer_enum_type_spin_speed_r_p_m1600
|
||||
- laundry_care_washer_enum_type_spin_speed_ul_off
|
||||
- laundry_care_washer_enum_type_spin_speed_ul_low
|
||||
- laundry_care_washer_enum_type_spin_speed_ul_medium
|
||||
- laundry_care_washer_enum_type_spin_speed_ul_high
|
||||
b_s_h_common_option_finish_in_relative:
|
||||
example: 3600
|
||||
required: false
|
||||
selector:
|
||||
number:
|
||||
min: 0
|
||||
step: 1
|
||||
mode: box
|
||||
unit_of_measurement: s
|
||||
laundry_care_washer_option_i_dos1_active:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
laundry_care_washer_option_i_dos2_active:
|
||||
example: false
|
||||
required: false
|
||||
selector:
|
||||
boolean:
|
||||
laundry_care_washer_option_vario_perfect:
|
||||
example: laundry_care_common_enum_type_vario_perfect_eco_perfect
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
mode: dropdown
|
||||
translation_key: vario_perfect
|
||||
options:
|
||||
- laundry_care_common_enum_type_vario_perfect_off
|
||||
- laundry_care_common_enum_type_vario_perfect_eco_perfect
|
||||
- laundry_care_common_enum_type_vario_perfect_speed_perfect
|
||||
pause_program:
|
||||
fields:
|
||||
device_id:
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -23,8 +23,10 @@ import voluptuous as vol
|
||||
from homeassistant.components import onboarding
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import AbortFlow
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import instance_id
|
||||
from homeassistant.helpers.selector import TextSelector
|
||||
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
|
||||
from homeassistant.helpers.service_info.zeroconf import ZeroconfServiceInfo
|
||||
@ -88,7 +90,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
# Tell device we want a token, user must now press the button within 30 seconds
|
||||
# The first attempt will always fail, but this opens the window to press the button
|
||||
token = await async_request_token(self.ip_address)
|
||||
token = await async_request_token(self.hass, self.ip_address)
|
||||
errors: dict[str, str] | None = None
|
||||
|
||||
if token is None:
|
||||
@ -250,7 +252,7 @@ class HomeWizardConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
errors: dict[str, str] | None = None
|
||||
|
||||
token = await async_request_token(self.ip_address)
|
||||
token = await async_request_token(self.hass, self.ip_address)
|
||||
|
||||
if user_input is not None:
|
||||
if token is None:
|
||||
@ -353,7 +355,7 @@ async def async_try_connect(ip_address: str, token: str | None = None) -> Device
|
||||
await energy_api.close()
|
||||
|
||||
|
||||
async def async_request_token(ip_address: str) -> str | None:
|
||||
async def async_request_token(hass: HomeAssistant, ip_address: str) -> str | None:
|
||||
"""Try to request a token from the device.
|
||||
|
||||
This method is used to request a token from the device,
|
||||
@ -362,8 +364,12 @@ async def async_request_token(ip_address: str) -> str | None:
|
||||
|
||||
api = HomeWizardEnergyV2(ip_address)
|
||||
|
||||
# Get a part of the unique id to make the token unique
|
||||
# This is to prevent token conflicts when multiple HA instances are used
|
||||
uuid = await instance_id.async_get(hass)
|
||||
|
||||
try:
|
||||
return await api.get_token("home-assistant")
|
||||
return await api.get_token(f"home-assistant#{uuid[:6]}")
|
||||
except DisabledError:
|
||||
return None
|
||||
finally:
|
||||
|
@ -47,7 +47,7 @@ class MigrateToV2ApiRepairFlow(RepairsFlow):
|
||||
|
||||
# Tell device we want a token, user must now press the button within 30 seconds
|
||||
# The first attempt will always fail, but this opens the window to press the button
|
||||
token = await async_request_token(ip_address)
|
||||
token = await async_request_token(self.hass, ip_address)
|
||||
errors: dict[str, str] | None = None
|
||||
|
||||
if token is None:
|
||||
|
@ -58,7 +58,7 @@
|
||||
"services": {
|
||||
"send_raw_node_command": {
|
||||
"name": "Send raw node command",
|
||||
"description": "[%key:component::isy994::options::step::init::description%]",
|
||||
"description": "Sends a “raw” (e.g., DON, DOF) ISY REST device command to a node using its Home Assistant entity ID. This is useful for devices that aren’t fully supported in Home Assistant yet, such as controls for many NodeServer nodes.",
|
||||
"fields": {
|
||||
"command": {
|
||||
"name": "Command",
|
||||
|
@ -13,7 +13,7 @@ from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import SCAN_INTERVAL
|
||||
from .const import DOMAIN, SCAN_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -75,16 +75,28 @@ class LaCrosseUpdateCoordinator(DataUpdateCoordinator[list[Sensor]]):
|
||||
try:
|
||||
# Fetch last hour of data
|
||||
for sensor in self.devices:
|
||||
sensor.data = (
|
||||
await self.api.get_sensor_status(
|
||||
data = await self.api.get_sensor_status(
|
||||
sensor=sensor,
|
||||
tz=self.hass.config.time_zone,
|
||||
)
|
||||
)["data"]["current"]
|
||||
_LOGGER.debug("Got data: %s", sensor.data)
|
||||
_LOGGER.debug("Got data: %s", data)
|
||||
|
||||
if data_error := data.get("error"):
|
||||
if data_error == "no_readings":
|
||||
sensor.data = None
|
||||
_LOGGER.debug("No readings for %s", sensor.name)
|
||||
continue
|
||||
_LOGGER.debug("Error: %s", data_error)
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN, translation_key="update_error"
|
||||
)
|
||||
|
||||
sensor.data = data["data"]["current"]
|
||||
|
||||
except HTTPError as error:
|
||||
raise UpdateFailed from error
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN, translation_key="update_error"
|
||||
) from error
|
||||
|
||||
# Verify that we have permission to read the sensors
|
||||
for sensor in self.devices:
|
||||
|
@ -64,6 +64,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=get_value,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
"Humidity": LaCrosseSensorEntityDescription(
|
||||
key="Humidity",
|
||||
@ -71,6 +72,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=get_value,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
"HeatIndex": LaCrosseSensorEntityDescription(
|
||||
key="HeatIndex",
|
||||
@ -79,6 +81,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=get_value,
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
"WindSpeed": LaCrosseSensorEntityDescription(
|
||||
key="WindSpeed",
|
||||
@ -86,6 +89,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
value_fn=get_value,
|
||||
native_unit_of_measurement=UnitOfSpeed.KILOMETERS_PER_HOUR,
|
||||
device_class=SensorDeviceClass.WIND_SPEED,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
"Rain": LaCrosseSensorEntityDescription(
|
||||
key="Rain",
|
||||
@ -93,12 +97,14 @@ SENSOR_DESCRIPTIONS = {
|
||||
value_fn=get_value,
|
||||
native_unit_of_measurement=UnitOfPrecipitationDepth.MILLIMETERS,
|
||||
device_class=SensorDeviceClass.PRECIPITATION,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
"WindHeading": LaCrosseSensorEntityDescription(
|
||||
key="WindHeading",
|
||||
translation_key="wind_heading",
|
||||
value_fn=get_value,
|
||||
native_unit_of_measurement=DEGREE,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
"WetDry": LaCrosseSensorEntityDescription(
|
||||
key="WetDry",
|
||||
@ -117,6 +123,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
value_fn=get_value,
|
||||
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.HPA,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
"FeelsLike": LaCrosseSensorEntityDescription(
|
||||
key="FeelsLike",
|
||||
@ -125,6 +132,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
value_fn=get_value,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
"WindChill": LaCrosseSensorEntityDescription(
|
||||
key="WindChill",
|
||||
@ -133,6 +141,7 @@ SENSOR_DESCRIPTIONS = {
|
||||
value_fn=get_value,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
suggested_display_precision=2,
|
||||
),
|
||||
}
|
||||
# map of API returned unit of measurement strings to their corresponding unit of measurement
|
||||
|
@ -42,5 +42,10 @@
|
||||
"name": "Wind chill"
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"update_error": {
|
||||
"message": "Error updating data"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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:
|
||||
|
122
homeassistant/components/letpot/binary_sensor.py
Normal file
122
homeassistant/components/letpot/binary_sensor.py
Normal file
@ -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",
|
||||
|
@ -44,7 +44,7 @@ rules:
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
test-coverage: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
@ -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
homeassistant/components/letpot/sensor.py
Normal file
110
homeassistant/components/letpot/sensor.py
Normal 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"
|
||||
|
@ -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
|
||||
|
12
homeassistant/components/motionmount/icons.json
Normal file
12
homeassistant/components/motionmount/icons.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"motionmount_error_status": {
|
||||
"default": "mdi:alert-circle-outline",
|
||||
"state": {
|
||||
"none": "mdi:check-circle-outline"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -56,12 +56,12 @@ rules:
|
||||
dynamic-devices:
|
||||
status: exempt
|
||||
comment: Single device per config entry
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-category: done
|
||||
entity-device-class: done
|
||||
entity-disabled-by-default: done
|
||||
entity-translations: done
|
||||
exception-translations: done
|
||||
icon-translations: todo
|
||||
icon-translations: done
|
||||
reconfiguration-flow: todo
|
||||
repair-issues:
|
||||
status: exempt
|
||||
|
@ -6,6 +6,7 @@ import motionmount
|
||||
from motionmount import MotionMountSystemError
|
||||
|
||||
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
@ -47,6 +48,8 @@ class MotionMountErrorStatusSensor(MotionMountEntity, SensorEntity):
|
||||
"internal",
|
||||
]
|
||||
_attr_translation_key = "motionmount_error_status"
|
||||
_attr_entity_category = EntityCategory.DIAGNOSTIC
|
||||
_attr_entity_registry_enabled_default = False
|
||||
|
||||
def __init__(
|
||||
self, mm: motionmount.MotionMount, config_entry: MotionMountConfigEntry
|
||||
|
@ -6,7 +6,14 @@ from functools import lru_cache
|
||||
from types import TracebackType
|
||||
from typing import Self
|
||||
|
||||
from paho.mqtt.client import Client as MQTTClient
|
||||
from paho.mqtt.client import (
|
||||
CallbackOnConnect_v2,
|
||||
CallbackOnDisconnect_v2,
|
||||
CallbackOnPublish_v2,
|
||||
CallbackOnSubscribe_v2,
|
||||
CallbackOnUnsubscribe_v2,
|
||||
Client as MQTTClient,
|
||||
)
|
||||
|
||||
_MQTT_LOCK_COUNT = 7
|
||||
|
||||
@ -44,6 +51,12 @@ class AsyncMQTTClient(MQTTClient):
|
||||
that is not needed since we are running in an async event loop.
|
||||
"""
|
||||
|
||||
on_connect: CallbackOnConnect_v2
|
||||
on_disconnect: CallbackOnDisconnect_v2
|
||||
on_publish: CallbackOnPublish_v2
|
||||
on_subscribe: CallbackOnSubscribe_v2
|
||||
on_unsubscribe: CallbackOnUnsubscribe_v2
|
||||
|
||||
def setup(self) -> None:
|
||||
"""Set up the client.
|
||||
|
||||
|
@ -311,8 +311,8 @@ class MqttClientSetup:
|
||||
client_id = None
|
||||
transport: str = config.get(CONF_TRANSPORT, DEFAULT_TRANSPORT)
|
||||
self._client = AsyncMQTTClient(
|
||||
mqtt.CallbackAPIVersion.VERSION1,
|
||||
client_id,
|
||||
callback_api_version=mqtt.CallbackAPIVersion.VERSION2,
|
||||
client_id=client_id,
|
||||
protocol=proto,
|
||||
transport=transport, # type: ignore[arg-type]
|
||||
reconnect_on_failure=False,
|
||||
@ -476,9 +476,9 @@ class MQTT:
|
||||
mqttc.on_connect = self._async_mqtt_on_connect
|
||||
mqttc.on_disconnect = self._async_mqtt_on_disconnect
|
||||
mqttc.on_message = self._async_mqtt_on_message
|
||||
mqttc.on_publish = self._async_mqtt_on_callback
|
||||
mqttc.on_subscribe = self._async_mqtt_on_callback
|
||||
mqttc.on_unsubscribe = self._async_mqtt_on_callback
|
||||
mqttc.on_publish = self._async_mqtt_on_publish
|
||||
mqttc.on_subscribe = self._async_mqtt_on_subscribe_unsubscribe
|
||||
mqttc.on_unsubscribe = self._async_mqtt_on_subscribe_unsubscribe
|
||||
|
||||
# suppress exceptions at callback
|
||||
mqttc.suppress_exceptions = True
|
||||
@ -498,7 +498,7 @@ class MQTT:
|
||||
def _async_reader_callback(self, client: mqtt.Client) -> None:
|
||||
"""Handle reading data from the socket."""
|
||||
if (status := client.loop_read(MAX_PACKETS_TO_READ)) != 0:
|
||||
self._async_on_disconnect(status)
|
||||
self._async_handle_callback_exception(status)
|
||||
|
||||
@callback
|
||||
def _async_start_misc_periodic(self) -> None:
|
||||
@ -593,7 +593,7 @@ class MQTT:
|
||||
def _async_writer_callback(self, client: mqtt.Client) -> None:
|
||||
"""Handle writing data to the socket."""
|
||||
if (status := client.loop_write()) != 0:
|
||||
self._async_on_disconnect(status)
|
||||
self._async_handle_callback_exception(status)
|
||||
|
||||
def _on_socket_register_write(
|
||||
self, client: mqtt.Client, userdata: Any, sock: SocketType
|
||||
@ -983,9 +983,9 @@ class MQTT:
|
||||
self,
|
||||
_mqttc: mqtt.Client,
|
||||
_userdata: None,
|
||||
_flags: dict[str, int],
|
||||
result_code: int,
|
||||
properties: mqtt.Properties | None = None,
|
||||
_connect_flags: mqtt.ConnectFlags,
|
||||
reason_code: mqtt.ReasonCode,
|
||||
_properties: mqtt.Properties | None = None,
|
||||
) -> None:
|
||||
"""On connect callback.
|
||||
|
||||
@ -993,19 +993,20 @@ class MQTT:
|
||||
message.
|
||||
"""
|
||||
# pylint: disable-next=import-outside-toplevel
|
||||
import paho.mqtt.client as mqtt
|
||||
|
||||
if result_code != mqtt.CONNACK_ACCEPTED:
|
||||
if result_code in (
|
||||
mqtt.CONNACK_REFUSED_BAD_USERNAME_PASSWORD,
|
||||
mqtt.CONNACK_REFUSED_NOT_AUTHORIZED,
|
||||
):
|
||||
if reason_code.is_failure:
|
||||
# 24: Continue authentication
|
||||
# 25: Re-authenticate
|
||||
# 134: Bad user name or password
|
||||
# 135: Not authorized
|
||||
# 140: Bad authentication method
|
||||
if reason_code.value in (24, 25, 134, 135, 140):
|
||||
self._should_reconnect = False
|
||||
self.hass.async_create_task(self.async_disconnect())
|
||||
self.config_entry.async_start_reauth(self.hass)
|
||||
_LOGGER.error(
|
||||
"Unable to connect to the MQTT broker: %s",
|
||||
mqtt.connack_string(result_code),
|
||||
reason_code.getName(), # type: ignore[no-untyped-call]
|
||||
)
|
||||
self._async_connection_result(False)
|
||||
return
|
||||
@ -1016,7 +1017,7 @@ class MQTT:
|
||||
"Connected to MQTT server %s:%s (%s)",
|
||||
self.conf[CONF_BROKER],
|
||||
self.conf.get(CONF_PORT, DEFAULT_PORT),
|
||||
result_code,
|
||||
reason_code,
|
||||
)
|
||||
|
||||
birth: dict[str, Any]
|
||||
@ -1153,18 +1154,32 @@ class MQTT:
|
||||
self._mqtt_data.state_write_requests.process_write_state_requests(msg)
|
||||
|
||||
@callback
|
||||
def _async_mqtt_on_callback(
|
||||
def _async_mqtt_on_publish(
|
||||
self,
|
||||
_mqttc: mqtt.Client,
|
||||
_userdata: None,
|
||||
mid: int,
|
||||
_granted_qos_reason: tuple[int, ...] | mqtt.ReasonCodes | None = None,
|
||||
_properties_reason: mqtt.ReasonCodes | None = None,
|
||||
_reason_code: mqtt.ReasonCode,
|
||||
_properties: mqtt.Properties | None,
|
||||
) -> None:
|
||||
"""Publish callback."""
|
||||
self._async_mqtt_on_callback(mid)
|
||||
|
||||
@callback
|
||||
def _async_mqtt_on_subscribe_unsubscribe(
|
||||
self,
|
||||
_mqttc: mqtt.Client,
|
||||
_userdata: None,
|
||||
mid: int,
|
||||
_reason_code: list[mqtt.ReasonCode],
|
||||
_properties: mqtt.Properties | None,
|
||||
) -> None:
|
||||
"""Subscribe / Unsubscribe callback."""
|
||||
self._async_mqtt_on_callback(mid)
|
||||
|
||||
@callback
|
||||
def _async_mqtt_on_callback(self, mid: int) -> None:
|
||||
"""Publish / Subscribe / Unsubscribe callback."""
|
||||
# The callback signature for on_unsubscribe is different from on_subscribe
|
||||
# see https://github.com/eclipse/paho.mqtt.python/issues/687
|
||||
# properties and reason codes are not used in Home Assistant
|
||||
future = self._async_get_mid_future(mid)
|
||||
if future.done() and (future.cancelled() or future.exception()):
|
||||
# Timed out or cancelled
|
||||
@ -1180,19 +1195,28 @@ class MQTT:
|
||||
self._pending_operations[mid] = future
|
||||
return future
|
||||
|
||||
@callback
|
||||
def _async_handle_callback_exception(self, status: mqtt.MQTTErrorCode) -> None:
|
||||
"""Handle a callback exception."""
|
||||
# We don't import on the top because some integrations
|
||||
# should be able to optionally rely on MQTT.
|
||||
import paho.mqtt.client as mqtt # pylint: disable=import-outside-toplevel
|
||||
|
||||
_LOGGER.warning(
|
||||
"Error returned from MQTT server: %s",
|
||||
mqtt.error_string(status),
|
||||
)
|
||||
|
||||
@callback
|
||||
def _async_mqtt_on_disconnect(
|
||||
self,
|
||||
_mqttc: mqtt.Client,
|
||||
_userdata: None,
|
||||
result_code: int,
|
||||
_disconnect_flags: mqtt.DisconnectFlags,
|
||||
reason_code: mqtt.ReasonCode,
|
||||
properties: mqtt.Properties | None = None,
|
||||
) -> None:
|
||||
"""Disconnected callback."""
|
||||
self._async_on_disconnect(result_code)
|
||||
|
||||
@callback
|
||||
def _async_on_disconnect(self, result_code: int) -> None:
|
||||
if not self.connected:
|
||||
# This function is re-entrant and may be called multiple times
|
||||
# when there is a broken pipe error.
|
||||
@ -1203,11 +1227,11 @@ class MQTT:
|
||||
self.connected = False
|
||||
async_dispatcher_send(self.hass, MQTT_CONNECTION_STATE, False)
|
||||
_LOGGER.log(
|
||||
logging.INFO if result_code == 0 else logging.DEBUG,
|
||||
logging.INFO if reason_code == 0 else logging.DEBUG,
|
||||
"Disconnected from MQTT server %s:%s (%s)",
|
||||
self.conf[CONF_BROKER],
|
||||
self.conf.get(CONF_PORT, DEFAULT_PORT),
|
||||
result_code,
|
||||
reason_code,
|
||||
)
|
||||
|
||||
@callback
|
||||
|
@ -1023,14 +1023,14 @@ def try_connection(
|
||||
result: queue.Queue[bool] = queue.Queue(maxsize=1)
|
||||
|
||||
def on_connect(
|
||||
client_: mqtt.Client,
|
||||
userdata: None,
|
||||
flags: dict[str, Any],
|
||||
result_code: int,
|
||||
properties: mqtt.Properties | None = None,
|
||||
_mqttc: mqtt.Client,
|
||||
_userdata: None,
|
||||
_connect_flags: mqtt.ConnectFlags,
|
||||
reason_code: mqtt.ReasonCode,
|
||||
_properties: mqtt.Properties | None = None,
|
||||
) -> None:
|
||||
"""Handle connection result."""
|
||||
result.put(result_code == mqtt.CONNACK_ACCEPTED)
|
||||
result.put(not reason_code.is_failure)
|
||||
|
||||
client.on_connect = on_connect
|
||||
|
||||
|
@ -20,7 +20,7 @@ from .const import (
|
||||
PUBLIC_TARGET_IP,
|
||||
)
|
||||
from .models import Adapter
|
||||
from .network import Network, async_get_network
|
||||
from .network import Network, async_get_loaded_network, async_get_network
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -34,6 +34,12 @@ async def async_get_adapters(hass: HomeAssistant) -> list[Adapter]:
|
||||
return network.adapters
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_loaded_adapters(hass: HomeAssistant) -> list[Adapter]:
|
||||
"""Get the network adapter configuration."""
|
||||
return async_get_loaded_network(hass).adapters
|
||||
|
||||
|
||||
@bind_hass
|
||||
async def async_get_source_ip(
|
||||
hass: HomeAssistant, target_ip: str | UndefinedType = UNDEFINED
|
||||
@ -74,7 +80,14 @@ async def async_get_enabled_source_ips(
|
||||
hass: HomeAssistant,
|
||||
) -> list[IPv4Address | IPv6Address]:
|
||||
"""Build the list of enabled source ips."""
|
||||
adapters = await async_get_adapters(hass)
|
||||
return async_get_enabled_source_ips_from_adapters(await async_get_adapters(hass))
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_enabled_source_ips_from_adapters(
|
||||
adapters: list[Adapter],
|
||||
) -> list[IPv4Address | IPv6Address]:
|
||||
"""Build the list of enabled source ips."""
|
||||
sources: list[IPv4Address | IPv6Address] = []
|
||||
for adapter in adapters:
|
||||
if not adapter["enabled"]:
|
||||
@ -151,5 +164,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async_register_websocket_commands,
|
||||
)
|
||||
|
||||
await async_get_network(hass)
|
||||
|
||||
async_register_websocket_commands(hass)
|
||||
return True
|
||||
|
@ -12,8 +12,6 @@ DOMAIN: Final = "network"
|
||||
STORAGE_KEY: Final = "core.network"
|
||||
STORAGE_VERSION: Final = 1
|
||||
|
||||
DATA_NETWORK: Final = "network"
|
||||
|
||||
ATTR_ADAPTERS: Final = "adapters"
|
||||
ATTR_CONFIGURED_ADAPTERS: Final = "configured_adapters"
|
||||
DEFAULT_CONFIGURED_ADAPTERS: list[str] = []
|
||||
|
@ -9,11 +9,12 @@ from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
from .const import (
|
||||
ATTR_CONFIGURED_ADAPTERS,
|
||||
DATA_NETWORK,
|
||||
DEFAULT_CONFIGURED_ADAPTERS,
|
||||
DOMAIN,
|
||||
STORAGE_KEY,
|
||||
STORAGE_VERSION,
|
||||
)
|
||||
@ -22,8 +23,16 @@ from .util import async_load_adapters, enable_adapters, enable_auto_detected_ada
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
DATA_NETWORK: HassKey[Network] = HassKey(DOMAIN)
|
||||
|
||||
@singleton(DATA_NETWORK)
|
||||
|
||||
@callback
|
||||
def async_get_loaded_network(hass: HomeAssistant) -> Network:
|
||||
"""Get network singleton."""
|
||||
return hass.data[DATA_NETWORK]
|
||||
|
||||
|
||||
@singleton(DOMAIN)
|
||||
async def async_get_network(hass: HomeAssistant) -> Network:
|
||||
"""Get network singleton."""
|
||||
network = Network(hass)
|
||||
|
@ -153,7 +153,7 @@ class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
|
||||
)
|
||||
try:
|
||||
if datetime.now().timestamp() >= expiry_time:
|
||||
await self._update_refresh_token()
|
||||
await self.update_refresh_token()
|
||||
else:
|
||||
await self.api.authenticate_refresh(
|
||||
self.refresh_token, async_get_clientsession(self.hass)
|
||||
@ -178,7 +178,7 @@ class NiceGOUpdateCoordinator(DataUpdateCoordinator[dict[str, NiceGODevice]]):
|
||||
else:
|
||||
self.async_set_updated_data(devices)
|
||||
|
||||
async def _update_refresh_token(self) -> None:
|
||||
async def update_refresh_token(self) -> None:
|
||||
"""Update the refresh token with Nice G.O. API."""
|
||||
_LOGGER.debug("Updating the refresh token with Nice G.O. API")
|
||||
try:
|
||||
|
@ -2,21 +2,17 @@
|
||||
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import ClientError
|
||||
from nice_go import ApiError
|
||||
|
||||
from homeassistant.components.cover import (
|
||||
CoverDeviceClass,
|
||||
CoverEntity,
|
||||
CoverEntityFeature,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import NiceGOConfigEntry
|
||||
from .entity import NiceGOEntity
|
||||
from .util import retry
|
||||
|
||||
DEVICE_CLASSES = {
|
||||
"WallStation": CoverDeviceClass.GARAGE,
|
||||
@ -71,30 +67,18 @@ class NiceGOCoverEntity(NiceGOEntity, CoverEntity):
|
||||
"""Return if cover is closing."""
|
||||
return self.data.barrier_status == "closing"
|
||||
|
||||
@retry("close_cover_error")
|
||||
async def async_close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the garage door."""
|
||||
if self.is_closed:
|
||||
return
|
||||
|
||||
try:
|
||||
await self.coordinator.api.close_barrier(self._device_id)
|
||||
except (ApiError, ClientError) as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="close_cover_error",
|
||||
translation_placeholders={"exception": str(err)},
|
||||
) from err
|
||||
|
||||
@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
|
||||
|
@ -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
|
||||
|
||||
@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
|
||||
|
@ -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
|
||||
|
||||
@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
|
||||
|
66
homeassistant/components/nice_go/util.py
Normal file
66
homeassistant/components/nice_go/util.py
Normal 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
|
@ -24,7 +24,7 @@
|
||||
},
|
||||
"data": {
|
||||
"name": "Data",
|
||||
"description": "Some integrations provide extended functionality. For information on how to use _data_, refer to the integration documentation."
|
||||
"description": "Some integrations provide extended functionality via this field. For more information, refer to the integration documentation."
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -56,7 +56,7 @@
|
||||
},
|
||||
"data": {
|
||||
"name": "Data",
|
||||
"description": "Some integrations provide extended functionality. For information on how to use _data_, refer to the integration documentation.."
|
||||
"description": "Some integrations provide extended functionality via this field. For more information, refer to the integration documentation."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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} GB of {total} GB."
|
||||
},
|
||||
"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} GB of {total} GB."
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"authentication_failed": {
|
||||
"message": "Authentication failed"
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opower",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["opower"],
|
||||
"requirements": ["opower==0.8.9"]
|
||||
"requirements": ["opower==0.9.0"]
|
||||
}
|
||||
|
@ -8,6 +8,6 @@
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["plugwise"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["plugwise==1.7.1"],
|
||||
"requirements": ["plugwise==1.7.2"],
|
||||
"zeroconf": ["_plugwise._tcp.local."]
|
||||
}
|
||||
|
@ -14,7 +14,7 @@ from homeassistant.components.sensor import (
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import STATE_IDLE, UnitOfDataRate
|
||||
from homeassistant.const import STATE_IDLE, UnitOfDataRate, UnitOfInformation
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
@ -27,8 +27,14 @@ from .coordinator import QBittorrentDataCoordinator
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
SENSOR_TYPE_CURRENT_STATUS = "current_status"
|
||||
SENSOR_TYPE_CONNECTION_STATUS = "connection_status"
|
||||
SENSOR_TYPE_DOWNLOAD_SPEED = "download_speed"
|
||||
SENSOR_TYPE_UPLOAD_SPEED = "upload_speed"
|
||||
SENSOR_TYPE_DOWNLOAD_SPEED_LIMIT = "download_speed_limit"
|
||||
SENSOR_TYPE_UPLOAD_SPEED_LIMIT = "upload_speed_limit"
|
||||
SENSOR_TYPE_ALLTIME_DOWNLOAD = "alltime_download"
|
||||
SENSOR_TYPE_ALLTIME_UPLOAD = "alltime_upload"
|
||||
SENSOR_TYPE_GLOBAL_RATIO = "global_ratio"
|
||||
SENSOR_TYPE_ALL_TORRENTS = "all_torrents"
|
||||
SENSOR_TYPE_PAUSED_TORRENTS = "paused_torrents"
|
||||
SENSOR_TYPE_ACTIVE_TORRENTS = "active_torrents"
|
||||
@ -50,18 +56,54 @@ def get_state(coordinator: QBittorrentDataCoordinator) -> str:
|
||||
return STATE_IDLE
|
||||
|
||||
|
||||
def get_dl(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
def get_connection_status(coordinator: QBittorrentDataCoordinator) -> str:
|
||||
"""Get current download/upload state."""
|
||||
server_state = cast(Mapping, coordinator.data.get("server_state"))
|
||||
return cast(str, server_state.get("connection_status"))
|
||||
|
||||
|
||||
def get_download_speed(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
"""Get current download speed."""
|
||||
server_state = cast(Mapping, coordinator.data.get("server_state"))
|
||||
return cast(int, server_state.get("dl_info_speed"))
|
||||
|
||||
|
||||
def get_up(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
def get_upload_speed(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
"""Get current upload speed."""
|
||||
server_state = cast(Mapping[str, Any], coordinator.data.get("server_state"))
|
||||
return cast(int, server_state.get("up_info_speed"))
|
||||
|
||||
|
||||
def get_download_speed_limit(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
"""Get current download speed."""
|
||||
server_state = cast(Mapping, coordinator.data.get("server_state"))
|
||||
return cast(int, server_state.get("dl_rate_limit"))
|
||||
|
||||
|
||||
def get_upload_speed_limit(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
"""Get current upload speed."""
|
||||
server_state = cast(Mapping[str, Any], coordinator.data.get("server_state"))
|
||||
return cast(int, server_state.get("up_rate_limit"))
|
||||
|
||||
|
||||
def get_alltime_download(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
"""Get current download speed."""
|
||||
server_state = cast(Mapping, coordinator.data.get("server_state"))
|
||||
return cast(int, server_state.get("alltime_dl"))
|
||||
|
||||
|
||||
def get_alltime_upload(coordinator: QBittorrentDataCoordinator) -> int:
|
||||
"""Get current download speed."""
|
||||
server_state = cast(Mapping, coordinator.data.get("server_state"))
|
||||
return cast(int, server_state.get("alltime_ul"))
|
||||
|
||||
|
||||
def get_global_ratio(coordinator: QBittorrentDataCoordinator) -> float:
|
||||
"""Get current download speed."""
|
||||
server_state = cast(Mapping, coordinator.data.get("server_state"))
|
||||
return cast(float, server_state.get("global_ratio"))
|
||||
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class QBittorrentSensorEntityDescription(SensorEntityDescription):
|
||||
"""Entity description class for qBittorent sensors."""
|
||||
@ -77,6 +119,13 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
|
||||
options=[STATE_IDLE, STATE_UP_DOWN, STATE_SEEDING, STATE_DOWNLOADING],
|
||||
value_fn=get_state,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_CONNECTION_STATUS,
|
||||
translation_key="connection_status",
|
||||
device_class=SensorDeviceClass.ENUM,
|
||||
options=["connected", "firewalled", "disconnected"],
|
||||
value_fn=get_connection_status,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_DOWNLOAD_SPEED,
|
||||
translation_key="download_speed",
|
||||
@ -85,7 +134,7 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
|
||||
suggested_display_precision=2,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
|
||||
value_fn=get_dl,
|
||||
value_fn=get_download_speed,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_UPLOAD_SPEED,
|
||||
@ -95,7 +144,56 @@ SENSOR_TYPES: tuple[QBittorrentSensorEntityDescription, ...] = (
|
||||
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
|
||||
suggested_display_precision=2,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
|
||||
value_fn=get_up,
|
||||
value_fn=get_upload_speed,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_DOWNLOAD_SPEED_LIMIT,
|
||||
translation_key="download_speed_limit",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
|
||||
suggested_display_precision=2,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
|
||||
value_fn=get_download_speed_limit,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_UPLOAD_SPEED_LIMIT,
|
||||
translation_key="upload_speed_limit",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
device_class=SensorDeviceClass.DATA_RATE,
|
||||
native_unit_of_measurement=UnitOfDataRate.BYTES_PER_SECOND,
|
||||
suggested_display_precision=2,
|
||||
suggested_unit_of_measurement=UnitOfDataRate.MEGABYTES_PER_SECOND,
|
||||
value_fn=get_upload_speed_limit,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_ALLTIME_DOWNLOAD,
|
||||
translation_key="alltime_download",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
native_unit_of_measurement=UnitOfInformation.BYTES,
|
||||
suggested_display_precision=2,
|
||||
suggested_unit_of_measurement=UnitOfInformation.TEBIBYTES,
|
||||
value_fn=get_alltime_download,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_ALLTIME_UPLOAD,
|
||||
translation_key="alltime_upload",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
device_class=SensorDeviceClass.DATA_SIZE,
|
||||
native_unit_of_measurement="B",
|
||||
suggested_display_precision=2,
|
||||
suggested_unit_of_measurement="TiB",
|
||||
value_fn=get_alltime_upload,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_GLOBAL_RATIO,
|
||||
translation_key="global_ratio",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=get_global_ratio,
|
||||
entity_registry_enabled_default=False,
|
||||
),
|
||||
QBittorrentSensorEntityDescription(
|
||||
key=SENSOR_TYPE_ALL_TORRENTS,
|
||||
|
@ -26,6 +26,21 @@
|
||||
"upload_speed": {
|
||||
"name": "Upload speed"
|
||||
},
|
||||
"download_speed_limit": {
|
||||
"name": "Download speed limit"
|
||||
},
|
||||
"upload_speed_limit": {
|
||||
"name": "Upload speed limit"
|
||||
},
|
||||
"alltime_download": {
|
||||
"name": "Alltime download"
|
||||
},
|
||||
"alltime_upload": {
|
||||
"name": "Alltime upload"
|
||||
},
|
||||
"global_ratio": {
|
||||
"name": "Global ratio"
|
||||
},
|
||||
"current_status": {
|
||||
"name": "Status",
|
||||
"state": {
|
||||
@ -35,6 +50,14 @@
|
||||
"downloading": "Downloading"
|
||||
}
|
||||
},
|
||||
"connection_status": {
|
||||
"name": "Connection status",
|
||||
"state": {
|
||||
"connected": "Conencted",
|
||||
"firewalled": "Firewalled",
|
||||
"disconnected": "Disconnected"
|
||||
}
|
||||
},
|
||||
"active_torrents": {
|
||||
"name": "Active torrents",
|
||||
"unit_of_measurement": "torrents"
|
||||
|
@ -7,5 +7,5 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyseventeentrack"],
|
||||
"requirements": ["pyseventeentrack==1.0.1"]
|
||||
"requirements": ["pyseventeentrack==1.0.2"]
|
||||
}
|
||||
|
@ -27,25 +27,25 @@
|
||||
"services": {
|
||||
"transition_on": {
|
||||
"name": "Transition on",
|
||||
"description": "Transitions to a target volume level over time.",
|
||||
"description": "Transitions the volume level over a specified duration. If the device is powered off, the transition will start at the lowest volume level.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"name": "Transition duration",
|
||||
"description": "Time it takes to reach the target volume level."
|
||||
"description": "Time to transition to the target volume."
|
||||
},
|
||||
"volume": {
|
||||
"name": "Target volume",
|
||||
"description": "If not specified, the volume level is read from the device."
|
||||
"description": "Relative volume level. If not specified, the setting on the device is used."
|
||||
}
|
||||
}
|
||||
},
|
||||
"transition_off": {
|
||||
"name": "Transition off",
|
||||
"description": "Transitions volume off over time.",
|
||||
"description": "Transitions the volume level to the lowest setting over a specified duration, then powers off the device.",
|
||||
"fields": {
|
||||
"duration": {
|
||||
"name": "[%key:component::snooz::services::transition_on::fields::duration::name%]",
|
||||
"description": "Time it takes to turn off."
|
||||
"description": "Time to complete the transition."
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
"dependencies": ["ssdp"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/sonos",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["soco"],
|
||||
"loggers": ["soco", "sonos_websocket"],
|
||||
"requirements": ["soco==0.30.8", "sonos-websocket==0.1.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
|
@ -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"]
|
||||
}
|
||||
|
@ -2,12 +2,10 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StarlinkUpdateCoordinator
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkUpdateCoordinator
|
||||
|
||||
PLATFORMS = [
|
||||
Platform.BINARY_SENSOR,
|
||||
@ -19,21 +17,19 @@ PLATFORMS = [
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant, config_entry: StarlinkConfigEntry
|
||||
) -> bool:
|
||||
"""Set up Starlink from a config entry."""
|
||||
coordinator = StarlinkUpdateCoordinator(hass, entry)
|
||||
config_entry.runtime_data = StarlinkUpdateCoordinator(hass, config_entry)
|
||||
await config_entry.runtime_data.async_config_entry_first_refresh()
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
async def async_unload_entry(
|
||||
hass: HomeAssistant, config_entry: StarlinkConfigEntry
|
||||
) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
||||
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
|
||||
|
@ -10,26 +10,22 @@ from homeassistant.components.binary_sensor import (
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StarlinkData
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkData
|
||||
from .entity import StarlinkEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
config_entry: StarlinkConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up all binary sensors for this entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
StarlinkBinarySensorEntity(coordinator, description)
|
||||
StarlinkBinarySensorEntity(config_entry.runtime_data, description)
|
||||
for description in BINARY_SENSORS
|
||||
)
|
||||
|
||||
|
@ -10,26 +10,23 @@ from homeassistant.components.button import (
|
||||
ButtonEntity,
|
||||
ButtonEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StarlinkUpdateCoordinator
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkUpdateCoordinator
|
||||
from .entity import StarlinkEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
config_entry: StarlinkConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up all binary sensors for this entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
StarlinkButtonEntity(coordinator, description) for description in BUTTONS
|
||||
StarlinkButtonEntity(config_entry.runtime_data, description)
|
||||
for description in BUTTONS
|
||||
)
|
||||
|
||||
|
||||
|
@ -34,6 +34,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type StarlinkConfigEntry = ConfigEntry[StarlinkUpdateCoordinator]
|
||||
|
||||
|
||||
@dataclass
|
||||
class StarlinkData:
|
||||
@ -51,9 +53,9 @@ class StarlinkData:
|
||||
class StarlinkUpdateCoordinator(DataUpdateCoordinator[StarlinkData]):
|
||||
"""Coordinates updates between all Starlink sensors defined in this file."""
|
||||
|
||||
config_entry: ConfigEntry
|
||||
config_entry: StarlinkConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
|
||||
def __init__(self, hass: HomeAssistant, config_entry: StarlinkConfigEntry) -> None:
|
||||
"""Initialize an UpdateCoordinator for a group of sensors."""
|
||||
self.channel_context = ChannelContext(target=config_entry.data[CONF_IP_ADDRESS])
|
||||
self.history_stats_start = None
|
||||
|
@ -8,25 +8,22 @@ from homeassistant.components.device_tracker import (
|
||||
TrackerEntity,
|
||||
TrackerEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import ATTR_ALTITUDE, DOMAIN
|
||||
from .coordinator import StarlinkData
|
||||
from .const import ATTR_ALTITUDE
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkData
|
||||
from .entity import StarlinkEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
config_entry: StarlinkConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up all binary sensors for this entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
StarlinkDeviceTrackerEntity(coordinator, description)
|
||||
StarlinkDeviceTrackerEntity(config_entry.runtime_data, description)
|
||||
for description in DEVICE_TRACKERS
|
||||
)
|
||||
|
||||
|
@ -4,18 +4,15 @@ from dataclasses import asdict
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StarlinkUpdateCoordinator
|
||||
from .coordinator import StarlinkConfigEntry
|
||||
|
||||
TO_REDACT = {"id", "latitude", "longitude", "altitude"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: ConfigEntry
|
||||
hass: HomeAssistant, config_entry: StarlinkConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for Starlink config entries."""
|
||||
coordinator: StarlinkUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
return async_redact_data(asdict(coordinator.data), TO_REDACT)
|
||||
return async_redact_data(asdict(config_entry.runtime_data.data), TO_REDACT)
|
||||
|
@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
DEGREE,
|
||||
PERCENTAGE,
|
||||
@ -28,21 +27,19 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.typing import StateType
|
||||
from homeassistant.util.dt import now
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StarlinkData
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkData
|
||||
from .entity import StarlinkEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
config_entry: StarlinkConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up all sensors for this entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
StarlinkSensorEntity(coordinator, description) for description in SENSORS
|
||||
StarlinkSensorEntity(config_entry.runtime_data, description)
|
||||
for description in SENSORS
|
||||
)
|
||||
|
||||
|
||||
|
@ -11,25 +11,22 @@ from homeassistant.components.switch import (
|
||||
SwitchEntity,
|
||||
SwitchEntityDescription,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StarlinkData, StarlinkUpdateCoordinator
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkData, StarlinkUpdateCoordinator
|
||||
from .entity import StarlinkEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
config_entry: StarlinkConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up all binary sensors for this entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
StarlinkSwitchEntity(coordinator, description) for description in SWITCHES
|
||||
StarlinkSwitchEntity(config_entry.runtime_data, description)
|
||||
for description in SWITCHES
|
||||
)
|
||||
|
||||
|
||||
|
@ -8,26 +8,23 @@ from datetime import UTC, datetime, time, tzinfo
|
||||
import math
|
||||
|
||||
from homeassistant.components.time import TimeEntity, TimeEntityDescription
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import StarlinkData, StarlinkUpdateCoordinator
|
||||
from .coordinator import StarlinkConfigEntry, StarlinkData, StarlinkUpdateCoordinator
|
||||
from .entity import StarlinkEntity
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
config_entry: StarlinkConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up all time entities for this entry."""
|
||||
coordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
async_add_entities(
|
||||
StarlinkTimeEntity(coordinator, description) for description in TIMES
|
||||
StarlinkTimeEntity(config_entry.runtime_data, description)
|
||||
for description in TIMES
|
||||
)
|
||||
|
||||
|
||||
|
@ -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)
|
||||
|
@ -33,14 +33,12 @@ from homeassistant.const import (
|
||||
CONF_NAME,
|
||||
CONF_PASSWORD,
|
||||
CONF_PORT,
|
||||
CONF_SCAN_INTERVAL,
|
||||
CONF_SSL,
|
||||
CONF_USERNAME,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.selector import (
|
||||
SelectOptionDict,
|
||||
@ -67,7 +65,6 @@ from .const import (
|
||||
DEFAULT_BACKUP_PATH,
|
||||
DEFAULT_PORT,
|
||||
DEFAULT_PORT_SSL,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DEFAULT_SNAPSHOT_QUALITY,
|
||||
DEFAULT_TIMEOUT,
|
||||
DEFAULT_USE_SSL,
|
||||
@ -458,12 +455,6 @@ class SynologyDSMOptionsFlowHandler(OptionsFlow):
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
CONF_SCAN_INTERVAL,
|
||||
default=self.config_entry.options.get(
|
||||
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL
|
||||
),
|
||||
): cv.positive_int,
|
||||
vol.Required(
|
||||
CONF_SNAPSHOT_QUALITY,
|
||||
default=self.config_entry.options.get(
|
||||
|
@ -48,7 +48,6 @@ DEFAULT_VERIFY_SSL = False
|
||||
DEFAULT_PORT = 5000
|
||||
DEFAULT_PORT_SSL = 5001
|
||||
# Options
|
||||
DEFAULT_SCAN_INTERVAL = 15 # min
|
||||
DEFAULT_TIMEOUT = ClientTimeout(total=60, connect=15)
|
||||
DEFAULT_SNAPSHOT_QUALITY = SNAPSHOT_PROFILE_BALANCED
|
||||
DEFAULT_BACKUP_PATH = "ha_backup"
|
||||
|
@ -14,14 +14,12 @@ from synology_dsm.exceptions import (
|
||||
)
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_SCAN_INTERVAL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .common import SynoApi, raise_config_entry_auth_error
|
||||
from .const import (
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
SIGNAL_CAMERA_SOURCE_CHANGED,
|
||||
SYNOLOGY_AUTH_FAILED_EXCEPTIONS,
|
||||
SYNOLOGY_CONNECTION_EXCEPTIONS,
|
||||
@ -122,14 +120,7 @@ class SynologyDSMCentralUpdateCoordinator(SynologyDSMUpdateCoordinator[None]):
|
||||
api: SynoApi,
|
||||
) -> None:
|
||||
"""Initialize DataUpdateCoordinator for central device."""
|
||||
super().__init__(
|
||||
hass,
|
||||
entry,
|
||||
api,
|
||||
timedelta(
|
||||
minutes=entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
),
|
||||
)
|
||||
super().__init__(hass, entry, api, timedelta(minutes=15))
|
||||
|
||||
@async_re_login_on_expired
|
||||
async def _async_update_data(self) -> None:
|
||||
|
@ -7,7 +7,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/synology_dsm",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["synology_dsm"],
|
||||
"requirements": ["py-synologydsm-api==2.6.2"],
|
||||
"requirements": ["py-synologydsm-api==2.6.3"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Synology",
|
||||
|
@ -68,8 +68,6 @@
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"scan_interval": "Minutes between scans",
|
||||
"timeout": "Timeout (seconds)",
|
||||
"snap_profile_type": "Quality level of camera snapshots (0:high 1:medium 2:low)",
|
||||
"backup_share": "[%key:component::synology_dsm::config::step::backup_share::data::backup_share%]",
|
||||
"backup_path": "[%key:component::synology_dsm::config::step::backup_share::data::backup_path%]"
|
||||
|
@ -17,7 +17,6 @@ from tesla_fleet_api.exceptions import (
|
||||
TeslaFleetError,
|
||||
VehicleOffline,
|
||||
)
|
||||
from tesla_fleet_api.ratecalculator import RateCalculator
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
@ -66,7 +65,6 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
updated_once: bool
|
||||
pre2021: bool
|
||||
last_active: datetime
|
||||
rate: RateCalculator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@ -87,44 +85,36 @@ class TeslaFleetVehicleDataCoordinator(DataUpdateCoordinator[dict[str, Any]]):
|
||||
self.data = flatten(product)
|
||||
self.updated_once = False
|
||||
self.last_active = datetime.now()
|
||||
self.rate = RateCalculator(100, 86400, VEHICLE_INTERVAL_SECONDS, 3600, 5)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, Any]:
|
||||
"""Update vehicle data using TeslaFleet API."""
|
||||
|
||||
try:
|
||||
# Check if the vehicle is awake using a non-rate limited API call
|
||||
if self.data["state"] != TeslaFleetState.ONLINE:
|
||||
# Check if the vehicle is awake using a free API call
|
||||
response = await self.api.vehicle()
|
||||
self.data["state"] = response["response"]["state"]
|
||||
|
||||
if self.data["state"] != TeslaFleetState.ONLINE:
|
||||
return self.data
|
||||
|
||||
# This is a rated limited API call
|
||||
self.rate.consume()
|
||||
response = await self.api.vehicle_data(endpoints=ENDPOINTS)
|
||||
data = response["response"]
|
||||
|
||||
except VehicleOffline:
|
||||
self.data["state"] = TeslaFleetState.ASLEEP
|
||||
return self.data
|
||||
except RateLimited as e:
|
||||
except RateLimited:
|
||||
LOGGER.warning(
|
||||
"%s rate limited, will retry in %s seconds",
|
||||
"%s rate limited, will skip refresh",
|
||||
self.name,
|
||||
e.data.get("after"),
|
||||
)
|
||||
if "after" in e.data:
|
||||
self.update_interval = timedelta(seconds=int(e.data["after"]))
|
||||
return self.data
|
||||
except (InvalidToken, OAuthExpired, LoginRequired) as e:
|
||||
raise ConfigEntryAuthFailed from e
|
||||
except TeslaFleetError as e:
|
||||
raise UpdateFailed(e.message) from e
|
||||
|
||||
# Calculate ideal refresh interval
|
||||
self.update_interval = timedelta(seconds=self.rate.calculate())
|
||||
self.update_interval = VEHICLE_INTERVAL
|
||||
|
||||
self.updated_once = True
|
||||
|
||||
|
@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/tesla_fleet",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==0.9.8"]
|
||||
"requirements": ["tesla-fleet-api==0.9.10"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/teslemetry",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tesla-fleet-api"],
|
||||
"requirements": ["tesla-fleet-api==0.9.8", "teslemetry-stream==0.6.10"]
|
||||
"requirements": ["tesla-fleet-api==0.9.10", "teslemetry-stream==0.6.10"]
|
||||
}
|
||||
|
@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/tessie",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["tessie", "tesla-fleet-api"],
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.9.8"]
|
||||
"requirements": ["tessie-api==0.1.1", "tesla-fleet-api==0.9.10"]
|
||||
}
|
||||
|
@ -20,6 +20,9 @@ CAMERAS: tuple[str, ...] = (
|
||||
# Smart Camera (including doorbells)
|
||||
# https://developer.tuya.com/en/docs/iot/categorysgbj?id=Kaiuz37tlpbnu
|
||||
"sp",
|
||||
# Smart Camera - Low power consumption camera
|
||||
# Undocumented, see https://github.com/home-assistant/core/issues/132844
|
||||
"dghsxj",
|
||||
)
|
||||
|
||||
|
||||
|
@ -261,6 +261,20 @@ LIGHTS: dict[str, tuple[TuyaLightEntityDescription, ...]] = {
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
),
|
||||
# Smart Camera - Low power consumption camera
|
||||
# Undocumented, see https://github.com/home-assistant/core/issues/132844
|
||||
"dghsxj": (
|
||||
TuyaLightEntityDescription(
|
||||
key=DPCode.FLOODLIGHT_SWITCH,
|
||||
brightness=DPCode.FLOODLIGHT_LIGHTNESS,
|
||||
name="Floodlight",
|
||||
),
|
||||
TuyaLightEntityDescription(
|
||||
key=DPCode.BASIC_INDICATOR,
|
||||
name="Indicator light",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
),
|
||||
# Smart Gardening system
|
||||
# https://developer.tuya.com/en/docs/iot/categorysz?id=Kaiuz4e6h7up0
|
||||
"sz": (
|
||||
|
@ -174,6 +174,15 @@ NUMBERS: dict[str, tuple[NumberEntityDescription, ...]] = {
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
),
|
||||
# Smart Camera - Low power consumption camera
|
||||
# Undocumented, see https://github.com/home-assistant/core/issues/132844
|
||||
"dghsxj": (
|
||||
NumberEntityDescription(
|
||||
key=DPCode.BASIC_DEVICE_VOLUME,
|
||||
translation_key="volume",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
),
|
||||
# Dimmer Switch
|
||||
# https://developer.tuya.com/en/docs/iot/categorytgkg?id=Kaiuz0ktx7m0o
|
||||
"tgkg": (
|
||||
|
@ -128,6 +128,40 @@ SELECTS: dict[str, tuple[SelectEntityDescription, ...]] = {
|
||||
translation_key="motion_sensitivity",
|
||||
),
|
||||
),
|
||||
# Smart Camera - Low power consumption camera
|
||||
# Undocumented, see https://github.com/home-assistant/core/issues/132844
|
||||
"dghsxj": (
|
||||
SelectEntityDescription(
|
||||
key=DPCode.IPC_WORK_MODE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="ipc_work_mode",
|
||||
),
|
||||
SelectEntityDescription(
|
||||
key=DPCode.DECIBEL_SENSITIVITY,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="decibel_sensitivity",
|
||||
),
|
||||
SelectEntityDescription(
|
||||
key=DPCode.RECORD_MODE,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="record_mode",
|
||||
),
|
||||
SelectEntityDescription(
|
||||
key=DPCode.BASIC_NIGHTVISION,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="basic_nightvision",
|
||||
),
|
||||
SelectEntityDescription(
|
||||
key=DPCode.BASIC_ANTI_FLICKER,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="basic_anti_flicker",
|
||||
),
|
||||
SelectEntityDescription(
|
||||
key=DPCode.MOTION_SENSITIVITY,
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
translation_key="motion_sensitivity",
|
||||
),
|
||||
),
|
||||
# IoT Switch?
|
||||
# Note: Undocumented
|
||||
"tdq": (
|
||||
|
@ -632,6 +632,29 @@ SENSORS: dict[str, tuple[TuyaSensorEntityDescription, ...]] = {
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
),
|
||||
# Smart Camera - Low power consumption camera
|
||||
# Undocumented, see https://github.com/home-assistant/core/issues/132844
|
||||
"dghsxj": (
|
||||
TuyaSensorEntityDescription(
|
||||
key=DPCode.SENSOR_TEMPERATURE,
|
||||
translation_key="temperature",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
key=DPCode.SENSOR_HUMIDITY,
|
||||
translation_key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
TuyaSensorEntityDescription(
|
||||
key=DPCode.WIRELESS_ELECTRICITY,
|
||||
translation_key="battery",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
),
|
||||
# Fingerbot
|
||||
"szjqr": BATTERY_SENSORS,
|
||||
# Solar Light
|
||||
|
@ -44,6 +44,13 @@ SIRENS: dict[str, tuple[SirenEntityDescription, ...]] = {
|
||||
key=DPCode.SIREN_SWITCH,
|
||||
),
|
||||
),
|
||||
# Smart Camera - Low power consumption camera
|
||||
# Undocumented, see https://github.com/home-assistant/core/issues/132844
|
||||
"dghsxj": (
|
||||
SirenEntityDescription(
|
||||
key=DPCode.SIREN_SWITCH,
|
||||
),
|
||||
),
|
||||
# CO2 Detector
|
||||
# https://developer.tuya.com/en/docs/iot/categoryco2bj?id=Kaiuz3wes7yuy
|
||||
"co2bj": (
|
||||
|
@ -509,6 +509,65 @@ SWITCHES: dict[str, tuple[SwitchEntityDescription, ...]] = {
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
),
|
||||
# Smart Camera - Low power consumption camera
|
||||
# Undocumented, see https://github.com/home-assistant/core/issues/132844
|
||||
"dghsxj": (
|
||||
SwitchEntityDescription(
|
||||
key=DPCode.WIRELESS_BATTERYLOCK,
|
||||
translation_key="battery_lock",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=DPCode.CRY_DETECTION_SWITCH,
|
||||
translation_key="cry_detection",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=DPCode.DECIBEL_SWITCH,
|
||||
translation_key="sound_detection",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=DPCode.RECORD_SWITCH,
|
||||
translation_key="video_recording",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=DPCode.MOTION_RECORD,
|
||||
translation_key="motion_recording",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=DPCode.BASIC_PRIVATE,
|
||||
translation_key="privacy_mode",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=DPCode.BASIC_FLIP,
|
||||
translation_key="flip",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=DPCode.BASIC_OSD,
|
||||
translation_key="time_watermark",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=DPCode.BASIC_WDR,
|
||||
translation_key="wide_dynamic_range",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=DPCode.MOTION_TRACKING,
|
||||
translation_key="motion_tracking",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
SwitchEntityDescription(
|
||||
key=DPCode.MOTION_SWITCH,
|
||||
translation_key="motion_alarm",
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
),
|
||||
# Smart Gardening system
|
||||
# https://developer.tuya.com/en/docs/iot/categorysz?id=Kaiuz4e6h7up0
|
||||
"sz": (
|
||||
|
@ -27,6 +27,7 @@ PLATFORMS = [
|
||||
Platform.HUMIDIFIER,
|
||||
Platform.LIGHT,
|
||||
Platform.NUMBER,
|
||||
Platform.SELECT,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
]
|
||||
|
@ -29,6 +29,10 @@ VS_HUMIDIFIER_MODE_HUMIDITY = "humidity"
|
||||
VS_HUMIDIFIER_MODE_MANUAL = "manual"
|
||||
VS_HUMIDIFIER_MODE_SLEEP = "sleep"
|
||||
|
||||
NIGHT_LIGHT_LEVEL_BRIGHT = "bright"
|
||||
NIGHT_LIGHT_LEVEL_DIM = "dim"
|
||||
NIGHT_LIGHT_LEVEL_OFF = "off"
|
||||
|
||||
VeSyncHumidifierDevice = VeSyncHumid200300S | VeSyncSuperior6000S
|
||||
"""Humidifier device types"""
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user