mirror of
https://github.com/home-assistant/core.git
synced 2026-03-24 16:28:14 +01:00
Compare commits
79 Commits
setpoint_c
...
make-secur
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8474aa0d51 | ||
|
|
d46723c443 | ||
|
|
e151c9c78c | ||
|
|
7287c847f4 | ||
|
|
152e17aee7 | ||
|
|
c53adcb73b | ||
|
|
dab4a72128 | ||
|
|
c94e10efa7 | ||
|
|
ca5ea9ea35 | ||
|
|
63a09d8e28 | ||
|
|
b5a3c2c014 | ||
|
|
ef887c8edc | ||
|
|
d0eb90274d | ||
|
|
cac375dafb | ||
|
|
2c20b62229 | ||
|
|
b5c84b6b7a | ||
|
|
e5f9668ded | ||
|
|
e214ce690a | ||
|
|
a2c64f65e1 | ||
|
|
8bad30234a | ||
|
|
c4545b42d8 | ||
|
|
b0a60d1c42 | ||
|
|
e1e14bee10 | ||
|
|
3529aff4b1 | ||
|
|
16e314ccf1 | ||
|
|
d634fbcad7 | ||
|
|
b84ca80d55 | ||
|
|
41c2c621f0 | ||
|
|
b230e62868 | ||
|
|
12528ec128 | ||
|
|
7f4a7670a2 | ||
|
|
9bdc1b777e | ||
|
|
995e982d7f | ||
|
|
b92698e3d5 | ||
|
|
225052b932 | ||
|
|
34ae51677f | ||
|
|
3616a52b37 | ||
|
|
0128372258 | ||
|
|
21863cd9d7 | ||
|
|
d67caec5c1 | ||
|
|
8286014ae1 | ||
|
|
1ff8d2279a | ||
|
|
5dcbc1d5d9 | ||
|
|
3068653cc7 | ||
|
|
61b1a45889 | ||
|
|
573d4eba02 | ||
|
|
09895aa601 | ||
|
|
aa6a4c7eab | ||
|
|
662c44b125 | ||
|
|
5a80087cf4 | ||
|
|
c28dc32168 | ||
|
|
eef3472c43 | ||
|
|
f9bd9f4982 | ||
|
|
e4620a208d | ||
|
|
c6c5661b4b | ||
|
|
d0154e5019 | ||
|
|
16fb7ed21e | ||
|
|
d0a751abe4 | ||
|
|
a04b168a19 | ||
|
|
e9576452b2 | ||
|
|
c8c6815efd | ||
|
|
60ef69c21d | ||
|
|
d5b7792208 | ||
|
|
fdfc2f4845 | ||
|
|
184d834a91 | ||
|
|
0c98bf2676 | ||
|
|
229e1ee26b | ||
|
|
fdd2db6f23 | ||
|
|
2886863000 | ||
|
|
bf4170938c | ||
|
|
6b84815c57 | ||
|
|
01b873f3bc | ||
|
|
66b1728c13 | ||
|
|
d11668b868 | ||
|
|
ed3f70bc3f | ||
|
|
008eb39c3b | ||
|
|
a085d91a0d | ||
|
|
6395a0abd0 | ||
|
|
0de2e689f1 |
6
.github/copilot-instructions.md
vendored
6
.github/copilot-instructions.md
vendored
@@ -1,6 +1,12 @@
|
||||
<!-- 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@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
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@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
|
||||
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
|
||||
id: integrations
|
||||
with:
|
||||
filters: .integration_paths.yaml
|
||||
|
||||
@@ -137,6 +137,7 @@ homeassistant.components.calendar.*
|
||||
homeassistant.components.cambridge_audio.*
|
||||
homeassistant.components.camera.*
|
||||
homeassistant.components.canary.*
|
||||
homeassistant.components.casper_glow.*
|
||||
homeassistant.components.cert_expiry.*
|
||||
homeassistant.components.clickatell.*
|
||||
homeassistant.components.clicksend.*
|
||||
|
||||
6
CODEOWNERS
generated
6
CODEOWNERS
generated
@@ -273,6 +273,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/cambridge_audio/ @noahhusby
|
||||
/homeassistant/components/camera/ @home-assistant/core
|
||||
/tests/components/camera/ @home-assistant/core
|
||||
/homeassistant/components/casper_glow/ @mikeodr
|
||||
/tests/components/casper_glow/ @mikeodr
|
||||
/homeassistant/components/cast/ @emontnemery
|
||||
/tests/components/cast/ @emontnemery
|
||||
/homeassistant/components/ccm15/ @ocalvo
|
||||
@@ -947,6 +949,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/lg_thinq/ @LG-ThinQ-Integration
|
||||
/homeassistant/components/libre_hardware_monitor/ @Sab44
|
||||
/tests/components/libre_hardware_monitor/ @Sab44
|
||||
/homeassistant/components/lichess/ @aryanhasgithub
|
||||
/tests/components/lichess/ @aryanhasgithub
|
||||
/homeassistant/components/lidarr/ @tkdrob
|
||||
/tests/components/lidarr/ @tkdrob
|
||||
/homeassistant/components/liebherr/ @mettolen
|
||||
@@ -1699,6 +1703,8 @@ 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,6 +247,7 @@ DEFAULT_INTEGRATIONS = {
|
||||
"humidity",
|
||||
"motion",
|
||||
"occupancy",
|
||||
"temperature",
|
||||
"window",
|
||||
}
|
||||
DEFAULT_INTEGRATIONS_RECOVERY_MODE = {
|
||||
|
||||
36
homeassistant/components/airq/diagnostics.py
Normal file
36
homeassistant/components/airq/diagnostics.py
Normal file
@@ -0,0 +1,36 @@
|
||||
"""Diagnostics support for air-Q."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD, CONF_UNIQUE_ID
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from . import AirQConfigEntry
|
||||
|
||||
REDACT_CONFIG = {CONF_PASSWORD, CONF_UNIQUE_ID, CONF_IP_ADDRESS, "title"}
|
||||
REDACT_DEVICE_INFO = {"identifiers", "name"}
|
||||
REDACT_COORDINATOR_DATA = {"DeviceID"}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: AirQConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
return {
|
||||
"config_entry": async_redact_data(entry.as_dict(), REDACT_CONFIG),
|
||||
"device_info": async_redact_data(
|
||||
dict(coordinator.device_info), REDACT_DEVICE_INFO
|
||||
),
|
||||
"coordinator_data": async_redact_data(
|
||||
coordinator.data, REDACT_COORDINATOR_DATA
|
||||
),
|
||||
"options": {
|
||||
"clip_negative": coordinator.clip_negative,
|
||||
"return_average": coordinator.return_average,
|
||||
},
|
||||
}
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["androidtvremote2"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["androidtvremote2==0.2.3"],
|
||||
"requirements": ["androidtvremote2==0.3.1"],
|
||||
"zeroconf": ["_androidtvremote2._tcp.local."]
|
||||
}
|
||||
|
||||
@@ -662,7 +662,8 @@ class PipelineRun:
|
||||
"""Emit run start event."""
|
||||
self._device_id = device_id
|
||||
self._satellite_id = satellite_id
|
||||
self._start_debug_recording_thread()
|
||||
if self.start_stage in (PipelineStage.WAKE_WORD, PipelineStage.STT):
|
||||
self._start_debug_recording_thread()
|
||||
|
||||
data: dict[str, Any] = {
|
||||
"pipeline": self.pipeline.id,
|
||||
@@ -1504,9 +1505,7 @@ class PipelineRun:
|
||||
|
||||
def _start_debug_recording_thread(self) -> None:
|
||||
"""Start thread to record wake/stt audio if debug_recording_dir is set."""
|
||||
if self.debug_recording_thread is not None:
|
||||
# Already started
|
||||
return
|
||||
assert self.debug_recording_thread is None
|
||||
|
||||
# Directory to save audio for each pipeline run.
|
||||
# Configured in YAML for assist_pipeline.
|
||||
|
||||
@@ -155,7 +155,6 @@ _EXPERIMENTAL_TRIGGER_PLATFORMS = {
|
||||
"gate",
|
||||
"humidifier",
|
||||
"humidity",
|
||||
"input_boolean",
|
||||
"lawn_mower",
|
||||
"light",
|
||||
"lock",
|
||||
@@ -169,6 +168,7 @@ _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 = 2
|
||||
SECURETAR_CREATE_VERSION = 3
|
||||
|
||||
@@ -21,6 +21,6 @@
|
||||
"bluetooth-auto-recovery==1.5.3",
|
||||
"bluetooth-data-tools==1.28.4",
|
||||
"dbus-fast==3.1.2",
|
||||
"habluetooth==5.10.2"
|
||||
"habluetooth==5.11.1"
|
||||
]
|
||||
}
|
||||
|
||||
39
homeassistant/components/casper_glow/__init__.py
Normal file
39
homeassistant/components/casper_glow/__init__.py
Normal file
@@ -0,0 +1,39 @@
|
||||
"""The Casper Glow integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pycasperglow import CasperGlow
|
||||
|
||||
from homeassistant.components import bluetooth
|
||||
from homeassistant.const import CONF_ADDRESS, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [Platform.LIGHT]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:
|
||||
"""Set up Casper Glow from a config entry."""
|
||||
address: str = entry.data[CONF_ADDRESS]
|
||||
ble_device = bluetooth.async_ble_device_from_address(hass, address.upper(), True)
|
||||
if not ble_device:
|
||||
raise ConfigEntryNotReady(
|
||||
f"Could not find Casper Glow device with address {address}"
|
||||
)
|
||||
|
||||
glow = CasperGlow(ble_device)
|
||||
coordinator = CasperGlowCoordinator(hass, glow, entry.title)
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
entry.async_on_unload(coordinator.async_start())
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: CasperGlowConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
|
||||
151
homeassistant/components/casper_glow/config_flow.py
Normal file
151
homeassistant/components/casper_glow/config_flow.py
Normal file
@@ -0,0 +1,151 @@
|
||||
"""Config flow for Casper Glow integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from bluetooth_data_tools import human_readable_name
|
||||
from pycasperglow import CasperGlow, CasperGlowError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothServiceInfoBleak,
|
||||
async_discovered_service_info,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_ADDRESS
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
|
||||
from .const import DOMAIN, LOCAL_NAMES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class CasperGlowConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Casper Glow."""
|
||||
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""Initialize the config flow."""
|
||||
self._discovery_info: BluetoothServiceInfoBleak | None = None
|
||||
self._discovered_devices: dict[str, BluetoothServiceInfoBleak] = {}
|
||||
|
||||
async def async_step_bluetooth(
|
||||
self, discovery_info: BluetoothServiceInfoBleak
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the bluetooth discovery step."""
|
||||
await self.async_set_unique_id(format_mac(discovery_info.address))
|
||||
self._abort_if_unique_id_configured()
|
||||
self._discovery_info = discovery_info
|
||||
self.context["title_placeholders"] = {
|
||||
"name": human_readable_name(
|
||||
None, discovery_info.name, discovery_info.address
|
||||
)
|
||||
}
|
||||
return await self.async_step_bluetooth_confirm()
|
||||
|
||||
async def async_step_bluetooth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm a discovered Casper Glow device."""
|
||||
assert self._discovery_info is not None
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(
|
||||
title=self.context["title_placeholders"]["name"],
|
||||
data={CONF_ADDRESS: self._discovery_info.address},
|
||||
)
|
||||
glow = CasperGlow(self._discovery_info.device)
|
||||
try:
|
||||
await glow.handshake()
|
||||
except CasperGlowError:
|
||||
return self.async_abort(reason="cannot_connect")
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"Unexpected error during Casper Glow config flow "
|
||||
"(step=bluetooth_confirm, address=%s)",
|
||||
self._discovery_info.address,
|
||||
)
|
||||
return self.async_abort(reason="unknown")
|
||||
self._set_confirm_only()
|
||||
return self.async_show_form(
|
||||
step_id="bluetooth_confirm",
|
||||
description_placeholders=self.context["title_placeholders"],
|
||||
)
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the user step to pick discovered device."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
if user_input is not None:
|
||||
address = user_input[CONF_ADDRESS]
|
||||
discovery_info = self._discovered_devices[address]
|
||||
await self.async_set_unique_id(
|
||||
format_mac(discovery_info.address), raise_on_progress=False
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
glow = CasperGlow(discovery_info.device)
|
||||
try:
|
||||
await glow.handshake()
|
||||
except CasperGlowError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception(
|
||||
"Unexpected error during Casper Glow config flow "
|
||||
"(step=user, address=%s)",
|
||||
discovery_info.address,
|
||||
)
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=human_readable_name(
|
||||
None, discovery_info.name, discovery_info.address
|
||||
),
|
||||
data={
|
||||
CONF_ADDRESS: discovery_info.address,
|
||||
},
|
||||
)
|
||||
|
||||
if discovery := self._discovery_info:
|
||||
self._discovered_devices[discovery.address] = discovery
|
||||
else:
|
||||
current_addresses = self._async_current_ids(include_ignore=False)
|
||||
for discovery in async_discovered_service_info(self.hass):
|
||||
if (
|
||||
format_mac(discovery.address) in current_addresses
|
||||
or discovery.address in self._discovered_devices
|
||||
or not (
|
||||
discovery.name
|
||||
and any(
|
||||
discovery.name.startswith(local_name)
|
||||
for local_name in LOCAL_NAMES
|
||||
)
|
||||
)
|
||||
):
|
||||
continue
|
||||
self._discovered_devices[discovery.address] = discovery
|
||||
|
||||
if not self._discovered_devices:
|
||||
return self.async_abort(reason="no_devices_found")
|
||||
|
||||
data_schema = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_ADDRESS): vol.In(
|
||||
{
|
||||
service_info.address: human_readable_name(
|
||||
None, service_info.name, service_info.address
|
||||
)
|
||||
for service_info in self._discovered_devices.values()
|
||||
}
|
||||
),
|
||||
}
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=data_schema,
|
||||
errors=errors,
|
||||
)
|
||||
16
homeassistant/components/casper_glow/const.py
Normal file
16
homeassistant/components/casper_glow/const.py
Normal file
@@ -0,0 +1,16 @@
|
||||
"""Constants for the Casper Glow integration."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
from pycasperglow import BRIGHTNESS_LEVELS, DEVICE_NAME_PREFIX, DIMMING_TIME_MINUTES
|
||||
|
||||
DOMAIN = "casper_glow"
|
||||
|
||||
LOCAL_NAMES = {DEVICE_NAME_PREFIX}
|
||||
|
||||
SORTED_BRIGHTNESS_LEVELS = sorted(BRIGHTNESS_LEVELS)
|
||||
|
||||
DEFAULT_DIMMING_TIME_MINUTES: int = DIMMING_TIME_MINUTES[0]
|
||||
|
||||
# Interval between periodic state polls to catch externally-triggered changes.
|
||||
STATE_POLL_INTERVAL = timedelta(seconds=30)
|
||||
103
homeassistant/components/casper_glow/coordinator.py
Normal file
103
homeassistant/components/casper_glow/coordinator.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Coordinator for the Casper Glow integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from bleak import BleakError
|
||||
from bluetooth_data_tools import monotonic_time_coarse
|
||||
from pycasperglow import CasperGlow
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BluetoothChange,
|
||||
BluetoothScanningMode,
|
||||
BluetoothServiceInfoBleak,
|
||||
)
|
||||
from homeassistant.components.bluetooth.active_update_coordinator import (
|
||||
ActiveBluetoothDataUpdateCoordinator,
|
||||
)
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
|
||||
from .const import STATE_POLL_INTERVAL
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type CasperGlowConfigEntry = ConfigEntry[CasperGlowCoordinator]
|
||||
|
||||
|
||||
class CasperGlowCoordinator(ActiveBluetoothDataUpdateCoordinator[None]):
|
||||
"""Coordinator for Casper Glow BLE devices."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
device: CasperGlow,
|
||||
title: str,
|
||||
) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass=hass,
|
||||
logger=_LOGGER,
|
||||
address=device.address,
|
||||
mode=BluetoothScanningMode.PASSIVE,
|
||||
needs_poll_method=self._needs_poll,
|
||||
poll_method=self._async_update,
|
||||
connectable=True,
|
||||
)
|
||||
self.device = device
|
||||
self.last_dimming_time_minutes: int | None = (
|
||||
device.state.configured_dimming_time_minutes
|
||||
)
|
||||
self.title = title
|
||||
|
||||
@callback
|
||||
def _needs_poll(
|
||||
self,
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
seconds_since_last_poll: float | None,
|
||||
) -> bool:
|
||||
"""Return True if a poll is needed."""
|
||||
return (
|
||||
seconds_since_last_poll is None
|
||||
or seconds_since_last_poll >= STATE_POLL_INTERVAL.total_seconds()
|
||||
)
|
||||
|
||||
async def _async_update(self, service_info: BluetoothServiceInfoBleak) -> None:
|
||||
"""Poll device state."""
|
||||
await self.device.query_state()
|
||||
|
||||
async def _async_poll(self) -> None:
|
||||
"""Poll the device and log availability changes."""
|
||||
assert self._last_service_info
|
||||
|
||||
try:
|
||||
await self._async_poll_data(self._last_service_info)
|
||||
except BleakError as exc:
|
||||
if self.last_poll_successful:
|
||||
_LOGGER.info("%s is unavailable: %s", self.title, exc)
|
||||
self.last_poll_successful = False
|
||||
return
|
||||
except Exception:
|
||||
if self.last_poll_successful:
|
||||
_LOGGER.exception("%s: unexpected error while polling", self.title)
|
||||
self.last_poll_successful = False
|
||||
return
|
||||
finally:
|
||||
self._last_poll = monotonic_time_coarse()
|
||||
|
||||
if not self.last_poll_successful:
|
||||
_LOGGER.info("%s is back online", self.title)
|
||||
self.last_poll_successful = True
|
||||
|
||||
self._async_handle_bluetooth_poll()
|
||||
|
||||
@callback
|
||||
def _async_handle_bluetooth_event(
|
||||
self,
|
||||
service_info: BluetoothServiceInfoBleak,
|
||||
change: BluetoothChange,
|
||||
) -> None:
|
||||
"""Update BLE device reference on each advertisement."""
|
||||
self.device.set_ble_device(service_info.device)
|
||||
super()._async_handle_bluetooth_event(service_info, change)
|
||||
47
homeassistant/components/casper_glow/entity.py
Normal file
47
homeassistant/components/casper_glow/entity.py
Normal file
@@ -0,0 +1,47 @@
|
||||
"""Base entity for the Casper Glow integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable
|
||||
|
||||
from pycasperglow import CasperGlowError
|
||||
|
||||
from homeassistant.components.bluetooth.passive_update_coordinator import (
|
||||
PassiveBluetoothCoordinatorEntity,
|
||||
)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.device_registry import DeviceInfo, format_mac
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import CasperGlowCoordinator
|
||||
|
||||
|
||||
class CasperGlowEntity(PassiveBluetoothCoordinatorEntity[CasperGlowCoordinator]):
|
||||
"""Base class for Casper Glow entities."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
|
||||
"""Initialize a Casper Glow entity."""
|
||||
super().__init__(coordinator)
|
||||
self._device = coordinator.device
|
||||
self._attr_device_info = DeviceInfo(
|
||||
manufacturer="Casper",
|
||||
model="Glow",
|
||||
model_id="G01",
|
||||
connections={
|
||||
(dr.CONNECTION_BLUETOOTH, format_mac(coordinator.device.address))
|
||||
},
|
||||
)
|
||||
|
||||
async def _async_command(self, coro: Awaitable[None]) -> None:
|
||||
"""Execute a device command."""
|
||||
try:
|
||||
await coro
|
||||
except CasperGlowError as err:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="communication_error",
|
||||
translation_placeholders={"error": str(err)},
|
||||
) from err
|
||||
104
homeassistant/components/casper_glow/light.py
Normal file
104
homeassistant/components/casper_glow/light.py
Normal file
@@ -0,0 +1,104 @@
|
||||
"""Casper Glow integration light platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from pycasperglow import GlowState
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.device_registry import format_mac
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.util.percentage import (
|
||||
ordered_list_item_to_percentage,
|
||||
percentage_to_ordered_list_item,
|
||||
)
|
||||
|
||||
from .const import DEFAULT_DIMMING_TIME_MINUTES, SORTED_BRIGHTNESS_LEVELS
|
||||
from .coordinator import CasperGlowConfigEntry, CasperGlowCoordinator
|
||||
from .entity import CasperGlowEntity
|
||||
|
||||
PARALLEL_UPDATES = 1
|
||||
|
||||
|
||||
def _ha_brightness_to_device_pct(brightness: int) -> int:
|
||||
"""Convert HA brightness (1-255) to device percentage by snapping to nearest."""
|
||||
return percentage_to_ordered_list_item(
|
||||
SORTED_BRIGHTNESS_LEVELS, round(brightness * 100 / 255)
|
||||
)
|
||||
|
||||
|
||||
def _device_pct_to_ha_brightness(pct: int) -> int:
|
||||
"""Convert device brightness percentage (60-100) to HA brightness (1-255)."""
|
||||
percent = ordered_list_item_to_percentage(SORTED_BRIGHTNESS_LEVELS, pct)
|
||||
return round(percent * 255 / 100)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: CasperGlowConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the light platform for Casper Glow."""
|
||||
async_add_entities([CasperGlowLight(entry.runtime_data)])
|
||||
|
||||
|
||||
class CasperGlowLight(CasperGlowEntity, LightEntity):
|
||||
"""Representation of a Casper Glow light."""
|
||||
|
||||
_attr_supported_color_modes = {ColorMode.BRIGHTNESS}
|
||||
_attr_name = None
|
||||
|
||||
def __init__(self, coordinator: CasperGlowCoordinator) -> None:
|
||||
"""Initialize a Casper Glow light."""
|
||||
super().__init__(coordinator)
|
||||
self._attr_unique_id = format_mac(coordinator.device.address)
|
||||
self._update_from_state(coordinator.device.state)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Register state update callback when entity is added."""
|
||||
await super().async_added_to_hass()
|
||||
self.async_on_remove(
|
||||
self._device.register_callback(self._async_handle_state_update)
|
||||
)
|
||||
|
||||
@callback
|
||||
def _update_from_state(self, state: GlowState) -> None:
|
||||
"""Update entity attributes from device state."""
|
||||
if state.is_on is not None:
|
||||
self._attr_is_on = state.is_on
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
if state.brightness_level is not None:
|
||||
self._attr_brightness = _device_pct_to_ha_brightness(state.brightness_level)
|
||||
|
||||
@callback
|
||||
def _async_handle_state_update(self, state: GlowState) -> None:
|
||||
"""Handle a state update from the device."""
|
||||
self._update_from_state(state)
|
||||
self.async_write_ha_state()
|
||||
|
||||
async def async_turn_on(self, **kwargs: Any) -> None:
|
||||
"""Turn the light on."""
|
||||
brightness_pct: int | None = None
|
||||
if ATTR_BRIGHTNESS in kwargs:
|
||||
brightness_pct = _ha_brightness_to_device_pct(kwargs[ATTR_BRIGHTNESS])
|
||||
|
||||
await self._async_command(self._device.turn_on())
|
||||
self._attr_is_on = True
|
||||
self._attr_color_mode = ColorMode.BRIGHTNESS
|
||||
if brightness_pct is not None:
|
||||
await self._async_command(
|
||||
self._device.set_brightness_and_dimming_time(
|
||||
brightness_pct,
|
||||
self.coordinator.last_dimming_time_minutes
|
||||
if self.coordinator.last_dimming_time_minutes is not None
|
||||
else DEFAULT_DIMMING_TIME_MINUTES,
|
||||
)
|
||||
)
|
||||
self._attr_brightness = _device_pct_to_ha_brightness(brightness_pct)
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn the light off."""
|
||||
await self._async_command(self._device.turn_off())
|
||||
self._attr_is_on = False
|
||||
19
homeassistant/components/casper_glow/manifest.json
Normal file
19
homeassistant/components/casper_glow/manifest.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"domain": "casper_glow",
|
||||
"name": "Casper Glow",
|
||||
"bluetooth": [
|
||||
{
|
||||
"connectable": true,
|
||||
"local_name": "Jar*"
|
||||
}
|
||||
],
|
||||
"codeowners": ["@mikeodr"],
|
||||
"config_flow": true,
|
||||
"dependencies": ["bluetooth_adapters"],
|
||||
"documentation": "https://www.home-assistant.io/integrations/casper_glow",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_polling",
|
||||
"loggers": ["pycasperglow"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["pycasperglow==1.1.0"]
|
||||
}
|
||||
74
homeassistant/components/casper_glow/quality_scale.yaml
Normal file
74
homeassistant/components/casper_glow/quality_scale.yaml
Normal file
@@ -0,0 +1,74 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: No custom services.
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow-test-coverage: done
|
||||
config-flow: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: No custom actions/services.
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup: done
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions: done
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: done
|
||||
docs-installation-parameters: done
|
||||
entity-unavailable: done
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: No network discovery.
|
||||
discovery: done
|
||||
docs-data-update: done
|
||||
docs-examples: todo
|
||||
docs-known-limitations: done
|
||||
docs-supported-devices: done
|
||||
docs-supported-functions: done
|
||||
docs-troubleshooting: done
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: todo
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations:
|
||||
status: exempt
|
||||
comment: No entity translations needed.
|
||||
exception-translations:
|
||||
status: exempt
|
||||
comment: No custom services that raise exceptions.
|
||||
icon-translations:
|
||||
status: exempt
|
||||
comment: No icon translations needed.
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession:
|
||||
status: exempt
|
||||
comment: No web session is used by this integration.
|
||||
strict-typing: done
|
||||
34
homeassistant/components/casper_glow/strings.json
Normal file
34
homeassistant/components/casper_glow/strings.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"already_in_progress": "[%key:common::config_flow::abort::already_in_progress%]",
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"no_devices_found": "[%key:common::config_flow::abort::no_devices_found%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"flow_title": "{name}",
|
||||
"step": {
|
||||
"bluetooth_confirm": {
|
||||
"description": "Do you want to set up {name}?"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"address": "Bluetooth address"
|
||||
},
|
||||
"data_description": {
|
||||
"address": "The Bluetooth address of the Casper Glow light"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"exceptions": {
|
||||
"communication_error": {
|
||||
"message": "An error occurred while communicating with the Casper Glow: {error}"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -462,6 +462,10 @@
|
||||
"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"
|
||||
@@ -481,6 +485,10 @@
|
||||
"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,12 +2,15 @@
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.const import ATTR_TEMPERATURE, CONF_OPTIONS, UnitOfTemperature
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
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,
|
||||
@@ -16,6 +19,7 @@ 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
|
||||
|
||||
@@ -44,6 +48,33 @@ 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(
|
||||
@@ -53,17 +84,15 @@ 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)}
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
),
|
||||
"target_humidity_crossed_threshold": make_entity_numerical_state_crossed_threshold_trigger(
|
||||
{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)}
|
||||
{DOMAIN: NumericalDomainSpec(value_source=ATTR_HUMIDITY)},
|
||||
valid_unit="%",
|
||||
),
|
||||
"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,7 +14,29 @@
|
||||
- last
|
||||
- any
|
||||
|
||||
.number_or_entity: &number_or_entity
|
||||
.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
|
||||
required: false
|
||||
selector:
|
||||
choose:
|
||||
@@ -27,12 +49,24 @@
|
||||
selector:
|
||||
entity:
|
||||
filter:
|
||||
domain:
|
||||
- input_number
|
||||
- number
|
||||
- sensor
|
||||
- domain: input_number
|
||||
unit_of_measurement:
|
||||
- "°C"
|
||||
- "°F"
|
||||
- domain: sensor
|
||||
device_class: temperature
|
||||
- domain: number
|
||||
device_class: temperature
|
||||
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
|
||||
@@ -69,27 +103,29 @@ hvac_mode_changed:
|
||||
target_humidity_changed:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
above: *number_or_entity_humidity
|
||||
below: *number_or_entity_humidity
|
||||
|
||||
target_humidity_crossed_threshold:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity
|
||||
upper_limit: *number_or_entity
|
||||
lower_limit: *number_or_entity_humidity
|
||||
upper_limit: *number_or_entity_humidity
|
||||
|
||||
target_temperature_changed:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
above: *number_or_entity
|
||||
below: *number_or_entity
|
||||
above: *number_or_entity_temperature
|
||||
below: *number_or_entity_temperature
|
||||
unit: *trigger_unit_temperature
|
||||
|
||||
target_temperature_crossed_threshold:
|
||||
target: *trigger_climate_target
|
||||
fields:
|
||||
behavior: *trigger_behavior
|
||||
threshold_type: *trigger_threshold_type
|
||||
lower_limit: *number_or_entity
|
||||
upper_limit: *number_or_entity
|
||||
lower_limit: *number_or_entity_temperature
|
||||
upper_limit: *number_or_entity_temperature
|
||||
unit: *trigger_unit_temperature
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
"""Provides triggers for covers."""
|
||||
|
||||
from homeassistant.const import STATE_OFF, STATE_ON, STATE_UNAVAILABLE, STATE_UNKNOWN
|
||||
from homeassistant.core import HomeAssistant, State, split_entity_id
|
||||
from homeassistant.core import HomeAssistant, State
|
||||
from homeassistant.helpers.trigger import EntityTriggerBase, Trigger
|
||||
|
||||
from .const import ATTR_IS_CLOSED, DOMAIN, CoverDeviceClass
|
||||
@@ -13,14 +13,14 @@ class CoverTriggerBase(EntityTriggerBase[CoverDomainSpec]):
|
||||
|
||||
def _get_value(self, state: State) -> str | bool | None:
|
||||
"""Extract the relevant value from state based on domain spec."""
|
||||
domain_spec = self._domain_specs[split_entity_id(state.entity_id)[0]]
|
||||
domain_spec = self._domain_specs[state.domain]
|
||||
if domain_spec.value_source is not None:
|
||||
return state.attributes.get(domain_spec.value_source)
|
||||
return state.state
|
||||
|
||||
def is_valid_state(self, state: State) -> bool:
|
||||
"""Check if the state matches the target cover state."""
|
||||
domain_spec = self._domain_specs[split_entity_id(state.entity_id)[0]]
|
||||
domain_spec = self._domain_specs[state.domain]
|
||||
return self._get_value(state) == domain_spec.target_value
|
||||
|
||||
def is_valid_transition(self, from_state: State, to_state: State) -> bool:
|
||||
|
||||
@@ -9,7 +9,7 @@ from typing import Any, Literal, NotRequired, TypedDict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.core import HomeAssistant, callback, valid_entity_id
|
||||
from homeassistant.helpers import config_validation as cv, singleton, storage
|
||||
|
||||
from .const import DOMAIN
|
||||
@@ -244,6 +244,38 @@ class EnergyPreferencesUpdate(EnergyPreferences, total=False):
|
||||
"""all types optional."""
|
||||
|
||||
|
||||
def _reject_price_for_external_stat(
|
||||
*,
|
||||
stat_key: str,
|
||||
entity_price_key: str = "entity_energy_price",
|
||||
number_price_key: str = "number_energy_price",
|
||||
cost_stat_key: str = "stat_cost",
|
||||
) -> Callable[[dict[str, Any]], dict[str, Any]]:
|
||||
"""Return a validator that rejects entity/number price for external statistics.
|
||||
|
||||
Only rejects when the cost/compensation stat is not already set, since
|
||||
price fields are ignored when a cost stat is provided.
|
||||
"""
|
||||
|
||||
def validate(val: dict[str, Any]) -> dict[str, Any]:
|
||||
stat_id = val.get(stat_key)
|
||||
if stat_id is not None and not valid_entity_id(stat_id):
|
||||
if val.get(cost_stat_key) is not None:
|
||||
# Cost stat is already set; price fields are ignored, so allow.
|
||||
return val
|
||||
if (
|
||||
val.get(entity_price_key) is not None
|
||||
or val.get(number_price_key) is not None
|
||||
):
|
||||
raise vol.Invalid(
|
||||
"Entity or number price is not supported for external"
|
||||
f" statistics. Use {cost_stat_key} instead"
|
||||
)
|
||||
return val
|
||||
|
||||
return validate
|
||||
|
||||
|
||||
def _flow_from_ensure_single_price(
|
||||
val: FlowFromGridSourceType,
|
||||
) -> FlowFromGridSourceType:
|
||||
@@ -268,19 +300,25 @@ FLOW_FROM_GRID_SOURCE_SCHEMA = vol.All(
|
||||
vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None),
|
||||
}
|
||||
),
|
||||
_reject_price_for_external_stat(stat_key="stat_energy_from"),
|
||||
_flow_from_ensure_single_price,
|
||||
)
|
||||
|
||||
|
||||
FLOW_TO_GRID_SOURCE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("stat_energy_to"): str,
|
||||
vol.Optional("stat_compensation"): vol.Any(str, None),
|
||||
# entity_energy_to was removed in HA Core 2022.10
|
||||
vol.Remove("entity_energy_to"): vol.Any(str, None),
|
||||
vol.Optional("entity_energy_price"): vol.Any(str, None),
|
||||
vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None),
|
||||
}
|
||||
FLOW_TO_GRID_SOURCE_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required("stat_energy_to"): str,
|
||||
vol.Optional("stat_compensation"): vol.Any(str, None),
|
||||
# entity_energy_to was removed in HA Core 2022.10
|
||||
vol.Remove("entity_energy_to"): vol.Any(str, None),
|
||||
vol.Optional("entity_energy_price"): vol.Any(str, None),
|
||||
vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None),
|
||||
}
|
||||
),
|
||||
_reject_price_for_external_stat(
|
||||
stat_key="stat_energy_to", cost_stat_key="stat_compensation"
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -419,6 +457,13 @@ GRID_SOURCE_SCHEMA = vol.All(
|
||||
vol.Required("cost_adjustment_day"): vol.Coerce(float),
|
||||
}
|
||||
),
|
||||
_reject_price_for_external_stat(stat_key="stat_energy_from"),
|
||||
_reject_price_for_external_stat(
|
||||
stat_key="stat_energy_to",
|
||||
entity_price_key="entity_energy_price_export",
|
||||
number_price_key="number_energy_price_export",
|
||||
cost_stat_key="stat_compensation",
|
||||
),
|
||||
_grid_ensure_single_price_import,
|
||||
_grid_ensure_single_price_export,
|
||||
_grid_ensure_at_least_one_stat,
|
||||
@@ -442,27 +487,35 @@ BATTERY_SOURCE_SCHEMA = vol.Schema(
|
||||
vol.Optional("power_config"): POWER_CONFIG_SCHEMA,
|
||||
}
|
||||
)
|
||||
GAS_SOURCE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("type"): "gas",
|
||||
vol.Required("stat_energy_from"): str,
|
||||
vol.Optional("stat_rate"): str,
|
||||
vol.Optional("stat_cost"): vol.Any(str, None),
|
||||
# entity_energy_from was removed in HA Core 2022.10
|
||||
vol.Remove("entity_energy_from"): vol.Any(str, None),
|
||||
vol.Optional("entity_energy_price"): vol.Any(str, None),
|
||||
vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None),
|
||||
}
|
||||
|
||||
|
||||
GAS_SOURCE_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required("type"): "gas",
|
||||
vol.Required("stat_energy_from"): str,
|
||||
vol.Optional("stat_rate"): str,
|
||||
vol.Optional("stat_cost"): vol.Any(str, None),
|
||||
# entity_energy_from was removed in HA Core 2022.10
|
||||
vol.Remove("entity_energy_from"): vol.Any(str, None),
|
||||
vol.Optional("entity_energy_price"): vol.Any(str, None),
|
||||
vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None),
|
||||
}
|
||||
),
|
||||
_reject_price_for_external_stat(stat_key="stat_energy_from"),
|
||||
)
|
||||
WATER_SOURCE_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required("type"): "water",
|
||||
vol.Required("stat_energy_from"): str,
|
||||
vol.Optional("stat_rate"): str,
|
||||
vol.Optional("stat_cost"): vol.Any(str, None),
|
||||
vol.Optional("entity_energy_price"): vol.Any(str, None),
|
||||
vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None),
|
||||
}
|
||||
WATER_SOURCE_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Required("type"): "water",
|
||||
vol.Required("stat_energy_from"): str,
|
||||
vol.Optional("stat_rate"): str,
|
||||
vol.Optional("stat_cost"): vol.Any(str, None),
|
||||
vol.Optional("entity_energy_price"): vol.Any(str, None),
|
||||
vol.Optional("number_energy_price"): vol.Any(vol.Coerce(float), None),
|
||||
}
|
||||
),
|
||||
_reject_price_for_external_stat(stat_key="stat_energy_from"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"mqtt": ["esphome/discover/#"],
|
||||
"quality_scale": "platinum",
|
||||
"requirements": [
|
||||
"aioesphomeapi==44.5.2",
|
||||
"aioesphomeapi==44.6.2",
|
||||
"esphome-dashboard-api==1.3.0",
|
||||
"bleak-esphome==3.7.1"
|
||||
],
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["evohomeasync", "evohomeasync2"],
|
||||
"quality_scale": "legacy",
|
||||
"requirements": ["evohome-async==1.1.3"]
|
||||
"requirements": ["evohome-async==1.2.0"]
|
||||
}
|
||||
|
||||
@@ -124,15 +124,17 @@ 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.on(until=until))
|
||||
await self.coordinator.call_client_api(
|
||||
self._evo_device.set_on(until=until)
|
||||
)
|
||||
else: # STATE_OFF
|
||||
await self.coordinator.call_client_api(
|
||||
self._evo_device.off(until=until)
|
||||
self._evo_device.set_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.off())
|
||||
await self.coordinator.call_client_api(self._evo_device.set_off())
|
||||
|
||||
async def async_turn_away_mode_off(self) -> None:
|
||||
"""Turn away mode off."""
|
||||
@@ -140,8 +142,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.on())
|
||||
await self.coordinator.call_client_api(self._evo_device.set_on())
|
||||
|
||||
async def async_turn_off(self, **kwargs: Any) -> None:
|
||||
"""Turn off."""
|
||||
await self.coordinator.call_client_api(self._evo_device.off())
|
||||
await self.coordinator.call_client_api(self._evo_device.set_off())
|
||||
|
||||
@@ -283,6 +283,7 @@ class FritzBoxToolsFlowHandler(ConfigFlow, domain=DOMAIN):
|
||||
self._username = user_input[CONF_USERNAME]
|
||||
self._password = user_input[CONF_PASSWORD]
|
||||
self._use_tls = user_input[CONF_SSL]
|
||||
self._feature_device_discovery = user_input[CONF_FEATURE_DEVICE_TRACKING]
|
||||
|
||||
self._port = self._determine_port(user_input)
|
||||
|
||||
|
||||
@@ -13,6 +13,7 @@ from fritzconnection.core.exceptions import (
|
||||
FritzSecurityError,
|
||||
FritzServiceError,
|
||||
)
|
||||
from requests.exceptions import ConnectionError
|
||||
|
||||
from homeassistant.const import Platform
|
||||
|
||||
@@ -68,6 +69,7 @@ BUTTON_TYPE_WOL = "WakeOnLan"
|
||||
UPTIME_DEVIATION = 5
|
||||
|
||||
FRITZ_EXCEPTIONS = (
|
||||
ConnectionError,
|
||||
FritzActionError,
|
||||
FritzActionFailedError,
|
||||
FritzConnectionException,
|
||||
|
||||
@@ -21,5 +21,5 @@
|
||||
"integration_type": "system",
|
||||
"preview_features": { "winter_mode": {} },
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20260312.0"]
|
||||
"requirements": ["home-assistant-frontend==20260312.1"]
|
||||
}
|
||||
|
||||
@@ -116,7 +116,11 @@ async def _validate_config(
|
||||
|
||||
|
||||
CONFIG_FLOW = {
|
||||
"user": SchemaFlowFormStep(vol.Schema(CONFIG_SCHEMA), next_step="presets"),
|
||||
"user": SchemaFlowFormStep(
|
||||
vol.Schema(CONFIG_SCHEMA),
|
||||
validate_user_input=_validate_config,
|
||||
next_step="presets",
|
||||
),
|
||||
"presets": SchemaFlowFormStep(vol.Schema(PRESETS_SCHEMA)),
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"min_max_runtime": "Minimum run time must be less than the maximum run time."
|
||||
},
|
||||
"step": {
|
||||
"presets": {
|
||||
"data": {
|
||||
@@ -45,7 +48,7 @@
|
||||
},
|
||||
"options": {
|
||||
"error": {
|
||||
"min_max_runtime": "Minimum run time must be less than the maximum run time."
|
||||
"min_max_runtime": "[%key:component::generic_thermostat::config::error::min_max_runtime%]"
|
||||
},
|
||||
"step": {
|
||||
"init": {
|
||||
|
||||
44
homeassistant/components/google_weather/diagnostics.py
Normal file
44
homeassistant/components/google_weather/diagnostics.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Diagnostics support for Google Weather."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.diagnostics import async_redact_data
|
||||
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import CONF_REFERRER
|
||||
from .coordinator import GoogleWeatherConfigEntry
|
||||
|
||||
TO_REDACT = {
|
||||
CONF_API_KEY,
|
||||
CONF_REFERRER,
|
||||
CONF_LATITUDE,
|
||||
CONF_LONGITUDE,
|
||||
}
|
||||
|
||||
|
||||
async def async_get_config_entry_diagnostics(
|
||||
hass: HomeAssistant, entry: GoogleWeatherConfigEntry
|
||||
) -> dict[str, Any]:
|
||||
"""Return diagnostics for a config entry."""
|
||||
diag_data: dict[str, Any] = {
|
||||
"entry": entry.as_dict(),
|
||||
"subentries": {},
|
||||
}
|
||||
|
||||
for subentry_id, subentry_rt in entry.runtime_data.subentries_runtime_data.items():
|
||||
diag_data["subentries"][subentry_id] = {
|
||||
"observation_data": subentry_rt.coordinator_observation.data.to_dict()
|
||||
if subentry_rt.coordinator_observation.data
|
||||
else None,
|
||||
"daily_forecast_data": subentry_rt.coordinator_daily_forecast.data.to_dict()
|
||||
if subentry_rt.coordinator_daily_forecast.data
|
||||
else None,
|
||||
"hourly_forecast_data": subentry_rt.coordinator_hourly_forecast.data.to_dict()
|
||||
if subentry_rt.coordinator_hourly_forecast.data
|
||||
else None,
|
||||
}
|
||||
|
||||
return async_redact_data(diag_data, TO_REDACT)
|
||||
@@ -8,5 +8,5 @@
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["google_weather_api"],
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["python-google-weather-api==0.0.4"]
|
||||
"requirements": ["python-google-weather-api==0.0.6"]
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ rules:
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
diagnostics: done
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: No discovery.
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["greenplanet-energy-api==0.1.4"],
|
||||
"requirements": ["greenplanet-energy-api==0.1.10"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from contextlib import suppress
|
||||
from dataclasses import replace
|
||||
from datetime import datetime
|
||||
import logging
|
||||
import os
|
||||
@@ -15,6 +16,7 @@ from aiohasupervisor import SupervisorError
|
||||
from aiohasupervisor.models import (
|
||||
GreenOptions,
|
||||
HomeAssistantInfo,
|
||||
HomeAssistantOptions,
|
||||
HostInfo,
|
||||
InstalledAddon,
|
||||
NetworkInfo,
|
||||
@@ -22,20 +24,28 @@ 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 StaticPathConfig
|
||||
from homeassistant.components.http import (
|
||||
CONF_SERVER_HOST,
|
||||
CONF_SERVER_PORT,
|
||||
CONF_SSL_CERTIFICATE,
|
||||
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 (
|
||||
@@ -445,8 +455,30 @@ 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(
|
||||
hassio.update_hass_api(config.get("http", {}), refresh_token), eager_start=True
|
||||
update_hass_api(config.get("http", {}), refresh_token), eager_start=True
|
||||
)
|
||||
|
||||
last_timezone = None
|
||||
@@ -457,19 +489,25 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
nonlocal last_timezone
|
||||
nonlocal last_country
|
||||
|
||||
new_timezone = str(hass.config.time_zone)
|
||||
new_country = str(hass.config.country)
|
||||
new_timezone = hass.config.time_zone
|
||||
new_country = hass.config.country
|
||||
|
||||
if new_timezone != last_timezone or new_country != last_country:
|
||||
last_timezone = new_timezone
|
||||
last_country = new_country
|
||||
await hassio.update_hass_config(new_timezone, 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)
|
||||
|
||||
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, hassio)
|
||||
hass.data[DATA_KEY_SUPERVISOR_ISSUES] = issues = SupervisorIssues(hass)
|
||||
issues_task = hass.async_create_task(issues.setup(), eager_start=True)
|
||||
|
||||
async def async_service_handler(service: ServiceCall) -> None:
|
||||
@@ -617,7 +655,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, hassio)
|
||||
async_setup_discovery_view(hass)
|
||||
|
||||
# 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 HassIO, get_supervisor_client
|
||||
from .handler import get_supervisor_client
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_discovery_view(hass: HomeAssistant, hassio: HassIO) -> None:
|
||||
def async_setup_discovery_view(hass: HomeAssistant) -> None:
|
||||
"""Discovery setup."""
|
||||
hassio_discovery = HassIODiscovery(hass, hassio)
|
||||
hassio_discovery = HassIODiscovery(hass)
|
||||
supervisor_client = get_supervisor_client(hass)
|
||||
hass.http.register_view(hassio_discovery)
|
||||
|
||||
@@ -77,10 +77,9 @@ class HassIODiscovery(HomeAssistantView):
|
||||
name = "api:hassio_push:discovery"
|
||||
url = "/api/hassio_push/discovery/{uuid}"
|
||||
|
||||
def __init__(self, hass: HomeAssistant, hassio: HassIO) -> None:
|
||||
def __init__(self, hass: HomeAssistant) -> 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,13 +14,6 @@ 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
|
||||
|
||||
@@ -35,22 +28,6 @@ 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]]:
|
||||
@@ -95,37 +72,6 @@ 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 HassIO, get_supervisor_client
|
||||
from .handler import get_supervisor_client
|
||||
|
||||
ISSUE_KEY_UNHEALTHY = "unhealthy"
|
||||
ISSUE_KEY_UNSUPPORTED = "unsupported"
|
||||
@@ -175,10 +175,9 @@ class Issue:
|
||||
class SupervisorIssues:
|
||||
"""Create issues from supervisor events."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, client: HassIO) -> None:
|
||||
def __init__(self, hass: HomeAssistant) -> 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,6 +5,8 @@ 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 (
|
||||
@@ -13,7 +15,6 @@ 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,
|
||||
@@ -76,15 +77,7 @@ class ZBT2FirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
||||
context: ConfigFlowContext
|
||||
|
||||
ZIGBEE_BAUDRATE = 460800
|
||||
|
||||
# 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),
|
||||
]
|
||||
_flasher_cls = Zbt2Flasher
|
||||
|
||||
async def async_step_install_zigbee_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
|
||||
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from universal_silabs_flasher.flasher import Zbt2Flasher
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
@@ -23,7 +25,6 @@ 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__)
|
||||
@@ -134,8 +135,7 @@ async def async_setup_entry(
|
||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""Connect ZBT-2 firmware update entity."""
|
||||
|
||||
BOOTLOADER_RESET_METHODS = ZBT2FirmwareMixin.BOOTLOADER_RESET_METHODS
|
||||
APPLICATION_PROBE_METHODS = ZBT2FirmwareMixin.APPLICATION_PROBE_METHODS
|
||||
_flasher_cls = Zbt2Flasher
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -12,6 +12,7 @@ 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,
|
||||
@@ -39,7 +40,6 @@ from .util import (
|
||||
FirmwareInfo,
|
||||
OwningAddon,
|
||||
OwningIntegration,
|
||||
ResetTarget,
|
||||
async_firmware_flashing_context,
|
||||
async_flash_silabs_firmware,
|
||||
get_otbr_addon_manager,
|
||||
@@ -81,8 +81,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
"""Base flow to install firmware."""
|
||||
|
||||
ZIGBEE_BAUDRATE = 115200 # Default, subclasses may override
|
||||
BOOTLOADER_RESET_METHODS: list[ResetTarget] = [] # Default, subclasses may override
|
||||
APPLICATION_PROBE_METHODS: list[tuple[ApplicationType, int]] = []
|
||||
_flasher_cls: type[DeviceSpecificFlasher]
|
||||
|
||||
_picked_firmware_type: PickedFirmwareType
|
||||
_zigbee_flow_strategy: ZigbeeFlowStrategy = ZigbeeFlowStrategy.RECOMMENDED
|
||||
@@ -239,8 +238,7 @@ class BaseFirmwareInstallFlow(ConfigEntryBaseFlow, ABC):
|
||||
# isn't strictly necessary for functionality.
|
||||
self._probed_firmware_info = await probe_silabs_firmware_info(
|
||||
self._device,
|
||||
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||
flasher_cls=self._flasher_cls,
|
||||
)
|
||||
|
||||
firmware_install_required = self._probed_firmware_info is None or (
|
||||
@@ -311,9 +309,8 @@ 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==0.1.2",
|
||||
"universal-silabs-flasher==1.0.2",
|
||||
"ha-silabs-firmware-client==0.3.0"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ 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 (
|
||||
@@ -25,7 +26,6 @@ from .helpers import async_register_firmware_info_callback
|
||||
from .util import (
|
||||
ApplicationType,
|
||||
FirmwareInfo,
|
||||
ResetTarget,
|
||||
async_firmware_flashing_context,
|
||||
async_flash_silabs_firmware,
|
||||
)
|
||||
@@ -87,13 +87,11 @@ 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,
|
||||
@@ -282,9 +280,8 @@ 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,12 +10,9 @@ from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
import logging
|
||||
|
||||
from universal_silabs_flasher.const import (
|
||||
ApplicationType as FlasherApplicationType,
|
||||
ResetTarget as FlasherResetTarget,
|
||||
)
|
||||
from universal_silabs_flasher.const import ApplicationType as FlasherApplicationType
|
||||
from universal_silabs_flasher.firmware import parse_firmware_image
|
||||
from universal_silabs_flasher.flasher import Flasher
|
||||
from universal_silabs_flasher.flasher import BaseFlasher, DeviceSpecificFlasher, Flasher
|
||||
|
||||
from homeassistant.components.hassio import AddonError, AddonManager, AddonState
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
@@ -63,18 +60,6 @@ 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:
|
||||
@@ -310,23 +295,20 @@ async def guess_firmware_info(hass: HomeAssistant, device_path: str) -> Firmware
|
||||
async def probe_silabs_firmware_info(
|
||||
device: str,
|
||||
*,
|
||||
bootloader_reset_methods: Sequence[ResetTarget],
|
||||
application_probe_methods: Sequence[tuple[ApplicationType, int]],
|
||||
flasher_cls: type[BaseFlasher],
|
||||
application_probe_methods: Sequence[ApplicationType] | None = None,
|
||||
) -> FirmwareInfo | None:
|
||||
"""Probe the running firmware on a SiLabs 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
|
||||
),
|
||||
)
|
||||
flasher = flasher_cls(device=device)
|
||||
|
||||
try:
|
||||
await flasher.probe_app_type()
|
||||
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
|
||||
)
|
||||
)
|
||||
except Exception: # noqa: BLE001
|
||||
_LOGGER.debug("Failed to probe application type", exc_info=True)
|
||||
|
||||
@@ -349,20 +331,25 @@ 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."""
|
||||
|
||||
fw_info = await probe_silabs_firmware_info(
|
||||
device,
|
||||
bootloader_reset_methods=bootloader_reset_methods,
|
||||
application_probe_methods=application_probe_methods,
|
||||
flasher = Flasher(
|
||||
device=device,
|
||||
probe_methods=[
|
||||
(m.as_flasher_application_type(), b) for m, b in application_probe_methods
|
||||
],
|
||||
)
|
||||
if fw_info is None:
|
||||
|
||||
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:
|
||||
return None
|
||||
|
||||
return fw_info.firmware_type
|
||||
return ApplicationType.from_flasher_application_type(flasher.app_type)
|
||||
|
||||
|
||||
@asynccontextmanager
|
||||
@@ -385,36 +372,18 @@ 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(
|
||||
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
|
||||
),
|
||||
)
|
||||
flasher = flasher_cls(device=device)
|
||||
|
||||
try:
|
||||
# Enter the bootloader with indeterminate progress
|
||||
@@ -431,13 +400,9 @@ async def async_flash_silabs_firmware(
|
||||
|
||||
probed_firmware_info = await probe_silabs_firmware_info(
|
||||
device,
|
||||
bootloader_reset_methods=bootloader_reset_methods,
|
||||
flasher_cls=flasher_cls,
|
||||
# Only probe for the expected installed firmware type
|
||||
application_probe_methods=[
|
||||
(method, baudrate)
|
||||
for method, baudrate in application_probe_methods
|
||||
if method == expected_installed_firmware_type
|
||||
],
|
||||
application_probe_methods=[expected_installed_firmware_type],
|
||||
)
|
||||
|
||||
if probed_firmware_info is None:
|
||||
|
||||
@@ -5,6 +5,8 @@ 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,
|
||||
@@ -16,7 +18,6 @@ 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,
|
||||
@@ -81,18 +82,7 @@ class SkyConnectFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
||||
context: ConfigFlowContext
|
||||
|
||||
ZIGBEE_BAUDRATE = 115200
|
||||
# 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),
|
||||
]
|
||||
_flasher_cls = Zbt1Flasher
|
||||
|
||||
def _get_translation_placeholders(self) -> dict[str, str]:
|
||||
"""Shared translation placeholders."""
|
||||
|
||||
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from universal_silabs_flasher.flasher import Zbt1Flasher
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
@@ -23,7 +25,6 @@ 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,
|
||||
@@ -152,8 +153,7 @@ async def async_setup_entry(
|
||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""SkyConnect firmware update entity."""
|
||||
|
||||
BOOTLOADER_RESET_METHODS = SkyConnectFirmwareMixin.BOOTLOADER_RESET_METHODS
|
||||
APPLICATION_PROBE_METHODS = SkyConnectFirmwareMixin.APPLICATION_PROBE_METHODS
|
||||
_flasher_cls = Zbt1Flasher
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -7,6 +7,7 @@ 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 (
|
||||
@@ -25,7 +26,6 @@ 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,17 +83,7 @@ class YellowFirmwareMixin(ConfigEntryBaseFlow, FirmwareInstallFlowProtocol):
|
||||
"""Mixin for Home Assistant Yellow firmware methods."""
|
||||
|
||||
ZIGBEE_BAUDRATE = 115200
|
||||
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),
|
||||
]
|
||||
_flasher_cls = YellowFlasher
|
||||
|
||||
async def async_step_install_zigbee_firmware(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -159,8 +149,7 @@ class HomeAssistantYellowConfigFlow(
|
||||
# We do not actually use any portion of `BaseFirmwareConfigFlow` beyond this
|
||||
self._probed_firmware_info = await probe_silabs_firmware_info(
|
||||
self._device,
|
||||
bootloader_reset_methods=self.BOOTLOADER_RESET_METHODS,
|
||||
application_probe_methods=self.APPLICATION_PROBE_METHODS,
|
||||
flasher_cls=self._flasher_cls,
|
||||
)
|
||||
|
||||
# Kick off ZHA hardware discovery automatically if Zigbee firmware is running
|
||||
|
||||
@@ -4,6 +4,8 @@ from __future__ import annotations
|
||||
|
||||
import logging
|
||||
|
||||
from universal_silabs_flasher.flasher import YellowFlasher
|
||||
|
||||
from homeassistant.components.homeassistant_hardware.coordinator import (
|
||||
FirmwareUpdateCoordinator,
|
||||
)
|
||||
@@ -23,7 +25,6 @@ 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__)
|
||||
@@ -150,8 +151,7 @@ async def async_setup_entry(
|
||||
class FirmwareUpdateEntity(BaseFirmwareUpdateEntity):
|
||||
"""Yellow firmware update entity."""
|
||||
|
||||
BOOTLOADER_RESET_METHODS = YellowFirmwareMixin.BOOTLOADER_RESET_METHODS
|
||||
APPLICATION_PROBE_METHODS = YellowFirmwareMixin.APPLICATION_PROBE_METHODS
|
||||
_flasher_cls = YellowFlasher
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Mapping
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
@@ -57,3 +58,48 @@ class HuumConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
|
||||
)
|
||||
|
||||
async def async_step_reauth(
|
||||
self, entry_data: Mapping[str, Any]
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reauthentication upon an API authentication error."""
|
||||
return await self.async_step_reauth_confirm()
|
||||
|
||||
async def async_step_reauth_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm reauthentication dialog."""
|
||||
errors: dict[str, str] = {}
|
||||
reauth_entry = self._get_reauth_entry()
|
||||
|
||||
if user_input is not None:
|
||||
huum = Huum(
|
||||
reauth_entry.data[CONF_USERNAME],
|
||||
user_input[CONF_PASSWORD],
|
||||
session=async_get_clientsession(self.hass),
|
||||
)
|
||||
try:
|
||||
await huum.status()
|
||||
except Forbidden, NotAuthenticated:
|
||||
errors["base"] = "invalid_auth"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unknown error")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_update_reload_and_abort(
|
||||
reauth_entry,
|
||||
data_updates={CONF_PASSWORD: user_input[CONF_PASSWORD]},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="reauth_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_PASSWORD): str,
|
||||
}
|
||||
),
|
||||
description_placeholders={
|
||||
"username": reauth_entry.data[CONF_USERNAME],
|
||||
},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -12,8 +12,9 @@ from huum.schemas import HuumStatusResponse
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
|
||||
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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
@@ -54,6 +55,6 @@ class HuumDataUpdateCoordinator(DataUpdateCoordinator[HuumStatusResponse]):
|
||||
try:
|
||||
return await self.huum.status()
|
||||
except (Forbidden, NotAuthenticated) as err:
|
||||
raise UpdateFailed(
|
||||
raise ConfigEntryAuthFailed(
|
||||
"Could not log in to Huum with given credentials"
|
||||
) from err
|
||||
|
||||
@@ -38,7 +38,7 @@ rules:
|
||||
integration-owner: done
|
||||
log-when-unavailable: done
|
||||
parallel-updates: done
|
||||
reauthentication-flow: todo
|
||||
reauthentication-flow: done
|
||||
test-coverage:
|
||||
status: todo
|
||||
comment: |
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
|
||||
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
@@ -9,6 +10,16 @@
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"reauth_confirm": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]"
|
||||
},
|
||||
"data_description": {
|
||||
"password": "[%key:component::huum::config::step::user::data_description::password%]"
|
||||
},
|
||||
"description": "The authentication for {username} is no longer valid. Please enter the current password.",
|
||||
"title": "[%key:common::config_flow::title::reauth%]"
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
|
||||
@@ -2,22 +2,9 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
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.const import 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
|
||||
@@ -38,30 +25,7 @@ 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."""
|
||||
|
||||
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)
|
||||
coordinator = ImmichDataUpdateCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
|
||||
@@ -16,11 +16,19 @@ 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_HOST, CONF_PORT, CONF_SSL
|
||||
from homeassistant.const import (
|
||||
CONF_API_KEY,
|
||||
CONF_HOST,
|
||||
CONF_PORT,
|
||||
CONF_SSL,
|
||||
CONF_VERIFY_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
|
||||
@@ -46,24 +54,49 @@ class ImmichDataUpdateCoordinator(DataUpdateCoordinator[ImmichData]):
|
||||
|
||||
config_entry: ImmichConfigEntry
|
||||
|
||||
def __init__(
|
||||
self, hass: HomeAssistant, entry: ConfigEntry, api: Immich, is_admin: bool
|
||||
) -> None:
|
||||
def __init__(self, hass: HomeAssistant, config_entry: ImmichConfigEntry) -> None:
|
||||
"""Initialize the data update coordinator."""
|
||||
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]}"
|
||||
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],
|
||||
)
|
||||
)
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=entry,
|
||||
config_entry=config_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,13 +20,5 @@
|
||||
"turn_on": {
|
||||
"service": "mdi:toggle-switch"
|
||||
}
|
||||
},
|
||||
"triggers": {
|
||||
"turned_off": {
|
||||
"trigger": "mdi:toggle-switch-off"
|
||||
},
|
||||
"turned_on": {
|
||||
"trigger": "mdi:toggle-switch"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
{
|
||||
"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%]",
|
||||
@@ -21,15 +17,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"selector": {
|
||||
"trigger_behavior": {
|
||||
"options": {
|
||||
"any": "Any",
|
||||
"first": "First",
|
||||
"last": "Last"
|
||||
}
|
||||
}
|
||||
},
|
||||
"services": {
|
||||
"reload": {
|
||||
"description": "Reloads helpers from the YAML-configuration.",
|
||||
@@ -48,27 +35,5 @@
|
||||
"name": "[%key:common::action::turn_on%]"
|
||||
}
|
||||
},
|
||||
"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"
|
||||
}
|
||||
}
|
||||
"title": "Input boolean"
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
"""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
|
||||
@@ -1,18 +0,0 @@
|
||||
.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
|
||||
@@ -114,24 +114,26 @@ class KnxYamlBinarySensor(_KnxBinarySensor, KnxYamlEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize of KNX binary sensor."""
|
||||
self._device = XknxBinarySensor(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address_state=config[CONF_STATE_ADDRESS],
|
||||
invert=config[CONF_INVERT],
|
||||
sync_state=config[CONF_SYNC_STATE],
|
||||
ignore_internal_state=config[CONF_IGNORE_INTERNAL_STATE],
|
||||
context_timeout=config.get(CONF_CONTEXT_TIMEOUT),
|
||||
reset_after=config.get(CONF_RESET_AFTER),
|
||||
always_callback=True,
|
||||
)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=XknxBinarySensor(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address_state=config[CONF_STATE_ADDRESS],
|
||||
invert=config[CONF_INVERT],
|
||||
sync_state=config[CONF_SYNC_STATE],
|
||||
ignore_internal_state=config[CONF_IGNORE_INTERNAL_STATE],
|
||||
context_timeout=config.get(CONF_CONTEXT_TIMEOUT),
|
||||
reset_after=config.get(CONF_RESET_AFTER),
|
||||
always_callback=True,
|
||||
),
|
||||
unique_id=str(self._device.remote_value.group_address_state),
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
|
||||
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
|
||||
self._attr_force_update = self._device.ignore_internal_state
|
||||
self._attr_unique_id = str(self._device.remote_value.group_address_state)
|
||||
|
||||
|
||||
class KnxUiBinarySensor(_KnxBinarySensor, KnxUiEntity):
|
||||
|
||||
@@ -35,19 +35,18 @@ class KNXButton(KnxYamlEntity, ButtonEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize a KNX button."""
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=XknxRawValue(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
payload_length=config[CONF_PAYLOAD_LENGTH],
|
||||
group_address=config[KNX_ADDRESS],
|
||||
),
|
||||
self._device = XknxRawValue(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
payload_length=config[CONF_PAYLOAD_LENGTH],
|
||||
group_address=config[KNX_ADDRESS],
|
||||
)
|
||||
self._payload = config[CONF_PAYLOAD]
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_unique_id = (
|
||||
f"{self._device.remote_value.group_address}_{self._payload}"
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
unique_id=f"{self._device.remote_value.group_address}_{self._payload}",
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
|
||||
async def async_press(self) -> None:
|
||||
|
||||
@@ -119,7 +119,7 @@ async def async_setup_entry(
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
def _create_climate(xknx: XKNX, config: ConfigType) -> XknxClimate:
|
||||
def _create_climate_yaml(xknx: XKNX, config: ConfigType) -> XknxClimate:
|
||||
"""Return a KNX Climate device to be used within XKNX."""
|
||||
climate_mode = XknxClimateMode(
|
||||
xknx,
|
||||
@@ -646,9 +646,17 @@ class KnxYamlClimate(_KnxClimate, KnxYamlEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize of a KNX climate device."""
|
||||
self._device = _create_climate_yaml(knx_module.xknx, config)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=_create_climate(knx_module.xknx, config),
|
||||
unique_id=(
|
||||
f"{self._device.temperature.group_address_state}_"
|
||||
f"{self._device.target_temperature.group_address_state}_"
|
||||
f"{self._device.target_temperature.group_address}_"
|
||||
f"{self._device._setpoint_shift.group_address}" # noqa: SLF001
|
||||
),
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
default_hvac_mode: HVACMode = config[ClimateConf.DEFAULT_CONTROLLER_MODE]
|
||||
fan_max_step = config[ClimateConf.FAN_MAX_STEP]
|
||||
@@ -660,14 +668,6 @@ class KnxYamlClimate(_KnxClimate, KnxYamlEntity):
|
||||
fan_zero_mode=fan_zero_mode,
|
||||
)
|
||||
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_unique_id = (
|
||||
f"{self._device.temperature.group_address_state}_"
|
||||
f"{self._device.target_temperature.group_address_state}_"
|
||||
f"{self._device.target_temperature.group_address}_"
|
||||
f"{self._device._setpoint_shift.group_address}" # noqa: SLF001
|
||||
)
|
||||
|
||||
|
||||
class KnxUiClimate(_KnxClimate, KnxUiEntity):
|
||||
"""Representation of a KNX climate device configured from the UI."""
|
||||
|
||||
@@ -191,36 +191,34 @@ class KnxYamlCover(_KnxCover, KnxYamlEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize the cover."""
|
||||
self._device = XknxCover(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS),
|
||||
group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS),
|
||||
group_address_stop=config.get(CoverSchema.CONF_STOP_ADDRESS),
|
||||
group_address_position_state=config.get(
|
||||
CoverSchema.CONF_POSITION_STATE_ADDRESS
|
||||
),
|
||||
group_address_angle=config.get(CoverSchema.CONF_ANGLE_ADDRESS),
|
||||
group_address_angle_state=config.get(CoverSchema.CONF_ANGLE_STATE_ADDRESS),
|
||||
group_address_position=config.get(CoverSchema.CONF_POSITION_ADDRESS),
|
||||
travel_time_down=config[CoverConf.TRAVELLING_TIME_DOWN],
|
||||
travel_time_up=config[CoverConf.TRAVELLING_TIME_UP],
|
||||
invert_updown=config[CoverConf.INVERT_UPDOWN],
|
||||
invert_position=config[CoverConf.INVERT_POSITION],
|
||||
invert_angle=config[CoverConf.INVERT_ANGLE],
|
||||
)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=XknxCover(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address_long=config.get(CoverSchema.CONF_MOVE_LONG_ADDRESS),
|
||||
group_address_short=config.get(CoverSchema.CONF_MOVE_SHORT_ADDRESS),
|
||||
group_address_stop=config.get(CoverSchema.CONF_STOP_ADDRESS),
|
||||
group_address_position_state=config.get(
|
||||
CoverSchema.CONF_POSITION_STATE_ADDRESS
|
||||
),
|
||||
group_address_angle=config.get(CoverSchema.CONF_ANGLE_ADDRESS),
|
||||
group_address_angle_state=config.get(
|
||||
CoverSchema.CONF_ANGLE_STATE_ADDRESS
|
||||
),
|
||||
group_address_position=config.get(CoverSchema.CONF_POSITION_ADDRESS),
|
||||
travel_time_down=config[CoverConf.TRAVELLING_TIME_DOWN],
|
||||
travel_time_up=config[CoverConf.TRAVELLING_TIME_UP],
|
||||
invert_updown=config[CoverConf.INVERT_UPDOWN],
|
||||
invert_position=config[CoverConf.INVERT_POSITION],
|
||||
invert_angle=config[CoverConf.INVERT_ANGLE],
|
||||
unique_id=(
|
||||
f"{self._device.updown.group_address}_"
|
||||
f"{self._device.position_target.group_address}"
|
||||
),
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
self.init_base()
|
||||
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_unique_id = (
|
||||
f"{self._device.updown.group_address}_"
|
||||
f"{self._device.position_target.group_address}"
|
||||
)
|
||||
if custom_device_class := config.get(CONF_DEVICE_CLASS):
|
||||
self._attr_device_class = custom_device_class
|
||||
|
||||
|
||||
@@ -105,20 +105,21 @@ class KnxYamlDate(_KNXDate, KnxYamlEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize a KNX date."""
|
||||
self._device = XknxDateDevice(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
localtime=False,
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
sync_state=config[CONF_SYNC_STATE],
|
||||
)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=XknxDateDevice(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
localtime=False,
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
sync_state=config[CONF_SYNC_STATE],
|
||||
),
|
||||
unique_id=str(self._device.remote_value.group_address),
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_unique_id = str(self._device.remote_value.group_address)
|
||||
|
||||
|
||||
class KnxUiDate(_KNXDate, KnxUiEntity):
|
||||
|
||||
@@ -110,20 +110,21 @@ class KnxYamlDateTime(_KNXDateTime, KnxYamlEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize a KNX datetime."""
|
||||
self._device = XknxDateTimeDevice(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
localtime=False,
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
sync_state=config[CONF_SYNC_STATE],
|
||||
)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=XknxDateTimeDevice(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
localtime=False,
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
sync_state=config[CONF_SYNC_STATE],
|
||||
),
|
||||
unique_id=str(self._device.remote_value.group_address),
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_unique_id = str(self._device.remote_value.group_address)
|
||||
|
||||
|
||||
class KnxUiDateTime(_KNXDateTime, KnxUiEntity):
|
||||
|
||||
@@ -6,7 +6,7 @@ from typing import TYPE_CHECKING, Any
|
||||
|
||||
from xknx.devices import Device as XknxDevice
|
||||
|
||||
from homeassistant.const import CONF_ENTITY_CATEGORY, EntityCategory
|
||||
from homeassistant.const import CONF_ENTITY_CATEGORY, CONF_NAME, EntityCategory
|
||||
from homeassistant.helpers.device_registry import DeviceInfo
|
||||
from homeassistant.helpers.entity import Entity
|
||||
from homeassistant.helpers.entity_platform import EntityPlatform
|
||||
@@ -52,14 +52,11 @@ class _KnxEntityBase(Entity):
|
||||
"""Representation of a KNX entity."""
|
||||
|
||||
_attr_should_poll = False
|
||||
|
||||
_attr_unique_id: str
|
||||
_knx_module: KNXModule
|
||||
_device: XknxDevice
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Return the name of the KNX device."""
|
||||
return self._device.name
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
@@ -100,16 +97,23 @@ class _KnxEntityBase(Entity):
|
||||
class KnxYamlEntity(_KnxEntityBase):
|
||||
"""Representation of a KNX entity configured from YAML."""
|
||||
|
||||
def __init__(self, knx_module: KNXModule, device: XknxDevice) -> None:
|
||||
def __init__(
|
||||
self,
|
||||
knx_module: KNXModule,
|
||||
unique_id: str,
|
||||
name: str,
|
||||
entity_category: EntityCategory | None,
|
||||
) -> None:
|
||||
"""Initialize the YAML entity."""
|
||||
self._knx_module = knx_module
|
||||
self._device = device
|
||||
self._attr_name = name or None
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_entity_category = entity_category
|
||||
|
||||
|
||||
class KnxUiEntity(_KnxEntityBase):
|
||||
"""Representation of a KNX UI entity."""
|
||||
|
||||
_attr_unique_id: str
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
@@ -117,6 +121,8 @@ class KnxUiEntity(_KnxEntityBase):
|
||||
) -> None:
|
||||
"""Initialize the UI entity."""
|
||||
self._knx_module = knx_module
|
||||
|
||||
self._attr_name = entity_config[CONF_NAME]
|
||||
self._attr_unique_id = unique_id
|
||||
if entity_category := entity_config.get(CONF_ENTITY_CATEGORY):
|
||||
self._attr_entity_category = EntityCategory(entity_category)
|
||||
|
||||
@@ -208,35 +208,32 @@ class KnxYamlFan(_KnxFan, KnxYamlEntity):
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize of KNX fan."""
|
||||
max_step = config.get(FanConf.MAX_STEP)
|
||||
self._device = XknxFan(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address_speed=config.get(KNX_ADDRESS),
|
||||
group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS),
|
||||
group_address_oscillation=config.get(FanSchema.CONF_OSCILLATION_ADDRESS),
|
||||
group_address_oscillation_state=config.get(
|
||||
FanSchema.CONF_OSCILLATION_STATE_ADDRESS
|
||||
),
|
||||
group_address_switch=config.get(FanSchema.CONF_SWITCH_ADDRESS),
|
||||
group_address_switch_state=config.get(FanSchema.CONF_SWITCH_STATE_ADDRESS),
|
||||
max_step=max_step,
|
||||
sync_state=config.get(CONF_SYNC_STATE, True),
|
||||
)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=XknxFan(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address_speed=config.get(KNX_ADDRESS),
|
||||
group_address_speed_state=config.get(FanSchema.CONF_STATE_ADDRESS),
|
||||
group_address_oscillation=config.get(
|
||||
FanSchema.CONF_OSCILLATION_ADDRESS
|
||||
),
|
||||
group_address_oscillation_state=config.get(
|
||||
FanSchema.CONF_OSCILLATION_STATE_ADDRESS
|
||||
),
|
||||
group_address_switch=config.get(FanSchema.CONF_SWITCH_ADDRESS),
|
||||
group_address_switch_state=config.get(
|
||||
FanSchema.CONF_SWITCH_STATE_ADDRESS
|
||||
),
|
||||
max_step=max_step,
|
||||
sync_state=config.get(CONF_SYNC_STATE, True),
|
||||
unique_id=(
|
||||
str(self._device.speed.group_address)
|
||||
if self._device.speed.group_address
|
||||
else str(self._device.switch.group_address)
|
||||
),
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
# FanSpeedMode.STEP if max_step is set
|
||||
self._step_range: tuple[int, int] | None = (1, max_step) if max_step else None
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
|
||||
if self._device.speed.group_address:
|
||||
self._attr_unique_id = str(self._device.speed.group_address)
|
||||
else:
|
||||
self._attr_unique_id = str(self._device.switch.group_address)
|
||||
|
||||
|
||||
class KnxUiFan(_KnxFan, KnxUiEntity):
|
||||
|
||||
@@ -558,15 +558,16 @@ class KnxYamlLight(_KnxLight, KnxYamlEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize of KNX light."""
|
||||
self._device = _create_yaml_light(knx_module.xknx, config)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=_create_yaml_light(knx_module.xknx, config),
|
||||
unique_id=self._device_unique_id(),
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
self._attr_color_mode = next(iter(self.supported_color_modes))
|
||||
self._attr_max_color_temp_kelvin: int = config[LightSchema.CONF_MAX_KELVIN]
|
||||
self._attr_min_color_temp_kelvin: int = config[LightSchema.CONF_MIN_KELVIN]
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_unique_id = self._device_unique_id()
|
||||
|
||||
def _device_unique_id(self) -> str:
|
||||
"""Return unique id for this device."""
|
||||
|
||||
@@ -46,12 +46,13 @@ class KNXNotify(KnxYamlEntity, NotifyEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize a KNX notification."""
|
||||
self._device = _create_notification_instance(knx_module.xknx, config)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=_create_notification_instance(knx_module.xknx, config),
|
||||
unique_id=str(self._device.remote_value.group_address),
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_unique_id = str(self._device.remote_value.group_address)
|
||||
|
||||
async def async_send_message(self, message: str, title: str | None = None) -> None:
|
||||
"""Send a notification to knx bus."""
|
||||
|
||||
@@ -109,16 +109,19 @@ class KnxYamlNumber(_KnxNumber, KnxYamlEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize a KNX number."""
|
||||
self._device = NumericValue(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
value_type=config[CONF_TYPE],
|
||||
)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=NumericValue(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
value_type=config[CONF_TYPE],
|
||||
),
|
||||
unique_id=str(self._device.sensor_value.group_address),
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
dpt_string = self._device.sensor_value.dpt_class.dpt_number_str()
|
||||
dpt_info = get_supported_dpts()[dpt_string]
|
||||
@@ -131,7 +134,6 @@ class KnxYamlNumber(_KnxNumber, KnxYamlEntity):
|
||||
dpt_info["sensor_device_class"],
|
||||
),
|
||||
)
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_mode = config[CONF_MODE]
|
||||
self._attr_native_max_value = config.get(
|
||||
NumberConf.MAX,
|
||||
@@ -149,7 +151,6 @@ class KnxYamlNumber(_KnxNumber, KnxYamlEntity):
|
||||
CONF_UNIT_OF_MEASUREMENT,
|
||||
dpt_info["unit"],
|
||||
)
|
||||
self._attr_unique_id = str(self._device.sensor_value.group_address)
|
||||
|
||||
self._device.sensor_value.value = max(0, self._attr_native_min_value)
|
||||
|
||||
|
||||
@@ -83,18 +83,19 @@ class KnxYamlScene(_KnxScene, KnxYamlEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize KNX scene."""
|
||||
self._device = XknxScene(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address=config[KNX_ADDRESS],
|
||||
scene_number=config[SceneSchema.CONF_SCENE_NUMBER],
|
||||
)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=XknxScene(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address=config[KNX_ADDRESS],
|
||||
scene_number=config[SceneSchema.CONF_SCENE_NUMBER],
|
||||
unique_id=(
|
||||
f"{self._device.scene_value.group_address}_{self._device.scene_number}"
|
||||
),
|
||||
)
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_unique_id = (
|
||||
f"{self._device.scene_value.group_address}_{self._device.scene_number}"
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -199,16 +199,22 @@ class KNXPlatformSchema(ABC):
|
||||
}
|
||||
|
||||
|
||||
COMMON_ENTITY_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=""): cv.string,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
class BinarySensorSchema(KNXPlatformSchema):
|
||||
"""Voluptuous schema for KNX binary sensors."""
|
||||
|
||||
PLATFORM = Platform.BINARY_SENSOR
|
||||
DEFAULT_NAME = "KNX Binary Sensor"
|
||||
|
||||
ENTITY_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
|
||||
vol.Optional(CONF_IGNORE_INTERNAL_STATE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_INVERT, default=False): cv.boolean,
|
||||
@@ -218,7 +224,6 @@ class BinarySensorSchema(KNXPlatformSchema):
|
||||
),
|
||||
vol.Optional(CONF_DEVICE_CLASS): BINARY_SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_RESET_AFTER): cv.positive_float,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
),
|
||||
)
|
||||
@@ -230,7 +235,6 @@ class ButtonSchema(KNXPlatformSchema):
|
||||
PLATFORM = Platform.BUTTON
|
||||
|
||||
CONF_VALUE = "value"
|
||||
DEFAULT_NAME = "KNX Button"
|
||||
|
||||
payload_or_value_msg = f"Please use only one of `{CONF_PAYLOAD}` or `{CONF_VALUE}`"
|
||||
length_or_type_msg = (
|
||||
@@ -238,9 +242,8 @@ class ButtonSchema(KNXPlatformSchema):
|
||||
)
|
||||
|
||||
ENTITY_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(KNX_ADDRESS): ga_validator,
|
||||
vol.Exclusive(
|
||||
CONF_PAYLOAD, "payload_or_value", msg=payload_or_value_msg
|
||||
@@ -254,7 +257,6 @@ class ButtonSchema(KNXPlatformSchema):
|
||||
vol.Exclusive(
|
||||
CONF_TYPE, "length_or_type", msg=length_or_type_msg
|
||||
): object,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
),
|
||||
vol.Any(
|
||||
@@ -322,7 +324,6 @@ class ClimateSchema(KNXPlatformSchema):
|
||||
CONF_SWING_HORIZONTAL_ADDRESS = "swing_horizontal_address"
|
||||
CONF_SWING_HORIZONTAL_STATE_ADDRESS = "swing_horizontal_state_address"
|
||||
|
||||
DEFAULT_NAME = "KNX Climate"
|
||||
DEFAULT_SETPOINT_SHIFT_MODE = "DPT6010"
|
||||
DEFAULT_SETPOINT_SHIFT_MAX = 6
|
||||
DEFAULT_SETPOINT_SHIFT_MIN = -6
|
||||
@@ -331,9 +332,8 @@ class ClimateSchema(KNXPlatformSchema):
|
||||
DEFAULT_FAN_SPEED_MODE = "percent"
|
||||
|
||||
ENTITY_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(
|
||||
ClimateConf.SETPOINT_SHIFT_MAX, default=DEFAULT_SETPOINT_SHIFT_MAX
|
||||
): vol.All(int, vol.Range(min=0, max=32)),
|
||||
@@ -399,7 +399,6 @@ class ClimateSchema(KNXPlatformSchema):
|
||||
): vol.Coerce(HVACMode),
|
||||
vol.Optional(ClimateConf.MIN_TEMP): vol.Coerce(float),
|
||||
vol.Optional(ClimateConf.MAX_TEMP): vol.Coerce(float),
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_FAN_SPEED_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_FAN_SPEED_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(ClimateConf.FAN_MAX_STEP, default=3): cv.byte,
|
||||
@@ -433,12 +432,10 @@ class CoverSchema(KNXPlatformSchema):
|
||||
CONF_ANGLE_STATE_ADDRESS = "angle_state_address"
|
||||
|
||||
DEFAULT_TRAVEL_TIME = 25
|
||||
DEFAULT_NAME = "KNX Cover"
|
||||
|
||||
ENTITY_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_MOVE_LONG_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_MOVE_SHORT_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_STOP_ADDRESS): ga_list_validator,
|
||||
@@ -456,7 +453,6 @@ class CoverSchema(KNXPlatformSchema):
|
||||
vol.Optional(CoverConf.INVERT_POSITION, default=False): cv.boolean,
|
||||
vol.Optional(CoverConf.INVERT_ANGLE, default=False): cv.boolean,
|
||||
vol.Optional(CONF_DEVICE_CLASS): COVER_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
),
|
||||
vol.Any(
|
||||
@@ -481,16 +477,12 @@ class DateSchema(KNXPlatformSchema):
|
||||
|
||||
PLATFORM = Platform.DATE
|
||||
|
||||
DEFAULT_NAME = "KNX Date"
|
||||
|
||||
ENTITY_SCHEMA = vol.Schema(
|
||||
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
|
||||
vol.Required(KNX_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -500,16 +492,12 @@ class DateTimeSchema(KNXPlatformSchema):
|
||||
|
||||
PLATFORM = Platform.DATETIME
|
||||
|
||||
DEFAULT_NAME = "KNX DateTime"
|
||||
|
||||
ENTITY_SCHEMA = vol.Schema(
|
||||
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
|
||||
vol.Required(KNX_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -571,12 +559,9 @@ class FanSchema(KNXPlatformSchema):
|
||||
CONF_SWITCH_ADDRESS = "switch_address"
|
||||
CONF_SWITCH_STATE_ADDRESS = "switch_state_address"
|
||||
|
||||
DEFAULT_NAME = "KNX Fan"
|
||||
|
||||
ENTITY_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(KNX_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_SWITCH_ADDRESS): ga_list_validator,
|
||||
@@ -584,7 +569,6 @@ class FanSchema(KNXPlatformSchema):
|
||||
vol.Optional(CONF_OSCILLATION_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_OSCILLATION_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(FanConf.MAX_STEP): cv.byte,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
|
||||
}
|
||||
),
|
||||
@@ -629,7 +613,6 @@ class LightSchema(KNXPlatformSchema):
|
||||
CONF_MIN_KELVIN = "min_kelvin"
|
||||
CONF_MAX_KELVIN = "max_kelvin"
|
||||
|
||||
DEFAULT_NAME = "KNX Light"
|
||||
DEFAULT_COLOR_TEMP_MODE = "absolute"
|
||||
DEFAULT_MIN_KELVIN = 2700 # 370 mireds
|
||||
DEFAULT_MAX_KELVIN = 6000 # 166 mireds
|
||||
@@ -661,9 +644,8 @@ class LightSchema(KNXPlatformSchema):
|
||||
)
|
||||
|
||||
ENTITY_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(KNX_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_BRIGHTNESS_ADDRESS): ga_list_validator,
|
||||
@@ -713,7 +695,6 @@ class LightSchema(KNXPlatformSchema):
|
||||
vol.Optional(CONF_MAX_KELVIN, default=DEFAULT_MAX_KELVIN): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1)
|
||||
),
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
),
|
||||
vol.Any(
|
||||
@@ -759,14 +740,10 @@ class NotifySchema(KNXPlatformSchema):
|
||||
|
||||
PLATFORM = Platform.NOTIFY
|
||||
|
||||
DEFAULT_NAME = "KNX Notify"
|
||||
|
||||
ENTITY_SCHEMA = vol.Schema(
|
||||
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_TYPE, default="latin_1"): string_type_validator,
|
||||
vol.Required(KNX_ADDRESS): ga_validator,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -775,12 +752,10 @@ class NumberSchema(KNXPlatformSchema):
|
||||
"""Voluptuous schema for KNX numbers."""
|
||||
|
||||
PLATFORM = Platform.NUMBER
|
||||
DEFAULT_NAME = "KNX Number"
|
||||
|
||||
ENTITY_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
|
||||
vol.Optional(CONF_MODE, default=NumberMode.AUTO): vol.Coerce(
|
||||
NumberMode
|
||||
@@ -793,7 +768,6 @@ class NumberSchema(KNXPlatformSchema):
|
||||
vol.Optional(NumberConf.STEP): cv.positive_float,
|
||||
vol.Optional(CONF_DEVICE_CLASS): NUMBER_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
),
|
||||
_number_limit_sub_validator,
|
||||
@@ -807,15 +781,12 @@ class SceneSchema(KNXPlatformSchema):
|
||||
|
||||
CONF_SCENE_NUMBER = "scene_number"
|
||||
|
||||
DEFAULT_NAME = "KNX SCENE"
|
||||
ENTITY_SCHEMA = vol.Schema(
|
||||
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Required(KNX_ADDRESS): ga_list_validator,
|
||||
vol.Required(SceneConf.SCENE_NUMBER): vol.All(
|
||||
vol.Coerce(int), vol.Range(min=1, max=64)
|
||||
),
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -827,12 +798,10 @@ class SelectSchema(KNXPlatformSchema):
|
||||
|
||||
CONF_OPTION = "option"
|
||||
CONF_OPTIONS = "options"
|
||||
DEFAULT_NAME = "KNX Select"
|
||||
|
||||
ENTITY_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
|
||||
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
|
||||
vol.Required(CONF_PAYLOAD_LENGTH): vol.All(
|
||||
@@ -846,7 +815,6 @@ class SelectSchema(KNXPlatformSchema):
|
||||
],
|
||||
vol.Required(KNX_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
),
|
||||
select_options_sub_validator,
|
||||
@@ -861,12 +829,10 @@ class SensorSchema(KNXPlatformSchema):
|
||||
CONF_ALWAYS_CALLBACK = "always_callback"
|
||||
CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
|
||||
CONF_SYNC_STATE = CONF_SYNC_STATE
|
||||
DEFAULT_NAME = "KNX Sensor"
|
||||
|
||||
ENTITY_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
|
||||
vol.Optional(CONF_ALWAYS_CALLBACK, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SENSOR_STATE_CLASS): STATE_CLASSES_SCHEMA,
|
||||
@@ -874,7 +840,6 @@ class SensorSchema(KNXPlatformSchema):
|
||||
vol.Required(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_DEVICE_CLASS): SENSOR_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_UNIT_OF_MEASUREMENT): cv.string,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
),
|
||||
_sensor_attribute_sub_validator,
|
||||
@@ -889,16 +854,13 @@ class SwitchSchema(KNXPlatformSchema):
|
||||
CONF_INVERT = CONF_INVERT
|
||||
CONF_STATE_ADDRESS = CONF_STATE_ADDRESS
|
||||
|
||||
DEFAULT_NAME = "KNX Switch"
|
||||
ENTITY_SCHEMA = vol.Schema(
|
||||
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_INVERT, default=False): cv.boolean,
|
||||
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
|
||||
vol.Required(KNX_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_DEVICE_CLASS): SWITCH_DEVICE_CLASSES_SCHEMA,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -908,17 +870,13 @@ class TextSchema(KNXPlatformSchema):
|
||||
|
||||
PLATFORM = Platform.TEXT
|
||||
|
||||
DEFAULT_NAME = "KNX Text"
|
||||
|
||||
ENTITY_SCHEMA = vol.Schema(
|
||||
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
|
||||
vol.Optional(CONF_TYPE, default="latin_1"): string_type_validator,
|
||||
vol.Optional(CONF_MODE, default=TextMode.TEXT): vol.Coerce(TextMode),
|
||||
vol.Required(KNX_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -928,16 +886,12 @@ class TimeSchema(KNXPlatformSchema):
|
||||
|
||||
PLATFORM = Platform.TIME
|
||||
|
||||
DEFAULT_NAME = "KNX Time"
|
||||
|
||||
ENTITY_SCHEMA = vol.Schema(
|
||||
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_RESPOND_TO_READ, default=False): cv.boolean,
|
||||
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
|
||||
vol.Required(KNX_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_STATE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -962,27 +916,21 @@ class WeatherSchema(KNXPlatformSchema):
|
||||
CONF_KNX_AIR_PRESSURE_ADDRESS = "address_air_pressure"
|
||||
CONF_KNX_HUMIDITY_ADDRESS = "address_humidity"
|
||||
|
||||
DEFAULT_NAME = "KNX Weather Station"
|
||||
|
||||
ENTITY_SCHEMA = vol.All(
|
||||
vol.Schema(
|
||||
{
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
|
||||
vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_WIND_BEARING_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_ENTITY_CATEGORY): ENTITY_CATEGORIES_SCHEMA,
|
||||
}
|
||||
),
|
||||
ENTITY_SCHEMA = COMMON_ENTITY_SCHEMA.extend(
|
||||
{
|
||||
vol.Optional(CONF_SYNC_STATE, default=True): sync_state_validator,
|
||||
vol.Required(CONF_KNX_TEMPERATURE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_BRIGHTNESS_SOUTH_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_BRIGHTNESS_EAST_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_BRIGHTNESS_WEST_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_BRIGHTNESS_NORTH_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_WIND_SPEED_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_WIND_BEARING_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_RAIN_ALARM_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_FROST_ALARM_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_WIND_ALARM_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_DAY_NIGHT_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_AIR_PRESSURE_ADDRESS): ga_list_validator,
|
||||
vol.Optional(CONF_KNX_HUMIDITY_ADDRESS): ga_list_validator,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -65,9 +65,12 @@ class KNXSelect(KnxYamlEntity, SelectEntity, RestoreEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize a KNX select."""
|
||||
self._device = _create_raw_value(knx_module.xknx, config)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=_create_raw_value(knx_module.xknx, config),
|
||||
unique_id=str(self._device.remote_value.group_address),
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
self._option_payloads: dict[str, int] = {
|
||||
option[SelectSchema.CONF_OPTION]: option[CONF_PAYLOAD]
|
||||
@@ -75,8 +78,6 @@ class KNXSelect(KnxYamlEntity, SelectEntity, RestoreEntity):
|
||||
}
|
||||
self._attr_options = list(self._option_payloads)
|
||||
self._attr_current_option = None
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_unique_id = str(self._device.remote_value.group_address)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
"""Restore last state."""
|
||||
|
||||
@@ -202,16 +202,19 @@ class KnxYamlSensor(_KnxSensor, KnxYamlEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize of a KNX sensor."""
|
||||
self._device = XknxSensor(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address_state=config[SensorSchema.CONF_STATE_ADDRESS],
|
||||
sync_state=config[CONF_SYNC_STATE],
|
||||
always_callback=True,
|
||||
value_type=config[CONF_TYPE],
|
||||
)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=XknxSensor(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address_state=config[SensorSchema.CONF_STATE_ADDRESS],
|
||||
sync_state=config[CONF_SYNC_STATE],
|
||||
always_callback=True,
|
||||
value_type=config[CONF_TYPE],
|
||||
),
|
||||
unique_id=str(self._device.sensor_value.group_address_state),
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
dpt_string = self._device.sensor_value.dpt_class.dpt_number_str()
|
||||
dpt_info = get_supported_dpts()[dpt_string]
|
||||
@@ -220,7 +223,6 @@ class KnxYamlSensor(_KnxSensor, KnxYamlEntity):
|
||||
CONF_DEVICE_CLASS,
|
||||
dpt_info["sensor_device_class"],
|
||||
)
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_extra_state_attributes = {}
|
||||
self._attr_force_update = config[SensorSchema.CONF_ALWAYS_CALLBACK]
|
||||
self._attr_native_unit_of_measurement = config.get(
|
||||
@@ -231,7 +233,6 @@ class KnxYamlSensor(_KnxSensor, KnxYamlEntity):
|
||||
CONF_STATE_CLASS,
|
||||
dpt_info["sensor_state_class"],
|
||||
)
|
||||
self._attr_unique_id = str(self._device.sensor_value.group_address_state)
|
||||
|
||||
|
||||
class KnxUiSensor(_KnxSensor, KnxUiEntity):
|
||||
|
||||
@@ -107,20 +107,21 @@ class KnxYamlSwitch(_KnxSwitch, KnxYamlEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize of KNX switch."""
|
||||
self._device = XknxSwitch(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
invert=config[SwitchSchema.CONF_INVERT],
|
||||
)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=XknxSwitch(
|
||||
xknx=knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(SwitchSchema.CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
invert=config[SwitchSchema.CONF_INVERT],
|
||||
),
|
||||
unique_id=str(self._device.switch.group_address),
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_device_class = config.get(CONF_DEVICE_CLASS)
|
||||
self._attr_unique_id = str(self._device.switch.group_address)
|
||||
|
||||
|
||||
class KnxUiSwitch(_KnxSwitch, KnxUiEntity):
|
||||
|
||||
@@ -112,20 +112,21 @@ class KnxYamlText(_KnxText, KnxYamlEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize a KNX text."""
|
||||
self._device = XknxNotification(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
value_type=config[CONF_TYPE],
|
||||
)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=XknxNotification(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
value_type=config[CONF_TYPE],
|
||||
),
|
||||
unique_id=str(self._device.remote_value.group_address),
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
self._attr_mode = config[CONF_MODE]
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_unique_id = str(self._device.remote_value.group_address)
|
||||
|
||||
|
||||
class KnxUiText(_KnxText, KnxUiEntity):
|
||||
|
||||
@@ -105,20 +105,21 @@ class KnxYamlTime(_KNXTime, KnxYamlEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize a KNX time."""
|
||||
self._device = XknxTimeDevice(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
localtime=False,
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
sync_state=config[CONF_SYNC_STATE],
|
||||
)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=XknxTimeDevice(
|
||||
knx_module.xknx,
|
||||
name=config[CONF_NAME],
|
||||
localtime=False,
|
||||
group_address=config[KNX_ADDRESS],
|
||||
group_address_state=config.get(CONF_STATE_ADDRESS),
|
||||
respond_to_read=config[CONF_RESPOND_TO_READ],
|
||||
sync_state=config[CONF_SYNC_STATE],
|
||||
),
|
||||
unique_id=str(self._device.remote_value.group_address),
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
self._attr_unique_id = str(self._device.remote_value.group_address)
|
||||
|
||||
|
||||
class KnxUiTime(_KNXTime, KnxUiEntity):
|
||||
|
||||
@@ -85,12 +85,13 @@ class KNXWeather(KnxYamlEntity, WeatherEntity):
|
||||
|
||||
def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
|
||||
"""Initialize of a KNX sensor."""
|
||||
self._device = _create_weather(knx_module.xknx, config)
|
||||
super().__init__(
|
||||
knx_module=knx_module,
|
||||
device=_create_weather(knx_module.xknx, config),
|
||||
unique_id=str(self._device._temperature.group_address_state), # noqa: SLF001
|
||||
name=config[CONF_NAME],
|
||||
entity_category=config.get(CONF_ENTITY_CATEGORY),
|
||||
)
|
||||
self._attr_unique_id = str(self._device._temperature.group_address_state) # noqa: SLF001
|
||||
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
|
||||
|
||||
@property
|
||||
def native_temperature(self) -> float | None:
|
||||
|
||||
@@ -41,7 +41,7 @@ class LGDevice(MediaPlayerEntity):
|
||||
"""Representation of an LG soundbar device."""
|
||||
|
||||
_attr_should_poll = False
|
||||
_attr_state = MediaPlayerState.ON
|
||||
_attr_state = MediaPlayerState.OFF
|
||||
_attr_supported_features = (
|
||||
MediaPlayerEntityFeature.VOLUME_SET
|
||||
| MediaPlayerEntityFeature.VOLUME_MUTE
|
||||
@@ -79,6 +79,8 @@ class LGDevice(MediaPlayerEntity):
|
||||
self._treble = 0
|
||||
self._device = None
|
||||
self._support_play_control = False
|
||||
self._device_on = False
|
||||
self._stream_type = 0
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, unique_id)}, name=host
|
||||
)
|
||||
@@ -113,6 +115,7 @@ class LGDevice(MediaPlayerEntity):
|
||||
if "i_curr_func" in data:
|
||||
self._function = data["i_curr_func"]
|
||||
if "b_powerstatus" in data:
|
||||
self._device_on = data["b_powerstatus"]
|
||||
if data["b_powerstatus"]:
|
||||
self._attr_state = MediaPlayerState.ON
|
||||
else:
|
||||
@@ -157,17 +160,34 @@ class LGDevice(MediaPlayerEntity):
|
||||
|
||||
def _update_playinfo(self, data: dict[str, Any]) -> None:
|
||||
"""Update the player info."""
|
||||
if "i_stream_type" in data:
|
||||
if self._stream_type != data["i_stream_type"]:
|
||||
self._stream_type = data["i_stream_type"]
|
||||
# Ask device for current play info when stream type changed.
|
||||
self._device.get_play()
|
||||
if data["i_stream_type"] == 0:
|
||||
# If the stream type is 0 (aka the soundbar is used as an actual soundbar)
|
||||
# the last track info should be cleared and the state should only be on or off,
|
||||
# as all playing/paused are not applicable in this mode
|
||||
self._attr_media_image_url = None
|
||||
self._attr_media_artist = None
|
||||
self._attr_media_title = None
|
||||
if self._device_on:
|
||||
self._attr_state = MediaPlayerState.ON
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.OFF
|
||||
if "i_play_ctrl" in data:
|
||||
if data["i_play_ctrl"] == 0:
|
||||
self._attr_state = MediaPlayerState.PLAYING
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.PAUSED
|
||||
if self._device_on and self._stream_type != 0:
|
||||
if data["i_play_ctrl"] == 0:
|
||||
self._attr_state = MediaPlayerState.PLAYING
|
||||
else:
|
||||
self._attr_state = MediaPlayerState.PAUSED
|
||||
if "s_albumart" in data:
|
||||
self._attr_media_image_url = data["s_albumart"]
|
||||
self._attr_media_image_url = data["s_albumart"].strip() or None
|
||||
if "s_artist" in data:
|
||||
self._attr_media_artist = data["s_artist"]
|
||||
self._attr_media_artist = data["s_artist"].strip() or None
|
||||
if "s_title" in data:
|
||||
self._attr_media_title = data["s_title"]
|
||||
self._attr_media_title = data["s_title"].strip() or None
|
||||
if "b_support_play_ctrl" in data:
|
||||
self._support_play_control = data["b_support_play_ctrl"]
|
||||
|
||||
|
||||
31
homeassistant/components/lichess/__init__.py
Normal file
31
homeassistant/components/lichess/__init__.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""The Lichess integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .coordinator import LichessConfigEntry, LichessCoordinator
|
||||
|
||||
_PLATFORMS: list[Platform] = [
|
||||
Platform.SENSOR,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: LichessConfigEntry) -> bool:
|
||||
"""Set up Lichess from a config entry."""
|
||||
|
||||
coordinator = LichessCoordinator(hass, entry)
|
||||
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
entry.runtime_data = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, _PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: LichessConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
return await hass.config_entries.async_unload_platforms(entry, _PLATFORMS)
|
||||
52
homeassistant/components/lichess/config_flow.py
Normal file
52
homeassistant/components/lichess/config_flow.py
Normal file
@@ -0,0 +1,52 @@
|
||||
"""Config flow for the Lichess integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from aiolichess import AioLichess
|
||||
from aiolichess.exceptions import AioLichessError, AuthError
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
|
||||
from .const import DOMAIN
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class LichessConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Lichess."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
session = async_get_clientsession(self.hass)
|
||||
client = AioLichess(session=session)
|
||||
try:
|
||||
user = await client.get_all(token=user_input[CONF_API_TOKEN])
|
||||
except AuthError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except AioLichessError:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
username = user.username
|
||||
player_id = user.id
|
||||
await self.async_set_unique_id(player_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(title=username, data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user",
|
||||
data_schema=vol.Schema({vol.Required(CONF_API_TOKEN): str}),
|
||||
errors=errors,
|
||||
)
|
||||
3
homeassistant/components/lichess/const.py
Normal file
3
homeassistant/components/lichess/const.py
Normal file
@@ -0,0 +1,3 @@
|
||||
"""Constants for the Lichess integration."""
|
||||
|
||||
DOMAIN = "lichess"
|
||||
44
homeassistant/components/lichess/coordinator.py
Normal file
44
homeassistant/components/lichess/coordinator.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""Coordinator for Lichess."""
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
|
||||
from aiolichess import AioLichess
|
||||
from aiolichess.exceptions import AioLichessError
|
||||
from aiolichess.models import LichessStatistics
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_API_TOKEN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
type LichessConfigEntry = ConfigEntry[LichessCoordinator]
|
||||
|
||||
|
||||
class LichessCoordinator(DataUpdateCoordinator[LichessStatistics]):
|
||||
"""Coordinator for Lichess."""
|
||||
|
||||
config_entry: LichessConfigEntry
|
||||
|
||||
def __init__(self, hass: HomeAssistant, config_entry: LichessConfigEntry) -> None:
|
||||
"""Initialize the coordinator."""
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
config_entry=config_entry,
|
||||
name=config_entry.title,
|
||||
update_interval=timedelta(hours=1),
|
||||
)
|
||||
self.client = AioLichess(session=async_get_clientsession(hass))
|
||||
|
||||
async def _async_update_data(self) -> LichessStatistics:
|
||||
"""Update data for Lichess."""
|
||||
try:
|
||||
return await self.client.get_statistics(
|
||||
token=self.config_entry.data[CONF_API_TOKEN]
|
||||
)
|
||||
except AioLichessError as err:
|
||||
raise UpdateFailed("Error in communicating with Lichess") from err
|
||||
26
homeassistant/components/lichess/entity.py
Normal file
26
homeassistant/components/lichess/entity.py
Normal file
@@ -0,0 +1,26 @@
|
||||
"""Base entity for Lichess integration."""
|
||||
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import LichessCoordinator
|
||||
|
||||
|
||||
class LichessEntity(CoordinatorEntity[LichessCoordinator]):
|
||||
"""Base entity for Lichess integration."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(self, coordinator: LichessCoordinator) -> None:
|
||||
"""Initialize the entity."""
|
||||
super().__init__(coordinator)
|
||||
if TYPE_CHECKING:
|
||||
assert coordinator.config_entry.unique_id is not None
|
||||
self._attr_device_info = DeviceInfo(
|
||||
identifiers={(DOMAIN, coordinator.config_entry.unique_id)},
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
manufacturer="Lichess",
|
||||
)
|
||||
30
homeassistant/components/lichess/icons.json
Normal file
30
homeassistant/components/lichess/icons.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"blitz_games": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"blitz_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"bullet_games": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"bullet_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"classical_games": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"classical_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
},
|
||||
"rapid_games": {
|
||||
"default": "mdi:chess-pawn"
|
||||
},
|
||||
"rapid_rating": {
|
||||
"default": "mdi:chart-line"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
11
homeassistant/components/lichess/manifest.json
Normal file
11
homeassistant/components/lichess/manifest.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"domain": "lichess",
|
||||
"name": "Lichess",
|
||||
"codeowners": ["@aryanhasgithub"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/lichess",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"quality_scale": "bronze",
|
||||
"requirements": ["aiolichess==1.2.0"]
|
||||
}
|
||||
72
homeassistant/components/lichess/quality_scale.yaml
Normal file
72
homeassistant/components/lichess/quality_scale.yaml
Normal file
@@ -0,0 +1,72 @@
|
||||
rules:
|
||||
# Bronze
|
||||
action-setup:
|
||||
status: exempt
|
||||
comment: There are no custom actions present
|
||||
appropriate-polling: done
|
||||
brands: done
|
||||
common-modules: done
|
||||
config-flow: done
|
||||
config-flow-test-coverage: done
|
||||
dependency-transparency: done
|
||||
docs-actions:
|
||||
status: exempt
|
||||
comment: There are no custom actions present
|
||||
docs-high-level-description: done
|
||||
docs-installation-instructions: done
|
||||
docs-removal-instructions: done
|
||||
entity-event-setup:
|
||||
status: exempt
|
||||
comment: The entities do not explicitly subscribe to events
|
||||
entity-unique-id: done
|
||||
has-entity-name: done
|
||||
runtime-data: done
|
||||
test-before-configure: done
|
||||
test-before-setup: done
|
||||
unique-config-entry: done
|
||||
|
||||
# Silver
|
||||
action-exceptions:
|
||||
status: exempt
|
||||
comment: There are no custom actions
|
||||
config-entry-unloading: done
|
||||
docs-configuration-parameters: todo
|
||||
docs-installation-parameters: todo
|
||||
entity-unavailable: todo
|
||||
integration-owner: done
|
||||
log-when-unavailable: todo
|
||||
parallel-updates: todo
|
||||
reauthentication-flow: todo
|
||||
test-coverage: done
|
||||
|
||||
# Gold
|
||||
devices: done
|
||||
diagnostics: todo
|
||||
discovery-update-info:
|
||||
status: exempt
|
||||
comment: The integration does not use discovery
|
||||
discovery:
|
||||
status: exempt
|
||||
comment: The integration does not use discovery
|
||||
docs-data-update: todo
|
||||
docs-examples: todo
|
||||
docs-known-limitations: todo
|
||||
docs-supported-devices: todo
|
||||
docs-supported-functions: todo
|
||||
docs-troubleshooting: todo
|
||||
docs-use-cases: todo
|
||||
dynamic-devices: todo
|
||||
entity-category: done
|
||||
entity-device-class: todo
|
||||
entity-disabled-by-default: todo
|
||||
entity-translations: todo
|
||||
exception-translations: todo
|
||||
icon-translations: todo
|
||||
reconfiguration-flow: todo
|
||||
repair-issues: todo
|
||||
stale-devices: todo
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
inject-websession: done
|
||||
strict-typing: todo
|
||||
116
homeassistant/components/lichess/sensor.py
Normal file
116
homeassistant/components/lichess/sensor.py
Normal file
@@ -0,0 +1,116 @@
|
||||
"""Sensor platform for Lichess integration."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
|
||||
from aiolichess.models import LichessStatistics
|
||||
|
||||
from homeassistant.components.sensor import (
|
||||
SensorEntity,
|
||||
SensorEntityDescription,
|
||||
SensorStateClass,
|
||||
)
|
||||
from homeassistant.const import EntityCategory
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
|
||||
from . import LichessConfigEntry
|
||||
from .coordinator import LichessCoordinator
|
||||
from .entity import LichessEntity
|
||||
|
||||
|
||||
@dataclass(kw_only=True, frozen=True)
|
||||
class LichessEntityDescription(SensorEntityDescription):
|
||||
"""Sensor description for Lichess player."""
|
||||
|
||||
value_fn: Callable[[LichessStatistics], int | None]
|
||||
|
||||
|
||||
SENSORS: tuple[LichessEntityDescription, ...] = (
|
||||
LichessEntityDescription(
|
||||
key="bullet_rating",
|
||||
translation_key="bullet_rating",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda state: state.bullet_rating,
|
||||
),
|
||||
LichessEntityDescription(
|
||||
key="bullet_games",
|
||||
translation_key="bullet_games",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda state: state.bullet_games,
|
||||
),
|
||||
LichessEntityDescription(
|
||||
key="blitz_rating",
|
||||
translation_key="blitz_rating",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda state: state.blitz_rating,
|
||||
),
|
||||
LichessEntityDescription(
|
||||
key="blitz_games",
|
||||
translation_key="blitz_games",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda state: state.blitz_games,
|
||||
),
|
||||
LichessEntityDescription(
|
||||
key="rapid_rating",
|
||||
translation_key="rapid_rating",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda state: state.rapid_rating,
|
||||
),
|
||||
LichessEntityDescription(
|
||||
key="rapid_games",
|
||||
translation_key="rapid_games",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda state: state.rapid_games,
|
||||
),
|
||||
LichessEntityDescription(
|
||||
key="classical_rating",
|
||||
translation_key="classical_rating",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
value_fn=lambda state: state.classical_rating,
|
||||
),
|
||||
LichessEntityDescription(
|
||||
key="classical_games",
|
||||
translation_key="classical_games",
|
||||
state_class=SensorStateClass.TOTAL_INCREASING,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
value_fn=lambda state: state.classical_games,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: LichessConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Initialize the entries."""
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
async_add_entities(
|
||||
LichessPlayerSensor(coordinator, description) for description in SENSORS
|
||||
)
|
||||
|
||||
|
||||
class LichessPlayerSensor(LichessEntity, SensorEntity):
|
||||
"""Lichess sensor."""
|
||||
|
||||
entity_description: LichessEntityDescription
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: LichessCoordinator,
|
||||
description: LichessEntityDescription,
|
||||
) -> None:
|
||||
"""Initialize the sensor."""
|
||||
super().__init__(coordinator)
|
||||
self.entity_description = description
|
||||
self._attr_unique_id = f"{coordinator.config_entry.unique_id}.{description.key}"
|
||||
|
||||
@property
|
||||
def native_value(self) -> int | None:
|
||||
"""Return the state of the sensor."""
|
||||
return self.entity_description.value_fn(self.coordinator.data)
|
||||
54
homeassistant/components/lichess/strings.json
Normal file
54
homeassistant/components/lichess/strings.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||
},
|
||||
"error": {
|
||||
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"api_token": "[%key:common::config_flow::data::api_token%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_token": "The Lichess API token of the player."
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"sensor": {
|
||||
"blitz_games": {
|
||||
"name": "Blitz games",
|
||||
"unit_of_measurement": "[%key:component::lichess::entity::sensor::bullet_games::unit_of_measurement%]"
|
||||
},
|
||||
"blitz_rating": {
|
||||
"name": "Blitz rating"
|
||||
},
|
||||
"bullet_games": {
|
||||
"name": "Bullet games",
|
||||
"unit_of_measurement": "games"
|
||||
},
|
||||
"bullet_rating": {
|
||||
"name": "Bullet rating"
|
||||
},
|
||||
"classical_games": {
|
||||
"name": "Classical games",
|
||||
"unit_of_measurement": "[%key:component::lichess::entity::sensor::bullet_games::unit_of_measurement%]"
|
||||
},
|
||||
"classical_rating": {
|
||||
"name": "Classical rating"
|
||||
},
|
||||
"rapid_games": {
|
||||
"name": "Rapid games",
|
||||
"unit_of_measurement": "[%key:component::lichess::entity::sensor::bullet_games::unit_of_measurement%]"
|
||||
},
|
||||
"rapid_rating": {
|
||||
"name": "Rapid rating"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ from pyliebherrhomeapi.exceptions import (
|
||||
from homeassistant.const import CONF_API_KEY, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
@@ -83,6 +84,28 @@ async def async_setup_entry(hass: HomeAssistant, entry: LiebherrConfigEntry) ->
|
||||
_LOGGER.exception("Unexpected error scanning for new devices")
|
||||
return
|
||||
|
||||
# Remove stale devices no longer returned by the API
|
||||
current_device_ids = {device.device_id for device in devices}
|
||||
device_registry = dr.async_get(hass)
|
||||
for device_entry in dr.async_entries_for_config_entry(
|
||||
device_registry, entry.entry_id
|
||||
):
|
||||
device_ids = {
|
||||
identifier[1]
|
||||
for identifier in device_entry.identifiers
|
||||
if identifier[0] == DOMAIN
|
||||
}
|
||||
if device_ids - current_device_ids:
|
||||
# Shut down coordinator if one exists
|
||||
for device_id in device_ids:
|
||||
if coordinator := data.coordinators.pop(device_id, None):
|
||||
await coordinator.async_shutdown()
|
||||
device_registry.async_update_device(
|
||||
device_id=device_entry.id,
|
||||
remove_config_entry_id=entry.entry_id,
|
||||
)
|
||||
|
||||
# Add new devices
|
||||
new_coordinators: list[LiebherrCoordinator] = []
|
||||
for device in devices:
|
||||
if device.device_id not in data.coordinators:
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["pyliebherrhomeapi"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["pyliebherrhomeapi==0.3.0"],
|
||||
"quality_scale": "gold",
|
||||
"requirements": ["pyliebherrhomeapi==0.4.0"],
|
||||
"zeroconf": [
|
||||
{
|
||||
"name": "liebherr*",
|
||||
|
||||
@@ -68,7 +68,7 @@ rules:
|
||||
repair-issues:
|
||||
status: exempt
|
||||
comment: No repair issues to implement at this time.
|
||||
stale-devices: todo
|
||||
stale-devices: done
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
|
||||
@@ -199,15 +199,15 @@ class LiebherrSelectEntity(LiebherrEntity, SelectEntity):
|
||||
def _select_control(self) -> SelectControl | None:
|
||||
"""Get the select control for this entity."""
|
||||
for control in self.coordinator.data.controls:
|
||||
if not isinstance(
|
||||
control,
|
||||
IceMakerControl | HydroBreezeControl | BioFreshPlusControl,
|
||||
):
|
||||
continue
|
||||
if (
|
||||
isinstance(control, self.entity_description.control_type)
|
||||
and control.zone_id == self._zone_id
|
||||
):
|
||||
if TYPE_CHECKING:
|
||||
assert isinstance(
|
||||
control,
|
||||
IceMakerControl | HydroBreezeControl | BioFreshPlusControl,
|
||||
)
|
||||
return control
|
||||
return None
|
||||
|
||||
|
||||
@@ -617,8 +617,10 @@ 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
|
||||
@@ -723,7 +725,7 @@ class OvenProgramId(MieleEnum, missing_to_none=True):
|
||||
belgian_sponge_cake = 624
|
||||
goose_unstuffed = 625
|
||||
rack_of_lamb_with_vegetables = 634
|
||||
yorkshire_pudding = 635
|
||||
yorkshire_pudding = 635, 2352
|
||||
meat_loaf = 636
|
||||
defrost_meat = 647
|
||||
defrost_vegetables = 654
|
||||
@@ -1123,7 +1125,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 = 3383
|
||||
basmati_rice_steam_cooking = 3382, 3383
|
||||
basmati_rice_rapid_steam_cooking = 3384
|
||||
jasmine_rice_steam_cooking = 3386
|
||||
jasmine_rice_rapid_steam_cooking = 3387
|
||||
@@ -1131,7 +1133,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 = 3395
|
||||
long_grain_rice_general_steam_cooking = 3394, 3395
|
||||
long_grain_rice_general_rapid_steam_cooking = 3396
|
||||
chongming_steam_cooking = 3398
|
||||
chongming_rapid_steam_cooking = 3399
|
||||
|
||||
@@ -560,6 +560,7 @@
|
||||
"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 the cover.",
|
||||
"description": "Sets the absolute position of a cover.",
|
||||
"fields": {
|
||||
"absolute_position": {
|
||||
"description": "Absolute position to move to.",
|
||||
|
||||
@@ -13,7 +13,7 @@ from homeassistant.const import (
|
||||
CONF_PASSWORD,
|
||||
CONF_USERNAME,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant, callback, split_entity_id
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers import device_registry as dr, entity_registry as er
|
||||
from homeassistant.helpers.device_registry import DeviceEntry
|
||||
|
||||
@@ -103,10 +103,8 @@ def _async_device_as_dict(hass: HomeAssistant, device: DeviceEntry) -> dict[str,
|
||||
# The context doesn't provide useful information in this case.
|
||||
state_dict.pop("context", None)
|
||||
|
||||
entity_domain = split_entity_id(state.entity_id)[0]
|
||||
|
||||
# Retract some sensitive state attributes
|
||||
if entity_domain == device_tracker.DOMAIN:
|
||||
if state.domain == device_tracker.DOMAIN:
|
||||
state_dict["attributes"] = async_redact_data(
|
||||
state_dict["attributes"], REDACT_STATE_DEVICE_TRACKER
|
||||
)
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/opendisplay",
|
||||
"integration_type": "device",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["opendisplay"],
|
||||
"quality_scale": "silver",
|
||||
"requirements": ["py-opendisplay==5.5.0"]
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user