Compare commits

..

59 Commits

Author SHA1 Message Date
abmantis
280dda6ea9 Replace ding with new ring event in Ring integration doorbel 2026-04-09 00:16:38 +01:00
Abílio Costa
57568fdc2c Add standard event type for doorbell event entities (#167630) 2026-04-09 00:02:05 +01:00
Oluwatobi Mustapha
4c8a660b2d Redact Z-Wave add-on options sensitive error details (#167239)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-08 21:17:26 +02:00
puddly
b0511519a1 Expose async serial port scanning helper in USB integration (#167706) 2026-04-08 14:29:27 -04:00
Marc Mueller
038b583888 Update types packages (#167700) 2026-04-08 19:20:57 +02:00
Raphael Hehl
018c130988 Update UniFi Access quality scale: mark documentation rules as done (#166898)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-08 16:18:19 +02:00
David Bishop
462e9965d7 Mark Govee local devices unavailable when they stop responding (#167566)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 14:18:55 +01:00
Franck Nijhof
ea4d85f96c Extract arithmetic template filters into the math Jinja2 extension (#167309)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-08 14:27:22 +02:00
Petro31
1a4d518ef2 Update template fan tests to use new framework (#167625) 2026-04-08 13:51:15 +02:00
TimL
a48a770ca4 Add Infrared platform to SMLIGHT (#167568) 2026-04-08 12:35:48 +01:00
Tom
e4aeee9d85 Fix ProxmoxVE migration causing reauthentication (#167624) 2026-04-08 13:22:25 +02:00
Raphael Hehl
726edf3a3b Unifi access protect api key hint (#167404)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-08 13:21:54 +02:00
epenet
b98aa0ad91 Use runtime_data in rdw integration (#167654)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:18:26 +02:00
David
f82b8cb7c7 Bump pylutron-caseta to 0.28.0 (#167642) 2026-04-08 13:17:45 +02:00
epenet
d6342d51cc Use runtime_data in radiotherm (#167650)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:16:21 +02:00
epenet
1eead15c24 Use runtime_data in Rabbit Air (#167649)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:15:46 +02:00
epenet
2e6137325c Use runtime_data in ridwell integration (#167658)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 13:14:23 +02:00
Kurt Chrisford
8d3d4a1b5c Add diagnostics to Actron Air (#167145) 2026-04-08 13:12:56 +02:00
Tomer
3e5132bf85 Victron GX reauthentication-flow (#167614)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
2026-04-08 12:58:16 +02:00
epenet
65e4b26006 Use suggested uom in Renault charging power sensor (#167646) 2026-04-08 12:32:26 +02:00
epenet
13f1a42d69 Use runtime_data in roon integration (#167660)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:16:32 +02:00
epenet
5be48affcf Use runtime_data in rova integration (#167661)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:15:59 +02:00
epenet
8994f501f1 Use runtime_data in rympro integration (#167663)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:15:34 +02:00
epenet
7f49ecffd3 Use runtime_data in romy integration (#167665)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:14:29 +02:00
epenet
a560967861 Use runtime_data in roomba integration (#167667)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:13:44 +02:00
epenet
82202ee1c2 Use runtime_data in ruckus_unleashed integration (#167662)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:13:26 +02:00
Franck Nijhof
b697b3a54e Extract version template function into a version Jinja2 extension (#167172)
Co-authored-by: Joostlek <joostlek@outlook.com>
2026-04-08 12:10:22 +02:00
Kurt Chrisford
6cf5bbe2f5 Bump actronneoapi to 0.5.0 (#167669) 2026-04-08 12:06:48 +02:00
epenet
c0c61533e6 Use runtime_data in risco integration (#167659)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 12:05:04 +02:00
epenet
15e434431d Use runtime_data in renson integration (#167664)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:58:10 +02:00
epenet
0452bb91c7 Cleanup unused renault base entity method (#167643) 2026-04-08 11:57:55 +02:00
epenet
5620fc9e96 Use runtime_data in recollect_waste integration (#167655)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:57:44 +02:00
epenet
1a5ef199da Remove duplicated FlussConfigEntry type aliases (#167676)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 11:48:02 +02:00
Joost Lekkerkerker
e98eec113e Add DHCP discovery to MyStrom (#167084)
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
2026-04-08 11:36:20 +02:00
Mattheinrichs
c74d4047d8 Add diagnostics support to tplink_omada (#166802)
Co-authored-by: Ariel Ebersberger <31776703+justanotherariel@users.noreply.github.com>
2026-04-08 10:37:01 +02:00
epenet
f5ae250720 Improve type hints in ipma system_health (#167670) 2026-04-08 10:30:27 +02:00
epenet
bea4eea871 Use runtime_data in rainforest_eagle integration (#167652)
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-08 08:42:10 +02:00
Maciej Bieniek
f4f202a8a1 Fix Tractive switch availability (#167599) 2026-04-08 07:44:45 +02:00
Erwin Douna
c30ccf3750 Bump pyportainer 1.0.38 (#167627) 2026-04-08 05:36:10 +02:00
Raphael Hehl
0b8390cf21 Bump py-unifi-access to 1.1.5 (#167633)
Co-authored-by: RaHehl <rahehl@users.noreply.github.com>
2026-04-08 00:47:58 +02:00
Joakim Plate
1a048a7845 Move logging for loading/unloading config entry to integration logger (#167415) 2026-04-07 23:43:11 +02:00
Erik Montnemery
08097c67eb Bump securetar to 2026.4.1 (#167617) 2026-04-07 20:19:51 +01:00
Oliver Verity
550e53d192 Add support for storing OpenAI conversation responses (#165723) 2026-04-07 20:19:25 +01:00
Fabian Munkes
09ee76c265 Add initial support for PlayerOptions: Number entities to Music Assistant (#162669)
Co-authored-by: Artur Pragacz <49985303+arturpragacz@users.noreply.github.com>
2026-04-07 20:51:26 +02:00
Norbert Rittel
f7b2f5e8f1 Improve Remote action naming consistency (#167382) 2026-04-07 18:48:05 +02:00
G Johansson
a1414717ad Bump holidays to 0.94 (#167604) 2026-04-07 18:41:28 +02:00
Erik Montnemery
2f0889ac02 Fix securetar size calculation when encrypting backup (#167602) 2026-04-07 18:40:06 +02:00
Joakim Plate
323b3a4d96 Add contour and position names to gardena (#167512) 2026-04-07 18:29:05 +02:00
Erwin Douna
8aa0e9f6c3 Refactor to active_containers (#167529) 2026-04-07 18:27:43 +02:00
Erik Montnemery
906475249c Bump securetar to 2026.4.0 (#167600) 2026-04-07 16:00:06 +02:00
Oliver Verity
354b5860bb Add read-only MCP Assist context snapshot resource (#167396) 2026-04-07 06:57:55 -07:00
Ludovic BOUÉ
74957969f7 Add select entities for Roborock q10 s5+ (#166142)
Co-authored-by: Ludovic BOUÉ <132135057+lboue@users.noreply.github.com>
2026-04-07 06:55:43 -07:00
Leo Periou
b52ce22ee7 fix EWS deviceType problem (#167597) 2026-04-07 15:49:21 +02:00
Jan Čermák
920ffdb9b5 Remove homeassistant/actions/helpers/info from builder workflow (#167573) 2026-04-07 14:52:05 +02:00
Artur Pragacz
4a454dff02 Set up condition and trigger helpers in check config script (#167589)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-07 14:50:37 +02:00
Stefan S
481eb66bc5 Add unit 'µA' for the units of electric current (#166786)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-04-07 14:48:21 +02:00
Retha Runolfsson
b76627a442 Add light sensor button to switchbot air purifier (#167134)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-07 14:13:35 +02:00
Denis Shulyaka
1aa214fb61 Enable minimal thinking budget by default for Anthropic integration (#167593) 2026-04-07 13:36:49 +02:00
markhannon
6e30de3a1c Bump zcc-helper to 3.8 (#167555) 2026-04-07 13:31:51 +02:00
230 changed files with 6177 additions and 1840 deletions

View File

@@ -47,10 +47,6 @@ jobs:
with:
python-version-file: ".python-version"
- name: Get information
id: info
uses: home-assistant/actions/helpers/info@5752577ea7cc5aefb064b0b21432f18fe4d6ba90 # zizmor: ignore[unpinned-uses]
- name: Get version
id: version
uses: home-assistant/actions/helpers/version@master # zizmor: ignore[unpinned-uses]

View File

@@ -709,7 +709,7 @@ jobs:
run: |
. venv/bin/activate
python --version
pylint homeassistant
pylint --ignore-missing-annotations=y homeassistant
- name: Run pylint (partially)
if: needs.info.outputs.test_full_suite == 'false'
shell: bash
@@ -718,7 +718,7 @@ jobs:
run: |
. venv/bin/activate
python --version
pylint $(printf "homeassistant/components/%s " ${INTEGRATIONS_GLOB})
pylint --ignore-missing-annotations=y $(printf "homeassistant/components/%s " ${INTEGRATIONS_GLOB})
pylint-tests:
name: Check pylint on tests

View File

@@ -1,11 +1,7 @@
"""The Actron Air integration."""
from actron_neo_api import (
ActronAirACSystem,
ActronAirAPI,
ActronAirAPIError,
ActronAirAuthError,
)
from actron_neo_api import ActronAirAPI, ActronAirAPIError, ActronAirAuthError
from actron_neo_api.models.system import ActronAirSystemInfo
from homeassistant.const import CONF_API_TOKEN, Platform
from homeassistant.core import HomeAssistant
@@ -25,7 +21,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) ->
"""Set up Actron Air integration from a config entry."""
api = ActronAirAPI(refresh_token=entry.data[CONF_API_TOKEN])
systems: list[ActronAirACSystem] = []
systems: list[ActronAirSystemInfo] = []
try:
systems = await api.get_ac_systems()
@@ -44,9 +40,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ActronAirConfigEntry) ->
system_coordinators: dict[str, ActronAirSystemCoordinator] = {}
for system in systems:
coordinator = ActronAirSystemCoordinator(hass, entry, api, system)
_LOGGER.debug("Setting up coordinator for system: %s", system["serial"])
_LOGGER.debug("Setting up coordinator for system: %s", system.serial)
await coordinator.async_config_entry_first_refresh()
system_coordinators[system["serial"]] = coordinator
system_coordinators[system.serial] = coordinator
entry.runtime_data = ActronAirRuntimeData(
api=api,

View File

@@ -18,7 +18,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
from .entity import ActronAirAcEntity, ActronAirZoneEntity, handle_actron_api_errors
from .entity import ActronAirAcEntity, ActronAirZoneEntity, actron_air_command
PARALLEL_UPDATES = 0
@@ -136,19 +136,19 @@ class ActronSystemClimate(ActronAirAcEntity, ActronAirClimateEntity):
"""Return the target temperature."""
return self._status.user_aircon_settings.temperature_setpoint_cool_c
@handle_actron_api_errors
@actron_air_command
async def async_set_fan_mode(self, fan_mode: str) -> None:
"""Set a new fan mode."""
api_fan_mode = FAN_MODE_MAPPING_HA_TO_ACTRONAIR.get(fan_mode)
await self._status.user_aircon_settings.set_fan_mode(api_fan_mode)
@handle_actron_api_errors
@actron_air_command
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode."""
ac_mode = HVAC_MODE_MAPPING_HA_TO_ACTRONAIR.get(hvac_mode)
await self._status.ac_system.set_system_mode(ac_mode)
@handle_actron_api_errors
@actron_air_command
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the temperature."""
temp = kwargs.get(ATTR_TEMPERATURE)
@@ -212,13 +212,13 @@ class ActronZoneClimate(ActronAirZoneEntity, ActronAirClimateEntity):
"""Return the target temperature."""
return self._zone.temperature_setpoint_cool_c
@handle_actron_api_errors
@actron_air_command
async def async_set_hvac_mode(self, hvac_mode: HVACMode) -> None:
"""Set the HVAC mode."""
is_enabled = hvac_mode != HVACMode.OFF
await self._zone.enable(is_enabled)
@handle_actron_api_errors
@actron_air_command
async def async_set_temperature(self, **kwargs: Any) -> None:
"""Set the temperature."""
await self._zone.set_temperature(temperature=kwargs.get(ATTR_TEMPERATURE))

View File

@@ -38,10 +38,10 @@ class ActronAirConfigFlow(ConfigFlow, domain=DOMAIN):
_LOGGER.error("OAuth2 flow failed: %s", err)
return self.async_abort(reason="oauth2_error")
self._device_code = device_code_response["device_code"]
self._user_code = device_code_response["user_code"]
self._verification_uri = device_code_response["verification_uri_complete"]
self._expires_minutes = str(device_code_response["expires_in"] // 60)
self._device_code = device_code_response.device_code
self._user_code = device_code_response.user_code
self._verification_uri = device_code_response.verification_uri_complete
self._expires_minutes = str(device_code_response.expires_in // 60)
async def _wait_for_authorization() -> None:
"""Wait for the user to authorize the device."""

View File

@@ -6,12 +6,12 @@ from dataclasses import dataclass
from datetime import timedelta
from actron_neo_api import (
ActronAirACSystem,
ActronAirAPI,
ActronAirAPIError,
ActronAirAuthError,
ActronAirStatus,
)
from actron_neo_api.models.system import ActronAirSystemInfo
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
@@ -38,7 +38,7 @@ class ActronAirRuntimeData:
type ActronAirConfigEntry = ConfigEntry[ActronAirRuntimeData]
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirStatus]):
"""System coordinator for Actron Air integration."""
def __init__(
@@ -46,7 +46,7 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
hass: HomeAssistant,
entry: ActronAirConfigEntry,
api: ActronAirAPI,
system: ActronAirACSystem,
system: ActronAirSystemInfo,
) -> None:
"""Initialize the coordinator."""
super().__init__(
@@ -57,7 +57,7 @@ class ActronAirSystemCoordinator(DataUpdateCoordinator[ActronAirACSystem]):
config_entry=entry,
)
self.system = system
self.serial_number = system["serial"]
self.serial_number = system.serial
self.api = api
self.status = self.api.state_manager.get_status(self.serial_number)
self.last_seen = dt_util.utcnow()

View File

@@ -0,0 +1,35 @@
"""Diagnostics support for Actron Air."""
from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.const import CONF_API_TOKEN
from homeassistant.core import HomeAssistant
from .coordinator import ActronAirConfigEntry
TO_REDACT = {CONF_API_TOKEN, "master_serial", "serial_number", "serial"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant,
entry: ActronAirConfigEntry,
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinators: dict[int, Any] = {}
for idx, coordinator in enumerate(entry.runtime_data.system_coordinators.values()):
coordinators[idx] = {
"system": async_redact_data(
coordinator.system.model_dump(mode="json"), TO_REDACT
),
"status": async_redact_data(
coordinator.data.model_dump(mode="json", exclude={"last_known_state"}),
TO_REDACT,
),
}
return {
"entry_data": async_redact_data(entry.data, TO_REDACT),
"coordinators": coordinators,
}

View File

@@ -14,10 +14,14 @@ from .const import DOMAIN
from .coordinator import ActronAirSystemCoordinator
def handle_actron_api_errors[_EntityT: ActronAirEntity, **_P](
def actron_air_command[_EntityT: ActronAirEntity, **_P](
func: Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, Any]],
) -> Callable[Concatenate[_EntityT, _P], Coroutine[Any, Any, None]]:
"""Decorate Actron Air API calls to handle ActronAirAPIError exceptions."""
"""Decorator for Actron Air API calls.
Handles ActronAirAPIError exceptions, and requests a coordinator update
to update the status of the devices as soon as possible.
"""
@wraps(func)
async def wrapper(self: _EntityT, *args: _P.args, **kwargs: _P.kwargs) -> None:
@@ -30,6 +34,7 @@ def handle_actron_api_errors[_EntityT: ActronAirEntity, **_P](
translation_key="api_error",
translation_placeholders={"error": str(err)},
) from err
self.coordinator.async_set_updated_data(self.coordinator.data)
return wrapper

View File

@@ -13,5 +13,5 @@
"integration_type": "hub",
"iot_class": "cloud_polling",
"quality_scale": "silver",
"requirements": ["actron-neo-api==0.4.1"]
"requirements": ["actron-neo-api==0.5.0"]
}

View File

@@ -41,7 +41,7 @@ rules:
# Gold
devices: done
diagnostics: todo
diagnostics: done
discovery-update-info:
status: exempt
comment: This integration uses DHCP discovery, however is cloud polling. Therefore there is no information to update.

View File

@@ -10,7 +10,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import ActronAirConfigEntry, ActronAirSystemCoordinator
from .entity import ActronAirAcEntity, handle_actron_api_errors
from .entity import ActronAirAcEntity, actron_air_command
PARALLEL_UPDATES = 0
@@ -105,12 +105,12 @@ class ActronAirSwitch(ActronAirAcEntity, SwitchEntity):
"""Return true if the switch is on."""
return self.entity_description.is_on_fn(self.coordinator)
@handle_actron_api_errors
@actron_air_command
async def async_turn_on(self, **kwargs: Any) -> None:
"""Turn the switch on."""
await self.entity_description.set_fn(self.coordinator, True)
@handle_actron_api_errors
@actron_air_command
async def async_turn_off(self, **kwargs: Any) -> None:
"""Turn the switch off."""
await self.entity_description.set_fn(self.coordinator, False)

View File

@@ -36,13 +36,15 @@ class PromptCaching(StrEnum):
AUTOMATIC = "automatic"
MIN_THINKING_BUDGET = 1024
DEFAULT = {
CONF_CHAT_MODEL: "claude-haiku-4-5",
CONF_CODE_EXECUTION: False,
CONF_MAX_TOKENS: 3000,
CONF_PROMPT_CACHING: PromptCaching.PROMPT.value,
CONF_TEMPERATURE: 1.0,
CONF_THINKING_BUDGET: 0,
CONF_THINKING_BUDGET: MIN_THINKING_BUDGET,
CONF_THINKING_EFFORT: "low",
CONF_TOOL_SEARCH: False,
CONF_WEB_SEARCH: False,
@@ -50,8 +52,6 @@ DEFAULT = {
CONF_WEB_SEARCH_MAX_USES: 5,
}
MIN_THINKING_BUDGET = 1024
NON_THINKING_MODELS = [
"claude-3-haiku",
]

View File

@@ -8,6 +8,6 @@
"integration_type": "service",
"iot_class": "calculated",
"quality_scale": "internal",
"requirements": ["cronsim==2.7", "securetar==2026.2.0"],
"requirements": ["cronsim==2.7", "securetar==2026.4.1"],
"single_config_entry": true
}

View File

@@ -22,6 +22,7 @@ from securetar import (
SecureTarFile,
SecureTarReadError,
SecureTarRootKeyContext,
get_archive_max_ciphertext_size,
)
from homeassistant.core import HomeAssistant
@@ -383,9 +384,12 @@ def _encrypt_backup(
if prefix not in expected_archives:
LOGGER.debug("Unknown inner tar file %s will not be encrypted", obj.name)
continue
output_archive.import_tar(
input_tar.extractfile(obj), obj, derived_key_id=inner_tar_idx
)
if (fileobj := input_tar.extractfile(obj)) is None:
LOGGER.debug(
"Non regular inner tar file %s will not be encrypted", obj.name
)
continue
output_archive.import_tar(fileobj, obj, derived_key_id=inner_tar_idx)
inner_tar_idx += 1
@@ -419,7 +423,7 @@ class _CipherBackupStreamer:
hass: HomeAssistant,
backup: AgentBackup,
open_stream: Callable[[], Coroutine[Any, Any, AsyncIterator[bytes]]],
password: str | None,
password: str,
) -> None:
"""Initialize."""
self._workers: list[_CipherWorkerStatus] = []
@@ -431,7 +435,9 @@ class _CipherBackupStreamer:
def size(self) -> int:
"""Return the maximum size of the decrypted or encrypted backup."""
return self._backup.size + self._num_tar_files() * tarfile.RECORDSIZE
return get_archive_max_ciphertext_size(
self._backup.size, SECURETAR_CREATE_VERSION, self._num_tar_files()
)
def _num_tar_files(self) -> int:
"""Return the number of inner tar files."""

View File

@@ -20,7 +20,7 @@ from homeassistant.helpers.typing import ConfigType
from homeassistant.util import dt as dt_util
from homeassistant.util.hass_dict import HassKey
from .const import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES, DOMAIN
from .const import ATTR_EVENT_TYPE, ATTR_EVENT_TYPES, DOMAIN, DoorbellEventType
_LOGGER = logging.getLogger(__name__)
DATA_COMPONENT: HassKey[EntityComponent[EventEntity]] = HassKey(DOMAIN)
@@ -44,6 +44,7 @@ __all__ = [
"DOMAIN",
"PLATFORM_SCHEMA",
"PLATFORM_SCHEMA_BASE",
"DoorbellEventType",
"EventDeviceClass",
"EventEntity",
"EventEntityDescription",
@@ -189,6 +190,21 @@ class EventEntity(RestoreEntity, cached_properties=CACHED_PROPERTIES_WITH_ATTR_)
async def async_internal_added_to_hass(self) -> None:
"""Call when the event entity is added to hass."""
await super().async_internal_added_to_hass()
if (
self.device_class == EventDeviceClass.DOORBELL
and DoorbellEventType.RING not in self.event_types
):
report_issue = self._suggest_report_issue()
_LOGGER.warning(
"Entity %s is a doorbell event entity but does not support "
"the '%s' event type. This will stop working in "
"Home Assistant 2027.4, please %s",
self.entity_id,
DoorbellEventType.RING,
report_issue,
)
if (
(state := await self.async_get_last_state())
and state.state is not None

View File

@@ -1,5 +1,13 @@
"""Provides the constants needed for the component."""
from enum import StrEnum
DOMAIN = "event"
ATTR_EVENT_TYPE = "event_type"
ATTR_EVENT_TYPES = "event_types"
class DoorbellEventType(StrEnum):
"""Standard event types for doorbell device class."""
RING = "ring"

View File

@@ -15,7 +15,14 @@
"name": "Button"
},
"doorbell": {
"name": "Doorbell"
"name": "Doorbell",
"state_attributes": {
"event_type": {
"state": {
"ring": "Ring"
}
}
}
},
"motion": {
"name": "Motion"

View File

@@ -2,18 +2,14 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import HomeAssistant
from .coordinator import FlussDataUpdateCoordinator
from .coordinator import FlussConfigEntry, FlussDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.BUTTON]
type FlussConfigEntry = ConfigEntry[FlussDataUpdateCoordinator]
async def async_setup_entry(
hass: HomeAssistant,
entry: FlussConfigEntry,

View File

@@ -1,16 +1,13 @@
"""Support for Fluss Devices."""
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import FlussApiClientError, FlussDataUpdateCoordinator
from .coordinator import FlussApiClientError, FlussConfigEntry
from .entity import FlussEntity
type FlussConfigEntry = ConfigEntry[FlussDataUpdateCoordinator]
async def async_setup_entry(
hass: HomeAssistant,

View File

@@ -38,6 +38,7 @@ PLATFORMS: list[Platform] = [
Platform.SELECT,
Platform.SENSOR,
Platform.SWITCH,
Platform.TEXT,
Platform.VALVE,
]
LOGGER = logging.getLogger(__name__)

View File

@@ -0,0 +1,12 @@
{
"entity": {
"text": {
"contour_name": {
"default": "mdi:vector-polygon"
},
"position_name": {
"default": "mdi:map-marker-radius"
}
}
}
}

View File

@@ -154,6 +154,14 @@
"state": {
"name": "[%key:common::state::open%]"
}
},
"text": {
"contour_name": {
"name": "Contour {number}"
},
"position_name": {
"name": "Position {number}"
}
}
}
}

View File

@@ -0,0 +1,88 @@
"""Support for text entities."""
from __future__ import annotations
from dataclasses import dataclass
from gardena_bluetooth.const import AquaContourContours, AquaContourPosition
from gardena_bluetooth.parse import CharacteristicNullString
from homeassistant.components.text import TextEntity, TextEntityDescription
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .coordinator import GardenaBluetoothConfigEntry
from .entity import GardenaBluetoothDescriptorEntity
@dataclass(frozen=True, kw_only=True)
class GardenaBluetoothTextEntityDescription(TextEntityDescription):
"""Description of entity."""
char: CharacteristicNullString
@property
def context(self) -> set[str]:
"""Context needed for update coordinator."""
return {self.char.uuid}
DESCRIPTIONS = (
*(
GardenaBluetoothTextEntityDescription(
key=f"position_{i}_name",
translation_key="position_name",
translation_placeholders={"number": str(i)},
has_entity_name=True,
char=getattr(AquaContourPosition, f"position_name_{i}"),
native_max=20,
entity_category=EntityCategory.CONFIG,
)
for i in range(1, 6)
),
*(
GardenaBluetoothTextEntityDescription(
key=f"contour_{i}_name",
translation_key="contour_name",
translation_placeholders={"number": str(i)},
has_entity_name=True,
char=getattr(AquaContourContours, f"contour_name_{i}"),
native_max=20,
entity_category=EntityCategory.CONFIG,
)
for i in range(1, 6)
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: GardenaBluetoothConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up text based on a config entry."""
coordinator = entry.runtime_data
entities = [
GardenaBluetoothTextEntity(coordinator, description, description.context)
for description in DESCRIPTIONS
if description.char.unique_id in coordinator.characteristics
]
async_add_entities(entities)
class GardenaBluetoothTextEntity(GardenaBluetoothDescriptorEntity, TextEntity):
"""Representation of a text entity."""
entity_description: GardenaBluetoothTextEntityDescription
@property
def native_value(self) -> str | None:
"""Return the value reported by the text."""
char = self.entity_description.char
return self.coordinator.get_cached(char)
async def async_set_value(self, value: str) -> None:
"""Change the text."""
char = self.entity_description.char
await self.coordinator.write(char, value)

View File

@@ -11,4 +11,8 @@ CONF_LISTENING_PORT_DEFAULT = 4002
CONF_DISCOVERY_INTERVAL_DEFAULT = 60
SCAN_INTERVAL = timedelta(seconds=30)
# A device is considered unavailable if we have not heard a status response
# from it for three consecutive poll cycles. This tolerates a single dropped
# UDP response plus some jitter before flapping the entity state.
DEVICE_TIMEOUT = SCAN_INTERVAL * 3
DISCOVERY_TIMEOUT = 5

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
from datetime import datetime
import logging
from typing import Any
@@ -22,7 +23,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, MANUFACTURER
from .const import DEVICE_TIMEOUT, DOMAIN, MANUFACTURER
from .coordinator import GoveeLocalApiCoordinator, GoveeLocalConfigEntry
_LOGGER = logging.getLogger(__name__)
@@ -118,6 +119,19 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
serial_number=device.fingerprint,
)
@property
def available(self) -> bool:
"""Return if the device is reachable.
The underlying library updates ``lastseen`` whenever the device
replies to a status request. The coordinator polls every
``SCAN_INTERVAL``, so if we have not heard back within
``DEVICE_TIMEOUT`` we consider the device offline.
"""
if not super().available:
return False
return datetime.now() - self._device.lastseen < DEVICE_TIMEOUT
@property
def is_on(self) -> bool:
"""Return true if device is on (brightness above 0)."""
@@ -205,8 +219,8 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
@callback
def _update_callback(self, device: GoveeDevice) -> None:
if self.hass:
self.async_write_ha_state()
"""Handle device state updates pushed by the library."""
self.async_write_ha_state()
def _save_last_color_state(self) -> None:
color_mode = self.color_mode

View File

@@ -98,7 +98,9 @@ class HabiticaCalendarEntity(HabiticaBase, CalendarEntity):
start_date, end_date - timedelta(days=1), inc=True
)
# if no end_date is given, return only the next recurrence
return [recurrences.after(start_date, inc=True)]
if (next_date := recurrences.after(start_date, inc=True)) is None:
return []
return [next_date]
class HabiticaTodosCalendarEntity(HabiticaCalendarEntity):

View File

@@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/holiday",
"iot_class": "local_polling",
"requirements": ["holidays==0.93", "babel==2.15.0"]
"requirements": ["holidays==0.94", "babel==2.15.0"]
}

View File

@@ -13,7 +13,7 @@ from homeassistant.components.homeassistant_hardware.util import guess_firmware_
from homeassistant.components.usb import (
USBDevice,
async_register_port_event_callback,
scan_serial_ports,
async_scan_serial_ports,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
@@ -163,7 +163,7 @@ async def async_migrate_entry(
key not in config_entry.data
for key in (VID, PID, MANUFACTURER, PRODUCT, SERIAL_NUMBER)
):
serial_ports = await hass.async_add_executor_job(scan_serial_ports)
serial_ports = await async_scan_serial_ports(hass)
serial_ports_info = {port.device: port for port in serial_ports}
device = config_entry.data[DEVICE]

View File

@@ -1,5 +1,7 @@
"""Provide info to system health."""
from typing import Any
from homeassistant.components import system_health
from homeassistant.core import HomeAssistant, callback
@@ -14,7 +16,7 @@ def async_register(
register.async_register_info(system_health_info)
async def system_health_info(hass):
async def system_health_info(hass: HomeAssistant) -> dict[str, Any]:
"""Get info for the info page."""
return {
"api_endpoint_reachable": system_health.async_check_can_reach_url(

View File

@@ -10,7 +10,7 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["pylutron_caseta"],
"requirements": ["pylutron-caseta==0.27.0"],
"requirements": ["pylutron-caseta==0.28.0"],
"zeroconf": [
{
"properties": {

View File

@@ -10,10 +10,12 @@ See https://modelcontextprotocol.io/docs/concepts/architecture#implementation-ex
from collections.abc import Callable, Sequence
import json
import logging
from typing import Any
from typing import Any, cast
from mcp import types
from mcp.server import Server
from mcp.server.lowlevel.helper_types import ReadResourceContents
from pydantic import AnyUrl
import voluptuous as vol
from voluptuous_openapi import convert
@@ -25,6 +27,16 @@ from .const import STATELESS_LLM_API
_LOGGER = logging.getLogger(__name__)
SNAPSHOT_RESOURCE_URI = "homeassistant://assist/context-snapshot"
SNAPSHOT_RESOURCE_URL = AnyUrl(SNAPSHOT_RESOURCE_URI)
SNAPSHOT_RESOURCE_MIME_TYPE = "text/plain"
LIVE_CONTEXT_TOOL_NAME = "GetLiveContext"
def _has_live_context_tool(llm_api: llm.APIInstance) -> bool:
"""Return if the selected API exposes the live context tool."""
return any(tool.name == LIVE_CONTEXT_TOOL_NAME for tool in llm_api.tools)
def _format_tool(
tool: llm.Tool, custom_serializer: Callable[[Any], Any] | None
@@ -90,6 +102,47 @@ async def create_server(
],
)
@server.list_resources() # type: ignore[no-untyped-call,untyped-decorator]
async def handle_list_resources() -> list[types.Resource]:
llm_api = await get_api_instance()
if not _has_live_context_tool(llm_api):
return []
return [
types.Resource(
uri=SNAPSHOT_RESOURCE_URL,
name="assist_context_snapshot",
title="Assist context snapshot",
description=(
"A snapshot of the current Assist context, matching the"
" existing GetLiveContext tool output."
),
mimeType=SNAPSHOT_RESOURCE_MIME_TYPE,
)
]
@server.read_resource() # type: ignore[no-untyped-call,untyped-decorator]
async def handle_read_resource(uri: AnyUrl) -> Sequence[ReadResourceContents]:
if str(uri) != SNAPSHOT_RESOURCE_URI:
raise ValueError(f"Unknown resource: {uri}")
llm_api = await get_api_instance()
if not _has_live_context_tool(llm_api):
raise ValueError(f"Unknown resource: {uri}")
tool_response = await llm_api.async_call_tool(
llm.ToolInput(tool_name=LIVE_CONTEXT_TOOL_NAME, tool_args={})
)
if not tool_response.get("success"):
raise HomeAssistantError(cast(str, tool_response["error"]))
return [
ReadResourceContents(
content=cast(str, tool_response["result"]),
mime_type=SNAPSHOT_RESOURCE_MIME_TYPE,
)
]
@server.list_tools() # type: ignore[no-untyped-call,untyped-decorator]
async def list_tools() -> list[types.Tool]:
"""List available time tools."""

View File

@@ -49,7 +49,11 @@ if TYPE_CHECKING:
from homeassistant.helpers.typing import ConfigType
PLATFORMS = [Platform.BUTTON, Platform.MEDIA_PLAYER]
PLATFORMS = [
Platform.BUTTON,
Platform.MEDIA_PLAYER,
Platform.NUMBER,
]
CONNECT_TIMEOUT = 10
LISTEN_READY_TIMEOUT = 30

View File

@@ -80,3 +80,5 @@ ATTR_FANART_IMAGE = "fanart_image"
ATTR_CONF_EXPOSE_PLAYER_TO_HA = "expose_player_to_ha"
LOGGER = logging.getLogger(__package__)
PLAYER_OPTIONS_TRANSLATION_KEY_PREFIX = "player_options."

View File

@@ -6,8 +6,9 @@ from typing import TYPE_CHECKING
from music_assistant_models.enums import EventType
from music_assistant_models.event import MassEvent
from music_assistant_models.player import Player
from music_assistant_models.player import Player, PlayerOption
from homeassistant.const import EntityCategory
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import Entity
@@ -84,3 +85,45 @@ class MusicAssistantEntity(Entity):
async def async_on_update(self) -> None:
"""Handle player updates."""
class MusicAssistantPlayerOptionEntity(MusicAssistantEntity):
"""Base entity for Music Assistant Player Options."""
_attr_entity_category = EntityCategory.CONFIG
def __init__(
self, mass: MusicAssistantClient, player_id: str, player_option: PlayerOption
) -> None:
"""Initialize MusicAssistantPlayerOptionEntity."""
super().__init__(mass, player_id)
self.mass_option_key = player_option.key
self.mass_type = player_option.type
self.on_player_option_update(player_option)
async def async_added_to_hass(self) -> None:
"""Register callbacks."""
# need callbacks of parent to catch availability
await super().async_added_to_hass()
# main callback for player options
self.async_on_remove(
self.mass.subscribe(
self.__on_mass_player_options_update,
EventType.PLAYER_OPTIONS_UPDATED,
self.player_id,
)
)
def __on_mass_player_options_update(self, event: MassEvent) -> None:
"""Call when we receive an event from MusicAssistant."""
for option in self.player.options:
if option.key == self.mass_option_key:
self.on_player_option_update(option)
self.async_write_ha_state()
break
def on_player_option_update(self, player_option: PlayerOption) -> None:
"""Callback for player option updates."""

View File

@@ -0,0 +1,127 @@
"""Music Assistant Number platform."""
from __future__ import annotations
from typing import Final
from music_assistant_client.client import MusicAssistantClient
from music_assistant_models.player import PlayerOption, PlayerOptionType
from homeassistant.components.number import NumberEntity, NumberEntityDescription
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import MusicAssistantConfigEntry
from .const import PLAYER_OPTIONS_TRANSLATION_KEY_PREFIX
from .entity import MusicAssistantPlayerOptionEntity
from .helpers import catch_musicassistant_error
PLAYER_OPTIONS_TRANSLATION_KEYS_NUMBER: Final[list[str]] = [
"bass",
"dialogue_level",
"dialogue_lift",
"dts_dialogue_control",
"equalizer_high",
"equalizer_low",
"equalizer_mid",
"subwoofer_volume",
"treble",
]
async def async_setup_entry(
hass: HomeAssistant,
entry: MusicAssistantConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Music Assistant Number Entities (Player Options) from Config Entry."""
mass = entry.runtime_data.mass
def add_player(player_id: str) -> None:
"""Handle add player."""
player = mass.players.get(player_id)
if player is None:
return
entities: list[MusicAssistantPlayerConfigNumber] = []
for player_option in player.options:
if (
not player_option.read_only
and player_option.type
in (
PlayerOptionType.INTEGER,
PlayerOptionType.FLOAT,
)
and not player_option.options # these we map to select
):
# the MA translation key must have the format player_options.<translation key>
# we ignore entities with unknown translation keys.
if (
player_option.translation_key is None
or not player_option.translation_key.startswith(
PLAYER_OPTIONS_TRANSLATION_KEY_PREFIX
)
):
continue
translation_key = player_option.translation_key[
len(PLAYER_OPTIONS_TRANSLATION_KEY_PREFIX) :
]
if translation_key not in PLAYER_OPTIONS_TRANSLATION_KEYS_NUMBER:
continue
entities.append(
MusicAssistantPlayerConfigNumber(
mass,
player_id,
player_option=player_option,
entity_description=NumberEntityDescription(
key=player_option.key,
translation_key=translation_key,
),
)
)
async_add_entities(entities)
# register callback to add players when they are discovered
entry.runtime_data.platform_handlers.setdefault(Platform.NUMBER, add_player)
class MusicAssistantPlayerConfigNumber(MusicAssistantPlayerOptionEntity, NumberEntity):
"""Representation of a Number entity to control player provider dependent settings."""
def __init__(
self,
mass: MusicAssistantClient,
player_id: str,
player_option: PlayerOption,
entity_description: NumberEntityDescription,
) -> None:
"""Initialize MusicAssistantPlayerConfigNumber."""
super().__init__(mass, player_id, player_option)
self.entity_description = entity_description
@catch_musicassistant_error
async def async_set_native_value(self, value: float) -> None:
"""Set a new value."""
_value = round(value) if self.mass_type == PlayerOptionType.INTEGER else value
await self.mass.players.set_option(
self.player_id,
self.mass_option_key,
_value,
)
def on_player_option_update(self, player_option: PlayerOption) -> None:
"""Update on player option update."""
if player_option.min_value is not None:
self._attr_native_min_value = player_option.min_value
if player_option.max_value is not None:
self._attr_native_max_value = player_option.max_value
if player_option.step is not None:
self._attr_native_step = player_option.step
self._attr_native_value = (
player_option.value
if isinstance(player_option.value, (int, float))
else None
)

View File

@@ -53,6 +53,35 @@
"favorite_now_playing": {
"name": "Favorite current song"
}
},
"number": {
"bass": {
"name": "Bass"
},
"dialogue_level": {
"name": "Dialogue level"
},
"dialogue_lift": {
"name": "Dialogue lift"
},
"dts_dialogue_control": {
"name": "DTS dialogue control"
},
"equalizer_high": {
"name": "Equalizer high"
},
"equalizer_low": {
"name": "Equalizer low"
},
"equalizer_mid": {
"name": "Equalizer mid"
},
"subwoofer_volume": {
"name": "Subwoofer volume"
},
"treble": {
"name": "Treble"
}
}
},
"issues": {

View File

@@ -104,12 +104,8 @@ async def async_setup_entry(
def _create_entity(device: dict) -> MyNeoSelect:
"""Create a select entity for a device."""
if device["model"] == "EWS":
# According to the MyNeomitis API, EWS "relais" devices expose a "relayMode"
# field in their state, while "pilote" devices do not. We therefore use the
# presence of "relayMode" as an explicit heuristic to distinguish relais
# from pilote devices. If the upstream API changes this behavior, this
# detection logic must be revisited.
if "relayMode" in device.get("state", {}):
state = device.get("state") or {}
if state.get("deviceType") == 0:
description = SELECT_TYPES["relais"]
else:
description = SELECT_TYPES["pilote"]

View File

@@ -3,7 +3,7 @@
from __future__ import annotations
import logging
from typing import Any
from typing import TYPE_CHECKING, Any
import pymystrom
from pymystrom.exceptions import MyStromConnectionError
@@ -11,6 +11,7 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_HOST, CONF_NAME
from homeassistant.helpers.service_info.dhcp import DhcpServiceInfo
from .const import DOMAIN
@@ -31,6 +32,8 @@ class MyStromConfigFlow(ConfigFlow, domain=DOMAIN):
VERSION = 1
_host: str | None = None
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
@@ -51,3 +54,38 @@ class MyStromConfigFlow(ConfigFlow, domain=DOMAIN):
schema = self.add_suggested_values_to_schema(STEP_USER_DATA_SCHEMA, user_input)
return self.async_show_form(step_id="user", data_schema=schema, errors=errors)
async def async_step_dhcp(
self, discovery_info: DhcpServiceInfo
) -> ConfigFlowResult:
"""Handle DHCP discovery."""
mac_address = discovery_info.macaddress.upper()
await self.async_set_unique_id(mac_address)
self._abort_if_unique_id_configured(updates={CONF_HOST: discovery_info.ip})
try:
await pymystrom.get_device_info(discovery_info.ip)
except MyStromConnectionError:
return self.async_abort(reason="cannot_connect")
self._host = discovery_info.ip
self.context["title_placeholders"] = {"host": discovery_info.ip}
return await self.async_step_discovery_confirm()
async def async_step_discovery_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle discovery confirmation."""
if user_input is not None:
return self.async_create_entry(
title=DEFAULT_NAME,
data={CONF_HOST: self._host},
)
self._set_confirm_only()
if TYPE_CHECKING:
assert self._host is not None
return self.async_show_form(
step_id="discovery_confirm",
description_placeholders={CONF_HOST: self._host},
)

View File

@@ -4,6 +4,14 @@
"codeowners": ["@fabaff"],
"config_flow": true,
"dependencies": ["http"],
"dhcp": [
{
"hostname": "mystrom-*"
},
{
"registered_devices": true
}
],
"documentation": "https://www.home-assistant.io/integrations/mystrom",
"integration_type": "device",
"iot_class": "local_polling",

View File

@@ -1,12 +1,16 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]"
},
"step": {
"discovery_confirm": {
"description": "Do you want to set up the myStrom device at {host}?"
},
"user": {
"data": {
"host": "[%key:common::config_flow::data::host%]",

View File

@@ -168,7 +168,7 @@ class NumberDeviceClass(StrEnum):
CURRENT = "current"
"""Current.
Unit of measurement: `A`, `mA`
Unit of measurement: `A`, `mA`, `μA`
"""
DATA_RATE = "data_rate"

View File

@@ -46,6 +46,7 @@ from .const import (
CONF_MAX_TOKENS,
CONF_PROMPT,
CONF_REASONING_EFFORT,
CONF_STORE_RESPONSES,
CONF_TEMPERATURE,
CONF_TOP_P,
DEFAULT_AI_TASK_NAME,
@@ -58,6 +59,7 @@ from .const import (
RECOMMENDED_CHAT_MODEL,
RECOMMENDED_MAX_TOKENS,
RECOMMENDED_REASONING_EFFORT,
RECOMMENDED_STORE_RESPONSES,
RECOMMENDED_STT_OPTIONS,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_TOP_P,
@@ -208,7 +210,9 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
CONF_TEMPERATURE, RECOMMENDED_TEMPERATURE
),
"user": call.context.user_id,
"store": False,
"store": conversation_subentry.data.get(
CONF_STORE_RESPONSES, RECOMMENDED_STORE_RESPONSES
),
}
if model.startswith("o"):

View File

@@ -55,6 +55,7 @@ from .const import (
CONF_REASONING_SUMMARY,
CONF_RECOMMENDED,
CONF_SERVICE_TIER,
CONF_STORE_RESPONSES,
CONF_TEMPERATURE,
CONF_TOP_P,
CONF_TTS_SPEED,
@@ -82,6 +83,7 @@ from .const import (
RECOMMENDED_REASONING_EFFORT,
RECOMMENDED_REASONING_SUMMARY,
RECOMMENDED_SERVICE_TIER,
RECOMMENDED_STORE_RESPONSES,
RECOMMENDED_STT_MODEL,
RECOMMENDED_STT_OPTIONS,
RECOMMENDED_TEMPERATURE,
@@ -357,6 +359,10 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
CONF_TEMPERATURE,
default=RECOMMENDED_TEMPERATURE,
): NumberSelector(NumberSelectorConfig(min=0, max=2, step=0.05)),
vol.Optional(
CONF_STORE_RESPONSES,
default=RECOMMENDED_STORE_RESPONSES,
): bool,
}
if user_input is not None:
@@ -641,7 +647,9 @@ class OpenAISubentryFlowHandler(ConfigSubentryFlow):
"strict": False,
}
},
store=False,
store=self.options.get(
CONF_STORE_RESPONSES, RECOMMENDED_STORE_RESPONSES
),
)
location_data = location_schema(json.loads(response.output_text) or {})

View File

@@ -24,6 +24,7 @@ CONF_PROMPT = "prompt"
CONF_REASONING_EFFORT = "reasoning_effort"
CONF_REASONING_SUMMARY = "reasoning_summary"
CONF_RECOMMENDED = "recommended"
CONF_STORE_RESPONSES = "store_responses"
CONF_SERVICE_TIER = "service_tier"
CONF_TEMPERATURE = "temperature"
CONF_TOP_P = "top_p"
@@ -42,6 +43,7 @@ RECOMMENDED_CHAT_MODEL = "gpt-4o-mini"
RECOMMENDED_IMAGE_MODEL = "gpt-image-1.5"
RECOMMENDED_MAX_TOKENS = 3000
RECOMMENDED_REASONING_EFFORT = "low"
RECOMMENDED_STORE_RESPONSES = False
RECOMMENDED_REASONING_SUMMARY = "auto"
RECOMMENDED_SERVICE_TIER = "auto"
RECOMMENDED_STT_MODEL = "gpt-4o-mini-transcribe"

View File

@@ -75,6 +75,7 @@ from .const import (
CONF_REASONING_EFFORT,
CONF_REASONING_SUMMARY,
CONF_SERVICE_TIER,
CONF_STORE_RESPONSES,
CONF_TEMPERATURE,
CONF_TOP_P,
CONF_VERBOSITY,
@@ -94,6 +95,7 @@ from .const import (
RECOMMENDED_REASONING_EFFORT,
RECOMMENDED_REASONING_SUMMARY,
RECOMMENDED_SERVICE_TIER,
RECOMMENDED_STORE_RESPONSES,
RECOMMENDED_STT_MODEL,
RECOMMENDED_TEMPERATURE,
RECOMMENDED_TOP_P,
@@ -508,7 +510,7 @@ class OpenAIBaseLLMEntity(Entity):
max_output_tokens=options.get(CONF_MAX_TOKENS, RECOMMENDED_MAX_TOKENS),
user=chat_log.conversation_id,
service_tier=options.get(CONF_SERVICE_TIER, RECOMMENDED_SERVICE_TIER),
store=False,
store=options.get(CONF_STORE_RESPONSES, RECOMMENDED_STORE_RESPONSES),
stream=True,
)
@@ -611,8 +613,10 @@ class OpenAIBaseLLMEntity(Entity):
if image_model != "gpt-image-1-mini":
image_tool["input_fidelity"] = "high"
tools.append(image_tool)
# Keep image state on OpenAI so follow-up prompts can continue by
# conversation ID without resending the generated image data.
model_args["store"] = True
model_args["tool_choice"] = ToolChoiceTypesParam(type="image_generation")
model_args["store"] = True # Avoid sending image data back and forth
if tools:
model_args["tools"] = tools

View File

@@ -51,9 +51,13 @@
"data": {
"chat_model": "[%key:common::generic::model%]",
"max_tokens": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::max_tokens%]",
"store_responses": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::store_responses%]",
"temperature": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::temperature%]",
"top_p": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data::top_p%]"
},
"data_description": {
"store_responses": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::data_description::store_responses%]"
},
"title": "[%key:component::openai_conversation::config_subentries::conversation::step::advanced::title%]"
},
"init": {
@@ -109,9 +113,13 @@
"data": {
"chat_model": "[%key:common::generic::model%]",
"max_tokens": "Maximum tokens to return in response",
"store_responses": "Store requests and responses in OpenAI",
"temperature": "Temperature",
"top_p": "Top P"
},
"data_description": {
"store_responses": "If enabled, requests and responses are stored by OpenAI and visible in your OpenAI dashboard logs"
},
"title": "Advanced settings"
},
"init": {

View File

@@ -214,19 +214,19 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
else None,
)
# Separately fetch stats for running containers
running_containers = [
# Separately fetch stats for active containers
active_containers = [
container
for container in containers
if container.state
in (DockerContainerState.RUNNING, DockerContainerState.PAUSED)
]
if running_containers:
if active_containers:
container_stats = dict(
zip(
(
self._get_container_name(container.names[0])
for container in running_containers
for container in active_containers
),
await asyncio.gather(
*(
@@ -234,7 +234,7 @@ class PortainerCoordinator(DataUpdateCoordinator[dict[int, PortainerCoordinatorD
endpoint_id=endpoint.id,
container_id=container.id,
)
for container in running_containers
for container in active_containers
)
),
strict=False,

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "local_polling",
"quality_scale": "platinum",
"requirements": ["pyportainer==1.0.37"]
"requirements": ["pyportainer==1.0.38"]
}

View File

@@ -189,6 +189,14 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ProxmoxConfigEntry) ->
# Migration for additional configuration options added to support API tokens
if entry.version < 3:
data = dict(entry.data)
# If CONF_REALM wasn't there yet, extract from username
if CONF_REALM not in data:
data[CONF_REALM] = DEFAULT_REALM
if "@" in data.get(CONF_USERNAME, ""):
username, realm = data[CONF_USERNAME].split("@", 1)
data[CONF_USERNAME] = username
data[CONF_REALM] = realm.lower()
realm = data[CONF_REALM].lower()
# If the realm is one of the base providers, set the provider to match the realm.

View File

@@ -5,21 +5,17 @@ from __future__ import annotations
from rabbitair import Client, UdpClient
from homeassistant.components import zeroconf
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import RabbitAirDataUpdateCoordinator
from .coordinator import RabbitAirConfigEntry, RabbitAirDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.FAN]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: RabbitAirConfigEntry) -> bool:
"""Set up Rabbit Air from a config entry."""
hass.data.setdefault(DOMAIN, {})
host: str = entry.data[CONF_HOST]
token: str = entry.data[CONF_ACCESS_TOKEN]
@@ -30,7 +26,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh()
hass.data[DOMAIN][entry.entry_id] = coordinator
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -39,14 +35,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: RabbitAirConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def update_listener(hass: HomeAssistant, entry: RabbitAirConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -12,6 +12,8 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.debounce import Debouncer
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
type RabbitAirConfigEntry = ConfigEntry[RabbitAirDataUpdateCoordinator]
_LOGGER = logging.getLogger(__name__)
@@ -43,10 +45,10 @@ class RabbitAirDebouncer(Debouncer[Coroutine[Any, Any, None]]):
class RabbitAirDataUpdateCoordinator(DataUpdateCoordinator[State]):
"""Class to manage fetching data from single endpoint."""
config_entry: ConfigEntry
config_entry: RabbitAirConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, device: Client
self, hass: HomeAssistant, config_entry: RabbitAirConfigEntry, device: Client
) -> None:
"""Initialize global data updater."""
self.device = device

View File

@@ -7,13 +7,12 @@ from typing import Any
from rabbitair import Model
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import RabbitAirDataUpdateCoordinator
from .coordinator import RabbitAirConfigEntry, RabbitAirDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -31,7 +30,7 @@ class RabbitAirBaseEntity(CoordinatorEntity[RabbitAirDataUpdateCoordinator]):
def __init__(
self,
coordinator: RabbitAirDataUpdateCoordinator,
entry: ConfigEntry,
entry: RabbitAirConfigEntry,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator)

View File

@@ -7,7 +7,6 @@ from typing import Any
from rabbitair import Mode, Model, Speed
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util.percentage import (
@@ -15,8 +14,7 @@ from homeassistant.util.percentage import (
percentage_to_ordered_list_item,
)
from .const import DOMAIN
from .coordinator import RabbitAirDataUpdateCoordinator
from .coordinator import RabbitAirConfigEntry, RabbitAirDataUpdateCoordinator
from .entity import RabbitAirBaseEntity
SPEED_LIST = [
@@ -40,12 +38,11 @@ PRESET_MODES = {
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: RabbitAirConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a config entry."""
coordinator: RabbitAirDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([RabbitAirFanEntity(coordinator, entry)])
async_add_entities([RabbitAirFanEntity(entry.runtime_data, entry)])
class RabbitAirFanEntity(RabbitAirBaseEntity, FanEntity):
@@ -61,7 +58,7 @@ class RabbitAirFanEntity(RabbitAirBaseEntity, FanEntity):
def __init__(
self,
coordinator: RabbitAirDataUpdateCoordinator,
entry: ConfigEntry,
entry: RabbitAirConfigEntry,
) -> None:
"""Initialize the entity."""
super().__init__(coordinator, entry)

View File

@@ -8,13 +8,11 @@ from urllib.error import URLError
from radiotherm.validate import RadiothermTstatError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
from .coordinator import RadioThermUpdateCoordinator
from .coordinator import RadioThermConfigEntry, RadioThermUpdateCoordinator
from .data import async_get_init_data
from .util import async_set_time
@@ -38,7 +36,7 @@ async def _async_call_or_raise_not_ready[_T](
raise ConfigEntryNotReady(msg) from ex
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: RadioThermConfigEntry) -> bool:
"""Set up Radio Thermostat from a config entry."""
host = entry.data[CONF_HOST]
init_coro = async_get_init_data(hass, host)
@@ -54,21 +52,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
time_coro = async_set_time(hass, init_data.tstat)
await _async_call_or_raise_not_ready(time_coro, host)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(_async_update_listener))
return True
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def _async_update_listener(
hass: HomeAssistant, entry: RadioThermConfigEntry
) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: RadioThermConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -17,13 +17,11 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, PRECISION_HALVES, UnitOfTemperature
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import DOMAIN
from .coordinator import RadioThermUpdateCoordinator
from .coordinator import RadioThermConfigEntry, RadioThermUpdateCoordinator
from .entity import RadioThermostatEntity
ATTR_FAN_ACTION = "fan_action"
@@ -101,12 +99,11 @@ def round_temp(temperature):
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: RadioThermConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up climate for a radiotherm device."""
coordinator: RadioThermUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([RadioThermostat(coordinator)])
async_add_entities([RadioThermostat(entry.runtime_data)])
class RadioThermostat(RadioThermostatEntity, ClimateEntity):

View File

@@ -14,6 +14,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .data import RadioThermInitData, RadioThermUpdate, async_get_data
type RadioThermConfigEntry = ConfigEntry[RadioThermUpdateCoordinator]
_LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(seconds=15)
@@ -22,12 +24,12 @@ UPDATE_INTERVAL = timedelta(seconds=15)
class RadioThermUpdateCoordinator(DataUpdateCoordinator[RadioThermUpdate]):
"""DataUpdateCoordinator to gather data for radio thermostats."""
config_entry: ConfigEntry
config_entry: RadioThermConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RadioThermConfigEntry,
init_data: RadioThermInitData,
) -> None:
"""Initialize DataUpdateCoordinator."""

View File

@@ -5,12 +5,10 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import RadioThermUpdateCoordinator
from .coordinator import RadioThermConfigEntry, RadioThermUpdateCoordinator
from .entity import RadioThermostatEntity
PARALLEL_UPDATES = 1
@@ -18,12 +16,11 @@ PARALLEL_UPDATES = 1
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: RadioThermConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up switches for a radiotherm device."""
coordinator: RadioThermUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([RadioThermHoldSwitch(coordinator)])
async_add_entities([RadioThermHoldSwitch(entry.runtime_data)])
class RadioThermHoldSwitch(RadioThermostatEntity, SwitchEntity):

View File

@@ -2,29 +2,27 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import EagleDataCoordinator
from .coordinator import EagleDataCoordinator, RainforestEagleConfigEntry
PLATFORMS = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, entry: RainforestEagleConfigEntry
) -> bool:
"""Set up Rainforest Eagle from a config entry."""
coordinator = EagleDataCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: RainforestEagleConfigEntry
) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -23,17 +23,21 @@ from .const import (
)
from .data import UPDATE_100_ERRORS
type RainforestEagleConfigEntry = ConfigEntry[EagleDataCoordinator]
_LOGGER = logging.getLogger(__name__)
class EagleDataCoordinator(DataUpdateCoordinator):
"""Get the latest data from the Eagle device."""
config_entry: ConfigEntry
config_entry: RainforestEagleConfigEntry
eagle100_reader: Eagle100Reader | None = None
eagle200_meter: aioeagle.ElectricMeter | None = None
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
def __init__(
self, hass: HomeAssistant, config_entry: RainforestEagleConfigEntry
) -> None:
"""Initialize the data object."""
if config_entry.data[CONF_TYPE] == TYPE_EAGLE_100:
self.model = "EAGLE-100"

View File

@@ -5,22 +5,19 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import CONF_CLOUD_ID, CONF_INSTALL_CODE, DOMAIN
from .coordinator import EagleDataCoordinator
from .const import CONF_CLOUD_ID, CONF_INSTALL_CODE
from .coordinator import RainforestEagleConfigEntry
TO_REDACT = {CONF_CLOUD_ID, CONF_INSTALL_CODE}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: RainforestEagleConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: EagleDataCoordinator = hass.data[DOMAIN][config_entry.entry_id]
return {
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),
"data": coordinator.data,
"data": config_entry.runtime_data.data,
}

View File

@@ -8,7 +8,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfEnergy, UnitOfPower
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
@@ -17,7 +16,7 @@ from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import EagleDataCoordinator
from .coordinator import EagleDataCoordinator, RainforestEagleConfigEntry
SENSORS = (
SensorEntityDescription(
@@ -46,11 +45,11 @@ SENSORS = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: RainforestEagleConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
entities = [EagleSensor(coordinator, description) for description in SENSORS]
if coordinator.data.get("zigbee:Price") not in (None, "invalid"):

View File

@@ -2,30 +2,25 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import RDWDataUpdateCoordinator
from .coordinator import RDWConfigEntry, RDWDataUpdateCoordinator
PLATFORMS = [Platform.BINARY_SENSOR, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: RDWConfigEntry) -> bool:
"""Set up RDW from a config entry."""
coordinator = RDWDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: RDWConfigEntry) -> bool:
"""Unload RDW config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
del hass.data[DOMAIN][entry.entry_id]
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -12,14 +12,13 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import RDWDataUpdateCoordinator
from .coordinator import RDWConfigEntry, RDWDataUpdateCoordinator
@dataclass(frozen=True, kw_only=True)
@@ -46,11 +45,11 @@ BINARY_SENSORS: tuple[RDWBinarySensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: RDWConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up RDW binary sensors based on a config entry."""
coordinator: RDWDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
RDWBinarySensorEntity(
coordinator=coordinator,

View File

@@ -11,13 +11,15 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import CONF_LICENSE_PLATE, DOMAIN, LOGGER, SCAN_INTERVAL
type RDWConfigEntry = ConfigEntry[RDWDataUpdateCoordinator]
class RDWDataUpdateCoordinator(DataUpdateCoordinator[Vehicle]):
"""Class to manage fetching RDW data."""
config_entry: ConfigEntry
config_entry: RDWConfigEntry
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
def __init__(self, hass: HomeAssistant, config_entry: RDWConfigEntry) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,

View File

@@ -4,17 +4,14 @@ from __future__ import annotations
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import RDWDataUpdateCoordinator
from .coordinator import RDWConfigEntry
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: RDWConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: RDWDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
data: dict[str, Any] = coordinator.data.to_dict()
data: dict[str, Any] = entry.runtime_data.data.to_dict()
return data

View File

@@ -13,14 +13,13 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import CONF_LICENSE_PLATE, DOMAIN
from .coordinator import RDWDataUpdateCoordinator
from .coordinator import RDWConfigEntry, RDWDataUpdateCoordinator
@dataclass(frozen=True, kw_only=True)
@@ -48,11 +47,11 @@ SENSORS: tuple[RDWSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: RDWConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up RDW sensors based on a config entry."""
coordinator: RDWDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
RDWSensorEntity(
coordinator=coordinator,

View File

@@ -9,19 +9,20 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN, LOGGER
from .coordinator import ReCollectWasteDataUpdateCoordinator
from .const import CONF_PLACE_ID, CONF_SERVICE_ID, LOGGER
from .coordinator import RecollectWasteConfigEntry, ReCollectWasteDataUpdateCoordinator
PLATFORMS = [Platform.CALENDAR, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, entry: RecollectWasteConfigEntry
) -> bool:
"""Set up ReCollect Waste as config entry."""
coordinator = ReCollectWasteDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -30,18 +31,18 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def async_reload_entry(
hass: HomeAssistant, entry: RecollectWasteConfigEntry
) -> None:
"""Handle an options update."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: RecollectWasteConfigEntry
) -> bool:
"""Unload an ReCollect Waste config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@@ -7,19 +7,17 @@ import datetime
from aiorecollect.client import PickupEvent
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import ReCollectWasteDataUpdateCoordinator
from .coordinator import RecollectWasteConfigEntry, ReCollectWasteDataUpdateCoordinator
from .entity import ReCollectWasteEntity
from .util import async_get_pickup_type_names
@callback
def async_get_calendar_event_from_pickup_event(
entry: ConfigEntry, pickup_event: PickupEvent
entry: RecollectWasteConfigEntry, pickup_event: PickupEvent
) -> CalendarEvent:
"""Get a HASS CalendarEvent from an aiorecollect PickupEvent."""
pickup_type_string = ", ".join(
@@ -36,13 +34,11 @@ def async_get_calendar_event_from_pickup_event(
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: RecollectWasteConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up ReCollect Waste sensors based on a config entry."""
coordinator: ReCollectWasteDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities([ReCollectWasteCalendar(coordinator, entry)])
async_add_entities([ReCollectWasteCalendar(entry.runtime_data, entry)])
class ReCollectWasteCalendar(ReCollectWasteEntity, CalendarEntity):
@@ -54,7 +50,7 @@ class ReCollectWasteCalendar(ReCollectWasteEntity, CalendarEntity):
def __init__(
self,
coordinator: ReCollectWasteDataUpdateCoordinator,
entry: ConfigEntry,
entry: RecollectWasteConfigEntry,
) -> None:
"""Initialize the ReCollect Waste entity."""
super().__init__(coordinator, entry)

View File

@@ -8,17 +8,13 @@ from aiorecollect.client import Client
from aiorecollect.errors import RecollectError
import voluptuous as vol
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.const import CONF_FRIENDLY_NAME
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client
from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN, LOGGER
from .coordinator import RecollectWasteConfigEntry
DATA_SCHEMA = vol.Schema(
{vol.Required(CONF_PLACE_ID): str, vol.Required(CONF_SERVICE_ID): str}
@@ -33,7 +29,7 @@ class RecollectWasteConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
config_entry: RecollectWasteConfigEntry,
) -> RecollectWasteOptionsFlowHandler:
"""Define the config flow to handle options."""
return RecollectWasteOptionsFlowHandler()

View File

@@ -14,15 +14,19 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import CONF_PLACE_ID, CONF_SERVICE_ID, LOGGER
type RecollectWasteConfigEntry = ConfigEntry[ReCollectWasteDataUpdateCoordinator]
DEFAULT_UPDATE_INTERVAL = timedelta(days=1)
class ReCollectWasteDataUpdateCoordinator(DataUpdateCoordinator[list[PickupEvent]]):
"""Class to manage fetching ReCollect Waste data."""
config_entry: ConfigEntry
config_entry: RecollectWasteConfigEntry
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
def __init__(
self, hass: HomeAssistant, config_entry: RecollectWasteConfigEntry
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,

View File

@@ -6,12 +6,11 @@ import dataclasses
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant
from .const import CONF_PLACE_ID, DOMAIN
from .coordinator import ReCollectWasteDataUpdateCoordinator
from .const import CONF_PLACE_ID
from .coordinator import RecollectWasteConfigEntry
CONF_AREA_NAME = "area_name"
CONF_TITLE = "title"
@@ -26,15 +25,13 @@ TO_REDACT = {
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: RecollectWasteConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: ReCollectWasteDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
return async_redact_data(
{
"entry": entry.as_dict(),
"data": [dataclasses.asdict(event) for event in coordinator.data],
"data": [dataclasses.asdict(event) for event in entry.runtime_data.data],
},
TO_REDACT,
)

View File

@@ -1,11 +1,10 @@
"""Define a base ReCollect Waste entity."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import CONF_PLACE_ID, CONF_SERVICE_ID, DOMAIN
from .coordinator import ReCollectWasteDataUpdateCoordinator
from .coordinator import RecollectWasteConfigEntry, ReCollectWasteDataUpdateCoordinator
class ReCollectWasteEntity(CoordinatorEntity[ReCollectWasteDataUpdateCoordinator]):
@@ -16,7 +15,7 @@ class ReCollectWasteEntity(CoordinatorEntity[ReCollectWasteDataUpdateCoordinator
def __init__(
self,
coordinator: ReCollectWasteDataUpdateCoordinator,
entry: ConfigEntry,
entry: RecollectWasteConfigEntry,
) -> None:
"""Initialize the sensor."""
super().__init__(coordinator)

View File

@@ -9,12 +9,11 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, LOGGER
from .coordinator import ReCollectWasteDataUpdateCoordinator
from .const import LOGGER
from .coordinator import RecollectWasteConfigEntry, ReCollectWasteDataUpdateCoordinator
from .entity import ReCollectWasteEntity
from .util import async_get_pickup_type_names
@@ -38,14 +37,12 @@ SENSOR_DESCRIPTIONS = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: RecollectWasteConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up ReCollect Waste sensors based on a config entry."""
coordinator: ReCollectWasteDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
ReCollectWasteSensor(coordinator, entry, description)
ReCollectWasteSensor(entry.runtime_data, entry, description)
for description in SENSOR_DESCRIPTIONS
)
@@ -63,7 +60,7 @@ class ReCollectWasteSensor(ReCollectWasteEntity, SensorEntity):
def __init__(
self,
coordinator: ReCollectWasteDataUpdateCoordinator,
entry: ConfigEntry,
entry: RecollectWasteConfigEntry,
description: SensorEntityDescription,
) -> None:
"""Initialize."""

View File

@@ -41,7 +41,7 @@
},
"services": {
"delete_command": {
"description": "Deletes a command or a list of commands from the database.",
"description": "Deletes a command or a list of commands from a remote's database.",
"fields": {
"command": {
"description": "The single command or the list of commands to be deleted.",
@@ -52,10 +52,10 @@
"name": "Device"
}
},
"name": "Delete command"
"name": "Delete remote command"
},
"learn_command": {
"description": "Learns a command or a list of commands from a device.",
"description": "Teaches a remote a command or list of commands from a device.",
"fields": {
"alternative": {
"description": "If code must be stored as an alternative. This is useful for discrete codes. Discrete codes are used for toggles that only perform one function. For example, a code to only turn a device on. If it is on already, sending the code won't change the state.",
@@ -78,7 +78,7 @@
"name": "Timeout"
}
},
"name": "Learn command"
"name": "Learn remote command"
},
"send_command": {
"description": "Sends a command or a list of commands to a device.",
@@ -104,15 +104,15 @@
"name": "Repeats"
}
},
"name": "Send command"
"name": "Send remote command"
},
"toggle": {
"description": "Sends the toggle command.",
"name": "[%key:common::action::toggle%]"
"name": "Toggle via remote"
},
"turn_off": {
"description": "Sends the turn off command.",
"name": "[%key:common::action::turn_off%]"
"name": "Turn off via remote"
},
"turn_on": {
"description": "Sends the turn on command.",
@@ -122,7 +122,7 @@
"name": "Activity"
}
},
"name": "[%key:common::action::turn_on%]"
"name": "Turn on via remote"
}
},
"title": "Remote",

View File

@@ -3,12 +3,10 @@
from __future__ import annotations
from dataclasses import dataclass
from typing import cast
from renault_api.kamereon.models import KamereonVehicleDataAttributes
from homeassistant.helpers.entity import Entity, EntityDescription
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .coordinator import RenaultDataUpdateCoordinator
@@ -54,10 +52,6 @@ class RenaultDataEntity[T: KamereonVehicleDataAttributes](
super().__init__(vehicle.coordinators[description.coordinator])
RenaultEntity.__init__(self, vehicle, description)
def _get_data_attr(self, key: str) -> StateType:
"""Return the attribute value from the coordinator data."""
return cast(StateType, getattr(self.coordinator.data, key))
@property
def assumed_state(self) -> bool:
"""Return True if unable to access real state of the entity."""

View File

@@ -88,15 +88,6 @@ class RenaultSensor[T: KamereonVehicleDataAttributes](
return self.entity_description.value_lambda(self)
def _get_charging_power(
entity: RenaultSensor[KamereonVehicleBatteryStatusData],
) -> StateType:
"""Return the charging_power of this entity."""
if (data := entity.coordinator.data.chargingInstantaneousPower) is None:
return None
return data / 1000
def _get_charge_state_formatted(
entity: RenaultSensor[KamereonVehicleBatteryStatusData],
) -> str | None:
@@ -190,9 +181,10 @@ SENSOR_TYPES: tuple[RenaultSensorEntityDescription[Any], ...] = (
condition_lambda=lambda a: a.details.reports_charging_power_in_watts(),
coordinator="battery",
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.KILO_WATT,
native_unit_of_measurement=UnitOfPower.WATT,
suggested_unit_of_measurement=UnitOfPower.KILO_WATT,
state_class=SensorStateClass.MEASUREMENT,
value_lambda=_get_charging_power,
value_lambda=lambda e: e.coordinator.data.chargingInstantaneousPower,
translation_key="charging_power",
),
RenaultSensorEntityDescription[KamereonVehicleBatteryStatusData](

View File

@@ -2,17 +2,13 @@
from __future__ import annotations
from dataclasses import dataclass
from renson_endura_delta.renson import RensonVentilation
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
from .coordinator import RensonCoordinator
from .coordinator import RensonConfigEntry, RensonCoordinator, RensonData
PLATFORMS = [
Platform.BINARY_SENSOR,
@@ -25,15 +21,7 @@ PLATFORMS = [
]
@dataclass
class RensonData:
"""Renson data class."""
api: RensonVentilation
coordinator: RensonCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: RensonConfigEntry) -> bool:
"""Set up Renson from a config entry."""
api = RensonVentilation(entry.data[CONF_HOST])
@@ -44,7 +32,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = RensonData(
entry.runtime_data = RensonData(
api,
coordinator,
)
@@ -54,9 +42,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: RensonConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -21,13 +21,11 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import RensonCoordinator
from .coordinator import RensonConfigEntry, RensonCoordinator
from .entity import RensonEntity
@@ -85,15 +83,13 @@ BINARY_SENSORS: tuple[RensonBinarySensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RensonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Call the Renson integration to setup."""
api: RensonVentilation = hass.data[DOMAIN][config_entry.entry_id].api
coordinator: RensonCoordinator = hass.data[DOMAIN][
config_entry.entry_id
].coordinator
api = config_entry.runtime_data.api
coordinator = config_entry.runtime_data.coordinator
async_add_entities(
RensonBinarySensor(description, api, coordinator)

View File

@@ -12,13 +12,11 @@ from homeassistant.components.button import (
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import RensonCoordinator, RensonData
from .const import DOMAIN
from .coordinator import RensonConfigEntry, RensonCoordinator
from .entity import RensonEntity
@@ -53,12 +51,12 @@ ENTITY_DESCRIPTIONS: tuple[RensonButtonEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RensonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Renson button platform."""
data: RensonData = hass.data[DOMAIN][config_entry.entry_id]
data = config_entry.runtime_data
entities = [
RensonButton(description, data.api, data.coordinator)

View File

@@ -3,6 +3,7 @@
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Any
@@ -15,18 +16,29 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from .const import DOMAIN
type RensonConfigEntry = ConfigEntry[RensonData]
@dataclass
class RensonData:
"""Renson data class."""
api: RensonVentilation
coordinator: RensonCoordinator
_LOGGER = logging.getLogger(__name__)
class RensonCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Data update coordinator for Renson."""
config_entry: ConfigEntry
config_entry: RensonConfigEntry
def __init__(
self,
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RensonConfigEntry,
api: RensonVentilation,
) -> None:
"""Initialize my coordinator."""

View File

@@ -16,7 +16,6 @@ from renson_endura_delta.renson import Level, RensonVentilation
import voluptuous as vol
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -27,8 +26,7 @@ from homeassistant.util.percentage import (
)
from homeassistant.util.scaling import int_states_in_range
from .const import DOMAIN
from .coordinator import RensonCoordinator
from .coordinator import RensonConfigEntry, RensonCoordinator
from .entity import RensonEntity
_LOGGER = logging.getLogger(__name__)
@@ -84,15 +82,13 @@ SPEED_RANGE: tuple[float, float] = (1, 4)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RensonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Renson fan platform."""
api: RensonVentilation = hass.data[DOMAIN][config_entry.entry_id].api
coordinator: RensonCoordinator = hass.data[DOMAIN][
config_entry.entry_id
].coordinator
api = config_entry.runtime_data.api
coordinator = config_entry.runtime_data.coordinator
async_add_entities([RensonFan(api, coordinator)])

View File

@@ -12,13 +12,11 @@ from homeassistant.components.number import (
NumberEntity,
NumberEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory, UnitOfTime
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import RensonCoordinator
from .coordinator import RensonConfigEntry, RensonCoordinator
from .entity import RensonEntity
_LOGGER = logging.getLogger(__name__)
@@ -39,15 +37,13 @@ RENSON_NUMBER_DESCRIPTION = NumberEntityDescription(
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RensonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Renson number platform."""
api: RensonVentilation = hass.data[DOMAIN][config_entry.entry_id].api
coordinator: RensonCoordinator = hass.data[DOMAIN][
config_entry.entry_id
].coordinator
api = config_entry.runtime_data.api
coordinator = config_entry.runtime_data.coordinator
async_add_entities([RensonNumber(RENSON_NUMBER_DESCRIPTION, api, coordinator)])

View File

@@ -34,7 +34,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
PERCENTAGE,
@@ -45,9 +44,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import RensonData
from .const import DOMAIN
from .coordinator import RensonCoordinator
from .coordinator import RensonConfigEntry, RensonCoordinator
from .entity import RensonEntity
@@ -271,12 +268,12 @@ class RensonSensor(RensonEntity, SensorEntity):
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RensonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Renson sensor platform."""
data: RensonData = hass.data[DOMAIN][config_entry.entry_id]
data = config_entry.runtime_data
entities = [
RensonSensor(description, data.api, data.coordinator) for description in SENSORS

View File

@@ -9,12 +9,10 @@ from renson_endura_delta.field_enum import CURRENT_LEVEL_FIELD, DataType
from renson_endura_delta.renson import Level, RensonVentilation
from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import RensonCoordinator
from .const import DOMAIN
from .coordinator import RensonConfigEntry, RensonCoordinator
from .entity import RensonEntity
_LOGGER = logging.getLogger(__name__)
@@ -67,14 +65,12 @@ class RensonBreezeSwitch(RensonEntity, SwitchEntity):
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RensonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Call the Renson integration to setup."""
api: RensonVentilation = hass.data[DOMAIN][config_entry.entry_id].api
coordinator: RensonCoordinator = hass.data[DOMAIN][
config_entry.entry_id
].coordinator
api = config_entry.runtime_data.api
coordinator = config_entry.runtime_data.coordinator
async_add_entities([RensonBreezeSwitch(api, coordinator)])

View File

@@ -10,14 +10,11 @@ from renson_endura_delta.field_enum import DAYTIME_FIELD, NIGHTTIME_FIELD, Field
from renson_endura_delta.renson import RensonVentilation
from homeassistant.components.time import TimeEntity, TimeEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import RensonData
from .const import DOMAIN
from .coordinator import RensonCoordinator
from .coordinator import RensonConfigEntry, RensonCoordinator
from .entity import RensonEntity
@@ -49,15 +46,14 @@ ENTITY_DESCRIPTIONS: tuple[RensonTimeEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RensonConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Renson time platform."""
data: RensonData = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data.coordinator
entities = [
RensonTime(description, data.coordinator) for description in ENTITY_DESCRIPTIONS
RensonTime(description, coordinator) for description in ENTITY_DESCRIPTIONS
]
async_add_entities(entities)

View File

@@ -9,17 +9,17 @@ from homeassistant.const import Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from .const import DOMAIN, LOGGER, SENSOR_TYPE_NEXT_PICKUP
from .coordinator import RidwellDataUpdateCoordinator
from .const import LOGGER, SENSOR_TYPE_NEXT_PICKUP
from .coordinator import RidwellConfigEntry, RidwellDataUpdateCoordinator
PLATFORMS: list[Platform] = [Platform.CALENDAR, Platform.SENSOR, Platform.SWITCH]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: RidwellConfigEntry) -> bool:
"""Set up Ridwell from a config entry."""
coordinator = RidwellDataUpdateCoordinator(hass, entry)
await coordinator.async_initialize()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
entry.runtime_data = coordinator
entry.async_on_unload(entry.add_update_listener(options_update_listener))
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -27,17 +27,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def options_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def options_update_listener(
hass: HomeAssistant, entry: RidwellConfigEntry
) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: RidwellConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:

View File

@@ -7,7 +7,6 @@ import datetime
from aioridwell.model import PickupCategory, RidwellAccount, RidwellPickupEvent
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -16,15 +15,14 @@ from .const import (
CALENDAR_TITLE_ROTATING,
CALENDAR_TITLE_STATUS,
CONF_CALENDAR_TITLE,
DOMAIN,
)
from .coordinator import RidwellDataUpdateCoordinator
from .coordinator import RidwellConfigEntry, RidwellDataUpdateCoordinator
from .entity import RidwellEntity
@callback
def async_get_calendar_event_from_pickup_event(
pickup_event: RidwellPickupEvent, config_entry: ConfigEntry
pickup_event: RidwellPickupEvent, config_entry: RidwellConfigEntry
) -> CalendarEvent:
"""Get a HASS CalendarEvent from an aioridwell PickupEvent."""
pickup_items = []
@@ -66,11 +64,11 @@ def async_get_calendar_event_from_pickup_event(
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: RidwellConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Ridwell calendars based on a config entry."""
coordinator: RidwellDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
RidwellCalendar(coordinator, account)

View File

@@ -9,7 +9,7 @@ from aioridwell import async_get_client
from aioridwell.errors import InvalidCredentialsError, RidwellError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntry, ConfigFlow, ConfigFlowResult
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import callback
from homeassistant.helpers import aiohttp_client, config_validation as cv, selector
@@ -19,6 +19,7 @@ from homeassistant.helpers.schema_config_entry_flow import (
)
from .const import CALENDAR_TITLE_OPTIONS, CONF_CALENDAR_TITLE, DOMAIN, LOGGER
from .coordinator import RidwellConfigEntry
STEP_REAUTH_CONFIRM_DATA_SCHEMA = vol.Schema(
{
@@ -107,7 +108,7 @@ class RidwellConfigFlow(ConfigFlow, domain=DOMAIN):
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
config_entry: RidwellConfigEntry,
) -> SchemaOptionsFlowHandler:
"""Get options flow for this handler."""
try:

View File

@@ -19,6 +19,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
from .const import LOGGER
type RidwellConfigEntry = ConfigEntry[RidwellDataUpdateCoordinator]
UPDATE_INTERVAL = timedelta(hours=1)
@@ -27,9 +29,9 @@ class RidwellDataUpdateCoordinator(
):
"""Class to manage fetching data from single endpoint."""
config_entry: ConfigEntry
config_entry: RidwellConfigEntry
def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) -> None:
def __init__(self, hass: HomeAssistant, config_entry: RidwellConfigEntry) -> None:
"""Initialize."""
# These will be filled in by async_initialize; we give them these defaults to
# avoid arduous typing checks down the line:

View File

@@ -6,12 +6,10 @@ import dataclasses
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import RidwellDataUpdateCoordinator
from .coordinator import RidwellConfigEntry
CONF_TITLE = "title"
@@ -25,17 +23,15 @@ TO_REDACT = {
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: RidwellConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: RidwellDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
return async_redact_data(
{
"entry": entry.as_dict(),
"data": [
dataclasses.asdict(event)
for events in coordinator.data.values()
for events in entry.runtime_data.data.values()
for event in events
],
},

View File

@@ -13,12 +13,11 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN, SENSOR_TYPE_NEXT_PICKUP
from .coordinator import RidwellDataUpdateCoordinator
from .const import SENSOR_TYPE_NEXT_PICKUP
from .coordinator import RidwellConfigEntry, RidwellDataUpdateCoordinator
from .entity import RidwellEntity
ATTR_CATEGORY = "category"
@@ -35,11 +34,11 @@ SENSOR_DESCRIPTION = SensorEntityDescription(
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: RidwellConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Ridwell sensors based on a config entry."""
coordinator: RidwellDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
RidwellSensor(coordinator, account, SENSOR_DESCRIPTION)

View File

@@ -8,13 +8,11 @@ from aioridwell.errors import RidwellError
from aioridwell.model import EventState, RidwellAccount
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from .coordinator import RidwellDataUpdateCoordinator
from .coordinator import RidwellConfigEntry, RidwellDataUpdateCoordinator
from .entity import RidwellEntity
SWITCH_DESCRIPTION = SwitchEntityDescription(
@@ -25,11 +23,11 @@ SWITCH_DESCRIPTION = SwitchEntityDescription(
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: RidwellConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Ridwell sensors based on a config entry."""
coordinator: RidwellDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
RidwellSwitch(coordinator, account, SWITCH_DESCRIPTION)

View File

@@ -7,6 +7,7 @@ from ring_doorbell import RingCapability, RingEvent as RingAlert
from ring_doorbell.const import KIND_DING, KIND_INTERCOM_UNLOCK, KIND_MOTION
from homeassistant.components.event import (
DoorbellEventType,
EventDeviceClass,
EventEntity,
EventEntityDescription,
@@ -34,7 +35,7 @@ EVENT_DESCRIPTIONS: tuple[RingEventEntityDescription, ...] = (
key=KIND_DING,
translation_key=KIND_DING,
device_class=EventDeviceClass.DOORBELL,
event_types=[KIND_DING],
event_types=[DoorbellEventType.RING],
capability=RingCapability.DING,
),
RingEventEntityDescription(
@@ -100,7 +101,10 @@ class RingEvent(RingBaseEntity[RingListenCoordinator, RingDeviceT], EventEntity)
@callback
def _handle_coordinator_update(self) -> None:
if (alert := self._get_coordinator_alert()) and not alert.is_update:
self._async_handle_event(alert.kind)
if alert.kind == KIND_DING:
self._async_handle_event(DoorbellEventType.RING)
else:
self._async_handle_event(alert.kind)
super()._handle_coordinator_update()
@property

View File

@@ -73,7 +73,14 @@
},
"event": {
"ding": {
"name": "Ding"
"name": "Ding",
"state_attributes": {
"event_type": {
"state": {
"ring": "[%key:component::event::entity_component::doorbell::state_attributes::event_type::state::ring%]"
}
}
}
},
"intercom_unlock": {
"name": "Intercom unlock"

View File

@@ -26,15 +26,13 @@ from homeassistant.helpers.typing import ConfigType
from .const import (
CONF_CONCURRENCY,
DATA_COORDINATOR,
DEFAULT_CONCURRENCY,
DOMAIN,
EVENTS_COORDINATOR,
SYSTEM_UPDATE_SIGNAL,
TYPE_LOCAL,
)
from .coordinator import RiscoDataUpdateCoordinator, RiscoEventsDataUpdateCoordinator
from .models import LocalData
from .models import CloudData, LocalData, RiscoConfigEntry, RiscoData
from .services import async_setup_services
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
@@ -58,7 +56,7 @@ def zone_update_signal(zone_id: int) -> str:
return f"risco_zone_update_{zone_id}"
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: RiscoConfigEntry) -> bool:
"""Set up Risco from a config entry."""
if is_local(entry):
return await _async_setup_local_entry(hass, entry)
@@ -66,7 +64,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return await _async_setup_cloud_entry(hass, entry)
async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _async_setup_local_entry(
hass: HomeAssistant, entry: RiscoConfigEntry
) -> bool:
data = entry.data
concurrency = entry.options.get(CONF_CONCURRENCY, DEFAULT_CONCURRENCY)
risco = RiscoLocal(
@@ -120,14 +120,15 @@ async def _async_setup_local_entry(hass: HomeAssistant, entry: ConfigEntry) -> b
entry.async_on_unload(entry.add_update_listener(_update_listener))
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = local_data
entry.runtime_data = RiscoData(local_data=local_data)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def _async_setup_cloud_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def _async_setup_cloud_entry(
hass: HomeAssistant, entry: RiscoConfigEntry
) -> bool:
data = entry.data
risco = RiscoCloud(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN])
try:
@@ -143,11 +144,12 @@ async def _async_setup_cloud_entry(hass: HomeAssistant, entry: ConfigEntry) -> b
entry.async_on_unload(entry.add_update_listener(_update_listener))
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
DATA_COORDINATOR: coordinator,
EVENTS_COORDINATOR: events_coordinator,
}
entry.runtime_data = RiscoData(
cloud_data=CloudData(
coordinator=coordinator,
events_coordinator=events_coordinator,
)
)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
await events_coordinator.async_refresh()
@@ -155,20 +157,16 @@ async def _async_setup_cloud_entry(hass: HomeAssistant, entry: ConfigEntry) -> b
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: RiscoConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
if is_local(entry):
local_data: LocalData = hass.data[DOMAIN][entry.entry_id]
await local_data.system.disconnect()
hass.data[DOMAIN].pop(entry.entry_id)
if unload_ok and (local_data := entry.runtime_data.local_data):
await local_data.system.disconnect()
return unload_ok
async def _update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:
async def _update_listener(hass: HomeAssistant, entry: RiscoConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -15,19 +15,16 @@ from homeassistant.components.alarm_control_panel import (
AlarmControlPanelState,
CodeFormat,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PIN
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import LocalData, is_local
from .const import (
CONF_CODE_ARM_REQUIRED,
CONF_CODE_DISARM_REQUIRED,
CONF_HA_STATES_TO_RISCO,
CONF_RISCO_STATES_TO_HA,
DATA_COORDINATOR,
DEFAULT_OPTIONS,
DOMAIN,
RISCO_ARM,
@@ -36,6 +33,7 @@ from .const import (
)
from .coordinator import RiscoDataUpdateCoordinator
from .entity import RiscoCloudEntity
from .models import RiscoConfigEntry
_LOGGER = logging.getLogger(__name__)
@@ -49,13 +47,13 @@ STATES_TO_SUPPORTED_FEATURES = {
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RiscoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Risco alarm control panel."""
options = {**DEFAULT_OPTIONS, **config_entry.options}
if is_local(config_entry):
local_data: LocalData = hass.data[DOMAIN][config_entry.entry_id]
risco_data = config_entry.runtime_data
if local_data := risco_data.local_data:
async_add_entities(
RiscoLocalAlarm(
local_data.system.id,
@@ -67,10 +65,8 @@ async def async_setup_entry(
)
for partition_id, partition in local_data.system.partitions.items()
)
else:
coordinator: RiscoDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
][DATA_COORDINATOR]
elif cloud_data := risco_data.cloud_data:
coordinator = cloud_data.coordinator
async_add_entities(
RiscoCloudAlarm(
coordinator, partition_id, config_entry.data[CONF_PIN], options

View File

@@ -15,16 +15,15 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import LocalData, is_local
from .const import DATA_COORDINATOR, DOMAIN, SYSTEM_UPDATE_SIGNAL
from .const import DOMAIN, SYSTEM_UPDATE_SIGNAL
from .coordinator import RiscoDataUpdateCoordinator
from .entity import RiscoCloudZoneEntity, RiscoLocalZoneEntity
from .models import RiscoConfigEntry
SYSTEM_ENTITY_DESCRIPTIONS = [
BinarySensorEntityDescription(
@@ -72,12 +71,12 @@ SYSTEM_ENTITY_DESCRIPTIONS = [
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RiscoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Risco alarm control panel."""
if is_local(config_entry):
local_data: LocalData = hass.data[DOMAIN][config_entry.entry_id]
risco_data = config_entry.runtime_data
if local_data := risco_data.local_data:
zone_entities = (
entity
for zone_id, zone in local_data.system.zones.items()
@@ -96,10 +95,8 @@ async def async_setup_entry(
)
async_add_entities(chain(system_entities, zone_entities))
else:
coordinator: RiscoDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
][DATA_COORDINATOR]
elif cloud_data := risco_data.cloud_data:
coordinator = cloud_data.coordinator
async_add_entities(
RiscoCloudBinarySensor(coordinator, zone_id, zone)
for zone_id, zone in coordinator.data.zones.items()

View File

@@ -10,12 +10,7 @@ from pyrisco import CannotConnectError, RiscoCloud, RiscoLocal, UnauthorizedErro
import voluptuous as vol
from homeassistant.components.alarm_control_panel import AlarmControlPanelState
from homeassistant.config_entries import (
ConfigEntry,
ConfigFlow,
ConfigFlowResult,
OptionsFlow,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult, OptionsFlow
from homeassistant.const import (
CONF_HOST,
CONF_PASSWORD,
@@ -42,6 +37,7 @@ from .const import (
RISCO_STATES,
TYPE_LOCAL,
)
from .models import RiscoConfigEntry
_LOGGER = logging.getLogger(__name__)
@@ -121,12 +117,12 @@ class RiscoConfigFlow(ConfigFlow, domain=DOMAIN):
def __init__(self) -> None:
"""Init the config flow."""
self._reauth_entry: ConfigEntry | None = None
self._reauth_entry: RiscoConfigEntry | None = None
@staticmethod
@callback
def async_get_options_flow(
config_entry: ConfigEntry,
config_entry: RiscoConfigEntry,
) -> RiscoOptionsFlowHandler:
"""Define the config flow to handle options."""
return RiscoOptionsFlowHandler(config_entry)
@@ -218,7 +214,7 @@ class RiscoConfigFlow(ConfigFlow, domain=DOMAIN):
class RiscoOptionsFlowHandler(OptionsFlow):
"""Handle a Risco options flow."""
def __init__(self, config_entry: ConfigEntry) -> None:
def __init__(self, config_entry: RiscoConfigEntry) -> None:
"""Initialize."""
self._data = {**DEFAULT_OPTIONS, **config_entry.options}

View File

@@ -1,11 +1,39 @@
"""Models for Risco integration."""
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass, field
from typing import Any
from typing import TYPE_CHECKING, Any
from pyrisco import RiscoLocal
from homeassistant.config_entries import ConfigEntry
if TYPE_CHECKING:
from .coordinator import (
RiscoDataUpdateCoordinator,
RiscoEventsDataUpdateCoordinator,
)
type RiscoConfigEntry = ConfigEntry[RiscoData]
@dataclass
class RiscoData:
"""Runtime data for the Risco integration."""
local_data: LocalData | None = None
cloud_data: CloudData | None = None
@dataclass
class CloudData:
"""A data class for cloud data passed to the platforms."""
coordinator: RiscoDataUpdateCoordinator
events_coordinator: RiscoEventsDataUpdateCoordinator
@dataclass
class LocalData:

View File

@@ -10,17 +10,16 @@ from pyrisco.cloud.event import Event
from homeassistant.components.binary_sensor import DOMAIN as BS_DOMAIN
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from . import is_local
from .const import DOMAIN, EVENTS_COORDINATOR
from .const import DOMAIN
from .coordinator import RiscoEventsDataUpdateCoordinator
from .entity import zone_unique_id
from .models import RiscoConfigEntry
CATEGORIES = {
2: "Alarm",
@@ -45,17 +44,15 @@ EVENT_ATTRIBUTES = [
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: RiscoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up sensors for device."""
if is_local(config_entry):
if not (cloud_data := config_entry.runtime_data.cloud_data):
# no events in local comm
return
coordinator: RiscoEventsDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
][EVENTS_COORDINATOR]
coordinator = cloud_data.events_coordinator
sensors = [
RiscoSensor(coordinator, category_id, [], name, config_entry.entry_id)
for category_id, name in CATEGORIES.items()

Some files were not shown because too many files have changed in this diff Show More