Compare commits

..

55 Commits

Author SHA1 Message Date
abmantis
b3fd7bc0cf Test refs 2026-03-03 15:24:51 +00:00
James
713b7cf36d Check Daikin zone temp keys before represent (#164297)
Co-authored-by: barneyonline <barneyonline@users.noreply.github.com>
2026-03-02 19:48:39 +00:00
Bram Kragten
cb016b014b Update frontend to 20260302.0 (#164612) 2026-03-02 18:53:01 +01:00
Michael Hansen
afb4523f63 Add device_id and satellite_id to conversation HTTP/websocket APIs (#164414) 2026-03-02 17:01:51 +01:00
Alex Brown
05ad4986ac Fix Matter clear lock user (#164493) 2026-03-02 16:28:49 +01:00
epenet
42dbd5f98f Migrate moat to runtime_data (#164605) 2026-03-02 16:14:25 +01:00
epenet
f58a514ce7 Migrate monzo to runtime_data (#164603) 2026-03-02 16:14:10 +01:00
Artur Pragacz
8fb384a5e1 Raise on vacuum area mapping not configured (#164595) 2026-03-02 15:36:48 +01:00
Samuel Xiao
c24302b5ce Switchbot Cloud: Fixed Smart Radiator Thermostat off line (#162714)
Co-authored-by: Ariel Ebersberger <ariel@ebersberger.io>
2026-03-02 14:44:34 +01:00
Jan-Philipp Benecke
999ad9b642 Bump aiotankerkoenig to 0.5.1 (#164590) 2026-03-02 14:44:29 +01:00
Pierre Sassoulas
36d6b4dafe Use clearer number notation for very small and very large literals (#164521) 2026-03-02 14:06:19 +01:00
Norbert Rittel
06870a2e25 Replace "the lock" with "a lock" in matter action descriptions (#164585) 2026-03-02 12:56:45 +01:00
willemstuursma
85eba2bb15 Bump DSMR parser to 1.5.0 (#164484) 2026-03-02 12:52:37 +01:00
Joost Lekkerkerker
5dd6dcc215 Add select for SmartThings Water spray level (#164520) 2026-03-02 12:17:31 +01:00
epenet
8bf894a514 Migrate microbees to runtime_data (#164564) 2026-03-02 12:04:34 +01:00
epenet
d3c67f2ae1 Migrate medcom_ble to runtime_data (#164557) 2026-03-02 12:03:35 +01:00
epenet
b60a282b60 Move motioneye coordinator to separate module (#164568) 2026-03-02 11:57:19 +01:00
epenet
0da1d40a19 Migrate meteoclimatic to runtime_data (#164559) 2026-03-02 11:50:46 +01:00
Robert Resch
aa3be915a0 Bump aiogithubapi to 26.0.0 (#164579) 2026-03-02 11:49:32 +01:00
Manu
0d97bfbc59 Bump pyloadapi to 2.0.0 (#164495) 2026-03-02 11:47:13 +01:00
epenet
fe830337c9 Migrate modem_callerid to runtime_data (#164566) 2026-03-02 11:45:58 +01:00
epenet
5210b7d847 Migrate moehlenhoff_alpha2 to runtime_data (#164571) 2026-03-02 11:45:10 +01:00
Mike Ryan
2f7ed4040b Bump python-fullykiosk from 0.0.14 to 0.0.15 (#164511) 2026-03-02 11:42:56 +01:00
Simone Chemelli
6376ba93a7 Bump aioamazondevices to 12.0.2 (#164518) 2026-03-02 11:37:39 +01:00
J. Nick Koston
fd3a1cc9f4 Bump yalexs-ble to 3.2.7 (#164555) 2026-03-02 11:36:05 +01:00
epenet
208013ab76 Move metoffice coordinators to separate module (#164562)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 11:31:57 +01:00
Alex Brown
770b3f910e Fix Matter lock credential slot iteration bound (#164478)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-02 10:56:03 +01:00
Norbert Rittel
5dce4a8eda Change one remaining string from "Overseerr" to "Seerr" (#164569) 2026-03-02 10:22:49 +01:00
Jan-Philipp Benecke
6fcc9da948 Fix large WebDAV backup metadata download (#164563)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-02 10:17:18 +01:00
epenet
bf93580ff9 Migrate modern_forms to runtime_data (#164570) 2026-03-02 10:10:03 +01:00
Jan-Philipp Benecke
0c2fe045d5 Bump aiowebdav2 to 0.6.1 (#164560) 2026-03-02 10:09:33 +01:00
Joost Lekkerkerker
e14a3a6b0e Fix SmartThings EHS power (#164395) 2026-03-02 08:35:37 +01:00
Joost Lekkerkerker
e032740e90 Add time platform to SmartThings (#164451) 2026-03-02 08:34:53 +01:00
Joost Lekkerkerker
78ad1e102d Add binary sensor for full dust bag in SmartThings (#164457) 2026-03-02 08:34:19 +01:00
Joost Lekkerkerker
4f97cc7b68 Add sound detection sensitivity select to SmartThings (#164466) 2026-03-02 08:33:47 +01:00
dependabot[bot]
df8f135532 Bump github/codeql-action from 4.32.3 to 4.32.4 (#164554) 2026-03-02 07:30:23 +01:00
J. Nick Koston
0066801b0f Bump yarl to 1.23.0 (#164542) 2026-03-02 07:22:37 +01:00
Joost Lekkerkerker
0aa66ed6cb Add select for SmartThings driving mode (#164522) 2026-03-01 19:11:58 +01:00
HadiAyache
6903463f14 Fix AccuWeather daily forecast crash when humidity average is missing (#163968)
Co-authored-by: Maciej Bieniek <bieniu@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-01 17:19:15 +01:00
Brett Adams
a473010fee Update Tessie quality scale to silver (#164104)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-03-01 16:53:39 +01:00
Robin Lintermann
ddf7a783a8 Bump smarla quality scale to silver (#164325) 2026-03-01 11:52:11 +01:00
Joost Lekkerkerker
513e4d52fe Add button to reset HEPA filter to SmartThings (#164464) 2026-03-01 07:33:10 +01:00
Klaas Schoute
17bb14e260 Update error handling messages for Powerfox Local integration (#164465) 2026-03-01 07:32:36 +01:00
Brett Adams
cd1258464b Fix OAuth token type narrowing in Teslemetry (#164505) 2026-03-01 07:31:34 +01:00
Allen Porter
d3f5e0e6d7 Update nest access token error handling to use specific OAuth2 token request exceptions (#164506) 2026-03-01 07:26:07 +01:00
Joost Lekkerkerker
e124829364 Rename Overseerr integration to Seerr (#164060) 2026-02-28 23:07:31 +01:00
Jan Bouwhuis
87b83dcc1b Remove the MQTT object_id option after 6 months of deprecation (#164460)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-28 20:12:23 +01:00
Erik Montnemery
be9b47539d Revert "Remove unnecessary volume_up/volume_down overrides from frontier_silicon media player" (#164463) 2026-02-28 20:11:52 +01:00
Joost Lekkerkerker
be6ddc314c Add sound detection switch to SmartThings (#164470) 2026-02-28 20:11:13 +01:00
David Bonnes
c6f8a7b7e4 Harden test of an invalid service call for Evohome (#164458) 2026-02-28 20:10:11 +01:00
Joost Lekkerkerker
53da5612e9 Add fan speed to SmartThings vacuum (#164452)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-02-28 20:09:43 +01:00
Michael Davie
6cc56b76f9 Bump env-canada to 0.13.2 (#164480)
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-02-28 20:08:17 +01:00
Tom Matheussen
03cb65d555 Require user code to be set when toggling Satel Integra switches (#164483) 2026-02-28 20:06:56 +01:00
Abílio Costa
73dd024933 Add merged PR count sensor to Github integration (#164405) 2026-02-28 15:13:17 +01:00
Barry vd. Heuvel
1c8c92bf8f Bump weheat to 2026.2.28 (#164456) 2026-02-28 14:40:58 +01:00
173 changed files with 2355 additions and 4184 deletions

View File

@@ -0,0 +1,8 @@
---
name: ban-word-list
description: Find words that are not allowed
---
# Ban Word List
If any of the words listed in the `list.md` file are found on new code, warn the user and ask them to change it.

View File

@@ -0,0 +1 @@
- potato

File diff suppressed because it is too large Load Diff

View File

@@ -28,11 +28,11 @@ jobs:
persist-credentials: false
- name: Initialize CodeQL
uses: github/codeql-action/init@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
uses: github/codeql-action/init@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
languages: python
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@9e907b5e64f6b83e7804b09294d44122997950d6 # v4.32.3
uses: github/codeql-action/analyze@89a39a4e59826350b863aa6b6252a07ad50cf83e # v4.32.4
with:
category: "/language:python"

View File

@@ -325,7 +325,6 @@ homeassistant.components.ld2410_ble.*
homeassistant.components.led_ble.*
homeassistant.components.lektrico.*
homeassistant.components.letpot.*
homeassistant.components.lg_infrared.*
homeassistant.components.libre_hardware_monitor.*
homeassistant.components.lidarr.*
homeassistant.components.lifx.*

View File

@@ -191,7 +191,7 @@ class AccuWeatherEntity(
{
ATTR_FORECAST_TIME: utc_from_timestamp(item["EpochDate"]).isoformat(),
ATTR_FORECAST_CLOUD_COVERAGE: item["CloudCoverDay"],
ATTR_FORECAST_HUMIDITY: item["RelativeHumidityDay"]["Average"],
ATTR_FORECAST_HUMIDITY: item["RelativeHumidityDay"].get("Average"),
ATTR_FORECAST_NATIVE_TEMP: item["TemperatureMax"][ATTR_VALUE],
ATTR_FORECAST_NATIVE_TEMP_LOW: item["TemperatureMin"][ATTR_VALUE],
ATTR_FORECAST_NATIVE_APPARENT_TEMP: item["RealFeelTemperatureMax"][

View File

@@ -8,5 +8,5 @@
"iot_class": "cloud_polling",
"loggers": ["aioamazondevices"],
"quality_scale": "platinum",
"requirements": ["aioamazondevices==12.0.0"]
"requirements": ["aioamazondevices==12.0.2"]
}

View File

@@ -30,5 +30,5 @@
"integration_type": "hub",
"iot_class": "cloud_push",
"loggers": ["pubnub", "yalexs"],
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.4"]
"requirements": ["yalexs==9.2.0", "yalexs-ble==3.2.7"]
}

View File

@@ -190,7 +190,7 @@ class BitcoinSensor(SensorEntity):
elif sensor_type == "miners_revenue_usd":
self._attr_native_value = f"{stats.miners_revenue_usd:.0f}"
elif sensor_type == "btc_mined":
self._attr_native_value = str(stats.btc_mined * 0.00000001)
self._attr_native_value = str(stats.btc_mined * 1e-8)
elif sensor_type == "trade_volume_usd":
self._attr_native_value = f"{stats.trade_volume_usd:.1f}"
elif sensor_type == "difficulty":
@@ -208,13 +208,13 @@ class BitcoinSensor(SensorEntity):
elif sensor_type == "blocks_size":
self._attr_native_value = f"{stats.blocks_size:.1f}"
elif sensor_type == "total_fees_btc":
self._attr_native_value = f"{stats.total_fees_btc * 0.00000001:.2f}"
self._attr_native_value = f"{stats.total_fees_btc * 1e-8:.2f}"
elif sensor_type == "total_btc_sent":
self._attr_native_value = f"{stats.total_btc_sent * 0.00000001:.2f}"
self._attr_native_value = f"{stats.total_btc_sent * 1e-8:.2f}"
elif sensor_type == "estimated_btc_sent":
self._attr_native_value = f"{stats.estimated_btc_sent * 0.00000001:.2f}"
self._attr_native_value = f"{stats.estimated_btc_sent * 1e-8:.2f}"
elif sensor_type == "total_btc":
self._attr_native_value = f"{stats.total_btc * 0.00000001:.2f}"
self._attr_native_value = f"{stats.total_btc * 1e-8:.2f}"
elif sensor_type == "total_blocks":
self._attr_native_value = f"{stats.total_blocks:.0f}"
elif sensor_type == "next_retarget":
@@ -222,7 +222,7 @@ class BitcoinSensor(SensorEntity):
elif sensor_type == "estimated_transaction_volume_usd":
self._attr_native_value = f"{stats.estimated_transaction_volume_usd:.2f}"
elif sensor_type == "miners_revenue_btc":
self._attr_native_value = f"{stats.miners_revenue_btc * 0.00000001:.1f}"
self._attr_native_value = f"{stats.miners_revenue_btc * 1e-8:.1f}"
elif sensor_type == "market_price_usd":
self._attr_native_value = f"{stats.market_price_usd:.2f}"

View File

@@ -48,6 +48,8 @@ def async_setup(hass: HomeAssistant) -> None:
vol.Optional("conversation_id"): vol.Any(str, None),
vol.Optional("language"): str,
vol.Optional("agent_id"): agent_id_validator,
vol.Optional("device_id"): vol.Any(str, None),
vol.Optional("satellite_id"): vol.Any(str, None),
}
)
@websocket_api.async_response
@@ -64,6 +66,8 @@ async def websocket_process(
context=connection.context(msg),
language=msg.get("language"),
agent_id=msg.get("agent_id"),
device_id=msg.get("device_id"),
satellite_id=msg.get("satellite_id"),
)
connection.send_result(msg["id"], result.as_dict())
@@ -248,6 +252,8 @@ class ConversationProcessView(http.HomeAssistantView):
vol.Optional("conversation_id"): str,
vol.Optional("language"): str,
vol.Optional("agent_id"): agent_id_validator,
vol.Optional("device_id"): vol.Any(str, None),
vol.Optional("satellite_id"): vol.Any(str, None),
}
)
)
@@ -262,6 +268,8 @@ class ConversationProcessView(http.HomeAssistantView):
context=self.context(request),
language=data.get("language"),
agent_id=data.get("agent_id"),
device_id=data.get("device_id"),
satellite_id=data.get("satellite_id"),
)
return self.json(result.as_dict())

View File

@@ -112,11 +112,12 @@ def _zone_is_configured(zone: DaikinZone) -> bool:
def _zone_temperature_lists(device: Appliance) -> tuple[list[str], list[str]]:
"""Return the decoded zone temperature lists."""
try:
heating = device.represent(DAIKIN_ZONE_TEMP_HEAT)[1]
cooling = device.represent(DAIKIN_ZONE_TEMP_COOL)[1]
except AttributeError, KeyError:
values = device.values
if DAIKIN_ZONE_TEMP_HEAT not in values or DAIKIN_ZONE_TEMP_COOL not in values:
return ([], [])
heating = device.represent(DAIKIN_ZONE_TEMP_HEAT)[1]
cooling = device.represent(DAIKIN_ZONE_TEMP_COOL)[1]
return (list(heating or []), list(cooling or []))

View File

@@ -7,5 +7,5 @@
"integration_type": "hub",
"iot_class": "local_push",
"loggers": ["dsmr_parser"],
"requirements": ["dsmr-parser==1.4.3"]
"requirements": ["dsmr-parser==1.5.0"]
}

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["env_canada"],
"requirements": ["env-canada==0.12.4"]
"requirements": ["env-canada==0.13.2"]
}

View File

@@ -189,7 +189,6 @@ async def platform_async_setup_entry(
info_type: type[_InfoT],
entity_type: type[_EntityT],
state_type: type[_StateT],
info_filter: Callable[[_InfoT], bool] | None = None,
) -> None:
"""Set up an esphome platform.
@@ -209,22 +208,10 @@ async def platform_async_setup_entry(
entity_type,
state_type,
)
if info_filter is not None:
def on_filtered_update(infos: list[EntityInfo]) -> None:
on_static_info_update(
[info for info in infos if info_filter(info)] # type: ignore[arg-type]
)
info_callback = on_filtered_update
else:
info_callback = on_static_info_update
entry_data.cleanup_callbacks.append(
entry_data.async_register_static_info_callback(
info_type,
info_callback,
on_static_info_update,
)
)

View File

@@ -29,7 +29,6 @@ from aioesphomeapi import (
Event,
EventInfo,
FanInfo,
InfraredInfo,
LightInfo,
LockInfo,
MediaPlayerInfo,
@@ -86,7 +85,6 @@ INFO_TYPE_TO_PLATFORM: dict[type[EntityInfo], Platform] = {
DateTimeInfo: Platform.DATETIME,
EventInfo: Platform.EVENT,
FanInfo: Platform.FAN,
InfraredInfo: Platform.INFRARED,
LightInfo: Platform.LIGHT,
LockInfo: Platform.LOCK,
MediaPlayerInfo: Platform.MEDIA_PLAYER,

View File

@@ -1,59 +0,0 @@
"""Infrared platform for ESPHome."""
from __future__ import annotations
from functools import partial
import logging
from aioesphomeapi import EntityState, InfraredCapability, InfraredInfo
from homeassistant.components.infrared import InfraredCommand, InfraredEntity
from homeassistant.core import callback
from .entity import (
EsphomeEntity,
convert_api_error_ha_error,
platform_async_setup_entry,
)
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 0
class EsphomeInfraredEntity(EsphomeEntity[InfraredInfo, EntityState], InfraredEntity):
"""ESPHome infrared entity using native API."""
@callback
def _on_device_update(self) -> None:
"""Call when device updates or entry data changes."""
super()._on_device_update()
if self._entry_data.available:
# Infrared entities should go available as soon as the device comes online
self.async_write_ha_state()
@convert_api_error_ha_error
async def async_send_command(self, command: InfraredCommand) -> None:
"""Send an IR command."""
timings = [
interval
for timing in command.get_raw_timings()
for interval in (timing.high_us, -timing.low_us)
]
_LOGGER.debug("Sending command: %s", timings)
self._client.infrared_rf_transmit_raw_timings(
self._static_info.key,
carrier_frequency=command.modulation,
timings=timings,
device_id=self._static_info.device_id,
)
async_setup_entry = partial(
platform_async_setup_entry,
info_type=InfraredInfo,
entity_type=EsphomeInfraredEntity,
state_type=EntityState,
info_filter=lambda info: bool(info.capabilities & InfraredCapability.TRANSMITTER),
)

View File

@@ -241,7 +241,7 @@ class EsphomeLight(EsphomeEntity[LightInfo, LightState], LightEntity):
if (color_temp_k := kwargs.get(ATTR_COLOR_TEMP_KELVIN)) is not None:
# Do not use kelvin_to_mired here to prevent precision loss
data["color_temperature"] = 1000000.0 / color_temp_k
data["color_temperature"] = 1_000_000.0 / color_temp_k
if color_temp_modes := _filter_color_modes(
color_modes, LightColorCapability.COLOR_TEMPERATURE
):

View File

@@ -21,5 +21,5 @@
"integration_type": "system",
"preview_features": { "winter_mode": {} },
"quality_scale": "internal",
"requirements": ["home-assistant-frontend==20260226.0"]
"requirements": ["home-assistant-frontend==20260302.0"]
}

View File

@@ -151,8 +151,6 @@ class AFSAPIDevice(MediaPlayerEntity):
# If call to get_volume fails set to 0 and try again next time.
if not self._max_volume:
self._max_volume = int(await afsapi.get_volume_steps() or 1) - 1
if self._max_volume:
self._attr_volume_step = 1 / self._max_volume
if self._attr_state != MediaPlayerState.OFF:
info_name = await afsapi.get_play_name()
@@ -241,6 +239,18 @@ class AFSAPIDevice(MediaPlayerEntity):
await self.fs_device.set_mute(mute)
# volume
async def async_volume_up(self) -> None:
"""Send volume up command."""
volume = await self.fs_device.get_volume()
volume = int(volume or 0) + 1
await self.fs_device.set_volume(min(volume, self._max_volume or 1))
async def async_volume_down(self) -> None:
"""Send volume down command."""
volume = await self.fs_device.get_volume()
volume = int(volume or 0) - 1
await self.fs_device.set_volume(max(volume, 0))
async def async_set_volume_level(self, volume: float) -> None:
"""Set volume command."""
if self._max_volume: # Can't do anything sensible if not set

View File

@@ -14,5 +14,5 @@
"iot_class": "local_polling",
"mqtt": ["fully/deviceInfo/+"],
"quality_scale": "bronze",
"requirements": ["python-fullykiosk==0.0.14"]
"requirements": ["python-fullykiosk==0.0.15"]
}

View File

@@ -78,6 +78,12 @@ query ($owner: String!, $repository: String!) {
number
}
}
merged_pull_request: pullRequests(
first:1
states: MERGED
) {
total: totalCount
}
release: latestRelease {
name
url

View File

@@ -28,6 +28,9 @@
"latest_tag": {
"default": "mdi:tag"
},
"merged_pulls_count": {
"default": "mdi:source-merge"
},
"pulls_count": {
"default": "mdi:source-pull"
},

View File

@@ -7,5 +7,5 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["aiogithubapi"],
"requirements": ["aiogithubapi==24.6.0"]
"requirements": ["aiogithubapi==26.0.0"]
}

View File

@@ -75,6 +75,13 @@ SENSOR_DESCRIPTIONS: tuple[GitHubSensorEntityDescription, ...] = (
state_class=SensorStateClass.MEASUREMENT,
value_fn=lambda data: data["pull_request"]["total"],
),
GitHubSensorEntityDescription(
key="merged_pulls_count",
translation_key="merged_pulls_count",
entity_category=EntityCategory.DIAGNOSTIC,
state_class=SensorStateClass.TOTAL,
value_fn=lambda data: data["merged_pull_request"]["total"],
),
GitHubSensorEntityDescription(
key="latest_commit",
translation_key="latest_commit",

View File

@@ -48,6 +48,10 @@
"latest_tag": {
"name": "Latest tag"
},
"merged_pulls_count": {
"name": "Merged pull requests",
"unit_of_measurement": "pull requests"
},
"pulls_count": {
"name": "Pull requests",
"unit_of_measurement": "pull requests"

View File

@@ -5,5 +5,5 @@
"documentation": "https://www.home-assistant.io/integrations/infrared",
"integration_type": "entity",
"quality_scale": "internal",
"requirements": ["infrared-protocols==1.1.0"]
"requirements": ["infrared-protocols==1.0.0"]
}

View File

@@ -1,20 +0,0 @@
"""LG IR Remote integration for Home Assistant."""
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
PLATFORMS = [Platform.BUTTON, Platform.MEDIA_PLAYER]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up LG IR from a config entry."""
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a LG IR config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -1,193 +0,0 @@
"""Button platform for LG IR integration."""
from __future__ import annotations
from dataclasses import dataclass
from infrared_protocols.codes.lg import LGTVCode, make_tv_command as make_lg_tv_command
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.components.infrared import async_send_command
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from .const import CONF_DEVICE_TYPE, CONF_INFRARED_ENTITY_ID, DOMAIN, LGDeviceType
@dataclass(frozen=True, kw_only=True)
class LgIrButtonEntityDescription(ButtonEntityDescription):
"""Describes LG IR button entity."""
command_code: LGTVCode
TV_BUTTON_DESCRIPTIONS: tuple[LgIrButtonEntityDescription, ...] = (
LgIrButtonEntityDescription(
key="power_on", translation_key="power_on", command_code=LGTVCode.POWER_ON
),
LgIrButtonEntityDescription(
key="power_off", translation_key="power_off", command_code=LGTVCode.POWER_OFF
),
LgIrButtonEntityDescription(
key="hdmi_1", translation_key="hdmi_1", command_code=LGTVCode.HDMI_1
),
LgIrButtonEntityDescription(
key="hdmi_2", translation_key="hdmi_2", command_code=LGTVCode.HDMI_2
),
LgIrButtonEntityDescription(
key="hdmi_3", translation_key="hdmi_3", command_code=LGTVCode.HDMI_3
),
LgIrButtonEntityDescription(
key="hdmi_4", translation_key="hdmi_4", command_code=LGTVCode.HDMI_4
),
LgIrButtonEntityDescription(
key="exit", translation_key="exit", command_code=LGTVCode.EXIT
),
LgIrButtonEntityDescription(
key="info", translation_key="info", command_code=LGTVCode.INFO
),
LgIrButtonEntityDescription(
key="guide", translation_key="guide", command_code=LGTVCode.GUIDE
),
LgIrButtonEntityDescription(
key="up", translation_key="up", command_code=LGTVCode.NAV_UP
),
LgIrButtonEntityDescription(
key="down", translation_key="down", command_code=LGTVCode.NAV_DOWN
),
LgIrButtonEntityDescription(
key="left", translation_key="left", command_code=LGTVCode.NAV_LEFT
),
LgIrButtonEntityDescription(
key="right", translation_key="right", command_code=LGTVCode.NAV_RIGHT
),
LgIrButtonEntityDescription(
key="ok", translation_key="ok", command_code=LGTVCode.OK
),
LgIrButtonEntityDescription(
key="back", translation_key="back", command_code=LGTVCode.BACK
),
LgIrButtonEntityDescription(
key="home", translation_key="home", command_code=LGTVCode.HOME
),
LgIrButtonEntityDescription(
key="menu", translation_key="menu", command_code=LGTVCode.MENU
),
LgIrButtonEntityDescription(
key="input", translation_key="input", command_code=LGTVCode.INPUT
),
LgIrButtonEntityDescription(
key="num_0", translation_key="num_0", command_code=LGTVCode.NUM_0
),
LgIrButtonEntityDescription(
key="num_1", translation_key="num_1", command_code=LGTVCode.NUM_1
),
LgIrButtonEntityDescription(
key="num_2", translation_key="num_2", command_code=LGTVCode.NUM_2
),
LgIrButtonEntityDescription(
key="num_3", translation_key="num_3", command_code=LGTVCode.NUM_3
),
LgIrButtonEntityDescription(
key="num_4", translation_key="num_4", command_code=LGTVCode.NUM_4
),
LgIrButtonEntityDescription(
key="num_5", translation_key="num_5", command_code=LGTVCode.NUM_5
),
LgIrButtonEntityDescription(
key="num_6", translation_key="num_6", command_code=LGTVCode.NUM_6
),
LgIrButtonEntityDescription(
key="num_7", translation_key="num_7", command_code=LGTVCode.NUM_7
),
LgIrButtonEntityDescription(
key="num_8", translation_key="num_8", command_code=LGTVCode.NUM_8
),
LgIrButtonEntityDescription(
key="num_9", translation_key="num_9", command_code=LGTVCode.NUM_9
),
)
HIFI_BUTTON_DESCRIPTIONS: tuple[LgIrButtonEntityDescription, ...] = (
LgIrButtonEntityDescription(
key="power_on",
translation_key="power_on",
command_code=LGTVCode.POWER_ON,
),
)
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LG IR buttons from config entry."""
infrared_entity_id = entry.data[CONF_INFRARED_ENTITY_ID]
device_type = entry.data.get(CONF_DEVICE_TYPE, LGDeviceType.TV)
if device_type == LGDeviceType.TV:
async_add_entities(
LgIrButton(entry, infrared_entity_id, description)
for description in TV_BUTTON_DESCRIPTIONS
)
class LgIrButton(ButtonEntity):
"""LG IR button entity."""
_attr_has_entity_name = True
_description: LgIrButtonEntityDescription
def __init__(
self,
entry: ConfigEntry,
infrared_entity_id: str,
description: LgIrButtonEntityDescription,
) -> None:
"""Initialize LG IR button."""
self._entry = entry
self._infrared_entity_id = infrared_entity_id
self._description = description
self.entity_description = description
self._attr_unique_id = f"{entry.entry_id}_{description.key}"
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)}, name="LG TV", manufacturer="LG"
)
async def async_added_to_hass(self) -> None:
"""Subscribe to infrared entity state changes."""
await super().async_added_to_hass()
@callback
def _async_ir_state_changed(event: Event[EventStateChangedData]) -> None:
"""Handle infrared entity state changes."""
new_state = event.data["new_state"]
self._attr_available = (
new_state is not None and new_state.state != STATE_UNAVAILABLE
)
self.async_write_ha_state()
self.async_on_remove(
async_track_state_change_event(
self.hass, [self._infrared_entity_id], _async_ir_state_changed
)
)
# Set initial availability based on current infrared entity state
ir_state = self.hass.states.get(self._infrared_entity_id)
self._attr_available = (
ir_state is not None and ir_state.state != STATE_UNAVAILABLE
)
async def async_press(self) -> None:
"""Press the button."""
await async_send_command(
self.hass,
self._infrared_entity_id,
make_lg_tv_command(self._description.command_code),
context=self._context,
)

View File

@@ -1,82 +0,0 @@
"""Config flow for LG IR integration."""
from typing import Any
import voluptuous as vol
from homeassistant.components.infrared import (
DOMAIN as INFRARED_DOMAIN,
async_get_emitters,
)
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.helpers.selector import (
EntitySelector,
EntitySelectorConfig,
SelectSelector,
SelectSelectorConfig,
SelectSelectorMode,
)
from .const import CONF_DEVICE_TYPE, CONF_INFRARED_ENTITY_ID, DOMAIN, LGDeviceType
DEVICE_TYPE_NAMES: dict[LGDeviceType, str] = {
LGDeviceType.TV: "TV",
}
class LgIrConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle config flow for LG IR."""
VERSION = 1
async def async_step_user(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle the initial step."""
entities = async_get_emitters(self.hass)
if not entities:
return self.async_abort(reason="no_emitters")
valid_entity_ids = [entity.entity_id for entity in entities]
if user_input is not None:
entity_id = user_input[CONF_INFRARED_ENTITY_ID]
device_type = user_input[CONF_DEVICE_TYPE]
await self.async_set_unique_id(f"lg_ir_{device_type}_{entity_id}")
self._abort_if_unique_id_configured()
# Get entity name for the title
entity_name = next(
(
entity.name or entity.entity_id
for entity in entities
if entity.entity_id == entity_id
),
entity_id,
)
device_type_name = DEVICE_TYPE_NAMES[LGDeviceType(device_type)]
title = f"LG {device_type_name} via {entity_name}"
return self.async_create_entry(title=title, data=user_input)
return self.async_show_form(
step_id="user",
data_schema=vol.Schema(
{
vol.Required(CONF_DEVICE_TYPE): SelectSelector(
SelectSelectorConfig(
options=[device_type.value for device_type in LGDeviceType],
translation_key=CONF_DEVICE_TYPE,
mode=SelectSelectorMode.DROPDOWN,
)
),
vol.Required(CONF_INFRARED_ENTITY_ID): EntitySelector(
EntitySelectorConfig(
domain=INFRARED_DOMAIN,
include_entities=valid_entity_ids,
)
),
}
),
)

View File

@@ -1,13 +0,0 @@
"""Constants for the LG IR integration."""
from enum import StrEnum
DOMAIN = "lg_infrared"
CONF_INFRARED_ENTITY_ID = "infrared_entity_id"
CONF_DEVICE_TYPE = "device_type"
class LGDeviceType(StrEnum):
"""LG device types."""
TV = "tv"

View File

@@ -1,11 +0,0 @@
{
"domain": "lg_infrared",
"name": "LG Infrared",
"codeowners": [],
"config_flow": true,
"dependencies": ["infrared"],
"documentation": "https://www.home-assistant.io/integrations/lg_infrared",
"integration_type": "device",
"iot_class": "local_push",
"quality_scale": "bronze"
}

View File

@@ -1,137 +0,0 @@
"""Media player platform for LG IR integration."""
from __future__ import annotations
from infrared_protocols.codes.lg import LGTVCode, make_tv_command as make_lg_tv_command
from homeassistant.components.infrared import async_send_command
from homeassistant.components.media_player import (
MediaPlayerDeviceClass,
MediaPlayerEntity,
MediaPlayerEntityFeature,
MediaPlayerState,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_UNAVAILABLE
from homeassistant.core import Event, EventStateChangedData, HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_track_state_change_event
from .const import CONF_DEVICE_TYPE, CONF_INFRARED_ENTITY_ID, DOMAIN, LGDeviceType
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up LG IR media player from config entry."""
infrared_entity_id = entry.data[CONF_INFRARED_ENTITY_ID]
device_type = entry.data.get(CONF_DEVICE_TYPE, LGDeviceType.TV)
if device_type == LGDeviceType.TV:
async_add_entities([LgIrTvMediaPlayer(entry, infrared_entity_id)])
class LgIrTvMediaPlayer(MediaPlayerEntity):
"""LG IR media player entity."""
_attr_has_entity_name = True
_attr_name = None
_attr_assumed_state = True
_attr_device_class = MediaPlayerDeviceClass.TV
_attr_supported_features = (
MediaPlayerEntityFeature.TURN_ON
| MediaPlayerEntityFeature.TURN_OFF
| MediaPlayerEntityFeature.VOLUME_STEP
| MediaPlayerEntityFeature.VOLUME_MUTE
| MediaPlayerEntityFeature.NEXT_TRACK
| MediaPlayerEntityFeature.PREVIOUS_TRACK
| MediaPlayerEntityFeature.PLAY
| MediaPlayerEntityFeature.PAUSE
| MediaPlayerEntityFeature.STOP
)
def __init__(self, entry: ConfigEntry, infrared_entity_id: str) -> None:
"""Initialize LG IR media player."""
self._entry = entry
self._infrared_entity_id = infrared_entity_id
self._attr_unique_id = f"{entry.entry_id}_media_player"
self._attr_state = MediaPlayerState.ON
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, entry.entry_id)}, name="LG TV", manufacturer="LG"
)
async def async_added_to_hass(self) -> None:
"""Subscribe to infrared entity state changes."""
await super().async_added_to_hass()
@callback
def _async_ir_state_changed(event: Event[EventStateChangedData]) -> None:
"""Handle infrared entity state changes."""
new_state = event.data["new_state"]
self._attr_available = (
new_state is not None and new_state.state != STATE_UNAVAILABLE
)
self.async_write_ha_state()
self.async_on_remove(
async_track_state_change_event(
self.hass, [self._infrared_entity_id], _async_ir_state_changed
)
)
# Set initial availability based on current infrared entity state
ir_state = self.hass.states.get(self._infrared_entity_id)
self._attr_available = (
ir_state is not None and ir_state.state != STATE_UNAVAILABLE
)
async def _send_command(self, code: LGTVCode) -> None:
"""Send an IR command using the LG protocol."""
await async_send_command(
self.hass,
self._infrared_entity_id,
make_lg_tv_command(code),
context=self._context,
)
async def async_turn_on(self) -> None:
"""Turn on the TV."""
await self._send_command(LGTVCode.POWER)
async def async_turn_off(self) -> None:
"""Turn off the TV."""
await self._send_command(LGTVCode.POWER)
async def async_volume_up(self) -> None:
"""Send volume up command."""
await self._send_command(LGTVCode.VOLUME_UP)
async def async_volume_down(self) -> None:
"""Send volume down command."""
await self._send_command(LGTVCode.VOLUME_DOWN)
async def async_mute_volume(self, mute: bool) -> None:
"""Send mute command."""
await self._send_command(LGTVCode.MUTE)
async def async_media_next_track(self) -> None:
"""Send channel up command."""
await self._send_command(LGTVCode.CHANNEL_UP)
async def async_media_previous_track(self) -> None:
"""Send channel down command."""
await self._send_command(LGTVCode.CHANNEL_DOWN)
async def async_media_play(self) -> None:
"""Send play command."""
await self._send_command(LGTVCode.PLAY)
async def async_media_pause(self) -> None:
"""Send pause command."""
await self._send_command(LGTVCode.PAUSE)
async def async_media_stop(self) -> None:
"""Send stop command."""
await self._send_command(LGTVCode.STOP)

View File

@@ -1,127 +0,0 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
appropriate-polling:
status: exempt
comment: |
This integration does not poll.
brands:
status: exempt
comment: |
This is a proof of concept integration, brand assets will be added later.
common-modules:
status: exempt
comment: |
This integration is simple and does not share patterns with others.
config-flow-test-coverage:
status: exempt
comment: |
This is a proof of concept integration, config flow tests will be added later.
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
This integration does not provide additional actions.
docs-high-level-description:
status: exempt
comment: |
This is a proof of concept integration, documentation will be added later.
docs-installation-instructions:
status: exempt
comment: |
This is a proof of concept integration, documentation will be added later.
docs-removal-instructions:
status: exempt
comment: |
This is a proof of concept integration, documentation will be added later.
entity-event-setup:
status: exempt
comment: |
This integration does not subscribe to events.
entity-unique-id: done
has-entity-name: done
runtime-data:
status: exempt
comment: |
This integration does not store runtime data.
test-before-configure: done
test-before-setup:
status: exempt
comment: |
Setup validation is handled by checking emitter existence in remote.py.
unique-config-entry: done
# Silver
action-exceptions: todo
config-entry-unloading: done
docs-configuration-parameters: todo
docs-installation-parameters: todo
entity-unavailable: done
integration-owner: todo
log-when-unavailable: todo
parallel-updates: todo
reauthentication-flow:
status: exempt
comment: |
This integration does not require authentication.
test-coverage: todo
# Gold
devices: done
diagnostics: todo
discovery-update-info:
status: exempt
comment: |
This integration does not support discovery.
discovery:
status: exempt
comment: |
This integration is configured manually via config flow.
docs-data-update: todo
docs-examples: todo
docs-known-limitations: todo
docs-supported-devices: todo
docs-supported-functions: todo
docs-troubleshooting: todo
docs-use-cases: todo
dynamic-devices:
status: exempt
comment: |
Each config entry creates a single device.
entity-category:
status: exempt
comment: |
The remote entity is the primary entity and does not need a category.
entity-device-class:
status: exempt
comment: |
Remote entities do not have a device class.
entity-disabled-by-default:
status: exempt
comment: |
The remote entity is the primary entity and should be enabled by default.
entity-translations: todo
exception-translations: todo
icon-translations: todo
reconfiguration-flow: todo
repair-issues:
status: exempt
comment: |
This integration does not have repairable issues.
stale-devices:
status: exempt
comment: |
Each config entry manages exactly one device.
# Platinum
async-dependency:
status: exempt
comment: |
This integration only depends on ir_proxy which is part of Home Assistant.
inject-websession:
status: exempt
comment: |
This integration does not make HTTP requests.
strict-typing: todo

View File

@@ -1,114 +0,0 @@
{
"config": {
"abort": {
"already_configured": "This LG device has already been configured with this transmitter.",
"no_emitters": "No infrared transmitter entities found. Please set up an infrared device first."
},
"step": {
"user": {
"data": {
"device_type": "Device type",
"infrared_entity_id": "Infrared transmitter"
},
"description": "Select the device type and the infrared transmitter entity to use for controlling your LG device.",
"title": "Set up LG IR Remote"
}
}
},
"entity": {
"button": {
"back": {
"name": "Back"
},
"down": {
"name": "Down"
},
"exit": {
"name": "Exit"
},
"guide": {
"name": "Guide"
},
"hdmi_1": {
"name": "HDMI 1"
},
"hdmi_2": {
"name": "HDMI 2"
},
"hdmi_3": {
"name": "HDMI 3"
},
"hdmi_4": {
"name": "HDMI 4"
},
"home": {
"name": "Home"
},
"info": {
"name": "Info"
},
"input": {
"name": "Input"
},
"left": {
"name": "Left"
},
"menu": {
"name": "Menu"
},
"num_0": {
"name": "0"
},
"num_1": {
"name": "1"
},
"num_2": {
"name": "2"
},
"num_3": {
"name": "3"
},
"num_4": {
"name": "4"
},
"num_5": {
"name": "5"
},
"num_6": {
"name": "6"
},
"num_7": {
"name": "7"
},
"num_8": {
"name": "8"
},
"num_9": {
"name": "9"
},
"ok": {
"name": "OK"
},
"power_off": {
"name": "Power off"
},
"power_on": {
"name": "Power on"
},
"right": {
"name": "Right"
},
"up": {
"name": "Up"
}
}
},
"selector": {
"device_type": {
"options": {
"hifi": "Hi-Fi",
"tv": "TV"
}
}
}
}

View File

@@ -9,11 +9,11 @@ from __future__ import annotations
from typing import TYPE_CHECKING, Any, TypedDict
from chip.clusters import Objects as clusters
from chip.clusters.Types import NullValue
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from .const import (
CLEAR_ALL_INDEX,
CRED_TYPE_FACE,
CRED_TYPE_FINGER_VEIN,
CRED_TYPE_FINGERPRINT,
@@ -156,11 +156,17 @@ def _get_attr(obj: Any, attr: str) -> Any:
"""Get attribute from object or dict.
Matter SDK responses can be either dataclass objects or dicts depending on
the SDK version and serialization context.
the SDK version and serialization context. NullValue (a truthy,
non-iterable singleton) is normalized to None.
"""
if isinstance(obj, dict):
return obj.get(attr)
return getattr(obj, attr, None)
value = obj.get(attr)
else:
value = getattr(obj, attr, None)
# The Matter SDK uses NullValue for nullable fields instead of None.
if value is NullValue:
return None
return value
def _get_supported_credential_types(feature_map: int) -> list[str]:
@@ -215,42 +221,6 @@ def _format_user_response(user_data: Any) -> LockUserData | None:
# --- Credential management helpers ---
async def _clear_user_credentials(
matter_client: MatterClient,
node_id: int,
endpoint_id: int,
user_index: int,
) -> None:
"""Clear all credentials for a specific user.
Fetches the user to get credential list, then clears each credential.
"""
get_user_response = await matter_client.send_device_command(
node_id=node_id,
endpoint_id=endpoint_id,
command=clusters.DoorLock.Commands.GetUser(userIndex=user_index),
)
creds = _get_attr(get_user_response, "credentials")
if not creds:
return
for cred in creds:
cred_type = _get_attr(cred, "credentialType")
cred_index = _get_attr(cred, "credentialIndex")
await matter_client.send_device_command(
node_id=node_id,
endpoint_id=endpoint_id,
command=clusters.DoorLock.Commands.ClearCredential(
credential=clusters.DoorLock.Structs.CredentialStruct(
credentialType=cred_type,
credentialIndex=cred_index,
),
),
timed_request_timeout_ms=LOCK_TIMED_REQUEST_TIMEOUT_MS,
)
class LockEndpointNotFoundError(HomeAssistantError):
"""Lock endpoint not found on node."""
@@ -550,33 +520,16 @@ async def clear_lock_user(
node: MatterNode,
user_index: int,
) -> None:
"""Clear a user from the lock, cleaning up credentials first.
"""Clear a user from the lock.
Per the Matter spec, ClearUser also clears all associated credentials
and schedules for the user.
Use index 0xFFFE (CLEAR_ALL_INDEX) to clear all users.
Raises HomeAssistantError on failure.
"""
lock_endpoint = _get_lock_endpoint_or_raise(node)
_ensure_usr_support(lock_endpoint)
if user_index == CLEAR_ALL_INDEX:
# Clear all: clear all credentials first, then all users
await matter_client.send_device_command(
node_id=node.node_id,
endpoint_id=lock_endpoint.endpoint_id,
command=clusters.DoorLock.Commands.ClearCredential(
credential=None,
),
timed_request_timeout_ms=LOCK_TIMED_REQUEST_TIMEOUT_MS,
)
else:
# Clear credentials for this specific user before deleting them
await _clear_user_credentials(
matter_client,
node.node_id,
lock_endpoint.endpoint_id,
user_index,
)
await matter_client.send_device_command(
node_id=node.node_id,
endpoint_id=lock_endpoint.endpoint_id,
@@ -598,6 +551,13 @@ _CREDENTIAL_TYPE_FEATURE_MAP: dict[str, int] = {
CRED_TYPE_FACE: DoorLockFeature.kFaceCredentials,
}
# Map credential type strings to the capacity attribute for slot iteration.
# Biometric types have no dedicated capacity attribute; fall back to total users.
_CREDENTIAL_TYPE_CAPACITY_ATTR = {
CRED_TYPE_PIN: clusters.DoorLock.Attributes.NumberOfPINUsersSupported,
CRED_TYPE_RFID: clusters.DoorLock.Attributes.NumberOfRFIDUsersSupported,
}
def _validate_credential_type_support(
lock_endpoint: MatterEndpoint, credential_type: str
@@ -736,13 +696,15 @@ async def set_lock_credential(
operation_type = clusters.DoorLock.Enums.DataOperationTypeEnum.kAdd
if credential_index is None:
# Auto-find first available credential slot
# Auto-find first available credential slot.
# Use the credential-type-specific capacity as the upper bound.
max_creds_attr = _CREDENTIAL_TYPE_CAPACITY_ATTR.get(
credential_type,
clusters.DoorLock.Attributes.NumberOfTotalUsersSupported,
)
max_creds_raw = lock_endpoint.get_attribute_value(None, max_creds_attr)
max_creds = (
lock_endpoint.get_attribute_value(
None,
clusters.DoorLock.Attributes.NumberOfCredentialsSupportedPerUser,
)
or 5
max_creds_raw if isinstance(max_creds_raw, int) and max_creds_raw > 0 else 5
)
for idx in range(1, max_creds + 1):
status_response = await matter_client.send_device_command(

View File

@@ -642,7 +642,7 @@
},
"services": {
"clear_lock_credential": {
"description": "Removes a credential from the lock.",
"description": "Removes a credential from a lock.",
"fields": {
"credential_index": {
"description": "The credential slot index to clear.",
@@ -666,7 +666,7 @@
"name": "Clear lock user"
},
"get_lock_credential_status": {
"description": "Returns the status of a credential slot on the lock.",
"description": "Returns the status of a credential slot on a lock.",
"fields": {
"credential_index": {
"description": "The credential slot index to query.",
@@ -684,7 +684,7 @@
"name": "Get lock info"
},
"get_lock_users": {
"description": "Returns all users configured on the lock with their credentials.",
"description": "Returns all users configured on a lock with their credentials.",
"name": "Get lock users"
},
"open_commissioning_window": {
@@ -698,7 +698,7 @@
"name": "Open commissioning window"
},
"set_lock_credential": {
"description": "Adds or updates a credential on the lock.",
"description": "Adds or updates a credential on a lock.",
"fields": {
"credential_data": {
"description": "The credential data. For PIN: digits only. For RFID: hexadecimal string.",

View File

@@ -3,19 +3,17 @@
from __future__ import annotations
from homeassistant.components import bluetooth
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DOMAIN
from .coordinator import MedcomBleUpdateCoordinator
from .coordinator import MedcomBleConfigEntry, MedcomBleUpdateCoordinator
# Supported platforms
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: MedcomBleConfigEntry) -> bool:
"""Set up Medcom BLE radiation monitor from a config entry."""
address = entry.unique_id
@@ -31,16 +29,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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: MedcomBleConfigEntry) -> 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

@@ -18,13 +18,17 @@ from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
_LOGGER = logging.getLogger(__name__)
type MedcomBleConfigEntry = ConfigEntry[MedcomBleUpdateCoordinator]
class MedcomBleUpdateCoordinator(DataUpdateCoordinator[MedcomBleDevice]):
"""Coordinator for Medcom BLE radiation monitor data."""
config_entry: ConfigEntry
config_entry: MedcomBleConfigEntry
def __init__(self, hass: HomeAssistant, entry: ConfigEntry, address: str) -> None:
def __init__(
self, hass: HomeAssistant, entry: MedcomBleConfigEntry, address: str
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import logging
from homeassistant import config_entries
from homeassistant.components.sensor import (
SensorEntity,
SensorEntityDescription,
@@ -15,8 +14,8 @@ from homeassistant.helpers.device_registry import CONNECTION_BLUETOOTH, DeviceIn
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, UNIT_CPM
from .coordinator import MedcomBleUpdateCoordinator
from .const import UNIT_CPM
from .coordinator import MedcomBleConfigEntry, MedcomBleUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
@@ -32,12 +31,12 @@ SENSORS_MAPPING_TEMPLATE: dict[str, SensorEntityDescription] = {
async def async_setup_entry(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
entry: MedcomBleConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Medcom BLE radiation monitor sensors."""
coordinator: MedcomBleUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
entities = []
_LOGGER.debug("got sensors: %s", coordinator.data.sensors)

View File

@@ -1,25 +1,27 @@
"""Support for Meteoclimatic weather data."""
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN, PLATFORMS
from .coordinator import MeteoclimaticUpdateCoordinator
from .const import PLATFORMS
from .coordinator import MeteoclimaticConfigEntry, MeteoclimaticUpdateCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, entry: MeteoclimaticConfigEntry
) -> bool:
"""Set up a Meteoclimatic entry."""
coordinator = MeteoclimaticUpdateCoordinator(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)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: MeteoclimaticConfigEntry
) -> bool:
"""Unload a config entry."""
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -14,13 +14,15 @@ from .const import CONF_STATION_CODE, SCAN_INTERVAL
_LOGGER = logging.getLogger(__name__)
type MeteoclimaticConfigEntry = ConfigEntry[MeteoclimaticUpdateCoordinator]
class MeteoclimaticUpdateCoordinator(DataUpdateCoordinator[dict[str, Any]]):
"""Coordinator for Meteoclimatic weather data."""
config_entry: ConfigEntry
config_entry: MeteoclimaticConfigEntry
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
def __init__(self, hass: HomeAssistant, entry: MeteoclimaticConfigEntry) -> None:
"""Initialize the coordinator."""
self._station_code = entry.data[CONF_STATION_CODE]
super().__init__(

View File

@@ -6,7 +6,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
DEGREE,
PERCENTAGE,
@@ -21,7 +20,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTRIBUTION, DOMAIN, MANUFACTURER, MODEL
from .coordinator import MeteoclimaticUpdateCoordinator
from .coordinator import MeteoclimaticConfigEntry, MeteoclimaticUpdateCoordinator
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
@@ -113,11 +112,11 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MeteoclimaticConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Meteoclimatic sensor platform."""
coordinator: MeteoclimaticUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
[MeteoclimaticSensor(coordinator, description) for description in SENSOR_TYPES],

View File

@@ -5,7 +5,6 @@ from typing import TYPE_CHECKING
from meteoclimatic import Condition
from homeassistant.components.weather import WeatherEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import UnitOfPressure, UnitOfSpeed, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
@@ -13,7 +12,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTRIBUTION, CONDITION_MAP, DOMAIN, MANUFACTURER, MODEL
from .coordinator import MeteoclimaticUpdateCoordinator
from .coordinator import MeteoclimaticConfigEntry, MeteoclimaticUpdateCoordinator
def format_condition(condition):
@@ -27,11 +26,11 @@ def format_condition(condition):
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MeteoclimaticConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Meteoclimatic weather platform."""
coordinator: MeteoclimaticUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities([MeteoclimaticWeather(coordinator)], False)

View File

@@ -3,9 +3,7 @@
from __future__ import annotations
import asyncio
import logging
from datapoint.Forecast import Forecast
from datapoint.Manager import Manager
from homeassistant.config_entries import ConfigEntry
@@ -19,10 +17,8 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
from .const import (
DEFAULT_SCAN_INTERVAL,
DOMAIN,
METOFFICE_COORDINATES,
METOFFICE_DAILY_COORDINATOR,
@@ -30,9 +26,7 @@ from .const import (
METOFFICE_NAME,
METOFFICE_TWICE_DAILY_COORDINATOR,
)
from .helpers import fetch_data
_LOGGER = logging.getLogger(__name__)
from .coordinator import MetOfficeUpdateCoordinator
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
@@ -40,55 +34,43 @@ PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a Met Office entry."""
latitude = entry.data[CONF_LATITUDE]
longitude = entry.data[CONF_LONGITUDE]
api_key = entry.data[CONF_API_KEY]
site_name = entry.data[CONF_NAME]
latitude: float = entry.data[CONF_LATITUDE]
longitude: float = entry.data[CONF_LONGITUDE]
api_key: str = entry.data[CONF_API_KEY]
site_name: str = entry.data[CONF_NAME]
coordinates = f"{latitude}_{longitude}"
connection = Manager(api_key=api_key)
async def async_update_hourly() -> Forecast:
return await hass.async_add_executor_job(
fetch_data, connection, latitude, longitude, "hourly"
)
async def async_update_daily() -> Forecast:
return await hass.async_add_executor_job(
fetch_data, connection, latitude, longitude, "daily"
)
async def async_update_twice_daily() -> Forecast:
return await hass.async_add_executor_job(
fetch_data, connection, latitude, longitude, "twice-daily"
)
metoffice_hourly_coordinator = TimestampDataUpdateCoordinator(
metoffice_hourly_coordinator = MetOfficeUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
entry,
name=f"MetOffice Hourly Coordinator for {site_name}",
update_method=async_update_hourly,
update_interval=DEFAULT_SCAN_INTERVAL,
connection=connection,
latitude=latitude,
longitude=longitude,
frequency="hourly",
)
metoffice_daily_coordinator = TimestampDataUpdateCoordinator(
metoffice_daily_coordinator = MetOfficeUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
entry,
name=f"MetOffice Daily Coordinator for {site_name}",
update_method=async_update_daily,
update_interval=DEFAULT_SCAN_INTERVAL,
connection=connection,
latitude=latitude,
longitude=longitude,
frequency="daily",
)
metoffice_twice_daily_coordinator = TimestampDataUpdateCoordinator(
metoffice_twice_daily_coordinator = MetOfficeUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
entry,
name=f"MetOffice Twice Daily Coordinator for {site_name}",
update_method=async_update_twice_daily,
update_interval=DEFAULT_SCAN_INTERVAL,
connection=connection,
latitude=latitude,
longitude=longitude,
frequency="twice-daily",
)
metoffice_hass_data = hass.data.setdefault(DOMAIN, {})

View File

@@ -0,0 +1,82 @@
"""Data update coordinator for the Met Office integration."""
from __future__ import annotations
import logging
from typing import Literal
from datapoint.exceptions import APIException
from datapoint.Forecast import Forecast
from datapoint.Manager import Manager
from requests import HTTPError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import (
TimestampDataUpdateCoordinator,
UpdateFailed,
)
from .const import DEFAULT_SCAN_INTERVAL
_LOGGER = logging.getLogger(__name__)
class MetOfficeUpdateCoordinator(TimestampDataUpdateCoordinator[Forecast]):
"""Coordinator for Met Office forecast data."""
config_entry: ConfigEntry
def __init__(
self,
hass: HomeAssistant,
entry: ConfigEntry,
name: str,
connection: Manager,
latitude: float,
longitude: float,
frequency: Literal["daily", "twice-daily", "hourly"],
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=name,
config_entry=entry,
update_interval=DEFAULT_SCAN_INTERVAL,
)
self._connection = connection
self._latitude = latitude
self._longitude = longitude
self._frequency = frequency
async def _async_update_data(self) -> Forecast:
"""Get data from Met Office."""
return await self.hass.async_add_executor_job(
fetch_data,
self._connection,
self._latitude,
self._longitude,
self._frequency,
)
def fetch_data(
connection: Manager,
latitude: float,
longitude: float,
frequency: Literal["daily", "twice-daily", "hourly"],
) -> Forecast:
"""Fetch weather and forecast from Datapoint API."""
try:
return connection.get_forecast(
latitude, longitude, frequency, convert_weather_code=False
)
except (ValueError, APIException) as err:
_LOGGER.error("Check Met Office connection: %s", err.args)
raise UpdateFailed from err
except HTTPError as err:
if err.response.status_code == 401:
raise ConfigEntryAuthFailed from err
raise

View File

@@ -2,38 +2,7 @@
from __future__ import annotations
import logging
from typing import Any, Literal
from datapoint.exceptions import APIException
from datapoint.Forecast import Forecast
from datapoint.Manager import Manager
from requests import HTTPError
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import UpdateFailed
_LOGGER = logging.getLogger(__name__)
def fetch_data(
connection: Manager,
latitude: float,
longitude: float,
frequency: Literal["daily", "twice-daily", "hourly"],
) -> Forecast:
"""Fetch weather and forecast from Datapoint API."""
try:
return connection.get_forecast(
latitude, longitude, frequency, convert_weather_code=False
)
except (ValueError, APIException) as err:
_LOGGER.error("Check Met Office connection: %s", err.args)
raise UpdateFailed from err
except HTTPError as err:
if err.response.status_code == 401:
raise ConfigEntryAuthFailed from err
raise
from typing import Any
def get_attribute(data: dict[str, Any] | None, attr_name: str) -> Any | None:

View File

@@ -5,8 +5,6 @@ from __future__ import annotations
from dataclasses import dataclass
from typing import Any
from datapoint.Forecast import Forecast
from homeassistant.components.sensor import (
DOMAIN as SENSOR_DOMAIN,
EntityCategory,
@@ -29,10 +27,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import get_device_info
from .const import (
@@ -43,6 +38,7 @@ from .const import (
METOFFICE_HOURLY_COORDINATOR,
METOFFICE_NAME,
)
from .coordinator import MetOfficeUpdateCoordinator
from .helpers import get_attribute
ATTR_LAST_UPDATE = "last_update"
@@ -220,7 +216,7 @@ async def async_setup_entry(
class MetOfficeCurrentSensor(
CoordinatorEntity[DataUpdateCoordinator[Forecast]], SensorEntity
CoordinatorEntity[MetOfficeUpdateCoordinator], SensorEntity
):
"""Implementation of a Met Office current weather condition sensor."""
@@ -231,7 +227,7 @@ class MetOfficeCurrentSensor(
def __init__(
self,
coordinator: DataUpdateCoordinator[Forecast],
coordinator: MetOfficeUpdateCoordinator,
hass_data: dict[str, Any],
description: MetOfficeSensorEntityDescription,
) -> None:

View File

@@ -5,8 +5,6 @@ from __future__ import annotations
from datetime import datetime
from typing import Any, cast
from datapoint.Forecast import Forecast
from homeassistant.components.weather import (
ATTR_FORECAST_CONDITION,
ATTR_FORECAST_IS_DAYTIME,
@@ -35,7 +33,6 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
from . import get_device_info
from .const import (
@@ -52,6 +49,7 @@ from .const import (
METOFFICE_TWICE_DAILY_COORDINATOR,
NIGHT_FORECAST_ATTRIBUTE_MAP,
)
from .coordinator import MetOfficeUpdateCoordinator
from .helpers import get_attribute
@@ -153,9 +151,9 @@ def _populate_forecast_data(
class MetOfficeWeather(
CoordinatorWeatherEntity[
TimestampDataUpdateCoordinator[Forecast],
TimestampDataUpdateCoordinator[Forecast],
TimestampDataUpdateCoordinator[Forecast],
MetOfficeUpdateCoordinator,
MetOfficeUpdateCoordinator,
MetOfficeUpdateCoordinator,
]
):
"""Implementation of a Met Office weather condition."""
@@ -177,9 +175,9 @@ class MetOfficeWeather(
def __init__(
self,
coordinator_daily: TimestampDataUpdateCoordinator[Forecast],
coordinator_hourly: TimestampDataUpdateCoordinator[Forecast],
coordinator_twice_daily: TimestampDataUpdateCoordinator[Forecast],
coordinator_daily: MetOfficeUpdateCoordinator,
coordinator_hourly: MetOfficeUpdateCoordinator,
coordinator_twice_daily: MetOfficeUpdateCoordinator,
hass_data: dict[str, Any],
) -> None:
"""Initialise the platform with a data instance."""
@@ -266,7 +264,7 @@ class MetOfficeWeather(
def _async_forecast_daily(self) -> list[WeatherForecast] | None:
"""Return the daily forecast in native units."""
coordinator = cast(
TimestampDataUpdateCoordinator[Forecast],
MetOfficeUpdateCoordinator,
self.forecast_coordinators["daily"],
)
timesteps = coordinator.data.timesteps
@@ -283,7 +281,7 @@ class MetOfficeWeather(
def _async_forecast_hourly(self) -> list[WeatherForecast] | None:
"""Return the hourly forecast in native units."""
coordinator = cast(
TimestampDataUpdateCoordinator[Forecast],
MetOfficeUpdateCoordinator,
self.forecast_coordinators["hourly"],
)
@@ -301,7 +299,7 @@ class MetOfficeWeather(
def _async_forecast_twice_daily(self) -> list[WeatherForecast] | None:
"""Return the twice daily forecast in native units."""
coordinator = cast(
TimestampDataUpdateCoordinator[Forecast],
MetOfficeUpdateCoordinator,
self.forecast_coordinators["twice_daily"],
)
timesteps = coordinator.data.timesteps

View File

@@ -13,22 +13,25 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import config_entry_oauth2_flow
from .const import DOMAIN, PLATFORMS
from .const import PLATFORMS
from .coordinator import MicroBeesUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
type MicroBeesConfigEntry = ConfigEntry[HomeAssistantMicroBeesData]
@dataclass(frozen=True, kw_only=True)
class HomeAssistantMicroBeesData:
"""Microbees data stored in the Home Assistant data object."""
"""Microbees data stored in the config entry runtime_data."""
connector: MicroBees
coordinator: MicroBeesUpdateCoordinator
session: config_entry_oauth2_flow.OAuth2Session
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_migrate_entry(hass: HomeAssistant, entry: MicroBeesConfigEntry) -> bool:
"""Migrate entry."""
_LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
@@ -45,7 +48,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: MicroBeesConfigEntry) -> bool:
"""Set up microBees from a config entry."""
implementation = (
await config_entry_oauth2_flow.async_get_config_entry_implementation(
@@ -67,7 +70,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
microbees = MicroBees(token=session.token[CONF_ACCESS_TOKEN])
coordinator = MicroBeesUpdateCoordinator(hass, entry, microbees)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = HomeAssistantMicroBeesData(
entry.runtime_data = HomeAssistantMicroBeesData(
connector=microbees,
coordinator=coordinator,
session=session,
@@ -76,9 +79,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: MicroBeesConfigEntry) -> 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

@@ -7,11 +7,10 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from . import MicroBeesConfigEntry
from .coordinator import MicroBeesUpdateCoordinator
from .entity import MicroBeesEntity
@@ -37,13 +36,11 @@ BINARYSENSOR_TYPES = {
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MicroBeesConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the microBees binary sensor platform."""
coordinator: MicroBeesUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
].coordinator
coordinator = entry.runtime_data.coordinator
async_add_entities(
MBBinarySensor(coordinator, entity_description, bee_id, binary_sensor.id)
for bee_id, bee in coordinator.data.bees.items()

View File

@@ -3,11 +3,10 @@
from typing import Any
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from . import MicroBeesConfigEntry
from .coordinator import MicroBeesUpdateCoordinator
from .entity import MicroBeesActuatorEntity
@@ -16,13 +15,11 @@ BUTTON_TRANSLATIONS = {51: "button_gate", 91: "button_panic"}
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MicroBeesConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the microBees button platform."""
coordinator: MicroBeesUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
].coordinator
coordinator = entry.runtime_data.coordinator
async_add_entities(
MBButton(coordinator, bee_id, button.id)
for bee_id, bee in coordinator.data.bees.items()

View File

@@ -7,13 +7,12 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from . import MicroBeesConfigEntry
from .coordinator import MicroBeesUpdateCoordinator
from .entity import MicroBeesActuatorEntity
@@ -27,13 +26,11 @@ THERMOVALVE_SENSOR_ID = 782
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MicroBeesConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the microBees climate platform."""
coordinator: MicroBeesUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
].coordinator
coordinator = entry.runtime_data.coordinator
async_add_entities(
MBClimate(
coordinator,

View File

@@ -1,19 +1,24 @@
"""The microBees Coordinator."""
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from datetime import timedelta
from http import HTTPStatus
import logging
from typing import TYPE_CHECKING
import aiohttp
from microBeesPy import Actuator, Bee, MicroBees, MicroBeesException, Sensor
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
if TYPE_CHECKING:
from . import MicroBeesConfigEntry
_LOGGER = logging.getLogger(__name__)
@@ -29,10 +34,13 @@ class MicroBeesCoordinatorData:
class MicroBeesUpdateCoordinator(DataUpdateCoordinator[MicroBeesCoordinatorData]):
"""MicroBees coordinator."""
config_entry: ConfigEntry
config_entry: MicroBeesConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, microbees: MicroBees
self,
hass: HomeAssistant,
config_entry: MicroBeesConfigEntry,
microbees: MicroBees,
) -> None:
"""Initialize microBees coordinator."""
super().__init__(

View File

@@ -9,14 +9,12 @@ from homeassistant.components.cover import (
CoverEntity,
CoverEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.event import async_call_later
from .const import DOMAIN
from .coordinator import MicroBeesUpdateCoordinator
from . import MicroBeesConfigEntry
from .entity import MicroBeesEntity
COVER_IDS = {47: "roller_shutter"}
@@ -24,13 +22,11 @@ COVER_IDS = {47: "roller_shutter"}
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MicroBeesConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the microBees cover platform."""
coordinator: MicroBeesUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
].coordinator
coordinator = entry.runtime_data.coordinator
async_add_entities(
MBCover(

View File

@@ -3,25 +3,22 @@
from typing import Any
from homeassistant.components.light import ATTR_RGBW_COLOR, ColorMode, LightEntity
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 . import MicroBeesConfigEntry
from .coordinator import MicroBeesUpdateCoordinator
from .entity import MicroBeesActuatorEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MicroBeesConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Config entry."""
coordinator: MicroBeesUpdateCoordinator = hass.data[DOMAIN][
entry.entry_id
].coordinator
coordinator = entry.runtime_data.coordinator
async_add_entities(
MBLight(coordinator, bee_id, light.id)
for bee_id, bee in coordinator.data.bees.items()

View File

@@ -8,7 +8,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_PARTS_PER_MILLION,
LIGHT_LUX,
@@ -19,7 +18,7 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DOMAIN
from . import MicroBeesConfigEntry
from .coordinator import MicroBeesUpdateCoordinator
from .entity import MicroBeesEntity
@@ -64,11 +63,11 @@ SENSOR_TYPES = {
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MicroBeesConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id].coordinator
coordinator = entry.runtime_data.coordinator
async_add_entities(
MBSensor(coordinator, desc, bee_id, sensor.id)

View File

@@ -3,12 +3,11 @@
from typing import Any
from homeassistant.components.switch import SwitchEntity
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 . import MicroBeesConfigEntry
from .coordinator import MicroBeesUpdateCoordinator
from .entity import MicroBeesActuatorEntity
@@ -18,11 +17,11 @@ SWITCH_PRODUCT_IDS = {25, 26, 27, 35, 38, 46, 63, 64, 65, 86}
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: MicroBeesConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id].coordinator
coordinator = entry.runtime_data.coordinator
async_add_entities(
MBSwitch(coordinator, bee_id, switch.id)

View File

@@ -14,27 +14,26 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN
PLATFORMS: list[Platform] = [Platform.SENSOR]
_LOGGER = logging.getLogger(__name__)
type MoatConfigEntry = ConfigEntry[PassiveBluetoothProcessorCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: MoatConfigEntry) -> bool:
"""Set up Moat BLE device from a config entry."""
address = entry.unique_id
assert address is not None
data = MoatBluetoothDeviceData()
coordinator = hass.data.setdefault(DOMAIN, {})[entry.entry_id] = (
PassiveBluetoothProcessorCoordinator(
hass,
_LOGGER,
address=address,
mode=BluetoothScanningMode.PASSIVE,
update_method=data.update,
)
coordinator = PassiveBluetoothProcessorCoordinator(
hass,
_LOGGER,
address=address,
mode=BluetoothScanningMode.PASSIVE,
update_method=data.update,
)
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(
coordinator.async_start()
@@ -42,9 +41,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: MoatConfigEntry) -> 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

@@ -4,12 +4,10 @@ from __future__ import annotations
from moat_ble import DeviceClass, DeviceKey, SensorUpdate, Units
from homeassistant import config_entries
from homeassistant.components.bluetooth.passive_update_processor import (
PassiveBluetoothDataProcessor,
PassiveBluetoothDataUpdate,
PassiveBluetoothEntityKey,
PassiveBluetoothProcessorCoordinator,
PassiveBluetoothProcessorEntity,
)
from homeassistant.components.sensor import (
@@ -28,7 +26,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.sensor import sensor_device_info_to_hass_device_info
from .const import DOMAIN
from . import MoatConfigEntry
SENSOR_DESCRIPTIONS = {
(DeviceClass.TEMPERATURE, Units.TEMP_CELSIUS): SensorEntityDescription(
@@ -104,13 +102,11 @@ def sensor_update_to_bluetooth_data_update(
async def async_setup_entry(
hass: HomeAssistant,
entry: config_entries.ConfigEntry,
entry: MoatConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Moat BLE sensors."""
coordinator: PassiveBluetoothProcessorCoordinator = hass.data[DOMAIN][
entry.entry_id
]
coordinator = entry.runtime_data
processor = PassiveBluetoothDataProcessor(sensor_update_to_bluetooth_data_update)
entry.async_on_unload(
processor.async_add_entities_listener(

View File

@@ -3,16 +3,20 @@
from phone_modem import PhoneModem
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.const import CONF_DEVICE, EVENT_HOMEASSISTANT_STOP, Platform
from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from .const import DATA_KEY_API, DOMAIN, EXCEPTIONS
from .const import EXCEPTIONS
PLATFORMS = [Platform.BUTTON, Platform.SENSOR]
type ModemCallerIdConfigEntry = ConfigEntry[PhoneModem]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, entry: ModemCallerIdConfigEntry
) -> bool:
"""Set up Modem Caller ID from a config entry."""
device = entry.data[CONF_DEVICE]
api = PhoneModem(device)
@@ -21,17 +25,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except EXCEPTIONS as ex:
raise ConfigEntryNotReady(f"Unable to open port: {device}") from ex
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = {DATA_KEY_API: api}
entry.async_on_unload(api.close)
async def _async_on_hass_stop(event: Event) -> None:
"""HA is shutting down, close modem port."""
api.close()
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_on_hass_stop)
)
entry.runtime_data = api
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: ModemCallerIdConfigEntry
) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
api = hass.data[DOMAIN].pop(entry.entry_id)[DATA_KEY_API]
await api.close()
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@@ -5,26 +5,25 @@ from __future__ import annotations
from phone_modem import PhoneModem
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_DEVICE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import DATA_KEY_API, DOMAIN
from . import ModemCallerIdConfigEntry
from .const import DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: ModemCallerIdConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Modem Caller ID sensor."""
api = hass.data[DOMAIN][entry.entry_id][DATA_KEY_API]
async_add_entities(
[
PhoneModemButton(
api,
entry.runtime_data,
entry.data[CONF_DEVICE],
entry.entry_id,
)

View File

@@ -5,7 +5,6 @@ from typing import Final
from phone_modem import exceptions
from serial import SerialException
DATA_KEY_API = "api"
DEFAULT_NAME = "Phone Modem"
DOMAIN = "modem_callerid"

View File

@@ -5,40 +5,30 @@ from __future__ import annotations
from phone_modem import PhoneModem
from homeassistant.components.sensor import RestoreSensor
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EVENT_HOMEASSISTANT_STOP, STATE_IDLE
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.const import STATE_IDLE
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from .const import CID, DATA_KEY_API, DOMAIN
from . import ModemCallerIdConfigEntry
from .const import CID, DOMAIN
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: ModemCallerIdConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up the Modem Caller ID sensor."""
api = hass.data[DOMAIN][entry.entry_id][DATA_KEY_API]
async_add_entities(
[
ModemCalleridSensor(
api,
entry.runtime_data,
entry.entry_id,
)
]
)
async def _async_on_hass_stop(event: Event) -> None:
"""HA is shutting down, close modem port."""
if hass.data[DOMAIN][entry.entry_id][DATA_KEY_API]:
await hass.data[DOMAIN][entry.entry_id][DATA_KEY_API].close()
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, _async_on_hass_stop)
)
class ModemCalleridSensor(RestoreSensor):
"""Implementation of USB modem caller ID sensor."""

View File

@@ -8,12 +8,10 @@ from typing import Any, Concatenate
from aiomodernforms import ModernFormsConnectionError, ModernFormsError
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import ModernFormsDataUpdateCoordinator
from .coordinator import ModernFormsConfigEntry, ModernFormsDataUpdateCoordinator
from .entity import ModernFormsDeviceEntity
PLATFORMS = [
@@ -26,15 +24,14 @@ PLATFORMS = [
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: ModernFormsConfigEntry) -> bool:
"""Set up a Modern Forms device from a config entry."""
# Create Modern Forms instance for this entry
coordinator = ModernFormsDataUpdateCoordinator(hass, entry)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
entry.runtime_data = coordinator
# Set up all platforms for this device/entry.
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@@ -42,17 +39,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: ModernFormsConfigEntry
) -> bool:
"""Unload Modern Forms config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
del hass.data[DOMAIN][entry.entry_id]
if not hass.data[DOMAIN]:
del hass.data[DOMAIN]
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
def modernforms_exception_handler[

View File

@@ -3,23 +3,22 @@
from __future__ import annotations
from homeassistant.components.binary_sensor import BinarySensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.util import dt as dt_util
from .const import CLEAR_TIMER, DOMAIN
from .coordinator import ModernFormsDataUpdateCoordinator
from .const import CLEAR_TIMER
from .coordinator import ModernFormsConfigEntry, ModernFormsDataUpdateCoordinator
from .entity import ModernFormsDeviceEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: ModernFormsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Modern Forms binary sensors."""
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
binary_sensors: list[ModernFormsBinarySensor] = [
ModernFormsFanSleepTimerActive(entry.entry_id, coordinator),

View File

@@ -20,6 +20,9 @@ SCAN_INTERVAL = timedelta(seconds=5)
_LOGGER = logging.getLogger(__name__)
type ModernFormsConfigEntry = ConfigEntry[ModernFormsDataUpdateCoordinator]
class ModernFormsDataUpdateCoordinator(DataUpdateCoordinator[ModernFormsDeviceState]):
"""Class to manage fetching Modern Forms data from single endpoint."""

View File

@@ -3,27 +3,23 @@
from __future__ import annotations
from dataclasses import asdict
from typing import TYPE_CHECKING, Any
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import ModernFormsDataUpdateCoordinator
from .coordinator import ModernFormsConfigEntry
REDACT_CONFIG = {CONF_MAC}
REDACT_DEVICE_INFO = {"mac_address", "owner"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: ModernFormsConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
if TYPE_CHECKING:
assert coordinator is not None
coordinator = entry.runtime_data
return {
"config_entry": async_redact_data(entry.as_dict(), REDACT_CONFIG),

View File

@@ -8,7 +8,6 @@ from aiomodernforms.const import FAN_POWER_OFF, FAN_POWER_ON
import voluptuous as vol
from homeassistant.components.fan import FanEntity, FanEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -22,26 +21,23 @@ from . import modernforms_exception_handler
from .const import (
ATTR_SLEEP_TIME,
CLEAR_TIMER,
DOMAIN,
OPT_ON,
OPT_SPEED,
SERVICE_CLEAR_FAN_SLEEP_TIMER,
SERVICE_SET_FAN_SLEEP_TIMER,
)
from .coordinator import ModernFormsDataUpdateCoordinator
from .coordinator import ModernFormsConfigEntry, ModernFormsDataUpdateCoordinator
from .entity import ModernFormsDeviceEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: ModernFormsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a Modern Forms platform from config entry."""
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
coordinator = config_entry.runtime_data
platform = entity_platform.async_get_current_platform()

View File

@@ -8,7 +8,6 @@ from aiomodernforms.const import LIGHT_POWER_OFF, LIGHT_POWER_ON
import voluptuous as vol
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
@@ -21,13 +20,12 @@ from . import modernforms_exception_handler
from .const import (
ATTR_SLEEP_TIME,
CLEAR_TIMER,
DOMAIN,
OPT_BRIGHTNESS,
OPT_ON,
SERVICE_CLEAR_LIGHT_SLEEP_TIMER,
SERVICE_SET_LIGHT_SLEEP_TIMER,
)
from .coordinator import ModernFormsDataUpdateCoordinator
from .coordinator import ModernFormsConfigEntry, ModernFormsDataUpdateCoordinator
from .entity import ModernFormsDeviceEntity
BRIGHTNESS_RANGE = (1, 255)
@@ -35,14 +33,12 @@ BRIGHTNESS_RANGE = (1, 255)
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: ModernFormsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up a Modern Forms platform from config entry."""
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][
config_entry.entry_id
]
coordinator = config_entry.runtime_data
# if no light unit installed no light entity
if not coordinator.data.info.light_type:

View File

@@ -5,24 +5,23 @@ from __future__ import annotations
from datetime import datetime
from homeassistant.components.sensor import SensorDeviceClass, SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.util import dt as dt_util
from .const import CLEAR_TIMER, DOMAIN
from .coordinator import ModernFormsDataUpdateCoordinator
from .const import CLEAR_TIMER
from .coordinator import ModernFormsConfigEntry, ModernFormsDataUpdateCoordinator
from .entity import ModernFormsDeviceEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: ModernFormsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Modern Forms sensor based on a config entry."""
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
sensors: list[ModernFormsSensor] = [
ModernFormsFanTimerRemainingTimeSensor(entry.entry_id, coordinator),

View File

@@ -5,23 +5,21 @@ 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
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from . import modernforms_exception_handler
from .const import DOMAIN
from .coordinator import ModernFormsDataUpdateCoordinator
from .coordinator import ModernFormsConfigEntry, ModernFormsDataUpdateCoordinator
from .entity import ModernFormsDeviceEntity
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: ModernFormsConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Set up Modern Forms switch based on a config entry."""
coordinator: ModernFormsDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
switches = [
ModernFormsAwaySwitch(entry.entry_id, coordinator),

View File

@@ -4,41 +4,33 @@ from __future__ import annotations
from moehlenhoff_alpha2 import Alpha2Base
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, Platform
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import Alpha2BaseCoordinator
from .coordinator import Alpha2BaseCoordinator, Alpha2ConfigEntry
PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.CLIMATE, Platform.SENSOR]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: Alpha2ConfigEntry) -> bool:
"""Set up a config entry."""
base = Alpha2Base(entry.data[CONF_HOST])
coordinator = Alpha2BaseCoordinator(hass, entry, base)
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)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: Alpha2ConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok and entry.entry_id in hass.data[DOMAIN]:
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: Alpha2ConfigEntry) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(entry.entry_id)

View File

@@ -4,24 +4,22 @@ from homeassistant.components.binary_sensor import (
BinarySensorDeviceClass,
BinarySensorEntity,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import Alpha2BaseCoordinator
from .coordinator import Alpha2BaseCoordinator, Alpha2ConfigEntry
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: Alpha2ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add Alpha2 sensor entities from a config_entry."""
coordinator: Alpha2BaseCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
async_add_entities(
Alpha2IODeviceBatterySensor(coordinator, io_device_id)

View File

@@ -1,25 +1,23 @@
"""Button entity to set the time of the Alpha2 base."""
from homeassistant.components.button import ButtonEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util import dt as dt_util
from .const import DOMAIN
from .coordinator import Alpha2BaseCoordinator
from .coordinator import Alpha2BaseCoordinator, Alpha2ConfigEntry
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: Alpha2ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add Alpha2 button entities."""
coordinator: Alpha2BaseCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
async_add_entities([Alpha2TimeSyncButton(coordinator, config_entry.entry_id)])

View File

@@ -1,6 +1,5 @@
"""Support for Alpha2 room control unit via Alpha2 base."""
import logging
from typing import Any
from homeassistant.components.climate import (
@@ -9,26 +8,23 @@ from homeassistant.components.climate import (
HVACAction,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN, PRESET_AUTO, PRESET_DAY, PRESET_NIGHT
from .coordinator import Alpha2BaseCoordinator
_LOGGER = logging.getLogger(__name__)
from .const import PRESET_AUTO, PRESET_DAY, PRESET_NIGHT
from .coordinator import Alpha2BaseCoordinator, Alpha2ConfigEntry
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: Alpha2ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add Alpha2Climate entities from a config_entry."""
coordinator: Alpha2BaseCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
async_add_entities(
Alpha2Climate(coordinator, heat_area_id)

View File

@@ -17,14 +17,16 @@ _LOGGER = logging.getLogger(__name__)
UPDATE_INTERVAL = timedelta(seconds=60)
type Alpha2ConfigEntry = ConfigEntry[Alpha2BaseCoordinator]
class Alpha2BaseCoordinator(DataUpdateCoordinator[dict[str, dict]]):
"""Keep the base instance in one place and centralize the update."""
config_entry: ConfigEntry
config_entry: Alpha2ConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, base: Alpha2Base
self, hass: HomeAssistant, config_entry: Alpha2ConfigEntry, base: Alpha2Base
) -> None:
"""Initialize Alpha2Base data updater."""
self.base = base

View File

@@ -1,24 +1,22 @@
"""Support for Alpha2 heat control valve opening sensors."""
from homeassistant.components.sensor import SensorEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import DOMAIN
from .coordinator import Alpha2BaseCoordinator
from .coordinator import Alpha2BaseCoordinator, Alpha2ConfigEntry
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: Alpha2ConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Add Alpha2 sensor entities from a config_entry."""
coordinator: Alpha2BaseCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
# HEATCTRL attribute ACTOR_PERCENT is not available in older firmware versions
async_add_entities(

View File

@@ -4,7 +4,6 @@ from __future__ import annotations
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_get_clientsession
@@ -14,15 +13,14 @@ from homeassistant.helpers.config_entry_oauth2_flow import (
)
from .api import AuthenticatedMonzoAPI
from .const import DOMAIN
from .coordinator import MonzoCoordinator
from .coordinator import MonzoConfigEntry, MonzoCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.SENSOR]
async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_migrate_entry(hass: HomeAssistant, entry: MonzoConfigEntry) -> bool:
"""Migrate entry."""
_LOGGER.debug("Migrating from version %s.%s", entry.version, entry.minor_version)
@@ -39,7 +37,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: MonzoConfigEntry) -> bool:
"""Set up Monzo from a config entry."""
implementation = await async_get_config_entry_implementation(hass, entry)
@@ -51,15 +49,12 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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: MonzoConfigEntry) -> 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

@@ -1,5 +1,7 @@
"""The Monzo integration."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
import logging
@@ -18,6 +20,8 @@ from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
type MonzoConfigEntry = ConfigEntry[MonzoCoordinator]
@dataclass
class MonzoData:
@@ -30,10 +34,13 @@ class MonzoData:
class MonzoCoordinator(DataUpdateCoordinator[MonzoData]):
"""Class to manage fetching Monzo data from the API."""
config_entry: ConfigEntry
config_entry: MonzoConfigEntry
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, api: AuthenticatedMonzoAPI
self,
hass: HomeAssistant,
config_entry: MonzoConfigEntry,
api: AuthenticatedMonzoAPI,
) -> None:
"""Initialize."""
super().__init__(

View File

@@ -11,14 +11,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 homeassistant.helpers.typing import StateType
from . import MonzoCoordinator
from .const import DOMAIN
from .coordinator import MonzoData
from .coordinator import MonzoConfigEntry, MonzoCoordinator, MonzoData
from .entity import MonzoBaseEntity
@@ -64,11 +61,11 @@ MODEL_POT = "Pot"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: MonzoConfigEntry,
async_add_entities: AddConfigEntryEntitiesCallback,
) -> None:
"""Defer sensor setup to the shared sensor module."""
coordinator: MonzoCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
accounts = [
MonzoSensor(

View File

@@ -56,7 +56,6 @@ from homeassistant.helpers.dispatcher import (
async_dispatcher_send,
)
from homeassistant.helpers.network import NoURLAvailableError, get_url
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
ATTR_EVENT_TYPE,
@@ -69,7 +68,6 @@ from .const import (
CONF_SURVEILLANCE_USERNAME,
CONF_WEBHOOK_SET,
CONF_WEBHOOK_SET_OVERWRITE,
DEFAULT_SCAN_INTERVAL,
DEFAULT_WEBHOOK_SET,
DEFAULT_WEBHOOK_SET_OVERWRITE,
DOMAIN,
@@ -84,6 +82,7 @@ from .const import (
WEB_HOOK_SENTINEL_KEY,
WEB_HOOK_SENTINEL_VALUE,
)
from .coordinator import MotionEyeUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [CAMERA_DOMAIN, SENSOR_DOMAIN, SWITCH_DOMAIN]
@@ -308,20 +307,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass, DOMAIN, "motionEye", entry.data[CONF_WEBHOOK_ID], handle_webhook
)
async def async_update_data() -> dict[str, Any] | None:
try:
return await client.async_get_cameras()
except MotionEyeClientError as exc:
raise UpdateFailed("Error communicating with API") from exc
coordinator = DataUpdateCoordinator(
hass,
_LOGGER,
config_entry=entry,
name=DOMAIN,
update_method=async_update_data,
update_interval=DEFAULT_SCAN_INTERVAL,
)
coordinator = MotionEyeUpdateCoordinator(hass, entry, client)
hass.data[DOMAIN][entry.entry_id] = {
CONF_CLIENT: client,
CONF_COORDINATOR: coordinator,

View File

@@ -43,7 +43,6 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import config_validation as cv, entity_platform
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import get_camera_from_cameras, is_acceptable_camera, listen_for_new_cameras
from .const import (
@@ -60,6 +59,7 @@ from .const import (
SERVICE_SNAPSHOT,
TYPE_MOTIONEYE_MJPEG_CAMERA,
)
from .coordinator import MotionEyeUpdateCoordinator
from .entity import MotionEyeEntity
PLATFORMS = [Platform.CAMERA]
@@ -153,7 +153,7 @@ class MotionEyeMjpegCamera(MotionEyeEntity, MjpegCamera):
password: str,
camera: dict[str, Any],
client: MotionEyeClient,
coordinator: DataUpdateCoordinator,
coordinator: MotionEyeUpdateCoordinator,
options: Mapping[str, str],
) -> None:
"""Initialize a MJPEG camera."""

View File

@@ -0,0 +1,41 @@
"""Coordinator for the motionEye integration."""
from __future__ import annotations
import logging
from typing import Any
from motioneye_client.client import MotionEyeClient, MotionEyeClientError
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
_LOGGER = logging.getLogger(__name__)
class MotionEyeUpdateCoordinator(DataUpdateCoordinator[dict[str, Any] | None]):
"""Coordinator for motionEye data."""
config_entry: ConfigEntry
def __init__(
self, hass: HomeAssistant, entry: ConfigEntry, client: MotionEyeClient
) -> None:
"""Initialize the coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
config_entry=entry,
update_interval=DEFAULT_SCAN_INTERVAL,
)
self.client = client
async def _async_update_data(self) -> dict[str, Any] | None:
try:
return await self.client.async_get_cameras()
except MotionEyeClientError as exc:
raise UpdateFailed("Error communicating with API") from exc

View File

@@ -10,12 +10,10 @@ from motioneye_client.const import KEY_ID
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import get_motioneye_device_identifier
from .coordinator import MotionEyeUpdateCoordinator
def get_motioneye_entity_unique_id(
@@ -25,7 +23,7 @@ def get_motioneye_entity_unique_id(
return f"{config_entry_id}_{camera_id}_{entity_type}"
class MotionEyeEntity(CoordinatorEntity):
class MotionEyeEntity(CoordinatorEntity[MotionEyeUpdateCoordinator]):
"""Base class for motionEye entities."""
_attr_has_entity_name = True
@@ -36,7 +34,7 @@ class MotionEyeEntity(CoordinatorEntity):
type_name: str,
camera: dict[str, Any],
client: MotionEyeClient,
coordinator: DataUpdateCoordinator,
coordinator: MotionEyeUpdateCoordinator,
options: Mapping[str, Any],
entity_description: EntityDescription | None = None,
) -> None:

View File

@@ -3,7 +3,6 @@
from __future__ import annotations
from collections.abc import Mapping
import logging
from typing import Any
from motioneye_client.client import MotionEyeClient
@@ -14,14 +13,12 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import get_camera_from_cameras, listen_for_new_cameras
from .const import CONF_CLIENT, CONF_COORDINATOR, DOMAIN, TYPE_MOTIONEYE_ACTION_SENSOR
from .coordinator import MotionEyeUpdateCoordinator
from .entity import MotionEyeEntity
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(
hass: HomeAssistant,
@@ -59,7 +56,7 @@ class MotionEyeActionSensor(MotionEyeEntity, SensorEntity):
config_entry_id: str,
camera: dict[str, Any],
client: MotionEyeClient,
coordinator: DataUpdateCoordinator,
coordinator: MotionEyeUpdateCoordinator,
options: Mapping[str, str],
) -> None:
"""Initialize an action sensor."""

View File

@@ -20,10 +20,10 @@ 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 homeassistant.helpers.update_coordinator import DataUpdateCoordinator
from . import get_camera_from_cameras, listen_for_new_cameras
from .const import CONF_CLIENT, CONF_COORDINATOR, DOMAIN, TYPE_MOTIONEYE_SWITCH_BASE
from .coordinator import MotionEyeUpdateCoordinator
from .entity import MotionEyeEntity
MOTIONEYE_SWITCHES = [
@@ -102,7 +102,7 @@ class MotionEyeSwitch(MotionEyeEntity, SwitchEntity):
config_entry_id: str,
camera: dict[str, Any],
client: MotionEyeClient,
coordinator: DataUpdateCoordinator,
coordinator: MotionEyeUpdateCoordinator,
options: Mapping[str, str],
entity_description: SwitchEntityDescription,
) -> None:

View File

@@ -107,7 +107,6 @@ ABBREVIATIONS = {
"modes": "modes",
"name": "name",
"o": "origin",
"obj_id": "object_id",
"off_dly": "off_delay",
"on_cmd_type": "on_command_type",
"ops": "options",

View File

@@ -268,7 +268,6 @@ CONF_VIA_DEVICE = "via_device"
CONF_DEPRECATED_VIA_HUB = "via_hub"
CONF_SUGGESTED_AREA = "suggested_area"
CONF_CONFIGURATION_URL = "configuration_url"
CONF_OBJECT_ID = "object_id"
CONF_SUPPORT_URL = "support_url"
DEFAULT_ALARM_CONTROL_PANEL_COMMAND_TEMPLATE = "{{action}}"

View File

@@ -29,7 +29,6 @@ from homeassistant.const import (
CONF_MODEL_ID,
CONF_NAME,
CONF_UNIQUE_ID,
CONF_URL,
CONF_VALUE_TEMPLATE,
)
from homeassistant.core import Event, HassJobType, HomeAssistant, callback
@@ -84,8 +83,6 @@ from .const import (
CONF_JSON_ATTRS_TEMPLATE,
CONF_JSON_ATTRS_TOPIC,
CONF_MANUFACTURER,
CONF_OBJECT_ID,
CONF_ORIGIN,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
CONF_QOS,
@@ -1412,58 +1409,12 @@ class MqttEntity(
"""Set entity_id from default_entity_id if defined in config."""
object_id: str
default_entity_id: str | None
# Setting the default entity_id through the CONF_OBJECT_ID is deprecated
# Support will be removed with HA Core 2026.4
if (
CONF_DEFAULT_ENTITY_ID not in self._config
and CONF_OBJECT_ID not in self._config
):
return
if (default_entity_id := self._config.get(CONF_DEFAULT_ENTITY_ID)) is None:
object_id = self._config[CONF_OBJECT_ID]
else:
_, _, object_id = default_entity_id.partition(".")
return
_, _, object_id = default_entity_id.partition(".")
self.entity_id = async_generate_entity_id(
self._entity_id_format, object_id, None, self.hass
)
if CONF_OBJECT_ID in self._config:
domain = self.entity_id.split(".")[0]
if not self._discovery:
async_create_issue(
self.hass,
DOMAIN,
self.entity_id,
issue_domain=DOMAIN,
is_fixable=False,
breaks_in_ha_version="2026.4",
severity=IssueSeverity.WARNING,
learn_more_url=f"{learn_more_url(domain)}#default_enity_id",
translation_placeholders={
"entity_id": self.entity_id,
"object_id": self._config[CONF_OBJECT_ID],
"domain": domain,
},
translation_key="deprecated_object_id",
)
elif CONF_DEFAULT_ENTITY_ID not in self._config:
if CONF_ORIGIN in self._config:
origin_name = self._config[CONF_ORIGIN][CONF_NAME]
url = self._config[CONF_ORIGIN].get(CONF_URL)
origin = f"[{origin_name}]({url})" if url else origin_name
else:
origin = "the integration"
_LOGGER.warning(
"The configuration for entity %s uses the deprecated option "
"`object_id` to set the default entity id. Replace the "
'`"object_id": "%s"` option with `"default_entity_id": '
'"%s"` in your published discovery configuration to fix this '
"issue, or contact the maintainer of %s that published this config "
"to fix this. This will stop working in Home Assistant Core 2026.4",
self.entity_id,
self._config[CONF_OBJECT_ID],
f"{domain}.{self._config[CONF_OBJECT_ID]}",
origin,
)
if self.unique_id is None:
return
@@ -1475,7 +1426,8 @@ class MqttEntity(
(entity_platform, DOMAIN, self.unique_id)
)
) and deleted_entry.entity_id != self.entity_id:
# Plan to update the entity_id basis on `object_id` if a deleted entity was found
# Plan to update the entity_id based on `default_entity_id`
# if a deleted entity was found
self._update_registry_entity_id = self.entity_id
@final

View File

@@ -42,7 +42,6 @@ from .const import (
CONF_JSON_ATTRS_TEMPLATE,
CONF_JSON_ATTRS_TOPIC,
CONF_MANUFACTURER,
CONF_OBJECT_ID,
CONF_ORIGIN,
CONF_PAYLOAD_AVAILABLE,
CONF_PAYLOAD_NOT_AVAILABLE,
@@ -173,7 +172,6 @@ MQTT_ENTITY_COMMON_SCHEMA = _MQTT_AVAILABILITY_SCHEMA.extend(
vol.Optional(CONF_JSON_ATTRS_TOPIC): valid_subscribe_topic,
vol.Optional(CONF_JSON_ATTRS_TEMPLATE): cv.template,
vol.Optional(CONF_DEFAULT_ENTITY_ID): cv.string,
vol.Optional(CONF_OBJECT_ID): cv.string,
vol.Optional(CONF_UNIQUE_ID): cv.string,
}
)

View File

@@ -1116,10 +1116,6 @@
}
},
"issues": {
"deprecated_object_id": {
"description": "Entity {entity_id} uses the `object_id` option which is deprecated. To fix the issue, replace the `object_id: {object_id}` option with `default_entity_id: {domain}.{object_id}` in your \"configuration.yaml\", and restart Home Assistant.",
"title": "Deprecated option object_id used"
},
"invalid_platform_config": {
"description": "Home Assistant detected an invalid config for a manually configured item.\n\nPlatform domain: **{domain}**\nConfiguration file: **{config_file}**\nNear line: **{line}**\nConfiguration found:\n```yaml\n{config}\n```\nError: **{error}**.\n\nMake sure the configuration is valid and [reload](/config/developer-tools/yaml) the manually configured MQTT items or restart Home Assistant to fix this issue.",
"title": "Invalid config found for MQTT {domain} item"

View File

@@ -7,7 +7,7 @@ import asyncio
from http import HTTPStatus
import logging
from aiohttp import ClientError, ClientResponseError, web
from aiohttp import ClientError, web
from google_nest_sdm.camera_traits import CameraClipPreviewTrait
from google_nest_sdm.device import Device
from google_nest_sdm.device_manager import DeviceManager
@@ -43,6 +43,8 @@ from homeassistant.exceptions import (
ConfigEntryAuthFailed,
ConfigEntryNotReady,
HomeAssistantError,
OAuth2TokenRequestError,
OAuth2TokenRequestReauthError,
Unauthorized,
)
from homeassistant.helpers import (
@@ -253,11 +255,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: NestConfigEntry) -> bool
auth = await api.new_auth(hass, entry)
try:
await auth.async_get_access_token()
except ClientResponseError as err:
if 400 <= err.status < 500:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="reauth_required"
) from err
except OAuth2TokenRequestReauthError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_key="reauth_required"
) from err
except OAuth2TokenRequestError as err:
raise ConfigEntryNotReady(
translation_domain=DOMAIN, translation_key="auth_server_error"
) from err

View File

@@ -218,7 +218,7 @@ def fix_coordinates(user_input: dict) -> dict:
# Ensure coordinates have acceptable length for the Netatmo API
for coordinate in (CONF_LAT_NE, CONF_LAT_SW, CONF_LON_NE, CONF_LON_SW):
if len(str(user_input[coordinate]).split(".")[1]) < 7:
user_input[coordinate] = user_input[coordinate] + 0.0000001
user_input[coordinate] = user_input[coordinate] + 1e-7
# Swap coordinates if entered in wrong order
if user_input[CONF_LAT_NE] < user_input[CONF_LAT_SW]:

View File

@@ -69,7 +69,7 @@ class OverseerrConfigFlow(ConfigFlow, domain=DOMAIN):
else:
if self.source == SOURCE_USER:
return self.async_create_entry(
title="Overseerr",
title="Seerr",
data={
CONF_HOST: host,
CONF_PORT: port,

View File

@@ -1,6 +1,6 @@
{
"domain": "overseerr",
"name": "Overseerr",
"name": "Seerr",
"after_dependencies": ["cloud"],
"codeowners": ["@joostlek", "@AmGarera"],
"config_flow": true,

View File

@@ -25,8 +25,8 @@
"url": "[%key:common::config_flow::data::url%]"
},
"data_description": {
"api_key": "The API key of the Overseerr instance.",
"url": "The URL of the Overseerr instance."
"api_key": "The API key of the Seerr instance.",
"url": "The URL of the Seerr instance."
}
}
}
@@ -114,7 +114,7 @@
"message": "[%key:common::config_flow::error::invalid_api_key%]"
},
"connection_error": {
"message": "Error connecting to the Overseerr instance: {error}"
"message": "Error connecting to the Seerr instance: {error}"
}
},
"selector": {
@@ -137,11 +137,11 @@
},
"services": {
"get_requests": {
"description": "Retrieves a list of media requests from Overseerr.",
"description": "Retrieves a list of media requests from Seerr.",
"fields": {
"config_entry_id": {
"description": "The Overseerr instance to get requests from.",
"name": "Overseerr instance"
"description": "The Seerr instance to get requests from.",
"name": "Seerr instance"
},
"requested_by": {
"description": "Filter the requests by the user ID that requested them.",

View File

@@ -49,12 +49,12 @@ class PowerfoxLocalDataUpdateCoordinator(DataUpdateCoordinator[LocalResponse]):
except PowerfoxAuthenticationError as err:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="invalid_auth",
translation_placeholders={"error": str(err)},
translation_key="auth_failed",
translation_placeholders={"host": self.config_entry.data[CONF_HOST]},
) from err
except PowerfoxConnectionError as err:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="update_failed",
translation_placeholders={"error": str(err)},
translation_key="connection_error",
translation_placeholders={"host": self.config_entry.data[CONF_HOST]},
) from err

View File

@@ -56,11 +56,11 @@
}
},
"exceptions": {
"invalid_auth": {
"message": "Error while authenticating with the device: {error}"
"auth_failed": {
"message": "Authentication with the Poweropti device at {host} failed. Please check your API key."
},
"update_failed": {
"message": "Error while updating the device: {error}"
"connection_error": {
"message": "Could not connect to the Poweropti device at {host}. Please check if the device is online and reachable."
}
}
}

View File

@@ -90,7 +90,7 @@ async def validate_input(hass: HomeAssistant, user_input: dict[str, Any]) -> Non
password=user_input[CONF_PASSWORD],
)
await pyload.login()
await pyload.get_status()
class PyLoadConfigFlow(ConfigFlow, domain=DOMAIN):

View File

@@ -64,19 +64,12 @@ class PyLoadCoordinator(DataUpdateCoordinator[PyLoadData]):
**await self.pyload.get_status(),
free_space=await self.pyload.free_space(),
)
except InvalidAuth:
try:
await self.pyload.login()
except InvalidAuth as exc:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="setup_authentication_exception",
translation_placeholders={CONF_USERNAME: self.pyload.username},
) from exc
_LOGGER.debug(
"Unable to retrieve data due to cookie expiration, retrying after 20 seconds"
)
return self.data
except InvalidAuth as e:
raise ConfigEntryAuthFailed(
translation_domain=DOMAIN,
translation_key="setup_authentication_exception",
translation_placeholders={CONF_USERNAME: self.pyload.username},
) from e
except CannotConnect as e:
raise UpdateFailed(
translation_domain=DOMAIN,
@@ -92,7 +85,6 @@ class PyLoadCoordinator(DataUpdateCoordinator[PyLoadData]):
"""Set up the coordinator."""
try:
await self.pyload.login()
self.version = await self.pyload.version()
except CannotConnect as e:
raise ConfigEntryNotReady(

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