mirror of
https://github.com/home-assistant/core.git
synced 2026-03-25 00:38:17 +01:00
Compare commits
1 Commits
make-secur
...
chore/bump
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1eb9651a8d |
6
.github/copilot-instructions.md
vendored
6
.github/copilot-instructions.md
vendored
@@ -1,12 +1,6 @@
|
||||
<!-- Automatically generated by gen_copilot_instructions.py, do not edit -->
|
||||
|
||||
|
||||
|
||||
# Copilot code review instructions
|
||||
|
||||
- Start review comments with a short, one-sentence summary of the suggested fix.
|
||||
- Do not add comments about code style, formatting or linting issues.
|
||||
|
||||
# GitHub Copilot & Claude Code Instructions
|
||||
|
||||
This repository contains the core of Home Assistant, a Python 3 based home automation application.
|
||||
|
||||
4
.github/workflows/ci.yaml
vendored
4
.github/workflows/ci.yaml
vendored
@@ -120,7 +120,7 @@ jobs:
|
||||
run: |
|
||||
echo "key=$(lsb_release -rs)-apt-${CACHE_VERSION}-${HA_SHORT_VERSION}" >> $GITHUB_OUTPUT
|
||||
- name: Filter for core changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: core
|
||||
with:
|
||||
filters: .core_files.yaml
|
||||
@@ -135,7 +135,7 @@ jobs:
|
||||
echo "Result:"
|
||||
cat .integration_paths.yaml
|
||||
- name: Filter for integration changes
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
id: integrations
|
||||
with:
|
||||
filters: .integration_paths.yaml
|
||||
|
||||
2
CODEOWNERS
generated
2
CODEOWNERS
generated
@@ -1703,8 +1703,6 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/tellduslive/ @fredrike
|
||||
/homeassistant/components/teltonika/ @karlbeecken
|
||||
/tests/components/teltonika/ @karlbeecken
|
||||
/homeassistant/components/temperature/ @home-assistant/core
|
||||
/tests/components/temperature/ @home-assistant/core
|
||||
/homeassistant/components/template/ @Petro31 @home-assistant/core
|
||||
/tests/components/template/ @Petro31 @home-assistant/core
|
||||
/homeassistant/components/tesla_fleet/ @Bre77
|
||||
|
||||
@@ -247,7 +247,6 @@ DEFAULT_INTEGRATIONS = {
|
||||
"humidity",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"temperature",
|
||||
"window",
|
||||
}
|
||||
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
||||
|
||||
@@ -662,8 +662,7 @@ class PipelineRun:
|
||||
"""Emit run start event."""
|
||||
self._device_id = device_id
|
||||
self._satellite_id = satellite_id
|
||||
if self.start_stage in (PipelineStage.WAKE_WORD, PipelineStage.STT):
|
||||
self._start_debug_recording_thread()
|
||||
self._start_debug_recording_thread()
|
||||
|
||||
data: dict[str, Any] = {
|
||||
"pipeline": self.pipeline.id,
|
||||
@@ -1505,7 +1504,9 @@ class PipelineRun:
|
||||
|
||||
def _start_debug_recording_thread(self) -> None:
|
||||
"""Start thread to record wake/stt audio if debug_recording_dir is set."""
|
||||
assert self.debug_recording_thread is None
|
||||
if self.debug_recording_thread is not None:
|
||||
# Already started
|
||||
return
|
||||
|
||||
# Directory to save audio for each pipeline run.
|
||||
# Configured in YAML for assist_pipeline.
|
||||
|
||||
@@ -155,6 +155,7 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"gate",
|
||||
"humidifier",
|
||||
"humidity",
|
||||
"input_boolean",
|
||||
"lawn_mower",
|
||||
"light",
|
||||
"lock",
|
||||
@@ -168,7 +169,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"select",
|
||||
"siren",
|
||||
"switch",
|
||||
"temperature",
|
||||
"text",
|
||||
"update",
|
||||
"vacuum",
|
||||
|
||||
@@ -34,4 +34,4 @@ EXCLUDE_DATABASE_FROM_BACKUP = [
|
||||
"home-assistant_v2.db-wal",
|
||||
]
|
||||
|
||||
SECURETAR_CREATE_VERSION = 3
|
||||
SECURETAR_CREATE_VERSION = 2
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.5.3",
|
||||
"bluetooth-data-tools==1.28.4",
|
||||
"dbus-fast==3.1.2",
|
||||
"habluetooth==5.11.1"
|
||||
"habluetooth==5.10.2"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -462,10 +462,6 @@
|
||||
"below": {
|
||||
"description": "Trigger when the target temperature is below this value.",
|
||||
"name": "Below"
|
||||
},
|
||||
"unit": {
|
||||
"description": "All values will be converted to this unit when evaluating the trigger.",
|
||||
"name": "Unit of measurement"
|
||||
}
|
||||
},
|
||||
"name": "Climate-control device target temperature changed"
|
||||
@@ -485,10 +481,6 @@
|
||||
"description": "Type of threshold crossing to trigger on.",
|
||||
"name": "Threshold type"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::climate::triggers::target_temperature_changed::fields::unit::description%]",
|
||||
"name": "[%key:component::climate::triggers::target_temperature_changed::fields::unit::name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "Upper threshold limit.",
|
||||
"name": "Upper threshold"
|
||||
|
||||
@@ -2,15 +2,12 @@
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.automation import DomainSpec, NumericalDomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
ENTITY_STATE_TRIGGER_SCHEMA_FIRST_LAST,
|
||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
||||
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
|
||||
EntityNumericalStateTriggerWithUnitBase,
|
||||
EntityTargetStateTriggerBase,
|
||||
Trigger,
|
||||
TriggerConfig,
|
||||
@@ -19,7 +16,6 @@ from homeassistant.helpers.trigger import (
|
||||
make_entity_target_state_trigger,
|
||||
make_entity_transition_trigger,
|
||||
)
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
from .const import ATTR_HUMIDITY, ATTR_HVAC_ACTION, DOMAIN, HVACAction, HVACMode
|
||||
|
||||
@@ -48,33 +44,6 @@ class HVACModeChangedTrigger(EntityTargetStateTriggerBase):
|
||||
self._to_states = set(self._options[CONF_HVAC_MODE])
|
||||
|
||||
|
||||
class _ClimateTargetTemperatureTriggerMixin(EntityNumericalStateTriggerWithUnitBase):
|
||||
"""Mixin for climate target temperature triggers with unit conversion."""
|
||||
|
||||
_base_unit = UnitOfTemperature.CELSIUS
|
||||
_domain_specs = {DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
_unit_converter = TemperatureConverter
|
||||
|
||||
def _get_entity_unit(self, state: State) -> str | None:
|
||||
"""Get the temperature unit of a climate entity from its state."""
|
||||
# Climate entities convert temperatures to the system unit via show_temp
|
||||
return self._hass.config.units.temperature_unit
|
||||
|
||||
|
||||
class ClimateTargetTemperatureChangedTrigger(
|
||||
_ClimateTargetTemperatureTriggerMixin,
|
||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
||||
):
|
||||
"""Trigger for climate target temperature value changes."""
|
||||
|
||||
|
||||
class ClimateTargetTemperatureCrossedThresholdTrigger(
|
||||
_ClimateTargetTemperatureTriggerMixin,
|
||||
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
|
||||
):
|
||||
"""Trigger for climate target temperature value crossing a threshold."""
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"hvac_mode_changed": HVACModeChangedTrigger,
|
||||
"started_cooling": make_entity_target_state_trigger(
|
||||
@@ -84,15 +53,17 @@ TRIGGERS: dict[str, type[Trigger]] = {
|
||||
{DOMAIN: DomainSpec(value_source=ATTR_HVAC_ACTION)}, HVACAction.DRYING
|
||||
),
|
||||
"target_humidity_changed": make_entity_numerical_state_changed_trigger(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)}
|
||||
),
|
||||
"target_humidity_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)}
|
||||
),
|
||||
"target_temperature_changed": make_entity_numerical_state_changed_trigger(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
),
|
||||
"target_temperature_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_TEMPERATURE)}
|
||||
),
|
||||
"target_temperature_changed": ClimateTargetTemperatureChangedTrigger,
|
||||
"target_temperature_crossed_threshold": ClimateTargetTemperatureCrossedThresholdTrigger,
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, HVACMode.OFF),
|
||||
"turned_on": make_entity_transition_trigger(
|
||||
DOMAIN,
|
||||
|
||||
@@ -14,29 +14,7 @@
|
||||
- last
|
||||
- any
|
||||
|
||||
.number_or_entity_humidity: &number_or_entity_humidity
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
unit_of_measurement: "%"
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement: "%"
|
||||
- domain: sensor
|
||||
device_class: humidity
|
||||
- domain: number
|
||||
device_class: humidity
|
||||
translation_key: number_or_entity
|
||||
|
||||
.number_or_entity_temperature: &number_or_entity_temperature
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
@@ -49,24 +27,12 @@
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "°C"
|
||||
- "°F"
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: number
|
||||
device_class: temperature
|
||||
domain:
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
translation_key: number_or_entity
|
||||
|
||||
.trigger_unit_temperature: &trigger_unit_temperature
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "°C"
|
||||
- "°F"
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
@@ -103,29 +69,27 @@ hvac_mode_changed:
|
||||
target_humidity_changed:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
above: *number_or_entity_humidity
|
||||
below: *number_or_entity_humidity
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
|
||||
target_humidity_crossed_threshold:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_humidity
|
||||
upper_limit: *number_or_entity_humidity
|
||||
lower_limit: *number_or_entity
|
||||
upper_limit: *number_or_entity
|
||||
|
||||
target_temperature_changed:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
above: *number_or_entity_temperature
|
||||
below: *number_or_entity_temperature
|
||||
unit: *trigger_unit_temperature
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
|
||||
target_temperature_crossed_threshold:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity_temperature
|
||||
upper_limit: *number_or_entity_temperature
|
||||
unit: *trigger_unit_temperature
|
||||
lower_limit: *number_or_entity
|
||||
upper_limit: *number_or_entity
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["evohomeasync", "evohomeasync2"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["evohome-async==1.2.0"]
|
||||
"requirements": ["evohome-async==1.1.3"]
|
||||
}
|
||||
|
||||
@@ -124,17 +124,15 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
||||
until = dt_util.as_utc(until) if until else None
|
||||
|
||||
if operation_mode == STATE_ON:
|
||||
await self.coordinator.call_client_api(
|
||||
self._evo_device.set_on(until=until)
|
||||
)
|
||||
await self.coordinator.call_client_api(self._evo_device.on(until=until))
|
||||
else: # STATE_OFF
|
||||
await self.coordinator.call_client_api(
|
||||
self._evo_device.set_off(until=until)
|
||||
self._evo_device.off(until=until)
|
||||
)
|
||||
|
||||
async def async_turn_away_mode_on(self) -> None:
|
||||
"""Turn away mode on."""
|
||||
await self.coordinator.call_client_api(self._evo_device.set_off())
|
||||
await self.coordinator.call_client_api(self._evo_device.off())
|
||||
|
||||
async def async_turn_away_mode_off(self) -> None:
|
||||
"""Turn away mode off."""
|
||||
@@ -142,8 +140,8 @@ class EvoDHW(EvoChild, WaterHeaterEntity):
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn on."""
|
||||
await self.coordinator.call_client_api(self._evo_device.set_on())
|
||||
await self.coordinator.call_client_api(self._evo_device.on())
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off."""
|
||||
await self.coordinator.call_client_api(self._evo_device.set_off())
|
||||
await self.coordinator.call_client_api(self._evo_device.off())
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260312.1"]
|
||||
"requirements": ["home-assistant-frontend==20260312.0"]
|
||||
}
|
||||
|
||||
@@ -116,11 +116,7 @@ async def _validate_config(
|
||||
|
||||
|
||||
CONFIG_FLOW = {
|
||||
"user": SchemaFlowFormStep(
|
||||
vol.Schema(CONFIG_SCHEMA),
|
||||
validate_user_input=_validate_config,
|
||||
next_step="presets",
|
||||
),
|
||||
"user": SchemaFlowFormStep(vol.Schema(CONFIG_SCHEMA), next_step="presets"),
|
||||
"presets": SchemaFlowFormStep(vol.Schema(PRESETS_SCHEMA)),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"min_max_runtime": "Minimum run time must be less than the maximum run time."
|
||||
},
|
||||
"step": {
|
||||
"presets": {
|
||||
"data": {
|
||||
@@ -48,7 +45,7 @@
|
||||
},
|
||||
"options": {
|
||||
"error": {
|
||||
"min_max_runtime": "[%key:component::generic_thermostat::config::error::min_max_runtime%]"
|
||||
"min_max_runtime": "Minimum run time must be less than the maximum run time."
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["greenplanet-energy-api==0.1.10"],
|
||||
"requirements": ["greenplanet-energy-api==0.1.4"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
from dataclasses import replace
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import os
|
||||
@@ -16,7 +15,6 @@ from aiohasupervisor import SupervisorError
|
||||
from aiohasupervisor.models import (
|
||||
GreenOptions,
|
||||
HomeAssistantInfo,
|
||||
HomeAssistantOptions,
|
||||
HostInfo,
|
||||
InstalledAddon,
|
||||
NetworkInfo,
|
||||
@@ -24,28 +22,20 @@ from aiohasupervisor.models import (
|
||||
RootInfo,
|
||||
StoreInfo,
|
||||
SupervisorInfo,
|
||||
SupervisorOptions,
|
||||
YellowOptions,
|
||||
)
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.auth.const import GROUP_ID_ADMIN
|
||||
from homeassistant.auth.models import RefreshToken
|
||||
from homeassistant.components import frontend, panel_custom
|
||||
from homeassistant.components.homeassistant import async_set_stop_handler
|
||||
from homeassistant.components.http import (
|
||||
CONF_SERVER_HOST,
|
||||
CONF_SERVER_PORT,
|
||||
CONF_SSL_CERTIFICATE,
|
||||
StaticPathConfig,
|
||||
)
|
||||
from homeassistant.components.http import StaticPathConfig
|
||||
from homeassistant.config_entries import SOURCE_SYSTEM, ConfigEntry
|
||||
from homeassistant.const import (
|
||||
ATTR_DEVICE_ID,
|
||||
ATTR_NAME,
|
||||
EVENT_CORE_CONFIG_UPDATE,
|
||||
HASSIO_USER_NAME,
|
||||
SERVER_PORT,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import (
|
||||
@@ -455,30 +445,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
require_admin=True,
|
||||
)
|
||||
|
||||
async def update_hass_api(http_config: dict[str, Any], refresh_token: RefreshToken):
|
||||
"""Update Home Assistant API data on Hass.io."""
|
||||
options = HomeAssistantOptions(
|
||||
ssl=CONF_SSL_CERTIFICATE in http_config,
|
||||
port=http_config.get(CONF_SERVER_PORT) or SERVER_PORT,
|
||||
refresh_token=refresh_token.token,
|
||||
)
|
||||
|
||||
if http_config.get(CONF_SERVER_HOST) is not None:
|
||||
options = replace(options, watchdog=False)
|
||||
_LOGGER.warning(
|
||||
"Found incompatible HTTP option 'server_host'. Watchdog feature"
|
||||
" disabled"
|
||||
)
|
||||
|
||||
try:
|
||||
await supervisor_client.homeassistant.set_options(options)
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning(
|
||||
"Failed to update Home Assistant options in Supervisor: %s", err
|
||||
)
|
||||
|
||||
update_hass_api_task = hass.async_create_task(
|
||||
update_hass_api(config.get("http", {}), refresh_token), eager_start=True
|
||||
hassio.update_hass_api(config.get("http", {}), refresh_token), eager_start=True
|
||||
)
|
||||
|
||||
last_timezone = None
|
||||
@@ -489,25 +457,19 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
nonlocal last_timezone
|
||||
nonlocal last_country
|
||||
|
||||
new_timezone = hass.config.time_zone
|
||||
new_country = hass.config.country
|
||||
new_timezone = str(hass.config.time_zone)
|
||||
new_country = str(hass.config.country)
|
||||
|
||||
if new_timezone != last_timezone or new_country != last_country:
|
||||
last_timezone = new_timezone
|
||||
last_country = new_country
|
||||
|
||||
try:
|
||||
await supervisor_client.supervisor.set_options(
|
||||
SupervisorOptions(timezone=new_timezone, country=new_country)
|
||||
)
|
||||
except SupervisorError as err:
|
||||
_LOGGER.warning("Failed to update Supervisor options: %s", err)
|
||||
await hassio.update_hass_config(new_timezone, new_country)
|
||||
|
||||
hass.bus.async_listen(EVENT_CORE_CONFIG_UPDATE, push_config)
|
||||
|
||||
push_config_task = hass.async_create_task(push_config(None), eager_start=True)
|
||||
# Start listening for problems with supervisor and making issues
|
||||
hass.data[DATA_KEY_SUPERVISOR_ISSUES] = issues = SupervisorIssues(hass)
|
||||
hass.data[DATA_KEY_SUPERVISOR_ISSUES] = issues = SupervisorIssues(hass, hassio)
|
||||
issues_task = hass.async_create_task(issues.setup(), eager_start=True)
|
||||
|
||||
async def async_service_handler(service: ServiceCall) -> None:
|
||||
@@ -655,7 +617,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
async_set_stop_handler(hass, _async_stop)
|
||||
|
||||
# Init discovery Hass.io feature
|
||||
async_setup_discovery_view(hass)
|
||||
async_setup_discovery_view(hass, hassio)
|
||||
|
||||
# Init auth Hass.io feature
|
||||
assert user is not None
|
||||
|
||||
@@ -21,15 +21,15 @@ from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
|
||||
from .const import ATTR_ADDON, ATTR_UUID, DOMAIN
|
||||
from .handler import get_supervisor_client
|
||||
from .handler import HassIO, get_supervisor_client
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_discovery_view(hass: HomeAssistant) -> None:
|
||||
def async_setup_discovery_view(hass: HomeAssistant, hassio: HassIO) -> None:
|
||||
"""Discovery setup."""
|
||||
hassio_discovery = HassIODiscovery(hass)
|
||||
hassio_discovery = HassIODiscovery(hass, hassio)
|
||||
supervisor_client = get_supervisor_client(hass)
|
||||
hass.http.register_view(hassio_discovery)
|
||||
|
||||
@@ -77,9 +77,10 @@ class HassIODiscovery(HomeAssistantView):
|
||||
name = "api:hassio_push:discovery"
|
||||
url = "/api/hassio_push/discovery/{uuid}"
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
def __init__(self, hass: HomeAssistant, hassio: HassIO) -> None:
|
||||
"""Initialize WebView."""
|
||||
self.hass = hass
|
||||
self.hassio = hassio
|
||||
self._supervisor_client = get_supervisor_client(hass)
|
||||
|
||||
async def post(self, request: web.Request, uuid: str) -> web.Response:
|
||||
|
||||
@@ -14,6 +14,13 @@ from aiohasupervisor.models import SupervisorOptions
|
||||
import aiohttp
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.auth.models import RefreshToken
|
||||
from homeassistant.components.http import (
|
||||
CONF_SERVER_HOST,
|
||||
CONF_SERVER_PORT,
|
||||
CONF_SSL_CERTIFICATE,
|
||||
)
|
||||
from homeassistant.const import SERVER_PORT
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
|
||||
@@ -28,6 +35,22 @@ class HassioAPIError(RuntimeError):
|
||||
"""Return if a API trow a error."""
|
||||
|
||||
|
||||
def _api_bool[**_P](
|
||||
funct: Callable[_P, Coroutine[Any, Any, dict[str, Any]]],
|
||||
) -> Callable[_P, Coroutine[Any, Any, bool]]:
|
||||
"""Return a boolean."""
|
||||
|
||||
async def _wrapper(*argv: _P.args, **kwargs: _P.kwargs) -> bool:
|
||||
"""Wrap function."""
|
||||
try:
|
||||
data = await funct(*argv, **kwargs)
|
||||
return data["result"] == "ok"
|
||||
except HassioAPIError:
|
||||
return False
|
||||
|
||||
return _wrapper
|
||||
|
||||
|
||||
def api_data[**_P](
|
||||
funct: Callable[_P, Coroutine[Any, Any, dict[str, Any]]],
|
||||
) -> Callable[_P, Coroutine[Any, Any, Any]]:
|
||||
@@ -72,6 +95,37 @@ class HassIO:
|
||||
"""
|
||||
return self.send_command("/ingress/panels", method="get")
|
||||
|
||||
@_api_bool
|
||||
async def update_hass_api(
|
||||
self, http_config: dict[str, Any], refresh_token: RefreshToken
|
||||
):
|
||||
"""Update Home Assistant API data on Hass.io."""
|
||||
port = http_config.get(CONF_SERVER_PORT) or SERVER_PORT
|
||||
options = {
|
||||
"ssl": CONF_SSL_CERTIFICATE in http_config,
|
||||
"port": port,
|
||||
"refresh_token": refresh_token.token,
|
||||
}
|
||||
|
||||
if http_config.get(CONF_SERVER_HOST) is not None:
|
||||
options["watchdog"] = False
|
||||
_LOGGER.warning(
|
||||
"Found incompatible HTTP option 'server_host'. Watchdog feature"
|
||||
" disabled"
|
||||
)
|
||||
|
||||
return await self.send_command("/homeassistant/options", payload=options)
|
||||
|
||||
@_api_bool
|
||||
def update_hass_config(self, timezone: str, country: str | None) -> Coroutine:
|
||||
"""Update Home-Assistant timezone data on Hass.io.
|
||||
|
||||
This method returns a coroutine.
|
||||
"""
|
||||
return self.send_command(
|
||||
"/supervisor/options", payload={"timezone": timezone, "country": country}
|
||||
)
|
||||
|
||||
async def send_command(
|
||||
self,
|
||||
command: str,
|
||||
|
||||
@@ -63,7 +63,7 @@ from .const import (
|
||||
UPDATE_KEY_SUPERVISOR,
|
||||
)
|
||||
from .coordinator import HassioDataUpdateCoordinator, get_addons_list, get_host_info
|
||||
from .handler import get_supervisor_client
|
||||
from .handler import HassIO, get_supervisor_client
|
||||
|
||||
ISSUE_KEY_UNHEALTHY = "unhealthy"
|
||||
ISSUE_KEY_UNSUPPORTED = "unsupported"
|
||||
@@ -175,9 +175,10 @@ class Issue:
|
||||
class SupervisorIssues:
|
||||
"""Create issues from supervisor events."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant) -> None:
|
||||
def __init__(self, hass: HomeAssistant, client: HassIO) -> None:
|
||||
"""Initialize supervisor issues."""
|
||||
self._hass = hass
|
||||
self._client = client
|
||||
self._unsupported_reasons: set[str] = set()
|
||||
self._unhealthy_reasons: set[str] = set()
|
||||
self._issues: dict[UUID, Issue] = {}
|
||||
|
||||
@@ -5,8 +5,6 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Protocol
|
||||
|
||||
from universal_silabs_flasher.flasher import Zbt2Flasher
|
||||
|
||||
from homeassistant.components import usb
|
||||
from homeassistant.components.homeassistant_hardware import firmware_config_flow
|
||||
from homeassistant.components.homeassistant_hardware.helpers import (
|
||||
@@ -15,6 +13,7 @@ from homeassistant.components.homeassistant_hardware.helpers import (
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
ResetTarget,
|
||||
)
|
||||
from homeassistant.components.usb import (
|
||||
usb_service_info_from_device,
|
||||
@@ -77,7 +76,15 @@ class ZBT2FirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
||||
context: ConfigFlowContext
|
||||
|
||||
ZIGBEE_BAUDRATE = 460800
|
||||
_flasher_cls = Zbt2Flasher
|
||||
|
||||
# Early ZBT-2 samples used RTS/DTR to trigger the bootloader, later ones use the
|
||||
# baudrate method. Since the two are mutually exclusive we just use both.
|
||||
BOOTLOADER_RESET_METHODS = [ResetTarget.RTS_DTR, ResetTarget.BAUDRATE]
|
||||
APPLICATION_PROBE_METHODS = [
|
||||
(ApplicationType.GECKO_BOOTLOADER, 115200),
|
||||
(ApplicationType.EZSP, ZIGBEE_BAUDRATE),
|
||||
(ApplicationType.SPINEL, 460800),
|
||||
]
|
||||
|
||||
async def async_step_install_zigbee_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
||||
@@ -4,8 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from universal_silabs_flasher.flasher import Zbt2Flasher
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
@@ -25,6 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeAssistantConnectZBT2ConfigEntry
|
||||
from .config_flow import ZBT2FirmwareMixin
|
||||
from .const import DOMAIN, FIRMWARE, FIRMWARE_VERSION, HARDWARE_NAME, SERIAL_NUMBER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -135,7 +134,8 @@ async def async_setup_entry(
|
||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""Connect ZBT-2 firmware update entity."""
|
||||
|
||||
_flasher_cls = Zbt2Flasher
|
||||
BOOTLOADER_RESET_METHODS = ZBT2FirmwareMixin.BOOTLOADER_RESET_METHODS
|
||||
APPLICATION_PROBE_METHODS = ZBT2FirmwareMixin.APPLICATION_PROBE_METHODS
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -12,7 +12,6 @@ from aiohttp import ClientError
|
||||
from ha_silabs_firmware_client import FirmwareUpdateClient, ManifestMissing
|
||||
from universal_silabs_flasher.common import Version
|
||||
from universal_silabs_flasher.firmware import NabuCasaMetadata
|
||||
from universal_silabs_flasher.flasher import DeviceSpecificFlasher
|
||||
|
||||
from homeassistant.components.hassio import (
|
||||
AddonError,
|
||||
@@ -40,6 +39,7 @@ from .util import (
|
||||
FirmwareInfo,
|
||||
OwningAddon,
|
||||
OwningIntegration,
|
||||
ResetTarget,
|
||||
async_firmware_flashing_context,
|
||||
async_flash_silabs_firmware,
|
||||
get_otbr_addon_manager,
|
||||
@@ -81,7 +81,8 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Base flow to install firmware."""
|
||||
|
||||
ZIGBEE_BAUDRATE = 115200 # Default, subclasses may override
|
||||
_flasher_cls: type[DeviceSpecificFlasher]
|
||||
BOOTLOADER_RESET_METHODS: list[ResetTarget] = [] # Default, subclasses may override
|
||||
APPLICATION_PROBE_METHODS: list[tuple[ApplicationType, int]] = []
|
||||
|
||||
_picked_firmware_type: PickedFirmwareType
|
||||
_zigbee_flow_strategy: ZigbeeFlowStrategy = ZigbeeFlowStrategy.RECOMMENDED
|
||||
@@ -238,7 +239,8 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
# isn't strictly necessary for functionality.
|
||||
self._probed_firmware_info = await probe_silabs_firmware_info(
|
||||
self._device,
|
||||
flasher_cls=self._flasher_cls,
|
||||
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||
)
|
||||
|
||||
firmware_install_required = self._probed_firmware_info is None or (
|
||||
@@ -309,8 +311,9 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
hass=self.hass,
|
||||
device=self._device,
|
||||
fw_data=fw_data,
|
||||
flasher_cls=self._flasher_cls,
|
||||
expected_installed_firmware_type=expected_installed_firmware_type,
|
||||
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||
progress_callback=lambda offset, total: self.async_update_progress(
|
||||
offset / total
|
||||
),
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"integration_type": "system",
|
||||
"requirements": [
|
||||
"serialx==0.6.2",
|
||||
"universal-silabs-flasher==1.0.2",
|
||||
"universal-silabs-flasher==0.1.2",
|
||||
"ha-silabs-firmware-client==0.3.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ import logging
|
||||
from typing import Any, cast
|
||||
|
||||
from ha_silabs_firmware_client import FirmwareManifest, FirmwareMetadata
|
||||
from universal_silabs_flasher.flasher import DeviceSpecificFlasher
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.components.update import (
|
||||
@@ -26,6 +25,7 @@ from .helpers import async_register_firmware_info_callback
|
||||
from .util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
ResetTarget,
|
||||
async_firmware_flashing_context,
|
||||
async_flash_silabs_firmware,
|
||||
)
|
||||
@@ -87,11 +87,13 @@ class BaseFirmwareUpdateEntity(
|
||||
|
||||
# Subclasses provide the mapping between firmware types and entity descriptions
|
||||
entity_description: FirmwareUpdateEntityDescription
|
||||
BOOTLOADER_RESET_METHODS: list[ResetTarget]
|
||||
APPLICATION_PROBE_METHODS: list[tuple[ApplicationType, int]]
|
||||
|
||||
_attr_supported_features = (
|
||||
UpdateEntityFeature.INSTALL | UpdateEntityFeature.PROGRESS
|
||||
)
|
||||
_attr_has_entity_name = True
|
||||
_flasher_cls: type[DeviceSpecificFlasher]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -280,8 +282,9 @@ class BaseFirmwareUpdateEntity(
|
||||
hass=self.hass,
|
||||
device=self._current_device,
|
||||
fw_data=fw_data,
|
||||
flasher_cls=self._flasher_cls,
|
||||
expected_installed_firmware_type=self.entity_description.expected_firmware_type,
|
||||
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||
progress_callback=self._update_progress,
|
||||
)
|
||||
finally:
|
||||
|
||||
@@ -10,9 +10,12 @@ from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
|
||||
from universal_silabs_flasher.const import ApplicationType as FlasherApplicationType
|
||||
from universal_silabs_flasher.const import (
|
||||
ApplicationType as FlasherApplicationType,
|
||||
ResetTarget as FlasherResetTarget,
|
||||
)
|
||||
from universal_silabs_flasher.firmware import parse_firmware_image
|
||||
from universal_silabs_flasher.flasher import BaseFlasher, DeviceSpecificFlasher, Flasher
|
||||
from universal_silabs_flasher.flasher import Flasher
|
||||
|
||||
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@@ -60,6 +63,18 @@ class ApplicationType(StrEnum):
|
||||
return FlasherApplicationType(self.value)
|
||||
|
||||
|
||||
class ResetTarget(StrEnum):
|
||||
"""Methods to reset a device into bootloader mode."""
|
||||
|
||||
RTS_DTR = "rts_dtr"
|
||||
BAUDRATE = "baudrate"
|
||||
YELLOW = "yellow"
|
||||
|
||||
def as_flasher_reset_target(self) -> FlasherResetTarget:
|
||||
"""Convert the reset target enum into one compatible with USF."""
|
||||
return FlasherResetTarget(self.value)
|
||||
|
||||
|
||||
@singleton(OTBR_ADDON_MANAGER_DATA)
|
||||
@callback
|
||||
def get_otbr_addon_manager(hass: HomeAssistant) -> WaitingAddonManager:
|
||||
@@ -295,20 +310,23 @@ async def guess_firmware_info(hass: HomeAssistant, device_path: str) -> Firmware
|
||||
async def probe_silabs_firmware_info(
|
||||
device: str,
|
||||
*,
|
||||
flasher_cls: type[BaseFlasher],
|
||||
application_probe_methods: Sequence[ApplicationType] | None = None,
|
||||
bootloader_reset_methods: Sequence[ResetTarget],
|
||||
application_probe_methods: Sequence[tuple[ApplicationType, int]],
|
||||
) -> FirmwareInfo | None:
|
||||
"""Probe the running firmware on a SiLabs device."""
|
||||
flasher = flasher_cls(device=device)
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
probe_methods=tuple(
|
||||
(m.as_flasher_application_type(), baudrate)
|
||||
for m, baudrate in application_probe_methods
|
||||
),
|
||||
bootloader_reset=tuple(
|
||||
m.as_flasher_reset_target() for m in bootloader_reset_methods
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
await flasher.probe_app_type(
|
||||
only=(
|
||||
[m.as_flasher_application_type() for m in application_probe_methods]
|
||||
if application_probe_methods is not None
|
||||
else None
|
||||
)
|
||||
)
|
||||
await flasher.probe_app_type()
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.debug("Failed to probe application type", exc_info=True)
|
||||
|
||||
@@ -331,25 +349,20 @@ async def probe_silabs_firmware_info(
|
||||
async def probe_silabs_firmware_type(
|
||||
device: str,
|
||||
*,
|
||||
bootloader_reset_methods: Sequence[ResetTarget],
|
||||
application_probe_methods: Sequence[tuple[ApplicationType, int]],
|
||||
) -> ApplicationType | None:
|
||||
"""Probe the running firmware type on a SiLabs device."""
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
probe_methods=[
|
||||
(m.as_flasher_application_type(), b) for m, b in application_probe_methods
|
||||
],
|
||||
|
||||
fw_info = await probe_silabs_firmware_info(
|
||||
device,
|
||||
bootloader_reset_methods=bootloader_reset_methods,
|
||||
application_probe_methods=application_probe_methods,
|
||||
)
|
||||
|
||||
try:
|
||||
await flasher.probe_app_type()
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.debug("Failed to probe application type", exc_info=True)
|
||||
|
||||
if flasher.app_type is None:
|
||||
if fw_info is None:
|
||||
return None
|
||||
|
||||
return ApplicationType.from_flasher_application_type(flasher.app_type)
|
||||
return fw_info.firmware_type
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -372,18 +385,36 @@ async def async_flash_silabs_firmware(
|
||||
hass: HomeAssistant,
|
||||
device: str,
|
||||
fw_data: bytes,
|
||||
flasher_cls: type[DeviceSpecificFlasher],
|
||||
expected_installed_firmware_type: ApplicationType,
|
||||
bootloader_reset_methods: Sequence[ResetTarget],
|
||||
application_probe_methods: Sequence[tuple[ApplicationType, int]],
|
||||
progress_callback: Callable[[int, int], None] | None = None,
|
||||
) -> FirmwareInfo:
|
||||
"""Flash firmware to the SiLabs device.
|
||||
|
||||
This function is meant to be used within a firmware update context.
|
||||
"""
|
||||
if not any(
|
||||
method == expected_installed_firmware_type
|
||||
for method, _ in application_probe_methods
|
||||
):
|
||||
raise ValueError(
|
||||
f"Expected installed firmware type {expected_installed_firmware_type!r}"
|
||||
f" not in application probe methods {application_probe_methods!r}"
|
||||
)
|
||||
|
||||
fw_image = await hass.async_add_executor_job(parse_firmware_image, fw_data)
|
||||
|
||||
flasher = flasher_cls(device=device)
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
probe_methods=tuple(
|
||||
(m.as_flasher_application_type(), baudrate)
|
||||
for m, baudrate in application_probe_methods
|
||||
),
|
||||
bootloader_reset=tuple(
|
||||
m.as_flasher_reset_target() for m in bootloader_reset_methods
|
||||
),
|
||||
)
|
||||
|
||||
try:
|
||||
# Enter the bootloader with indeterminate progress
|
||||
@@ -400,9 +431,13 @@ async def async_flash_silabs_firmware(
|
||||
|
||||
probed_firmware_info = await probe_silabs_firmware_info(
|
||||
device,
|
||||
flasher_cls=flasher_cls,
|
||||
bootloader_reset_methods=bootloader_reset_methods,
|
||||
# Only probe for the expected installed firmware type
|
||||
application_probe_methods=[expected_installed_firmware_type],
|
||||
application_probe_methods=[
|
||||
(method, baudrate)
|
||||
for method, baudrate in application_probe_methods
|
||||
if method == expected_installed_firmware_type
|
||||
],
|
||||
)
|
||||
|
||||
if probed_firmware_info is None:
|
||||
|
||||
@@ -5,8 +5,6 @@ from __future__ import annotations
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Protocol
|
||||
|
||||
from universal_silabs_flasher.flasher import Zbt1Flasher
|
||||
|
||||
from homeassistant.components import usb
|
||||
from homeassistant.components.homeassistant_hardware import (
|
||||
firmware_config_flow,
|
||||
@@ -18,6 +16,7 @@ from homeassistant.components.homeassistant_hardware.helpers import (
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
ResetTarget,
|
||||
)
|
||||
from homeassistant.components.usb import (
|
||||
usb_service_info_from_device,
|
||||
@@ -82,7 +81,18 @@ class SkyConnectFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
||||
context: ConfigFlowContext
|
||||
|
||||
ZIGBEE_BAUDRATE = 115200
|
||||
_flasher_cls = Zbt1Flasher
|
||||
# There is no hardware bootloader trigger
|
||||
BOOTLOADER_RESET_METHODS: list[ResetTarget] = []
|
||||
APPLICATION_PROBE_METHODS = [
|
||||
(ApplicationType.GECKO_BOOTLOADER, 115200),
|
||||
(ApplicationType.EZSP, ZIGBEE_BAUDRATE),
|
||||
(ApplicationType.SPINEL, 460800),
|
||||
# CPC baudrates can be removed once multiprotocol is removed
|
||||
(ApplicationType.CPC, 115200),
|
||||
(ApplicationType.CPC, 230400),
|
||||
(ApplicationType.CPC, 460800),
|
||||
(ApplicationType.ROUTER, 115200),
|
||||
]
|
||||
|
||||
def _get_translation_placeholders(self) -> dict[str, str]:
|
||||
"""Shared translation placeholders."""
|
||||
|
||||
@@ -4,8 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from universal_silabs_flasher.flasher import Zbt1Flasher
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
@@ -25,6 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeAssistantSkyConnectConfigEntry
|
||||
from .config_flow import SkyConnectFirmwareMixin
|
||||
from .const import (
|
||||
DOMAIN,
|
||||
FIRMWARE,
|
||||
@@ -153,7 +152,8 @@ async def async_setup_entry(
|
||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""SkyConnect firmware update entity."""
|
||||
|
||||
_flasher_cls = Zbt1Flasher
|
||||
BOOTLOADER_RESET_METHODS = SkyConnectFirmwareMixin.BOOTLOADER_RESET_METHODS
|
||||
APPLICATION_PROBE_METHODS = SkyConnectFirmwareMixin.APPLICATION_PROBE_METHODS
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -7,7 +7,6 @@ import asyncio
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, Any, Protocol, final
|
||||
|
||||
from universal_silabs_flasher.flasher import YellowFlasher
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.hassio import (
|
||||
@@ -26,6 +25,7 @@ from homeassistant.components.homeassistant_hardware.silabs_multiprotocol_addon
|
||||
from homeassistant.components.homeassistant_hardware.util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
ResetTarget,
|
||||
probe_silabs_firmware_info,
|
||||
)
|
||||
from homeassistant.config_entries import (
|
||||
@@ -83,7 +83,17 @@ class YellowFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
||||
"""Mixin for Home Assistant Yellow firmware methods."""
|
||||
|
||||
ZIGBEE_BAUDRATE = 115200
|
||||
_flasher_cls = YellowFlasher
|
||||
BOOTLOADER_RESET_METHODS = [ResetTarget.YELLOW]
|
||||
APPLICATION_PROBE_METHODS = [
|
||||
(ApplicationType.GECKO_BOOTLOADER, 115200),
|
||||
(ApplicationType.EZSP, ZIGBEE_BAUDRATE),
|
||||
(ApplicationType.SPINEL, 460800),
|
||||
# CPC baudrates can be removed once multiprotocol is removed
|
||||
(ApplicationType.CPC, 115200),
|
||||
(ApplicationType.CPC, 230400),
|
||||
(ApplicationType.CPC, 460800),
|
||||
(ApplicationType.ROUTER, 115200),
|
||||
]
|
||||
|
||||
async def async_step_install_zigbee_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -149,7 +159,8 @@ class HomeAssistantYellowConfigFlow(
|
||||
# We do not actually use any portion of `BaseFirmwareConfigFlow` beyond this
|
||||
self._probed_firmware_info = await probe_silabs_firmware_info(
|
||||
self._device,
|
||||
flasher_cls=self._flasher_cls,
|
||||
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||
)
|
||||
|
||||
# Kick off ZHA hardware discovery automatically if Zigbee firmware is running
|
||||
|
||||
@@ -4,8 +4,6 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from universal_silabs_flasher.flasher import YellowFlasher
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
@@ -25,6 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import HomeAssistantYellowConfigEntry
|
||||
from .config_flow import YellowFirmwareMixin
|
||||
from .const import DOMAIN, FIRMWARE, FIRMWARE_VERSION, MANUFACTURER, MODEL, RADIO_DEVICE
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@@ -151,7 +150,8 @@ async def async_setup_entry(
|
||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""Yellow firmware update entity."""
|
||||
|
||||
_flasher_cls = YellowFlasher
|
||||
BOOTLOADER_RESET_METHODS = YellowFirmwareMixin.BOOTLOADER_RESET_METHODS
|
||||
APPLICATION_PROBE_METHODS = YellowFirmwareMixin.APPLICATION_PROBE_METHODS
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -2,9 +2,22 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from aioimmich import Immich
|
||||
from aioimmich.const import CONNECT_ERRORS
|
||||
from aioimmich.exceptions import ImmichUnauthorizedError
|
||||
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_VERIFY_SSL,
|
||||
Platform,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -25,7 +38,30 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ImmichConfigEntry) -> bool:
|
||||
"""Set up Immich from a config entry."""
|
||||
|
||||
coordinator = ImmichDataUpdateCoordinator(hass, entry)
|
||||
session = async_get_clientsession(hass, entry.data[CONF_VERIFY_SSL])
|
||||
immich = Immich(
|
||||
session,
|
||||
entry.data[CONF_API_KEY],
|
||||
entry.data[CONF_HOST],
|
||||
entry.data[CONF_PORT],
|
||||
entry.data[CONF_SSL],
|
||||
"home-assistant",
|
||||
)
|
||||
|
||||
try:
|
||||
user_info = await immich.users.async_get_my_user()
|
||||
except ImmichUnauthorizedError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_error",
|
||||
) from err
|
||||
except CONNECT_ERRORS as err:
|
||||
raise ConfigEntryNotReady(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
|
||||
coordinator = ImmichDataUpdateCoordinator(hass, entry, immich, user_info.is_admin)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
|
||||
@@ -16,19 +16,11 @@ from aioimmich.server.models import (
|
||||
ImmichServerVersionCheck,
|
||||
)
|
||||
from awesomeversion import AwesomeVersion
|
||||
from yarl import URL
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_VERIFY_SSL,
|
||||
)
|
||||
from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SSL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -54,49 +46,24 @@ class ImmichDataUpdateCoordinator(DataUpdateCoordinator[ImmichData]):
|
||||
|
||||
config_entry: ImmichConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ImmichConfigEntry) -> None:
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, entry: ConfigEntry, api: Immich, is_admin: bool
|
||||
) -> None:
|
||||
"""Initialize the data update coordinator."""
|
||||
self.api = Immich(
|
||||
async_get_clientsession(hass, config_entry.data[CONF_VERIFY_SSL]),
|
||||
config_entry.data[CONF_API_KEY],
|
||||
config_entry.data[CONF_HOST],
|
||||
config_entry.data[CONF_PORT],
|
||||
config_entry.data[CONF_SSL],
|
||||
"home-assistant",
|
||||
)
|
||||
self.is_admin = False
|
||||
self.configuration_url = str(
|
||||
URL.build(
|
||||
scheme="https" if config_entry.data[CONF_SSL] else "http",
|
||||
host=config_entry.data[CONF_HOST],
|
||||
port=config_entry.data[CONF_PORT],
|
||||
)
|
||||
self.api = api
|
||||
self.is_admin = is_admin
|
||||
self.configuration_url = (
|
||||
f"{'https' if entry.data[CONF_SSL] else 'http'}://"
|
||||
f"{entry.data[CONF_HOST]}:{entry.data[CONF_PORT]}"
|
||||
)
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
config_entry=entry,
|
||||
name=DOMAIN,
|
||||
update_interval=timedelta(seconds=60),
|
||||
)
|
||||
|
||||
async def _async_setup(self) -> None:
|
||||
"""Handle setup of the coordinator."""
|
||||
try:
|
||||
user_info = await self.api.users.async_get_my_user()
|
||||
except ImmichUnauthorizedError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="auth_error",
|
||||
) from err
|
||||
except CONNECT_ERRORS as err:
|
||||
raise UpdateFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="cannot_connect",
|
||||
) from err
|
||||
|
||||
self.is_admin = user_info.is_admin
|
||||
|
||||
async def _async_update_data(self) -> ImmichData:
|
||||
"""Update data via internal method."""
|
||||
try:
|
||||
|
||||
@@ -20,5 +20,13 @@
|
||||
"turn_on": {
|
||||
"service": "mdi:toggle-switch"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"turned_off": {
|
||||
"trigger": "mdi:toggle-switch-off"
|
||||
},
|
||||
"turned_on": {
|
||||
"trigger": "mdi:toggle-switch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted toggles to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"entity_component": {
|
||||
"_": {
|
||||
"name": "[%key:component::input_boolean::title%]",
|
||||
@@ -17,6 +21,15 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"reload": {
|
||||
"description": "Reloads helpers from the YAML-configuration.",
|
||||
@@ -35,5 +48,27 @@
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
}
|
||||
},
|
||||
"title": "Input boolean"
|
||||
"title": "Input boolean",
|
||||
"triggers": {
|
||||
"turned_off": {
|
||||
"description": "Triggers after one or more toggles turn off.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::input_boolean::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::input_boolean::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Toggle turned off"
|
||||
},
|
||||
"turned_on": {
|
||||
"description": "Triggers after one or more toggles turn on.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::input_boolean::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::input_boolean::common::trigger_behavior_name%]"
|
||||
}
|
||||
},
|
||||
"name": "Toggle turned on"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
17
homeassistant/components/input_boolean/trigger.py
Normal file
17
homeassistant/components/input_boolean/trigger.py
Normal file
@@ -0,0 +1,17 @@
|
||||
"""Provides triggers for input booleans."""
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
|
||||
from . import DOMAIN
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for input booleans."""
|
||||
return TRIGGERS
|
||||
18
homeassistant/components/input_boolean/triggers.yaml
Normal file
18
homeassistant/components/input_boolean/triggers.yaml
Normal file
@@ -0,0 +1,18 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
domain: input_boolean
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
translation_key: trigger_behavior
|
||||
|
||||
turned_off: *trigger_common
|
||||
turned_on: *trigger_common
|
||||
@@ -617,10 +617,8 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
pyrolytic = 323
|
||||
descale = 326
|
||||
evaporate_water = 327
|
||||
rinse = 333
|
||||
shabbat_program = 335
|
||||
yom_tov = 336
|
||||
hydroclean = 341
|
||||
drying = 357, 2028
|
||||
heat_crockery = 358
|
||||
prove_dough = 359, 2023
|
||||
@@ -725,7 +723,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
belgian_sponge_cake = 624
|
||||
goose_unstuffed = 625
|
||||
rack_of_lamb_with_vegetables = 634
|
||||
yorkshire_pudding = 635, 2352
|
||||
yorkshire_pudding = 635
|
||||
meat_loaf = 636
|
||||
defrost_meat = 647
|
||||
defrost_vegetables = 654
|
||||
@@ -1125,7 +1123,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
wholegrain_rice = 3376
|
||||
parboiled_rice_steam_cooking = 3380
|
||||
parboiled_rice_rapid_steam_cooking = 3381
|
||||
basmati_rice_steam_cooking = 3382, 3383
|
||||
basmati_rice_steam_cooking = 3383
|
||||
basmati_rice_rapid_steam_cooking = 3384
|
||||
jasmine_rice_steam_cooking = 3386
|
||||
jasmine_rice_rapid_steam_cooking = 3387
|
||||
@@ -1133,7 +1131,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
huanghuanian_rapid_steam_cooking = 3390
|
||||
simiao_steam_cooking = 3392
|
||||
simiao_rapid_steam_cooking = 3393
|
||||
long_grain_rice_general_steam_cooking = 3394, 3395
|
||||
long_grain_rice_general_steam_cooking = 3395
|
||||
long_grain_rice_general_rapid_steam_cooking = 3396
|
||||
chongming_steam_cooking = 3398
|
||||
chongming_rapid_steam_cooking = 3399
|
||||
|
||||
@@ -560,7 +560,6 @@
|
||||
"hot_water": "Hot water",
|
||||
"huanghuanian_rapid_steam_cooking": "Huanghuanian (rapid steam cooking)",
|
||||
"huanghuanian_steam_cooking": "Huanghuanian (steam cooking)",
|
||||
"hydroclean": "HydroClean",
|
||||
"hygiene": "Hygiene",
|
||||
"intensive": "Intensive",
|
||||
"intensive_bake": "Intensive bake",
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
},
|
||||
"services": {
|
||||
"set_absolute_position": {
|
||||
"description": "Sets the absolute position of a cover.",
|
||||
"description": "Sets the absolute position of the cover.",
|
||||
"fields": {
|
||||
"absolute_position": {
|
||||
"description": "Absolute position to move to.",
|
||||
|
||||
@@ -13,5 +13,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["oralb_ble"],
|
||||
"requirements": ["oralb-ble==1.1.0"]
|
||||
"requirements": ["oralb-ble==1.0.2"]
|
||||
}
|
||||
|
||||
@@ -90,5 +90,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["PSNAWP==3.0.3", "pyrate-limiter==4.1.0"]
|
||||
"requirements": ["PSNAWP==3.0.3", "pyrate-limiter==4.0.2"]
|
||||
}
|
||||
|
||||
@@ -125,14 +125,6 @@ VM_BUTTONS: tuple[ProxmoxVMButtonEntityDescription, ...] = (
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
ProxmoxVMButtonEntityDescription(
|
||||
key="shutdown",
|
||||
translation_key="shutdown",
|
||||
press_action=lambda coordinator, node, vmid: (
|
||||
coordinator.proxmox.nodes(node).qemu(vmid).status.shutdown.post()
|
||||
),
|
||||
entity_category=EntityCategory.CONFIG,
|
||||
),
|
||||
)
|
||||
|
||||
CONTAINER_BUTTONS: tuple[ProxmoxContainerButtonEntityDescription, ...] = (
|
||||
|
||||
@@ -7,9 +7,6 @@
|
||||
"reset": {
|
||||
"default": "mdi:restart"
|
||||
},
|
||||
"shutdown": {
|
||||
"default": "mdi:power"
|
||||
},
|
||||
"start": {
|
||||
"default": "mdi:play"
|
||||
},
|
||||
|
||||
@@ -20,18 +20,12 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import (
|
||||
RoborockB01Q10UpdateCoordinator,
|
||||
RoborockConfigEntry,
|
||||
RoborockDataUpdateCoordinator,
|
||||
RoborockDataUpdateCoordinatorA01,
|
||||
RoborockWashingMachineUpdateCoordinator,
|
||||
)
|
||||
from .entity import (
|
||||
RoborockCoordinatedEntityA01,
|
||||
RoborockCoordinatedEntityB01Q10,
|
||||
RoborockEntity,
|
||||
RoborockEntityV1,
|
||||
)
|
||||
from .entity import RoborockCoordinatedEntityA01, RoborockEntity, RoborockEntityV1
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -103,14 +97,6 @@ ZEO_BUTTON_DESCRIPTIONS = [
|
||||
]
|
||||
|
||||
|
||||
Q10_BUTTON_DESCRIPTIONS = [
|
||||
ButtonEntityDescription(
|
||||
key="empty_dustbin",
|
||||
translation_key="empty_dustbin",
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: RoborockConfigEntry,
|
||||
@@ -153,14 +139,6 @@ async def async_setup_entry(
|
||||
if isinstance(coordinator, RoborockWashingMachineUpdateCoordinator)
|
||||
for description in ZEO_BUTTON_DESCRIPTIONS
|
||||
),
|
||||
(
|
||||
RoborockQ10EmptyDustbinButtonEntity(
|
||||
coordinator,
|
||||
description,
|
||||
)
|
||||
for coordinator in config_entry.runtime_data.b01_q10
|
||||
for description in Q10_BUTTON_DESCRIPTIONS
|
||||
),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -255,37 +233,3 @@ class RoborockButtonEntityA01(RoborockCoordinatedEntityA01, ButtonEntity):
|
||||
) from err
|
||||
finally:
|
||||
await self.coordinator.async_request_refresh()
|
||||
|
||||
|
||||
class RoborockQ10EmptyDustbinButtonEntity(
|
||||
RoborockCoordinatedEntityB01Q10, ButtonEntity
|
||||
):
|
||||
"""A class to define Q10 empty dustbin button entity."""
|
||||
|
||||
entity_description: ButtonEntityDescription
|
||||
coordinator: RoborockB01Q10UpdateCoordinator
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: RoborockB01Q10UpdateCoordinator,
|
||||
entity_description: ButtonEntityDescription,
|
||||
) -> None:
|
||||
"""Create a Q10 empty dustbin button entity."""
|
||||
self.entity_description = entity_description
|
||||
super().__init__(
|
||||
f"{entity_description.key}_{coordinator.duid_slug}",
|
||||
coordinator,
|
||||
)
|
||||
|
||||
async def async_press(self, **kwargs: Any) -> None:
|
||||
"""Press the button to empty dustbin."""
|
||||
try:
|
||||
await self.coordinator.api.vacuum.empty_dustbin()
|
||||
except RoborockException as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="command_failed",
|
||||
translation_placeholders={
|
||||
"command": "empty_dustbin",
|
||||
},
|
||||
) from err
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"loggers": ["roborock"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": [
|
||||
"python-roborock==5.0.0",
|
||||
"python-roborock==4.26.2",
|
||||
"vacuum-map-parser-roborock==0.1.4"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -84,9 +84,6 @@
|
||||
}
|
||||
},
|
||||
"button": {
|
||||
"empty_dustbin": {
|
||||
"name": "Empty dustbin"
|
||||
},
|
||||
"pause": {
|
||||
"name": "Pause"
|
||||
},
|
||||
|
||||
@@ -80,23 +80,23 @@ Q7_STATE_CODE_TO_STATE = {
|
||||
}
|
||||
|
||||
Q10_STATE_CODE_TO_STATE = {
|
||||
YXDeviceState.SLEEPING: VacuumActivity.IDLE,
|
||||
YXDeviceState.IDLE: VacuumActivity.IDLE,
|
||||
YXDeviceState.CLEANING: VacuumActivity.CLEANING,
|
||||
YXDeviceState.RETURNING_HOME: VacuumActivity.RETURNING,
|
||||
YXDeviceState.REMOTE_CONTROL_ACTIVE: VacuumActivity.CLEANING,
|
||||
YXDeviceState.CHARGING: VacuumActivity.DOCKED,
|
||||
YXDeviceState.PAUSED: VacuumActivity.PAUSED,
|
||||
YXDeviceState.ERROR: VacuumActivity.ERROR,
|
||||
YXDeviceState.UPDATING: VacuumActivity.DOCKED,
|
||||
YXDeviceState.EMPTYING_THE_BIN: VacuumActivity.DOCKED,
|
||||
YXDeviceState.MAPPING: VacuumActivity.CLEANING,
|
||||
YXDeviceState.RELOCATING: VacuumActivity.CLEANING,
|
||||
YXDeviceState.SWEEPING: VacuumActivity.CLEANING,
|
||||
YXDeviceState.MOPPING: VacuumActivity.CLEANING,
|
||||
YXDeviceState.SWEEP_AND_MOP: VacuumActivity.CLEANING,
|
||||
YXDeviceState.TRANSITIONING: VacuumActivity.CLEANING,
|
||||
YXDeviceState.WAITING_TO_CHARGE: VacuumActivity.DOCKED,
|
||||
YXDeviceState.SLEEP_STATE: VacuumActivity.IDLE,
|
||||
YXDeviceState.STANDBY_STATE: VacuumActivity.IDLE,
|
||||
YXDeviceState.CLEANING_STATE: VacuumActivity.CLEANING,
|
||||
YXDeviceState.TO_CHARGE_STATE: VacuumActivity.RETURNING,
|
||||
YXDeviceState.REMOTEING_STATE: VacuumActivity.CLEANING,
|
||||
YXDeviceState.CHARGING_STATE: VacuumActivity.DOCKED,
|
||||
YXDeviceState.PAUSE_STATE: VacuumActivity.PAUSED,
|
||||
YXDeviceState.FAULT_STATE: VacuumActivity.ERROR,
|
||||
YXDeviceState.UPGRADE_STATE: VacuumActivity.DOCKED,
|
||||
YXDeviceState.DUSTING: VacuumActivity.DOCKED,
|
||||
YXDeviceState.CREATING_MAP_STATE: VacuumActivity.CLEANING,
|
||||
YXDeviceState.RE_LOCATION_STATE: VacuumActivity.CLEANING,
|
||||
YXDeviceState.ROBOT_SWEEPING: VacuumActivity.CLEANING,
|
||||
YXDeviceState.ROBOT_MOPING: VacuumActivity.CLEANING,
|
||||
YXDeviceState.ROBOT_SWEEP_AND_MOPING: VacuumActivity.CLEANING,
|
||||
YXDeviceState.ROBOT_TRANSITIONING: VacuumActivity.CLEANING,
|
||||
YXDeviceState.ROBOT_WAIT_CHARGE: VacuumActivity.DOCKED,
|
||||
}
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
"""Provides triggers for switch platform."""
|
||||
|
||||
from homeassistant.components.input_boolean import DOMAIN as INPUT_BOOLEAN_DOMAIN
|
||||
from homeassistant.const import STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.automation import DomainSpec
|
||||
from homeassistant.helpers.trigger import Trigger, make_entity_target_state_trigger
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
SWITCH_DOMAIN_SPECS = {DOMAIN: DomainSpec(), INPUT_BOOLEAN_DOMAIN: DomainSpec()}
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"turned_on": make_entity_target_state_trigger(SWITCH_DOMAIN_SPECS, STATE_ON),
|
||||
"turned_off": make_entity_target_state_trigger(SWITCH_DOMAIN_SPECS, STATE_OFF),
|
||||
"turned_on": make_entity_target_state_trigger(DOMAIN, STATE_ON),
|
||||
"turned_off": make_entity_target_state_trigger(DOMAIN, STATE_OFF),
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
.trigger_common: &trigger_common
|
||||
target:
|
||||
entity:
|
||||
- domain: switch
|
||||
- domain: input_boolean
|
||||
domain: switch
|
||||
fields:
|
||||
behavior:
|
||||
required: true
|
||||
|
||||
@@ -153,7 +153,6 @@ STEP_WEBHOOKS_DATA_SCHEMA: vol.Schema = vol.Schema(
|
||||
vol.Required(CONF_TRUSTED_NETWORKS): vol.Coerce(str),
|
||||
}
|
||||
)
|
||||
SUBENTRY_SCHEMA: vol.Schema = vol.Schema({vol.Required(CONF_CHAT_ID): vol.Coerce(int)})
|
||||
OPTIONS_SCHEMA: vol.Schema = vol.Schema(
|
||||
{
|
||||
vol.Required(
|
||||
@@ -599,30 +598,24 @@ class AllowedChatIdsSubEntryFlowHandler(ConfigSubentryFlow):
|
||||
)
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
description_placeholders = DESCRIPTION_PLACEHOLDERS.copy()
|
||||
|
||||
if user_input is not None:
|
||||
config_entry: TelegramBotConfigEntry = self._get_entry()
|
||||
bot = config_entry.runtime_data.bot
|
||||
|
||||
# validate chat id
|
||||
chat_id: int = user_input[CONF_CHAT_ID]
|
||||
try:
|
||||
chat_info: ChatFullInfo = await bot.get_chat(chat_id)
|
||||
except BadRequest:
|
||||
errors["base"] = "chat_not_found"
|
||||
except TelegramError as err:
|
||||
errors["base"] = "telegram_error"
|
||||
description_placeholders[ERROR_MESSAGE] = str(err)
|
||||
|
||||
if not errors:
|
||||
chat_name = await _async_get_chat_name(bot, chat_id)
|
||||
if chat_name:
|
||||
return self.async_create_entry(
|
||||
title=chat_info.effective_name or str(chat_id),
|
||||
title=f"{chat_name} ({chat_id})",
|
||||
data={CONF_CHAT_ID: chat_id},
|
||||
unique_id=str(chat_id),
|
||||
)
|
||||
|
||||
errors["base"] = "chat_not_found"
|
||||
|
||||
service: TelegramNotificationService = self._get_entry().runtime_data
|
||||
description_placeholders = DESCRIPTION_PLACEHOLDERS.copy()
|
||||
description_placeholders["bot_username"] = f"@{service.bot.username}"
|
||||
description_placeholders["bot_url"] = f"https://t.me/{service.bot.username}"
|
||||
|
||||
@@ -646,7 +639,7 @@ class AllowedChatIdsSubEntryFlowHandler(ConfigSubentryFlow):
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=self.add_suggested_values_to_schema(
|
||||
SUBENTRY_SCHEMA,
|
||||
vol.Schema({vol.Required(CONF_CHAT_ID): vol.Coerce(int)}),
|
||||
suggested_values,
|
||||
),
|
||||
description_placeholders=description_placeholders,
|
||||
@@ -684,3 +677,11 @@ async def _get_most_recent_chat(
|
||||
)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
async def _async_get_chat_name(bot: Bot, chat_id: int) -> str:
|
||||
try:
|
||||
chat_info: ChatFullInfo = await bot.get_chat(chat_id)
|
||||
return chat_info.effective_name or str(chat_id)
|
||||
except BadRequest:
|
||||
return ""
|
||||
|
||||
@@ -92,8 +92,7 @@
|
||||
},
|
||||
"entry_type": "Allowed chat ID",
|
||||
"error": {
|
||||
"chat_not_found": "Chat not found",
|
||||
"telegram_error": "[%key:component::telegram_bot::config::error::telegram_error%]"
|
||||
"chat_not_found": "Chat not found"
|
||||
},
|
||||
"initiate_flow": {
|
||||
"user": "Add allowed chat ID"
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
"""Integration for temperature triggers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
DOMAIN = "temperature"
|
||||
CONFIG_SCHEMA = cv.empty_config_schema(DOMAIN)
|
||||
|
||||
__all__ = []
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the component."""
|
||||
return True
|
||||
@@ -1,10 +0,0 @@
|
||||
{
|
||||
"triggers": {
|
||||
"changed": {
|
||||
"trigger": "mdi:thermometer"
|
||||
},
|
||||
"crossed_threshold": {
|
||||
"trigger": "mdi:thermometer"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"domain": "temperature",
|
||||
"name": "Temperature",
|
||||
"codeowners": ["@home-assistant/core"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/temperature",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal"
|
||||
}
|
||||
@@ -1,76 +0,0 @@
|
||||
{
|
||||
"common": {
|
||||
"trigger_behavior_description": "The behavior of the targeted entities to trigger on.",
|
||||
"trigger_behavior_name": "Behavior"
|
||||
},
|
||||
"selector": {
|
||||
"number_or_entity": {
|
||||
"choices": {
|
||||
"entity": "Entity",
|
||||
"number": "Number"
|
||||
}
|
||||
},
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
},
|
||||
"trigger_threshold_type": {
|
||||
"options": {
|
||||
"above": "Above",
|
||||
"below": "Below",
|
||||
"between": "Between",
|
||||
"outside": "Outside"
|
||||
}
|
||||
}
|
||||
},
|
||||
"title": "Temperature",
|
||||
"triggers": {
|
||||
"changed": {
|
||||
"description": "Triggers after one or more temperatures change.",
|
||||
"fields": {
|
||||
"above": {
|
||||
"description": "Only trigger when temperature is above this value.",
|
||||
"name": "Above"
|
||||
},
|
||||
"below": {
|
||||
"description": "Only trigger when temperature is below this value.",
|
||||
"name": "Below"
|
||||
},
|
||||
"unit": {
|
||||
"description": "All values will be converted to this unit when evaluating the trigger.",
|
||||
"name": "Unit of measurement"
|
||||
}
|
||||
},
|
||||
"name": "Temperature changed"
|
||||
},
|
||||
"crossed_threshold": {
|
||||
"description": "Triggers after one or more temperatures cross a threshold.",
|
||||
"fields": {
|
||||
"behavior": {
|
||||
"description": "[%key:component::temperature::common::trigger_behavior_description%]",
|
||||
"name": "[%key:component::temperature::common::trigger_behavior_name%]"
|
||||
},
|
||||
"lower_limit": {
|
||||
"description": "The lower limit of the threshold.",
|
||||
"name": "Lower limit"
|
||||
},
|
||||
"threshold_type": {
|
||||
"description": "The type of threshold to use.",
|
||||
"name": "Threshold type"
|
||||
},
|
||||
"unit": {
|
||||
"description": "[%key:component::temperature::triggers::changed::fields::unit::description%]",
|
||||
"name": "[%key:component::temperature::triggers::changed::fields::unit::name%]"
|
||||
},
|
||||
"upper_limit": {
|
||||
"description": "The upper limit of the threshold.",
|
||||
"name": "Upper limit"
|
||||
}
|
||||
},
|
||||
"name": "Temperature crossed threshold"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
"""Provides triggers for temperature."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.components.climate import (
|
||||
ATTR_CURRENT_TEMPERATURE as CLIMATE_ATTR_CURRENT_TEMPERATURE,
|
||||
DOMAIN as CLIMATE_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN, SensorDeviceClass
|
||||
from homeassistant.components.water_heater import (
|
||||
ATTR_CURRENT_TEMPERATURE as WATER_HEATER_ATTR_CURRENT_TEMPERATURE,
|
||||
DOMAIN as WATER_HEATER_DOMAIN,
|
||||
)
|
||||
from homeassistant.components.weather import (
|
||||
ATTR_WEATHER_TEMPERATURE,
|
||||
ATTR_WEATHER_TEMPERATURE_UNIT,
|
||||
DOMAIN as WEATHER_DOMAIN,
|
||||
)
|
||||
from homeassistant.const import ATTR_UNIT_OF_MEASUREMENT, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.automation import NumericalDomainSpec
|
||||
from homeassistant.helpers.trigger import (
|
||||
EntityNumericalStateChangedTriggerWithUnitBase,
|
||||
EntityNumericalStateCrossedThresholdTriggerWithUnitBase,
|
||||
EntityNumericalStateTriggerWithUnitBase,
|
||||
Trigger,
|
||||
)
|
||||
from homeassistant.util.unit_conversion import TemperatureConverter
|
||||
|
||||
TEMPERATURE_DOMAIN_SPECS = {
|
||||
CLIMATE_DOMAIN: NumericalDomainSpec(
|
||||
value_source=CLIMATE_ATTR_CURRENT_TEMPERATURE,
|
||||
),
|
||||
SENSOR_DOMAIN: NumericalDomainSpec(
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
),
|
||||
WATER_HEATER_DOMAIN: NumericalDomainSpec(
|
||||
value_source=WATER_HEATER_ATTR_CURRENT_TEMPERATURE
|
||||
),
|
||||
WEATHER_DOMAIN: NumericalDomainSpec(
|
||||
value_source=ATTR_WEATHER_TEMPERATURE,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
class _TemperatureTriggerMixin(EntityNumericalStateTriggerWithUnitBase):
|
||||
"""Mixin for temperature triggers providing entity filtering, value extraction, and unit conversion."""
|
||||
|
||||
_base_unit = UnitOfTemperature.CELSIUS
|
||||
_domain_specs = TEMPERATURE_DOMAIN_SPECS
|
||||
_unit_converter = TemperatureConverter
|
||||
|
||||
def _get_entity_unit(self, state: State) -> str | None:
|
||||
"""Get the temperature unit of an entity from its state."""
|
||||
if state.domain == SENSOR_DOMAIN:
|
||||
return state.attributes.get(ATTR_UNIT_OF_MEASUREMENT)
|
||||
if state.domain == WEATHER_DOMAIN:
|
||||
return state.attributes.get(ATTR_WEATHER_TEMPERATURE_UNIT)
|
||||
# Climate and water_heater: show_temp converts to system unit
|
||||
return self._hass.config.units.temperature_unit
|
||||
|
||||
|
||||
class TemperatureChangedTrigger(
|
||||
_TemperatureTriggerMixin, EntityNumericalStateChangedTriggerWithUnitBase
|
||||
):
|
||||
"""Trigger for temperature value changes across multiple domains."""
|
||||
|
||||
|
||||
class TemperatureCrossedThresholdTrigger(
|
||||
_TemperatureTriggerMixin, EntityNumericalStateCrossedThresholdTriggerWithUnitBase
|
||||
):
|
||||
"""Trigger for temperature value crossing a threshold across multiple domains."""
|
||||
|
||||
|
||||
TRIGGERS: dict[str, type[Trigger]] = {
|
||||
"changed": TemperatureChangedTrigger,
|
||||
"crossed_threshold": TemperatureCrossedThresholdTrigger,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_triggers(hass: HomeAssistant) -> dict[str, type[Trigger]]:
|
||||
"""Return the triggers for temperature."""
|
||||
return TRIGGERS
|
||||
@@ -1,77 +0,0 @@
|
||||
.trigger_common_fields:
|
||||
behavior: &trigger_behavior
|
||||
required: true
|
||||
default: any
|
||||
selector:
|
||||
select:
|
||||
translation_key: trigger_behavior
|
||||
options:
|
||||
- first
|
||||
- last
|
||||
- any
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
choices:
|
||||
number:
|
||||
selector:
|
||||
number:
|
||||
mode: box
|
||||
entity:
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "°C"
|
||||
- "°F"
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: number
|
||||
device_class: temperature
|
||||
translation_key: number_or_entity
|
||||
|
||||
.trigger_threshold_type: &trigger_threshold_type
|
||||
required: true
|
||||
default: above
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- above
|
||||
- below
|
||||
- between
|
||||
- outside
|
||||
translation_key: trigger_threshold_type
|
||||
|
||||
.trigger_unit: &trigger_unit
|
||||
required: false
|
||||
selector:
|
||||
select:
|
||||
options:
|
||||
- "°C"
|
||||
- "°F"
|
||||
.trigger_target: &trigger_target
|
||||
entity:
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: climate
|
||||
- domain: water_heater
|
||||
- domain: weather
|
||||
|
||||
changed:
|
||||
target: *trigger_target
|
||||
fields:
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
unit: *trigger_unit
|
||||
|
||||
crossed_threshold:
|
||||
target: *trigger_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity
|
||||
upper_limit: *number_or_entity
|
||||
unit: *trigger_unit
|
||||
@@ -80,16 +80,6 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_ARM_AWAY_ACTION,
|
||||
CONF_ARM_CUSTOM_BYPASS_ACTION,
|
||||
CONF_ARM_HOME_ACTION,
|
||||
CONF_ARM_NIGHT_ACTION,
|
||||
CONF_ARM_VACATION_ACTION,
|
||||
CONF_DISARM_ACTION,
|
||||
CONF_TRIGGER_ACTION,
|
||||
)
|
||||
|
||||
DEFAULT_NAME = "Template Alarm Control Panel"
|
||||
|
||||
ALARM_CONTROL_PANEL_COMMON_SCHEMA = vol.Schema(
|
||||
@@ -162,7 +152,6 @@ async def async_setup_entry(
|
||||
StateAlarmControlPanelEntity,
|
||||
ALARM_CONTROL_PANEL_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -183,7 +172,6 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_ALARM_CONTROL_PANELS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -209,7 +197,6 @@ class AbstractTemplateAlarmControlPanel(
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity calls AbstractTemplateEntity.__init__.
|
||||
def __init__(self, name: str) -> None: # pylint: disable=super-init-not-called
|
||||
@@ -219,6 +206,7 @@ class AbstractTemplateAlarmControlPanel(
|
||||
self._attr_code_format = self._config[CONF_CODE_FORMAT].value
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_alarm_state",
|
||||
validator=tcv.strenum(self, CONF_STATE, AlarmControlPanelState),
|
||||
)
|
||||
|
||||
@@ -176,7 +176,6 @@ class AbstractTemplateBinarySensor(
|
||||
"""Representation of a template binary sensor features."""
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -190,6 +189,7 @@ class AbstractTemplateBinarySensor(
|
||||
self._delay_cancel: CALLBACK_TYPE | None = None
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_is_on",
|
||||
on_update=self._update_state,
|
||||
)
|
||||
|
||||
@@ -36,8 +36,6 @@ _LOGGER = logging.getLogger(__name__)
|
||||
DEFAULT_NAME = "Template Button"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
SCRIPT_FIELDS = (CONF_PRESS,)
|
||||
|
||||
BUTTON_YAML_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PRESS): cv.SCRIPT_SCHEMA,
|
||||
@@ -68,7 +66,6 @@ async def async_setup_platform(
|
||||
None,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -84,7 +81,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateButtonEntity,
|
||||
BUTTON_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -71,14 +71,6 @@ CONF_TILT_OPTIMISTIC = "tilt_optimistic"
|
||||
|
||||
CONF_OPEN_AND_CLOSE = "open_or_close"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CLOSE_ACTION,
|
||||
OPEN_ACTION,
|
||||
POSITION_ACTION,
|
||||
STOP_ACTION,
|
||||
TILT_ACTION,
|
||||
)
|
||||
|
||||
TILT_FEATURES = (
|
||||
CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
@@ -173,7 +165,6 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_COVERS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -190,7 +181,6 @@ async def async_setup_entry(
|
||||
StateCoverEntity,
|
||||
COVER_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -215,7 +205,6 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity):
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_extra_optimistic_options = (CONF_POSITION,)
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -223,6 +212,7 @@ class AbstractTemplateCover(AbstractTemplateEntity, CoverEntity):
|
||||
"""Initialize the features."""
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_current_cover_position",
|
||||
template_validators.strenum(
|
||||
self, CONF_STATE, CoverState, CoverState.OPEN, CoverState.CLOSED
|
||||
|
||||
@@ -3,9 +3,10 @@
|
||||
from abc import abstractmethod
|
||||
from collections.abc import Callable, Sequence
|
||||
from dataclasses import dataclass
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_OPTIMISTIC
|
||||
from homeassistant.const import CONF_DEVICE_ID, CONF_OPTIMISTIC, CONF_STATE
|
||||
from homeassistant.core import Context, HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.entity import Entity, async_generate_entity_id
|
||||
@@ -15,6 +16,8 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import CONF_DEFAULT_ENTITY_ID
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@dataclass
|
||||
class EntityTemplate:
|
||||
@@ -33,7 +36,7 @@ class AbstractTemplateEntity(Entity):
|
||||
_entity_id_format: str
|
||||
_optimistic_entity: bool = False
|
||||
_extra_optimistic_options: tuple[str, ...] | None = None
|
||||
_state_option: str | None = None
|
||||
_template: Template | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -50,18 +53,19 @@ class AbstractTemplateEntity(Entity):
|
||||
if self._optimistic_entity:
|
||||
optimistic = config.get(CONF_OPTIMISTIC)
|
||||
|
||||
if self._state_option is not None:
|
||||
assumed_optimistic = config.get(self._state_option) is None
|
||||
if self._extra_optimistic_options:
|
||||
assumed_optimistic = assumed_optimistic and all(
|
||||
config.get(option) is None
|
||||
for option in self._extra_optimistic_options
|
||||
)
|
||||
self._template = config.get(CONF_STATE)
|
||||
|
||||
self._attr_assumed_state = optimistic or (
|
||||
optimistic is None and assumed_optimistic
|
||||
assumed_optimistic = self._template is None
|
||||
if self._extra_optimistic_options:
|
||||
assumed_optimistic = assumed_optimistic and all(
|
||||
config.get(option) is None
|
||||
for option in self._extra_optimistic_options
|
||||
)
|
||||
|
||||
self._attr_assumed_state = optimistic or (
|
||||
optimistic is None and assumed_optimistic
|
||||
)
|
||||
|
||||
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
||||
_, _, object_id = default_entity_id.partition(".")
|
||||
self.entity_id = async_generate_entity_id(
|
||||
@@ -85,16 +89,12 @@ class AbstractTemplateEntity(Entity):
|
||||
@abstractmethod
|
||||
def setup_state_template(
|
||||
self,
|
||||
option: str,
|
||||
attribute: str,
|
||||
validator: Callable[[Any], Any] | None = None,
|
||||
on_update: Callable[[Any], None] | None = None,
|
||||
) -> None:
|
||||
"""Set up a template that manages the main state of the entity.
|
||||
|
||||
Requires _state_option to be set on the inheriting class. _state_option represents
|
||||
the configuration option that derives the state. E.g. Template weather entities main state option
|
||||
is 'condition', where switch is 'state'.
|
||||
"""
|
||||
"""Set up a template that manages the main state of the entity."""
|
||||
|
||||
@abstractmethod
|
||||
def setup_template(
|
||||
|
||||
@@ -87,15 +87,6 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Fan"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_OFF_ACTION,
|
||||
CONF_ON_ACTION,
|
||||
CONF_SET_DIRECTION_ACTION,
|
||||
CONF_SET_OSCILLATING_ACTION,
|
||||
CONF_SET_PERCENTAGE_ACTION,
|
||||
CONF_SET_PRESET_MODE_ACTION,
|
||||
)
|
||||
|
||||
FAN_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_DIRECTION): cv.template,
|
||||
@@ -168,7 +159,6 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_FANS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -184,7 +174,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateFanEntity,
|
||||
FAN_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -207,13 +196,13 @@ class AbstractTemplateFan(AbstractTemplateEntity, FanEntity):
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
def __init__(self, name: str, config: dict[str, Any]) -> None: # pylint: disable=super-init-not-called
|
||||
"""Initialize the features."""
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_is_on",
|
||||
template_validators.boolean(self, CONF_STATE),
|
||||
)
|
||||
|
||||
@@ -8,7 +8,6 @@ import logging
|
||||
from typing import Any
|
||||
|
||||
import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
from homeassistant.components import blueprint
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
@@ -26,7 +25,7 @@ from homeassistant.const import (
|
||||
SERVICE_RELOAD,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError, PlatformNotReady
|
||||
from homeassistant.exceptions import PlatformNotReady
|
||||
from homeassistant.helpers import issue_registry as ir, template
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import (
|
||||
@@ -35,7 +34,6 @@ from homeassistant.helpers.entity_platform import (
|
||||
async_get_platforms,
|
||||
)
|
||||
from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.script import async_validate_actions_config
|
||||
from homeassistant.helpers.script_variables import ScriptVariables
|
||||
from homeassistant.helpers.singleton import singleton
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
@@ -210,21 +208,6 @@ def _format_template(value: Any, field: str | None = None) -> Any:
|
||||
return str(value)
|
||||
|
||||
|
||||
def _get_config_breadcrumbs(config: ConfigType) -> str:
|
||||
"""Try to coerce entity information from the config."""
|
||||
breadcrumb = "Template Entity"
|
||||
# Default entity id should be in most legacy configuration because
|
||||
# it's created from the legacy slug. Vacuum and Lock do not have a
|
||||
# slug, therefore we need to use the name or unique_id.
|
||||
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
||||
breadcrumb = default_entity_id.split(".")[-1]
|
||||
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
|
||||
breadcrumb = f"unique_id: {unique_id}"
|
||||
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
|
||||
breadcrumb = name.template
|
||||
return breadcrumb
|
||||
|
||||
|
||||
def format_migration_config(
|
||||
config: ConfigType | list[ConfigType], depth: int = 0
|
||||
) -> ConfigType | list[ConfigType]:
|
||||
@@ -269,7 +252,16 @@ def create_legacy_template_issue(
|
||||
if domain not in PLATFORMS:
|
||||
return
|
||||
|
||||
breadcrumb = _get_config_breadcrumbs(config)
|
||||
breadcrumb = "Template Entity"
|
||||
# Default entity id should be in most legacy configuration because
|
||||
# it's created from the legacy slug. Vacuum and Lock do not have a
|
||||
# slug, therefore we need to use the name or unique_id.
|
||||
if (default_entity_id := config.get(CONF_DEFAULT_ENTITY_ID)) is not None:
|
||||
breadcrumb = default_entity_id.split(".")[-1]
|
||||
elif (unique_id := config.get(CONF_UNIQUE_ID)) is not None:
|
||||
breadcrumb = f"unique_id: {unique_id}"
|
||||
elif (name := config.get(CONF_NAME)) and isinstance(name, template.Template):
|
||||
breadcrumb = name.template
|
||||
|
||||
issue_id = f"{LEGACY_TEMPLATE_DEPRECATION_KEY}_{domain}_{breadcrumb}_{hashlib.md5(','.join(config.keys()).encode()).hexdigest()}"
|
||||
|
||||
@@ -304,39 +296,6 @@ def create_legacy_template_issue(
|
||||
)
|
||||
|
||||
|
||||
async def validate_template_scripts(
|
||||
hass: HomeAssistant,
|
||||
config: ConfigType,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Validate template scripts."""
|
||||
if not script_options:
|
||||
return
|
||||
|
||||
def _humanize(err: Exception, data: Any) -> str:
|
||||
"""Humanize vol.Invalid, stringify other exceptions."""
|
||||
if isinstance(err, vol.Invalid):
|
||||
return humanize_error(data, err)
|
||||
return str(err)
|
||||
|
||||
breadcrumb: str | None = None
|
||||
for script_option in script_options:
|
||||
if (script_config := config.pop(script_option, None)) is not None:
|
||||
try:
|
||||
config[script_option] = await async_validate_actions_config(
|
||||
hass, script_config
|
||||
)
|
||||
except (vol.Invalid, HomeAssistantError) as err:
|
||||
if not breadcrumb:
|
||||
breadcrumb = _get_config_breadcrumbs(config)
|
||||
_LOGGER.error(
|
||||
"The '%s' actions for %s failed to setup: %s",
|
||||
script_option,
|
||||
breadcrumb,
|
||||
_humanize(err, script_config),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_template_platform(
|
||||
hass: HomeAssistant,
|
||||
domain: str,
|
||||
@@ -347,7 +306,6 @@ async def async_setup_template_platform(
|
||||
discovery_info: DiscoveryInfoType | None,
|
||||
legacy_fields: dict[str, str] | None = None,
|
||||
legacy_key: str | None = None,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Set up the Template platform."""
|
||||
if discovery_info is None:
|
||||
@@ -379,14 +337,10 @@ async def async_setup_template_platform(
|
||||
# Trigger Configuration
|
||||
if "coordinator" in discovery_info:
|
||||
if trigger_entity_cls:
|
||||
entities = []
|
||||
for entity_config in discovery_info["entities"]:
|
||||
await validate_template_scripts(hass, entity_config, script_options)
|
||||
entities.append(
|
||||
trigger_entity_cls(
|
||||
hass, discovery_info["coordinator"], entity_config
|
||||
)
|
||||
)
|
||||
entities = [
|
||||
trigger_entity_cls(hass, discovery_info["coordinator"], config)
|
||||
for config in discovery_info["entities"]
|
||||
]
|
||||
async_add_entities(entities)
|
||||
else:
|
||||
raise PlatformNotReady(
|
||||
@@ -395,9 +349,6 @@ async def async_setup_template_platform(
|
||||
return
|
||||
|
||||
# Modern Configuration
|
||||
for entity_config in discovery_info["entities"]:
|
||||
await validate_template_scripts(hass, entity_config, script_options)
|
||||
|
||||
async_create_template_tracking_entities(
|
||||
state_entity_cls,
|
||||
async_add_entities,
|
||||
@@ -414,7 +365,6 @@ async def async_setup_template_entry(
|
||||
state_entity_cls: type[TemplateEntity],
|
||||
config_schema: vol.Schema | vol.All,
|
||||
replace_value_template: bool = False,
|
||||
script_options: tuple[str, ...] | None = None,
|
||||
) -> None:
|
||||
"""Setup the Template from a config entry."""
|
||||
options = dict(config_entry.options)
|
||||
@@ -427,7 +377,6 @@ async def async_setup_template_entry(
|
||||
options[CONF_STATE] = options.pop(CONF_VALUE_TEMPLATE)
|
||||
|
||||
validated_config = config_schema(options)
|
||||
await validate_template_scripts(hass, validated_config, script_options)
|
||||
|
||||
async_add_entities(
|
||||
[state_entity_cls(hass, validated_config, config_entry.entry_id)]
|
||||
|
||||
@@ -129,18 +129,6 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Light"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_EFFECT_ACTION,
|
||||
CONF_HS_ACTION,
|
||||
CONF_LEVEL_ACTION,
|
||||
CONF_OFF_ACTION,
|
||||
CONF_ON_ACTION,
|
||||
CONF_RGB_ACTION,
|
||||
CONF_RGBW_ACTION,
|
||||
CONF_RGBWW_ACTION,
|
||||
CONF_TEMPERATURE_ACTION,
|
||||
)
|
||||
|
||||
LIGHT_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Inclusive(CONF_EFFECT_ACTION, "effect"): cv.SCRIPT_SCHEMA,
|
||||
@@ -154,6 +142,8 @@ LIGHT_COMMON_SCHEMA = vol.Schema(
|
||||
vol.Optional(CONF_MIN_MIREDS): cv.template,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_OFF_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Required(CONF_ON_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGB_ACTION): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_RGB): cv.template,
|
||||
vol.Optional(CONF_RGBW_ACTION): cv.SCRIPT_SCHEMA,
|
||||
@@ -236,7 +226,6 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_LIGHTS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -253,7 +242,6 @@ async def async_setup_entry(
|
||||
StateLightEntity,
|
||||
LIGHT_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -359,7 +347,6 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
|
||||
_optimistic_entity = True
|
||||
_attr_max_color_temp_kelvin = DEFAULT_MAX_KELVIN
|
||||
_attr_min_color_temp_kelvin = DEFAULT_MIN_KELVIN
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -370,7 +357,7 @@ class AbstractTemplateLight(AbstractTemplateEntity, LightEntity):
|
||||
|
||||
# Setup state and brightness
|
||||
self.setup_state_template(
|
||||
"_attr_is_on", template_validators.boolean(self, CONF_STATE)
|
||||
CONF_STATE, "_attr_is_on", template_validators.boolean(self, CONF_STATE)
|
||||
)
|
||||
self.setup_template(
|
||||
CONF_LEVEL,
|
||||
|
||||
@@ -64,13 +64,6 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_LOCK,
|
||||
CONF_OPEN,
|
||||
CONF_UNLOCK,
|
||||
)
|
||||
|
||||
|
||||
LOCK_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_CODE_FORMAT): cv.template,
|
||||
@@ -119,7 +112,6 @@ async def async_setup_platform(
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -135,7 +127,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateLockEntity,
|
||||
LOCK_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -158,7 +149,6 @@ class AbstractTemplateLock(AbstractTemplateEntity, LockEntity):
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -167,6 +157,7 @@ class AbstractTemplateLock(AbstractTemplateEntity, LockEntity):
|
||||
self._code_format_template_error: TemplateError | None = None
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_lock_state",
|
||||
template_validators.strenum(
|
||||
self, CONF_STATE, LockState, LockState.LOCKED, LockState.UNLOCKED
|
||||
@@ -192,18 +183,16 @@ class AbstractTemplateLock(AbstractTemplateEntity, LockEntity):
|
||||
self._attr_supported_features |= supported_feature
|
||||
|
||||
def _set_state(self, state: LockState | None) -> None:
|
||||
if state is None:
|
||||
self._attr_is_locked = None
|
||||
return
|
||||
|
||||
self._attr_is_jammed = state == LockState.JAMMED
|
||||
self._attr_is_opening = state == LockState.OPENING
|
||||
self._attr_is_locking = state == LockState.LOCKING
|
||||
self._attr_is_open = state == LockState.OPEN
|
||||
self._attr_is_unlocking = state == LockState.UNLOCKING
|
||||
|
||||
# All other parameters need to be set False in order
|
||||
# for the lock to be unknown.
|
||||
if state is None:
|
||||
self._attr_is_locked = state
|
||||
else:
|
||||
self._attr_is_locked = state == LockState.LOCKED
|
||||
self._attr_is_locked = state == LockState.LOCKED
|
||||
|
||||
@callback
|
||||
def _update_code_format(self, render: str | TemplateError | None):
|
||||
|
||||
@@ -46,8 +46,6 @@ CONF_SET_VALUE = "set_value"
|
||||
DEFAULT_NAME = "Template Number"
|
||||
DEFAULT_OPTIMISTIC = False
|
||||
|
||||
SCRIPT_FIELDS = (CONF_SET_VALUE,)
|
||||
|
||||
NUMBER_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_MAX, default=DEFAULT_MAX_VALUE): cv.template,
|
||||
@@ -83,7 +81,6 @@ async def async_setup_platform(
|
||||
TriggerNumberEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -99,7 +96,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateNumberEntity,
|
||||
NUMBER_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -118,7 +114,6 @@ class AbstractTemplateNumber(AbstractTemplateEntity, NumberEntity):
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -130,6 +125,7 @@ class AbstractTemplateNumber(AbstractTemplateEntity, NumberEntity):
|
||||
self._attr_native_max_value = DEFAULT_MAX_VALUE
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_native_value",
|
||||
template_validators.number(self, CONF_STATE),
|
||||
)
|
||||
|
||||
@@ -47,8 +47,6 @@ CONF_SELECT_OPTION = "select_option"
|
||||
|
||||
DEFAULT_NAME = "Template Select"
|
||||
|
||||
SCRIPT_FIELDS = (CONF_SELECT_OPTION,)
|
||||
|
||||
SELECT_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(ATTR_OPTIONS): cv.template,
|
||||
@@ -81,7 +79,6 @@ async def async_setup_platform(
|
||||
TriggerSelectEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -97,7 +94,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
TemplateSelect,
|
||||
SELECT_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -116,7 +112,6 @@ class AbstractTemplateSelect(AbstractTemplateEntity, SelectEntity):
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -125,6 +120,7 @@ class AbstractTemplateSelect(AbstractTemplateEntity, SelectEntity):
|
||||
self._attr_options = []
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_current_option",
|
||||
cv.string,
|
||||
)
|
||||
|
||||
@@ -229,7 +229,6 @@ class AbstractTemplateSensor(AbstractTemplateEntity, RestoreSensor):
|
||||
"""Representation of a template sensor features."""
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -241,6 +240,7 @@ class AbstractTemplateSensor(AbstractTemplateEntity, RestoreSensor):
|
||||
self._attr_last_reset = None
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_native_value",
|
||||
self._validate_state,
|
||||
)
|
||||
|
||||
@@ -57,16 +57,11 @@ LEGACY_FIELDS = {
|
||||
|
||||
DEFAULT_NAME = "Template Switch"
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
CONF_TURN_OFF,
|
||||
CONF_TURN_ON,
|
||||
)
|
||||
|
||||
SWITCH_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_STATE): cv.template,
|
||||
vol.Optional(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TURN_ON): cv.SCRIPT_SCHEMA,
|
||||
vol.Optional(CONF_TURN_OFF): cv.SCRIPT_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -114,7 +109,6 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_SWITCHES,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -131,7 +125,6 @@ async def async_setup_entry(
|
||||
StateSwitchEntity,
|
||||
SWITCH_CONFIG_ENTRY_SCHEMA,
|
||||
True,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -155,7 +148,6 @@ class AbstractTemplateSwitch(AbstractTemplateEntity, SwitchEntity, RestoreEntity
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -163,6 +155,7 @@ class AbstractTemplateSwitch(AbstractTemplateEntity, SwitchEntity, RestoreEntity
|
||||
"""Initialize the features."""
|
||||
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_is_on",
|
||||
template_validators.boolean(self, CONF_STATE),
|
||||
)
|
||||
|
||||
@@ -292,16 +292,12 @@ class TemplateEntity(AbstractTemplateEntity):
|
||||
|
||||
def setup_state_template(
|
||||
self,
|
||||
option: str,
|
||||
attribute: str,
|
||||
validator: Callable[[Any], Any] | None = None,
|
||||
on_update: Callable[[Any], None] | None = None,
|
||||
) -> None:
|
||||
"""Set up a template that manages the main state of the entity.
|
||||
|
||||
Requires _state_option to be set on the inheriting class. _state_option represents
|
||||
the configuration option that derives the state. E.g. Template weather entities main state option
|
||||
is 'condition', where switch is 'state'.
|
||||
"""
|
||||
"""Set up a template that manages the main state of the entity."""
|
||||
|
||||
@callback
|
||||
def _update_state(result: Any) -> None:
|
||||
@@ -318,22 +314,13 @@ class TemplateEntity(AbstractTemplateEntity):
|
||||
self._attr_available = True
|
||||
|
||||
state = validator(result) if validator else result
|
||||
|
||||
if on_update:
|
||||
on_update(state)
|
||||
else:
|
||||
setattr(self, attribute, state)
|
||||
|
||||
if self._state_option is None:
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not implement '_state_option' for 'setup_state_template'."
|
||||
)
|
||||
|
||||
self.add_template(
|
||||
self._state_option,
|
||||
attribute,
|
||||
on_update=_update_state,
|
||||
none_on_template_error=False,
|
||||
option, attribute, on_update=_update_state, none_on_template_error=False
|
||||
)
|
||||
|
||||
def setup_template(
|
||||
|
||||
@@ -5,7 +5,7 @@ from __future__ import annotations
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.const import CONF_VARIABLES
|
||||
from homeassistant.const import CONF_STATE, CONF_VARIABLES
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.script_variables import ScriptVariables
|
||||
@@ -60,30 +60,17 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
|
||||
def setup_state_template(
|
||||
self,
|
||||
option: str,
|
||||
attribute: str,
|
||||
validator: Callable[[Any], Any] | None = None,
|
||||
on_update: Callable[[Any], None] | None = None,
|
||||
) -> None:
|
||||
"""Set up a template that manages the main state of the entity.
|
||||
|
||||
Requires _state_option to be set on the inheriting class. _state_option represents
|
||||
the configuration option that derives the state. E.g. Template weather entities main state option
|
||||
is 'condition', where switch is 'state'.
|
||||
"""
|
||||
if self._state_option is None:
|
||||
raise NotImplementedError(
|
||||
f"{self.__class__.__name__} does not implement '_state_option' for 'setup_state_template'."
|
||||
)
|
||||
|
||||
"""Set up a template that manages the main state of the entity."""
|
||||
if self.add_template(
|
||||
self._state_option,
|
||||
attribute,
|
||||
validator,
|
||||
on_update,
|
||||
none_on_template_error=False,
|
||||
option, attribute, validator, on_update, none_on_template_error=False
|
||||
):
|
||||
self._to_render_simple.append(self._state_option)
|
||||
self._parse_result.add(self._state_option)
|
||||
self._to_render_simple.append(option)
|
||||
self._parse_result.add(option)
|
||||
|
||||
def setup_template(
|
||||
self,
|
||||
@@ -162,7 +149,7 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
# Filter out state templates because they have unique behavior
|
||||
# with none_on_template_error.
|
||||
if (
|
||||
key != self._state_option
|
||||
key != CONF_STATE
|
||||
and key in self._templates
|
||||
and not self._templates[key].none_on_template_error
|
||||
):
|
||||
@@ -177,21 +164,17 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
|
||||
# If state fails to render, the entity should go unavailable. Render the
|
||||
# state as a simple template because the result should always be a string or None.
|
||||
if (
|
||||
state_option := self._state_option
|
||||
) is not None and state_option in self._to_render_simple:
|
||||
if CONF_STATE in self._to_render_simple:
|
||||
if (
|
||||
result := self._render_single_template(state_option, variables)
|
||||
result := self._render_single_template(CONF_STATE, variables)
|
||||
) is _SENTINEL:
|
||||
self._rendered = self._static_rendered
|
||||
self._state_render_error = True
|
||||
return
|
||||
|
||||
rendered[state_option] = result
|
||||
rendered[CONF_STATE] = result
|
||||
|
||||
self._render_single_templates(
|
||||
rendered, variables, [state_option] if state_option else []
|
||||
)
|
||||
self._render_single_templates(rendered, variables, [CONF_STATE])
|
||||
self._render_attributes(rendered, variables)
|
||||
self._rendered = rendered
|
||||
|
||||
@@ -199,10 +182,6 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
"""Get a rendered result and return the value."""
|
||||
# Handle any templates.
|
||||
write_state = False
|
||||
if self._state_render_error:
|
||||
# The state errored and the entity is unavailable, do not process any values.
|
||||
return True
|
||||
|
||||
for option, entity_template in self._templates.items():
|
||||
# Capture templates that did not render a result due to an exception and
|
||||
# ensure the state object updates. _SENTINEL is used to differentiate
|
||||
@@ -246,7 +225,18 @@ class TriggerEntity( # pylint: disable=hass-enforce-class-module
|
||||
if self._render_availability_template(variables):
|
||||
self._render_templates(variables)
|
||||
|
||||
write_state = self._handle_rendered_results()
|
||||
write_state = False
|
||||
# While transitioning platforms to the new framework, this
|
||||
# if-statement is necessary for backward compatibility with existing
|
||||
# trigger based platforms.
|
||||
if self._templates:
|
||||
# Handle any results that were rendered.
|
||||
write_state = self._handle_rendered_results()
|
||||
|
||||
# Check availability after rendering the results because the state
|
||||
# template could render the entity unavailable
|
||||
if not self.available:
|
||||
write_state = True
|
||||
|
||||
if len(self._rendered) > 0:
|
||||
# In some cases, the entity may be state optimistic or
|
||||
|
||||
@@ -65,8 +65,6 @@ CONF_SPECIFIC_VERSION = "specific_version"
|
||||
CONF_TITLE = "title"
|
||||
CONF_UPDATE_PERCENTAGE = "update_percentage"
|
||||
|
||||
SCRIPT_FIELDS = (CONF_INSTALL,)
|
||||
|
||||
UPDATE_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_BACKUP, default=False): cv.boolean,
|
||||
@@ -107,7 +105,6 @@ async def async_setup_platform(
|
||||
TriggerUpdateEntity,
|
||||
async_add_entities,
|
||||
discovery_info,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -123,7 +120,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
StateUpdateEntity,
|
||||
UPDATE_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -76,16 +76,6 @@ LEGACY_FIELDS = {
|
||||
CONF_VALUE_TEMPLATE: CONF_STATE,
|
||||
}
|
||||
|
||||
SCRIPT_FIELDS = (
|
||||
SERVICE_CLEAN_SPOT,
|
||||
SERVICE_LOCATE,
|
||||
SERVICE_PAUSE,
|
||||
SERVICE_RETURN_TO_BASE,
|
||||
SERVICE_SET_FAN_SPEED,
|
||||
SERVICE_START,
|
||||
SERVICE_STOP,
|
||||
)
|
||||
|
||||
VACUUM_COMMON_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_BATTERY_LEVEL): cv.template,
|
||||
@@ -160,7 +150,6 @@ async def async_setup_platform(
|
||||
discovery_info,
|
||||
LEGACY_FIELDS,
|
||||
legacy_key=CONF_VACUUMS,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -176,7 +165,6 @@ async def async_setup_entry(
|
||||
async_add_entities,
|
||||
TemplateStateVacuumEntity,
|
||||
VACUUM_CONFIG_ENTRY_SCHEMA,
|
||||
script_options=SCRIPT_FIELDS,
|
||||
)
|
||||
|
||||
|
||||
@@ -219,7 +207,6 @@ class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_optimistic_entity = True
|
||||
_state_option = CONF_STATE
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
# This ensures that the __init__ on AbstractTemplateEntity is not called twice.
|
||||
@@ -229,6 +216,7 @@ class AbstractTemplateVacuum(AbstractTemplateEntity, StateVacuumEntity):
|
||||
# List of valid fan speeds
|
||||
self._attr_fan_speed_list = config[CONF_FAN_SPEED_LIST]
|
||||
self.setup_state_template(
|
||||
CONF_STATE,
|
||||
"_attr_activity",
|
||||
template_validators.strenum(self, CONF_STATE, VacuumActivity),
|
||||
)
|
||||
|
||||
@@ -389,7 +389,6 @@ class AbstractTemplateWeather(AbstractTemplateEntity, WeatherEntity):
|
||||
"""Representation of a template weathers features."""
|
||||
|
||||
_entity_id_format = ENTITY_ID_FORMAT
|
||||
_state_option = CONF_CONDITION
|
||||
_optimistic_entity = True
|
||||
|
||||
# The super init is not called because TemplateEntity and TriggerEntity will call AbstractTemplateEntity.__init__.
|
||||
@@ -400,7 +399,8 @@ class AbstractTemplateWeather(AbstractTemplateEntity, WeatherEntity):
|
||||
"""Initialize the features."""
|
||||
|
||||
# Required options
|
||||
self.setup_state_template(
|
||||
self.setup_template(
|
||||
CONF_CONDITION,
|
||||
"_attr_condition",
|
||||
template_validators.item_in_list(self, CONF_CONDITION, CONDITION_CLASSES),
|
||||
)
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "local_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["tplink-omada-client==1.5.6"]
|
||||
"requirements": ["tplink-omada-client==1.5.3"]
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ from steamloop import (
|
||||
|
||||
from homeassistant.const import CONF_HOST, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
from .const import CONF_SECRET_KEY, DOMAIN, MANUFACTURER
|
||||
@@ -37,7 +37,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: TraneConfigEntry) -> boo
|
||||
) from err
|
||||
except AuthenticationError as err:
|
||||
await conn.disconnect()
|
||||
raise ConfigEntryError(
|
||||
raise ConfigEntryAuthFailed(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="authentication_failed",
|
||||
) from err
|
||||
|
||||
@@ -6,7 +6,7 @@ from unifi_access_api import ApiAuthError, ApiConnectionError, UnifiAccessApiCli
|
||||
|
||||
from homeassistant.const import CONF_API_TOKEN, CONF_HOST, CONF_VERIFY_SSL, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .coordinator import UnifiAccessConfigEntry, UnifiAccessCoordinator
|
||||
@@ -33,7 +33,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: UnifiAccessConfigEntry)
|
||||
try:
|
||||
await client.authenticate()
|
||||
except ApiAuthError as err:
|
||||
raise ConfigEntryAuthFailed(
|
||||
raise ConfigEntryNotReady(
|
||||
f"Authentication failed for UniFi Access at {entry.data[CONF_HOST]}"
|
||||
) from err
|
||||
except ApiConnectionError as err:
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -67,48 +66,3 @@ class UnifiAccessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth flow."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauth confirm."""
|
||||
errors: dict[str, str] = {}
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
|
||||
if user_input is not None:
|
||||
session = async_get_clientsession(
|
||||
self.hass, verify_ssl=reauth_entry.data[CONF_VERIFY_SSL]
|
||||
)
|
||||
client = UnifiAccessApiClient(
|
||||
host=reauth_entry.data[CONF_HOST],
|
||||
api_token=user_input[CONF_API_TOKEN],
|
||||
session=session,
|
||||
verify_ssl=reauth_entry.data[CONF_VERIFY_SSL],
|
||||
)
|
||||
try:
|
||||
await client.authenticate()
|
||||
except ApiAuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except ApiConnectionError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry,
|
||||
data_updates={CONF_API_TOKEN: user_input[CONF_API_TOKEN]},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema({vol.Required(CONF_API_TOKEN): str}),
|
||||
description_placeholders={CONF_HOST: reauth_entry.data[CONF_HOST]},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -30,7 +30,6 @@ from unifi_access_api.models.websocket import (
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -117,7 +116,7 @@ class UnifiAccessCoordinator(DataUpdateCoordinator[UnifiAccessData]):
|
||||
self.client.get_emergency_status(),
|
||||
)
|
||||
except ApiAuthError as err:
|
||||
raise ConfigEntryAuthFailed(f"Authentication failed: {err}") from err
|
||||
raise UpdateFailed(f"Authentication failed: {err}") from err
|
||||
except ApiConnectionError as err:
|
||||
raise UpdateFailed(f"Error connecting to API: {err}") from err
|
||||
except ApiError as err:
|
||||
@@ -212,6 +211,9 @@ class UnifiAccessCoordinator(DataUpdateCoordinator[UnifiAccessData]):
|
||||
async def _handle_insights_add(self, msg: WebsocketMessage) -> None:
|
||||
"""Handle access insights events (entry/exit)."""
|
||||
insights = cast(InsightsAdd, msg)
|
||||
door = insights.data.metadata.door
|
||||
if not door.id:
|
||||
return
|
||||
event_type = (
|
||||
"access_granted" if insights.data.result == "ACCESS" else "access_denied"
|
||||
)
|
||||
@@ -222,9 +224,7 @@ class UnifiAccessCoordinator(DataUpdateCoordinator[UnifiAccessData]):
|
||||
attrs["authentication"] = insights.data.metadata.authentication.display_name
|
||||
if insights.data.result:
|
||||
attrs["result"] = insights.data.result
|
||||
for door in insights.data.metadata.door:
|
||||
if door.id:
|
||||
self._dispatch_door_event(door.id, "access", event_type, attrs)
|
||||
self._dispatch_door_event(door.id, "access", event_type, attrs)
|
||||
|
||||
@callback
|
||||
def _dispatch_door_event(
|
||||
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["unifi_access_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["py-unifi-access==1.1.3"]
|
||||
"requirements": ["py-unifi-access==1.1.0"]
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: done
|
||||
reauthentication-flow: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -10,15 +9,6 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_token": "[%key:component::unifi_access::config::step::user::data_description::api_token%]"
|
||||
},
|
||||
"description": "The API token for UniFi Access at {host} is invalid. Please provide a new token."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]",
|
||||
|
||||
@@ -8,7 +8,9 @@ import logging
|
||||
|
||||
import pyvera as veraApi
|
||||
from requests.exceptions import RequestException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONF_EXCLUDE,
|
||||
@@ -19,6 +21,7 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .common import (
|
||||
ControllerData,
|
||||
@@ -32,7 +35,41 @@ from .const import CONF_CONTROLLER, DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
|
||||
VERA_ID_LIST_SCHEMA = vol.Schema([int])
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema(
|
||||
vol.All(
|
||||
cv.deprecated(DOMAIN),
|
||||
{
|
||||
DOMAIN: vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_CONTROLLER): cv.url,
|
||||
vol.Optional(CONF_EXCLUDE, default=[]): VERA_ID_LIST_SCHEMA,
|
||||
vol.Optional(CONF_LIGHTS, default=[]): VERA_ID_LIST_SCHEMA,
|
||||
}
|
||||
)
|
||||
},
|
||||
),
|
||||
extra=vol.ALLOW_EXTRA,
|
||||
)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, base_config: ConfigType) -> bool:
|
||||
"""Set up for Vera controllers."""
|
||||
hass.data[DOMAIN] = {}
|
||||
|
||||
if not (config := base_config.get(DOMAIN)):
|
||||
return True
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": config_entries.SOURCE_IMPORT},
|
||||
data=config,
|
||||
)
|
||||
)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
|
||||
@@ -46,7 +46,7 @@ def set_controller_data(
|
||||
hass: HomeAssistant, config_entry: ConfigEntry, data: ControllerData
|
||||
) -> None:
|
||||
"""Set controller data in hass data."""
|
||||
hass.data.setdefault(DOMAIN, {})[config_entry.entry_id] = data
|
||||
hass.data[DOMAIN][config_entry.entry_id] = data
|
||||
|
||||
|
||||
class SubscriptionRegistry(pv.AbstractSubscriptionRegistry):
|
||||
|
||||
@@ -12,6 +12,7 @@ from requests.exceptions import RequestException
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import (
|
||||
SOURCE_IMPORT,
|
||||
SOURCE_USER,
|
||||
ConfigEntry,
|
||||
ConfigFlow,
|
||||
@@ -20,6 +21,7 @@ from homeassistant.config_entries import (
|
||||
)
|
||||
from homeassistant.const import CONF_EXCLUDE, CONF_LIGHTS, CONF_SOURCE
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.typing import VolDictType
|
||||
|
||||
from .const import CONF_CONTROLLER, CONF_LEGACY_UNIQUE_ID, DOMAIN
|
||||
@@ -129,6 +131,31 @@ class VeraFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
},
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle a flow initialized by import."""
|
||||
|
||||
# If there are entities with the legacy unique_id, then this imported config
|
||||
# should also use the legacy unique_id for entity creation.
|
||||
entity_registry = er.async_get(self.hass)
|
||||
use_legacy_unique_id = (
|
||||
len(
|
||||
[
|
||||
entry
|
||||
for entry in entity_registry.entities.values()
|
||||
if entry.platform == DOMAIN and entry.unique_id.isdigit()
|
||||
]
|
||||
)
|
||||
> 0
|
||||
)
|
||||
|
||||
return await self.async_step_finish(
|
||||
{
|
||||
**import_data,
|
||||
CONF_SOURCE: SOURCE_IMPORT,
|
||||
CONF_LEGACY_UNIQUE_ID: use_legacy_unique_id,
|
||||
}
|
||||
)
|
||||
|
||||
async def async_step_finish(self, config: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Validate and create config entry."""
|
||||
base_url = config[CONF_CONTROLLER] = config[CONF_CONTROLLER].rstrip("/")
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"integration_type": "device",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["wolf_comm"],
|
||||
"requirements": ["wolf-comm==0.0.48"]
|
||||
"requirements": ["wolf-comm==0.0.23"]
|
||||
}
|
||||
|
||||
@@ -177,24 +177,6 @@ SENSOR_DESCRIPTIONS = {
|
||||
native_unit_of_measurement=UnitOfTime.MINUTES,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# Pressure present duration (in seconds) for Pressure Sensor
|
||||
(
|
||||
ExtendedSensorDeviceClass.PRESSURE_PRESENT_DURATION,
|
||||
Units.TIME_SECONDS,
|
||||
): SensorEntityDescription(
|
||||
key=str(ExtendedSensorDeviceClass.PRESSURE_PRESENT_DURATION),
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# Pressure not present duration (in seconds) for Pressure Sensor
|
||||
(
|
||||
ExtendedSensorDeviceClass.PRESSURE_NOT_PRESENT_DURATION,
|
||||
Units.TIME_SECONDS,
|
||||
): SensorEntityDescription(
|
||||
key=str(ExtendedSensorDeviceClass.PRESSURE_NOT_PRESENT_DURATION),
|
||||
native_unit_of_measurement=UnitOfTime.SECONDS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
),
|
||||
# Low frequency impedance sensor (ohm)
|
||||
(ExtendedSensorDeviceClass.IMPEDANCE_LOW, Units.OHM): SensorEntityDescription(
|
||||
key=str(ExtendedSensorDeviceClass.IMPEDANCE_LOW),
|
||||
|
||||
@@ -7,5 +7,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/yolink",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_push",
|
||||
"requirements": ["yolink-api==0.6.3"]
|
||||
"requirements": ["yolink-api==0.6.1"]
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ from yolink.const import (
|
||||
ATTR_DEVICE_MANIPULATOR,
|
||||
ATTR_DEVICE_MOTION_SENSOR,
|
||||
ATTR_DEVICE_MULTI_CAPS_LEAK_SENSOR,
|
||||
ATTR_DEVICE_MULTI_FUNCTIONAL_SENSOR,
|
||||
ATTR_DEVICE_MULTI_OUTLET,
|
||||
ATTR_DEVICE_MULTI_WATER_METER_CONTROLLER,
|
||||
ATTR_DEVICE_OUTLET,
|
||||
@@ -46,7 +45,6 @@ from homeassistant.components.sensor import (
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
PERCENTAGE,
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
EntityCategory,
|
||||
@@ -119,7 +117,6 @@ SENSOR_DEVICE_TYPE = [
|
||||
ATTR_DEVICE_SPRINKLER,
|
||||
ATTR_DEVICE_SPRINKLER_V2,
|
||||
ATTR_DEVICE_MULTI_CAPS_LEAK_SENSOR,
|
||||
ATTR_DEVICE_MULTI_FUNCTIONAL_SENSOR,
|
||||
]
|
||||
|
||||
BATTERY_POWER_SENSOR = [
|
||||
@@ -143,7 +140,6 @@ BATTERY_POWER_SENSOR = [
|
||||
ATTR_DEVICE_SMOKE_ALARM,
|
||||
ATTR_DEVICE_SPRINKLER_V2,
|
||||
ATTR_DEVICE_MULTI_CAPS_LEAK_SENSOR,
|
||||
ATTR_DEVICE_MULTI_FUNCTIONAL_SENSOR,
|
||||
]
|
||||
|
||||
MCU_DEV_TEMPERATURE_SENSOR = [
|
||||
@@ -202,10 +198,7 @@ def parse_data_humidity(device: YoLinkDevice, data: dict) -> int | None:
|
||||
|
||||
def parse_data_temperature(device: YoLinkDevice, data: dict) -> float | None:
|
||||
"""Parse temperature data."""
|
||||
if device.device_type in (
|
||||
ATTR_DEVICE_MULTI_CAPS_LEAK_SENSOR,
|
||||
ATTR_DEVICE_MULTI_FUNCTIONAL_SENSOR,
|
||||
):
|
||||
if device.device_type == ATTR_DEVICE_MULTI_CAPS_LEAK_SENSOR:
|
||||
return (
|
||||
state.get("temperature")
|
||||
if (state := data.get("state")) is not None
|
||||
@@ -252,7 +245,6 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = (
|
||||
ATTR_DEVICE_TH_SENSOR,
|
||||
ATTR_DEVICE_SOIL_TH_SENSOR,
|
||||
ATTR_DEVICE_MULTI_CAPS_LEAK_SENSOR,
|
||||
ATTR_DEVICE_MULTI_FUNCTIONAL_SENSOR,
|
||||
]
|
||||
),
|
||||
value=parse_data_temperature,
|
||||
@@ -405,19 +397,6 @@ SENSOR_TYPES: tuple[YoLinkSensorEntityDescription, ...] = (
|
||||
should_update_entity=lambda value: value is not None,
|
||||
value=lambda device, data: data.get("coreTemperature"),
|
||||
),
|
||||
YoLinkSensorEntityDescription(
|
||||
key="co",
|
||||
device_class=SensorDeviceClass.CO,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
exists_fn=lambda device: (
|
||||
device.device_type == ATTR_DEVICE_MULTI_FUNCTIONAL_SENSOR
|
||||
),
|
||||
should_update_entity=lambda value: value is not None,
|
||||
value=lambda device, data: (
|
||||
state.get("co") if (state := data.get("state")) is not None else None
|
||||
),
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,7 @@ async def warn_on_wrong_silabs_firmware(hass: HomeAssistant, device: str) -> boo
|
||||
|
||||
app_type = await probe_silabs_firmware_type(
|
||||
device,
|
||||
bootloader_reset_methods=(),
|
||||
application_probe_methods=[
|
||||
(ApplicationType.GECKO_BOOTLOADER, 115200),
|
||||
(ApplicationType.EZSP, 115200),
|
||||
|
||||
@@ -18,27 +18,17 @@ from zwave_js_server.const.command_class.notification import (
|
||||
)
|
||||
from zwave_js_server.model.driver import Driver
|
||||
|
||||
from homeassistant.components.automation import automations_with_entity
|
||||
from homeassistant.components.binary_sensor import (
|
||||
DOMAIN as BINARY_SENSOR_DOMAIN,
|
||||
BinarySensorDeviceClass,
|
||||
BinarySensorEntity,
|
||||
BinarySensorEntityDescription,
|
||||
)
|
||||
from homeassistant.components.script import scripts_with_entity
|
||||
from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN
|
||||
from homeassistant.const import EntityCategory, Platform
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.issue_registry import (
|
||||
IssueSeverity,
|
||||
async_create_issue,
|
||||
async_delete_issue,
|
||||
)
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
|
||||
from .const import DOMAIN
|
||||
from .entity import NewZwaveDiscoveryInfo, ZWaveBaseEntity
|
||||
@@ -413,91 +403,6 @@ def is_valid_notification_binary_sensor(
|
||||
return len(info.primary_value.metadata.states) > 1
|
||||
|
||||
|
||||
@callback
|
||||
def _async_check_legacy_entity_repair(
|
||||
hass: HomeAssistant,
|
||||
driver: Driver,
|
||||
entity: ZWaveLegacyDoorStateBinarySensor,
|
||||
) -> None:
|
||||
"""Schedule a repair issue check once HA has fully started."""
|
||||
|
||||
@callback
|
||||
def _async_do_check(hass: HomeAssistant) -> None:
|
||||
"""Create or delete a repair issue for a deprecated legacy door state entity."""
|
||||
ent_reg = er.async_get(hass)
|
||||
if entity.unique_id is None:
|
||||
return
|
||||
entity_id = ent_reg.async_get_entity_id(
|
||||
BINARY_SENSOR_DOMAIN, DOMAIN, entity.unique_id
|
||||
)
|
||||
if entity_id is None:
|
||||
return
|
||||
|
||||
issue_id = f"deprecated_legacy_door_state.{entity_id}"
|
||||
|
||||
# Delete any stale repair issue if the entity is disabled or missing —
|
||||
# the user has already dealt with it.
|
||||
entity_entry = ent_reg.async_get(entity_id)
|
||||
if entity_entry is None or entity_entry.disabled:
|
||||
async_delete_issue(hass, DOMAIN, issue_id)
|
||||
return
|
||||
|
||||
entity_automations = automations_with_entity(hass, entity_id)
|
||||
entity_scripts = scripts_with_entity(hass, entity_id)
|
||||
|
||||
# Delete any stale repair issue if the entity is no longer referenced
|
||||
# in any automation or script.
|
||||
if not entity_automations and not entity_scripts:
|
||||
async_delete_issue(hass, DOMAIN, issue_id)
|
||||
return
|
||||
|
||||
opening_state_value = get_opening_state_notification_value(entity.info.node)
|
||||
if opening_state_value is None:
|
||||
async_delete_issue(hass, DOMAIN, issue_id)
|
||||
return
|
||||
opening_state_unique_id = (
|
||||
f"{driver.controller.home_id}.{opening_state_value.value_id}"
|
||||
)
|
||||
opening_state_entity_id = ent_reg.async_get_entity_id(
|
||||
SENSOR_DOMAIN, DOMAIN, opening_state_unique_id
|
||||
)
|
||||
# Delete any stale repair issue if the replacement opening state sensor
|
||||
# no longer exists for some reason
|
||||
if opening_state_entity_id is None:
|
||||
async_delete_issue(hass, DOMAIN, issue_id)
|
||||
return
|
||||
|
||||
items = [
|
||||
f"- [{item.name or item.original_name or eid}](/config/{domain}/edit/{item.unique_id})"
|
||||
for domain, entity_ids in (
|
||||
("automation", entity_automations),
|
||||
("script", entity_scripts),
|
||||
)
|
||||
for eid in entity_ids
|
||||
if (item := ent_reg.async_get(eid))
|
||||
]
|
||||
|
||||
async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
is_fixable=False,
|
||||
is_persistent=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_legacy_door_state",
|
||||
translation_placeholders={
|
||||
"entity_id": entity_id,
|
||||
"entity_name": entity_entry.name
|
||||
or entity_entry.original_name
|
||||
or entity_id,
|
||||
"opening_state_entity_id": opening_state_entity_id,
|
||||
"items": "\n".join(items),
|
||||
},
|
||||
)
|
||||
|
||||
async_at_started(hass, _async_do_check)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
config_entry: ZwaveJSConfigEntry,
|
||||
@@ -541,9 +446,9 @@ async def async_setup_entry(
|
||||
isinstance(info, NewZwaveDiscoveryInfo)
|
||||
and info.entity_class is ZWaveLegacyDoorStateBinarySensor
|
||||
):
|
||||
entity = ZWaveLegacyDoorStateBinarySensor(config_entry, driver, info)
|
||||
entities.append(entity)
|
||||
_async_check_legacy_entity_repair(hass, driver, entity)
|
||||
entities.append(
|
||||
ZWaveLegacyDoorStateBinarySensor(config_entry, driver, info)
|
||||
)
|
||||
elif isinstance(info, NewZwaveDiscoveryInfo):
|
||||
pass # other entity classes are not migrated yet
|
||||
elif info.platform_hint == "notification":
|
||||
|
||||
@@ -142,6 +142,7 @@ ATTR_TWIST_ASSIST = "twist_assist"
|
||||
ADDON_SLUG = "core_zwave_js"
|
||||
|
||||
# Sensor entity description constants
|
||||
ENTITY_DESC_KEY_BATTERY_LEVEL = "battery_level"
|
||||
ENTITY_DESC_KEY_BATTERY_LIST_STATE = "battery_list_state"
|
||||
ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY = "battery_maximum_capacity"
|
||||
ENTITY_DESC_KEY_BATTERY_TEMPERATURE = "battery_temperature"
|
||||
@@ -157,11 +158,13 @@ ENTITY_DESC_KEY_HUMIDITY = "humidity"
|
||||
ENTITY_DESC_KEY_ILLUMINANCE = "illuminance"
|
||||
ENTITY_DESC_KEY_PRESSURE = "pressure"
|
||||
ENTITY_DESC_KEY_SIGNAL_STRENGTH = "signal_strength"
|
||||
ENTITY_DESC_KEY_TEMPERATURE = "temperature"
|
||||
ENTITY_DESC_KEY_TARGET_TEMPERATURE = "target_temperature"
|
||||
ENTITY_DESC_KEY_UV_INDEX = "uv_index"
|
||||
ENTITY_DESC_KEY_MEASUREMENT = "measurement"
|
||||
ENTITY_DESC_KEY_TOTAL_INCREASING = "total_increasing"
|
||||
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_POWER = "energy_production_power"
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME = "energy_production_time"
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL = "energy_production_total"
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TODAY = "energy_production_today"
|
||||
|
||||
@@ -75,12 +75,10 @@ from .models import (
|
||||
ZWaveValueDiscoverySchema,
|
||||
ZwaveValueID,
|
||||
)
|
||||
from .sensor import DISCOVERY_SCHEMAS as SENSOR_SCHEMAS
|
||||
|
||||
NEW_DISCOVERY_SCHEMAS: dict[Platform, list[NewZWaveDiscoverySchema]] = {
|
||||
Platform.BINARY_SENSOR: BINARY_SENSOR_SCHEMAS,
|
||||
Platform.EVENT: EVENT_SCHEMAS,
|
||||
Platform.SENSOR: SENSOR_SCHEMAS,
|
||||
}
|
||||
SUPPORTED_PLATFORMS = tuple(NEW_DISCOVERY_SCHEMAS)
|
||||
|
||||
@@ -207,13 +205,14 @@ DISCOVERY_SCHEMAS = [
|
||||
FanValueMapping(speeds=[(1, 33), (34, 67), (68, 99)]),
|
||||
),
|
||||
),
|
||||
# GE/Jasco - In-Wall Smart Fan Controls - 14314 / ZW4002
|
||||
# GE/Jasco - In-Wall Smart Fan Controls - 14287 / 55258 / ZW4002 and 14314 / ZW4002
|
||||
ZWaveDiscoverySchema(
|
||||
platform=Platform.FAN,
|
||||
hint="has_fan_value_mapping",
|
||||
manufacturer_id={0x0063},
|
||||
product_id={
|
||||
0x3131,
|
||||
0x3337, # 14287 / 55258 / ZW4002
|
||||
0x3138, # 14314 / ZW4002
|
||||
},
|
||||
product_type={0x4944},
|
||||
@@ -222,18 +221,6 @@ DISCOVERY_SCHEMAS = [
|
||||
FanValueMapping(speeds=[(1, 32), (33, 66), (67, 99)]),
|
||||
),
|
||||
),
|
||||
# GE/Jasco - In-Wall Smart Fan Controls - 14287 / 55258 / ZW4002
|
||||
ZWaveDiscoverySchema(
|
||||
platform=Platform.FAN,
|
||||
hint="has_fan_value_mapping",
|
||||
manufacturer_id={0x0063},
|
||||
product_id={0x3337},
|
||||
product_type={0x4944},
|
||||
primary_value=SWITCH_MULTILEVEL_CURRENT_VALUE_SCHEMA,
|
||||
data_template=FixedFanValueMappingDataTemplate(
|
||||
FanValueMapping(speeds=[(1, 33), (34, 66), (67, 99)]),
|
||||
),
|
||||
),
|
||||
# GE/Jasco - In-Wall Smart Fan Controls - 58446 / ZWA4013
|
||||
ZWaveDiscoverySchema(
|
||||
platform=Platform.FAN,
|
||||
@@ -891,7 +878,7 @@ DISCOVERY_SCHEMAS = [
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.BATTERY},
|
||||
type={ValueType.NUMBER},
|
||||
property={"maximumCapacity"},
|
||||
property={"level", "maximumCapacity"},
|
||||
),
|
||||
data_template=NumericSensorDataTemplate(),
|
||||
),
|
||||
@@ -1577,17 +1564,10 @@ def check_value(
|
||||
):
|
||||
return False
|
||||
# check available cc specific
|
||||
if schema.all_available_cc_specific is not None and (
|
||||
value.metadata.cc_specific is None
|
||||
or not all(
|
||||
key in value.metadata.cc_specific and value.metadata.cc_specific[key] == val
|
||||
for key, val in schema.all_available_cc_specific
|
||||
)
|
||||
):
|
||||
return False
|
||||
if schema.any_available_cc_specific is not None and (
|
||||
value.metadata.cc_specific is None
|
||||
or not any(
|
||||
if (
|
||||
schema.any_available_cc_specific is not None
|
||||
and value.metadata.cc_specific is not None
|
||||
and not any(
|
||||
key in value.metadata.cc_specific and value.metadata.cc_specific[key] == val
|
||||
for key, val in schema.any_available_cc_specific
|
||||
)
|
||||
|
||||
@@ -43,6 +43,7 @@ from zwave_js_server.const.command_class.multilevel_sensor import (
|
||||
POWER_SENSORS,
|
||||
PRESSURE_SENSORS,
|
||||
SIGNAL_STRENGTH_SENSORS,
|
||||
TEMPERATURE_SENSORS,
|
||||
UNIT_A_WEIGHTED_DECIBELS,
|
||||
UNIT_AMPERE as SENSOR_UNIT_AMPERE,
|
||||
UNIT_BTU_H,
|
||||
@@ -130,6 +131,7 @@ from homeassistant.const import (
|
||||
)
|
||||
|
||||
from .const import (
|
||||
ENTITY_DESC_KEY_BATTERY_LEVEL,
|
||||
ENTITY_DESC_KEY_BATTERY_LIST_STATE,
|
||||
ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY,
|
||||
ENTITY_DESC_KEY_BATTERY_TEMPERATURE,
|
||||
@@ -137,6 +139,7 @@ from .const import (
|
||||
ENTITY_DESC_KEY_CO2,
|
||||
ENTITY_DESC_KEY_CURRENT,
|
||||
ENTITY_DESC_KEY_ENERGY_MEASUREMENT,
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_POWER,
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME,
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TODAY,
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL,
|
||||
@@ -149,6 +152,7 @@ from .const import (
|
||||
ENTITY_DESC_KEY_PRESSURE,
|
||||
ENTITY_DESC_KEY_SIGNAL_STRENGTH,
|
||||
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
|
||||
ENTITY_DESC_KEY_TEMPERATURE,
|
||||
ENTITY_DESC_KEY_TOTAL_INCREASING,
|
||||
ENTITY_DESC_KEY_UV_INDEX,
|
||||
ENTITY_DESC_KEY_VOLTAGE,
|
||||
@@ -163,6 +167,7 @@ ENERGY_PRODUCTION_DEVICE_CLASS_MAP: dict[str, list[EnergyProductionParameter]] =
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL: [
|
||||
EnergyProductionParameter.TOTAL_PRODUCTION
|
||||
],
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_POWER: [EnergyProductionParameter.POWER],
|
||||
}
|
||||
|
||||
|
||||
@@ -184,6 +189,7 @@ MULTILEVEL_SENSOR_DEVICE_CLASS_MAP: dict[str, list[MultilevelSensorType]] = {
|
||||
ENTITY_DESC_KEY_POWER: POWER_SENSORS,
|
||||
ENTITY_DESC_KEY_PRESSURE: PRESSURE_SENSORS,
|
||||
ENTITY_DESC_KEY_SIGNAL_STRENGTH: SIGNAL_STRENGTH_SENSORS,
|
||||
ENTITY_DESC_KEY_TEMPERATURE: TEMPERATURE_SENSORS,
|
||||
ENTITY_DESC_KEY_VOLTAGE: VOLTAGE_SENSORS,
|
||||
ENTITY_DESC_KEY_UV_INDEX: [MultilevelSensorType.ULTRAVIOLET],
|
||||
}
|
||||
@@ -332,6 +338,10 @@ class NumericSensorDataTemplate(BaseDiscoverySchemaDataTemplate):
|
||||
def resolve_data(self, value: ZwaveValue) -> NumericSensorDataTemplateData:
|
||||
"""Resolve helper class data for a discovered value."""
|
||||
|
||||
if value.command_class == CommandClass.BATTERY and value.property_ == "level":
|
||||
return NumericSensorDataTemplateData(
|
||||
ENTITY_DESC_KEY_BATTERY_LEVEL, PERCENTAGE
|
||||
)
|
||||
if value.command_class == CommandClass.BATTERY and value.property_ in (
|
||||
"chargingStatus",
|
||||
"rechargeOrReplace",
|
||||
|
||||
@@ -149,8 +149,6 @@ class ZWaveValueDiscoverySchema(DataclassMustHaveAtLeastOne):
|
||||
any_available_states: set[tuple[int, str]] | None = None
|
||||
# [optional] the value's states map must include ANY of these keys
|
||||
any_available_states_keys: set[int] | None = None
|
||||
# [optional] the value's cc specific map must include ALL of these key/value pairs
|
||||
all_available_cc_specific: set[tuple[Any, Any]] | None = None
|
||||
# [optional] the value's cc specific map must include ANY of these key/value pairs
|
||||
any_available_cc_specific: set[tuple[Any, Any]] | None = None
|
||||
# [optional] the value's value must match this value
|
||||
|
||||
@@ -4,53 +4,21 @@ from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable, Mapping
|
||||
from dataclasses import dataclass
|
||||
from enum import IntEnum
|
||||
from typing import Any, cast
|
||||
|
||||
import voluptuous as vol
|
||||
from zwave_js_server.const import CommandClass, RssiError
|
||||
from zwave_js_server.const.command_class.energy_production import (
|
||||
CC_SPECIFIC_PARAMETER,
|
||||
CC_SPECIFIC_SCALE as ENERGY_PRODUCTION_CC_SPECIFIC_SCALE,
|
||||
EnergyProductionParameter,
|
||||
PowerScale,
|
||||
)
|
||||
from zwave_js_server.const.command_class.meter import (
|
||||
CC_SPECIFIC_METER_TYPE,
|
||||
CC_SPECIFIC_SCALE as METER_CC_SPECIFIC_SCALE,
|
||||
RESET_METER_OPTION_TARGET_VALUE,
|
||||
RESET_METER_OPTION_TYPE,
|
||||
VALUE_PROPERTY,
|
||||
ElectricScale,
|
||||
MeterType,
|
||||
)
|
||||
from zwave_js_server.const.command_class.multilevel_sensor import (
|
||||
CC_SPECIFIC_SCALE as MULTILEVEL_SENSOR_CC_SPECIFIC_SCALE,
|
||||
CC_SPECIFIC_SENSOR_TYPE,
|
||||
TEMPERATURE_SENSORS,
|
||||
TemperatureScale,
|
||||
)
|
||||
from zwave_js_server.exceptions import (
|
||||
BaseZwaveJSServerError,
|
||||
RssiErrorReceived,
|
||||
UnknownValueData,
|
||||
)
|
||||
from zwave_js_server.exceptions import BaseZwaveJSServerError, RssiErrorReceived
|
||||
from zwave_js_server.model.controller import Controller
|
||||
from zwave_js_server.model.controller.statistics import ControllerStatistics
|
||||
from zwave_js_server.model.driver import Driver
|
||||
from zwave_js_server.model.node import Node as ZwaveNode
|
||||
from zwave_js_server.model.node.statistics import NodeStatistics
|
||||
from zwave_js_server.model.value import Value as ZwaveValue
|
||||
from zwave_js_server.util.command_class.energy_production import (
|
||||
get_energy_production_scale_type,
|
||||
)
|
||||
from zwave_js_server.util.command_class.meter import (
|
||||
get_meter_scale_type,
|
||||
get_meter_type,
|
||||
)
|
||||
from zwave_js_server.util.command_class.multilevel_sensor import (
|
||||
get_multilevel_sensor_scale_type,
|
||||
)
|
||||
from zwave_js_server.util.command_class.meter import get_meter_type
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
DOMAIN as SENSOR_DOMAIN,
|
||||
@@ -59,7 +27,6 @@ from homeassistant.components.sensor import (
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import (
|
||||
CONCENTRATION_PARTS_PER_MILLION,
|
||||
LIGHT_LUX,
|
||||
@@ -67,7 +34,6 @@ from homeassistant.const import (
|
||||
SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
UV_INDEX,
|
||||
EntityCategory,
|
||||
Platform,
|
||||
UnitOfElectricCurrent,
|
||||
UnitOfElectricPotential,
|
||||
UnitOfEnergy,
|
||||
@@ -89,6 +55,7 @@ from .const import (
|
||||
ATTR_METER_TYPE_NAME,
|
||||
ATTR_VALUE,
|
||||
DOMAIN,
|
||||
ENTITY_DESC_KEY_BATTERY_LEVEL,
|
||||
ENTITY_DESC_KEY_BATTERY_LIST_STATE,
|
||||
ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY,
|
||||
ENTITY_DESC_KEY_BATTERY_TEMPERATURE,
|
||||
@@ -96,6 +63,7 @@ from .const import (
|
||||
ENTITY_DESC_KEY_CO2,
|
||||
ENTITY_DESC_KEY_CURRENT,
|
||||
ENTITY_DESC_KEY_ENERGY_MEASUREMENT,
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_POWER,
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TIME,
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TODAY,
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_TOTAL,
|
||||
@@ -108,32 +76,35 @@ from .const import (
|
||||
ENTITY_DESC_KEY_PRESSURE,
|
||||
ENTITY_DESC_KEY_SIGNAL_STRENGTH,
|
||||
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
|
||||
ENTITY_DESC_KEY_TEMPERATURE,
|
||||
ENTITY_DESC_KEY_TOTAL_INCREASING,
|
||||
ENTITY_DESC_KEY_UV_INDEX,
|
||||
ENTITY_DESC_KEY_VOLTAGE,
|
||||
LOGGER,
|
||||
SERVICE_RESET_METER,
|
||||
)
|
||||
from .discovery import ZwaveDiscoveryInfo
|
||||
from .discovery_data_template import (
|
||||
NumericSensorDataTemplate,
|
||||
NumericSensorDataTemplateData,
|
||||
)
|
||||
from .entity import NewZwaveDiscoveryInfo, ZWaveBaseEntity
|
||||
from .entity import ZWaveBaseEntity
|
||||
from .helpers import get_device_info, get_valueless_base_unique_id
|
||||
from .migrate import async_migrate_statistics_sensors
|
||||
from .models import (
|
||||
NewZWaveDiscoverySchema,
|
||||
ValueType,
|
||||
ZwaveDiscoveryInfo,
|
||||
ZwaveJSConfigEntry,
|
||||
ZWaveValueDiscoverySchema,
|
||||
)
|
||||
from .models import ZwaveJSConfigEntry
|
||||
|
||||
PARALLEL_UPDATES = 0
|
||||
|
||||
|
||||
# These descriptions should have a non None unit of measurement.
|
||||
ENTITY_DESCRIPTION_KEY_UNIT_MAP: dict[tuple[str, str], SensorEntityDescription] = {
|
||||
(ENTITY_DESC_KEY_BATTERY_LEVEL, PERCENTAGE): SensorEntityDescription(
|
||||
key=ENTITY_DESC_KEY_BATTERY_LEVEL,
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
(ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY, PERCENTAGE): SensorEntityDescription(
|
||||
key=ENTITY_DESC_KEY_BATTERY_MAXIMUM_CAPACITY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
@@ -253,6 +224,21 @@ ENTITY_DESCRIPTION_KEY_UNIT_MAP: dict[tuple[str, str], SensorEntityDescription]
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
|
||||
),
|
||||
(ENTITY_DESC_KEY_TEMPERATURE, UnitOfTemperature.CELSIUS): SensorEntityDescription(
|
||||
key=ENTITY_DESC_KEY_TEMPERATURE,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
),
|
||||
(
|
||||
ENTITY_DESC_KEY_TEMPERATURE,
|
||||
UnitOfTemperature.FAHRENHEIT,
|
||||
): SensorEntityDescription(
|
||||
key=ENTITY_DESC_KEY_TEMPERATURE,
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
),
|
||||
(
|
||||
ENTITY_DESC_KEY_TARGET_TEMPERATURE,
|
||||
UnitOfTemperature.CELSIUS,
|
||||
@@ -303,6 +289,16 @@ ENTITY_DESCRIPTION_KEY_UNIT_MAP: dict[tuple[str, str], SensorEntityDescription]
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
native_unit_of_measurement=UnitOfEnergy.WATT_HOUR,
|
||||
),
|
||||
(
|
||||
ENTITY_DESC_KEY_ENERGY_PRODUCTION_POWER,
|
||||
UnitOfPower.WATT,
|
||||
): SensorEntityDescription(
|
||||
key=ENTITY_DESC_KEY_POWER,
|
||||
name="Energy production power",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
),
|
||||
}
|
||||
|
||||
# These descriptions are without unit of measurement.
|
||||
@@ -605,7 +601,7 @@ async def async_setup_entry(
|
||||
assert driver is not None # Driver is ready before platforms are loaded.
|
||||
|
||||
@callback
|
||||
def async_add_sensor(info: ZwaveDiscoveryInfo | NewZwaveDiscoveryInfo) -> None:
|
||||
def async_add_sensor(info: ZwaveDiscoveryInfo) -> None:
|
||||
"""Add Z-Wave Sensor."""
|
||||
entities: list[ZWaveBaseEntity] = []
|
||||
|
||||
@@ -616,13 +612,7 @@ async def async_setup_entry(
|
||||
|
||||
entity_description = get_entity_description(data)
|
||||
|
||||
if isinstance(info, NewZwaveDiscoveryInfo) and (
|
||||
entity_class := info.entity_class
|
||||
) in (NewZWaveNumericSensor, NewZWaveMeterSensor):
|
||||
entities.append(entity_class(config_entry, driver, info))
|
||||
elif isinstance(info, NewZwaveDiscoveryInfo):
|
||||
pass # other entity classes are not migrated yet
|
||||
elif info.platform_hint == "numeric_sensor":
|
||||
if info.platform_hint == "numeric_sensor":
|
||||
entities.append(
|
||||
ZWaveNumericSensor(
|
||||
config_entry,
|
||||
@@ -812,62 +802,6 @@ class ZWaveNumericSensor(ZwaveSensor):
|
||||
return float(self.info.primary_value.value)
|
||||
|
||||
|
||||
class NewZWaveNumericSensor(ZWaveBaseEntity, SensorEntity):
|
||||
"""Representation of a Z-Wave Numeric sensor."""
|
||||
|
||||
_attr_force_update = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config_entry: ConfigEntry,
|
||||
driver: Driver,
|
||||
info: NewZwaveDiscoveryInfo,
|
||||
) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(config_entry, driver, info)
|
||||
entity_description = info.entity_description
|
||||
if not entity_description.name or entity_description.name is UNDEFINED:
|
||||
self._attr_name = self.generate_name(include_value_name=True)
|
||||
self._scale_type = self._get_scale_type()
|
||||
|
||||
def _get_scale_type(self) -> IntEnum | None:
|
||||
"""Return the scale type of the value."""
|
||||
primary_value = self.info.primary_value
|
||||
scale_type_function: Callable[[ZwaveValue], IntEnum] | None
|
||||
match primary_value.command_class:
|
||||
case CommandClass.METER:
|
||||
scale_type_function = get_meter_scale_type
|
||||
case CommandClass.SENSOR_MULTILEVEL:
|
||||
scale_type_function = get_multilevel_sensor_scale_type
|
||||
case CommandClass.ENERGY_PRODUCTION:
|
||||
scale_type_function = get_energy_production_scale_type
|
||||
case _:
|
||||
scale_type_function = None
|
||||
if scale_type_function is None:
|
||||
return None
|
||||
try:
|
||||
scale_type = scale_type_function(primary_value)
|
||||
except UnknownValueData:
|
||||
return None
|
||||
|
||||
return scale_type
|
||||
|
||||
@callback
|
||||
def on_value_update(self) -> None:
|
||||
"""Handle scale changes for this value on value updated event."""
|
||||
# TODO: Try to limit this to metadata updated event. # pylint: disable=fixme
|
||||
scale_type = self._get_scale_type()
|
||||
if scale_type is not self._scale_type:
|
||||
self.hass.config_entries.async_schedule_reload(self.config_entry.entry_id)
|
||||
|
||||
@property
|
||||
def native_value(self) -> float | None:
|
||||
"""Return state of the sensor."""
|
||||
if self.info.primary_value.value is None:
|
||||
return None
|
||||
return float(self.info.primary_value.value)
|
||||
|
||||
|
||||
class ZWaveMeterSensor(ZWaveNumericSensor):
|
||||
"""Representation of a Z-Wave Meter CC sensor."""
|
||||
|
||||
@@ -908,46 +842,6 @@ class ZWaveMeterSensor(ZWaveNumericSensor):
|
||||
)
|
||||
|
||||
|
||||
class NewZWaveMeterSensor(NewZWaveNumericSensor):
|
||||
"""Representation of a Z-Wave Meter CC sensor."""
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> Mapping[str, int | str] | None:
|
||||
"""Return extra state attributes."""
|
||||
meter_type = get_meter_type(self.info.primary_value)
|
||||
return {
|
||||
ATTR_METER_TYPE: meter_type.value,
|
||||
ATTR_METER_TYPE_NAME: meter_type.name,
|
||||
}
|
||||
|
||||
async def async_reset_meter(
|
||||
self, meter_type: int | None = None, value: int | None = None
|
||||
) -> None:
|
||||
"""Reset meter(s) on device."""
|
||||
node = self.info.node
|
||||
endpoint = self.info.primary_value.endpoint or 0
|
||||
options = {}
|
||||
if meter_type is not None:
|
||||
options[RESET_METER_OPTION_TYPE] = meter_type
|
||||
if value is not None:
|
||||
options[RESET_METER_OPTION_TARGET_VALUE] = value
|
||||
args = [options] if options else []
|
||||
try:
|
||||
await node.endpoints[endpoint].async_invoke_cc_api(
|
||||
CommandClass.METER, "reset", *args, wait_for_result=False
|
||||
)
|
||||
except BaseZwaveJSServerError as err:
|
||||
raise HomeAssistantError(
|
||||
f"Failed to reset meters on node {node} endpoint {endpoint}: {err}"
|
||||
) from err
|
||||
LOGGER.debug(
|
||||
"Meters on node %s endpoint %s reset with the following options: %s",
|
||||
node,
|
||||
endpoint,
|
||||
options,
|
||||
)
|
||||
|
||||
|
||||
class ZWaveListSensor(ZwaveSensor):
|
||||
"""Representation of a Z-Wave Numeric sensor with multiple states."""
|
||||
|
||||
@@ -1241,103 +1135,3 @@ class ZWaveStatisticsSensor(SensorEntity):
|
||||
|
||||
# Set initial state
|
||||
self._set_statistics(self.statistics_src.statistics)
|
||||
|
||||
|
||||
DISCOVERY_SCHEMAS: list[NewZWaveDiscoverySchema] = [
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.BATTERY},
|
||||
type={ValueType.NUMBER},
|
||||
property={"level"},
|
||||
),
|
||||
entity_class=NewZWaveNumericSensor,
|
||||
entity_description=SensorEntityDescription(
|
||||
key="battery_level",
|
||||
device_class=SensorDeviceClass.BATTERY,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
),
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.SENSOR_MULTILEVEL},
|
||||
type={ValueType.NUMBER},
|
||||
all_available_cc_specific={
|
||||
(MULTILEVEL_SENSOR_CC_SPECIFIC_SCALE, TemperatureScale.CELSIUS),
|
||||
},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_SENSOR_TYPE, sensor_type)
|
||||
for sensor_type in TEMPERATURE_SENSORS
|
||||
},
|
||||
),
|
||||
entity_class=NewZWaveNumericSensor,
|
||||
entity_description=SensorEntityDescription(
|
||||
key="temperature_celsius",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
),
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.SENSOR_MULTILEVEL},
|
||||
type={ValueType.NUMBER},
|
||||
all_available_cc_specific={
|
||||
(MULTILEVEL_SENSOR_CC_SPECIFIC_SCALE, TemperatureScale.FAHRENHEIT),
|
||||
},
|
||||
any_available_cc_specific={
|
||||
(CC_SPECIFIC_SENSOR_TYPE, sensor_type)
|
||||
for sensor_type in TEMPERATURE_SENSORS
|
||||
},
|
||||
),
|
||||
entity_class=NewZWaveNumericSensor,
|
||||
entity_description=SensorEntityDescription(
|
||||
key="temperature_fahrenheit",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfTemperature.FAHRENHEIT,
|
||||
),
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.METER},
|
||||
type={ValueType.NUMBER},
|
||||
property={VALUE_PROPERTY},
|
||||
all_available_cc_specific={
|
||||
(CC_SPECIFIC_METER_TYPE, MeterType.ELECTRIC),
|
||||
(METER_CC_SPECIFIC_SCALE, ElectricScale.AMPERE),
|
||||
},
|
||||
),
|
||||
entity_class=NewZWaveMeterSensor,
|
||||
entity_description=SensorEntityDescription(
|
||||
key="meter_current",
|
||||
device_class=SensorDeviceClass.CURRENT,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfElectricCurrent.AMPERE,
|
||||
),
|
||||
),
|
||||
NewZWaveDiscoverySchema(
|
||||
platform=Platform.SENSOR,
|
||||
primary_value=ZWaveValueDiscoverySchema(
|
||||
command_class={CommandClass.ENERGY_PRODUCTION},
|
||||
type={ValueType.NUMBER},
|
||||
all_available_cc_specific={
|
||||
(CC_SPECIFIC_PARAMETER, EnergyProductionParameter.POWER),
|
||||
(ENERGY_PRODUCTION_CC_SPECIFIC_SCALE, PowerScale.WATTS),
|
||||
},
|
||||
),
|
||||
entity_class=NewZWaveNumericSensor,
|
||||
entity_description=SensorEntityDescription(
|
||||
key="energy_production_power",
|
||||
name="Energy production power",
|
||||
device_class=SensorDeviceClass.POWER,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
native_unit_of_measurement=UnitOfPower.WATT,
|
||||
),
|
||||
),
|
||||
]
|
||||
|
||||
@@ -303,10 +303,6 @@
|
||||
}
|
||||
},
|
||||
"issues": {
|
||||
"deprecated_legacy_door_state": {
|
||||
"description": "The binary sensor `{entity_id}` is deprecated because it has been replaced with the opening state sensor `{opening_state_entity_id}`.\n\nThe entity was found in the following automations or scripts:\n{items}\n\nPlease update the above automations or scripts to use the opening state sensor `{opening_state_entity_id}` and disable the binary sensor `{entity_id}` to fix this issue.\n\nNote that `{opening_state_entity_id}` reports three states:\n- Closed\n- Open\n- Tilted (if supported by the device).",
|
||||
"title": "Deprecation: {entity_name}"
|
||||
},
|
||||
"device_config_file_changed": {
|
||||
"fix_flow": {
|
||||
"abort": {
|
||||
|
||||
@@ -39,7 +39,7 @@ class DomainSpec:
|
||||
class NumericalDomainSpec(DomainSpec):
|
||||
"""DomainSpec with an optional value converter for numerical triggers."""
|
||||
|
||||
value_converter: Callable[[float], float] | None = None
|
||||
value_converter: Callable[[Any], float] | None = None
|
||||
"""Optional converter for numerical values (e.g. uint8 → percentage)."""
|
||||
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user