Merge branch 'dev' into block_pyserial_asyncio

This commit is contained in:
J. Nick Koston 2024-05-05 10:06:24 -05:00 committed by GitHub
commit 1428ce4084
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
231 changed files with 3524 additions and 2621 deletions

View File

@ -519,6 +519,7 @@ omit =
homeassistant/components/guardian/util.py
homeassistant/components/guardian/valve.py
homeassistant/components/habitica/__init__.py
homeassistant/components/habitica/coordinator.py
homeassistant/components/habitica/sensor.py
homeassistant/components/harman_kardon_avr/media_player.py
homeassistant/components/harmony/data.py

View File

@ -1,6 +1,6 @@
repos:
- repo: https://github.com/astral-sh/ruff-pre-commit
rev: v0.4.2
rev: v0.4.3
hooks:
- id: ruff
args:

View File

@ -31,7 +31,6 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
from homeassistant.util.dt import utc_from_timestamp
from . import AccuWeatherData
@ -65,8 +64,6 @@ class AccuWeatherEntity(
CoordinatorWeatherEntity[
AccuWeatherObservationDataUpdateCoordinator,
AccuWeatherDailyForecastDataUpdateCoordinator,
TimestampDataUpdateCoordinator,
TimestampDataUpdateCoordinator,
]
):
"""Define an AccuWeather entity."""

View File

@ -17,15 +17,18 @@ from homeassistant.const import CONF_HOST, CONF_NAME, EVENT_HOMEASSISTANT_STOP,
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import DOMAIN
from .helpers import create_api, get_enable_ime
_LOGGER = logging.getLogger(__name__)
PLATFORMS: list[Platform] = [Platform.MEDIA_PLAYER, Platform.REMOTE]
AndroidTVRemoteConfigEntry = ConfigEntry[AndroidTVRemote]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry
) -> bool:
"""Set up Android TV Remote from a config entry."""
api = create_api(hass, entry.data[CONF_HOST], get_enable_ime(entry))
@ -64,7 +67,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
# update the config entry data and reload the config entry.
api.keep_reconnecting(reauth_needed)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = api
entry.runtime_data = api
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -77,17 +80,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, on_hass_stop)
)
entry.async_on_unload(entry.add_update_listener(update_listener))
entry.async_on_unload(api.disconnect)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
api: AndroidTVRemote = hass.data[DOMAIN].pop(entry.entry_id)
api.disconnect()
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:

View File

@ -4,23 +4,20 @@ from __future__ import annotations
from typing import Any
from androidtvremote2 import AndroidTVRemote
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_MAC
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from . import AndroidTVRemoteConfigEntry
TO_REDACT = {CONF_HOST, CONF_MAC}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: AndroidTVRemoteConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
api: AndroidTVRemote = hass.data[DOMAIN].pop(entry.entry_id)
api = entry.runtime_data
return async_redact_data(
{
"api_device_info": api.device_info,

View File

@ -8,6 +8,6 @@
"iot_class": "local_push",
"loggers": ["androidtvremote2"],
"quality_scale": "platinum",
"requirements": ["androidtvremote2==0.0.14"],
"requirements": ["androidtvremote2==0.0.15"],
"zeroconf": ["_androidtvremote2._tcp.local."]
}

View File

@ -14,12 +14,11 @@ from homeassistant.components.media_player import (
MediaPlayerState,
MediaType,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from . import AndroidTVRemoteConfigEntry
from .entity import AndroidTVRemoteBaseEntity
PARALLEL_UPDATES = 0
@ -27,11 +26,11 @@ PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: AndroidTVRemoteConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Android TV media player entity based on a config entry."""
api: AndroidTVRemote = hass.data[DOMAIN][config_entry.entry_id]
api = config_entry.runtime_data
async_add_entities([AndroidTVRemoteMediaPlayerEntity(api, config_entry)])
@ -53,7 +52,9 @@ class AndroidTVRemoteMediaPlayerEntity(AndroidTVRemoteBaseEntity, MediaPlayerEnt
| MediaPlayerEntityFeature.PLAY_MEDIA
)
def __init__(self, api: AndroidTVRemote, config_entry: ConfigEntry) -> None:
def __init__(
self, api: AndroidTVRemote, config_entry: AndroidTVRemoteConfigEntry
) -> None:
"""Initialize the entity."""
super().__init__(api, config_entry)

View File

@ -6,8 +6,6 @@ import asyncio
from collections.abc import Iterable
from typing import Any
from androidtvremote2 import AndroidTVRemote
from homeassistant.components.remote import (
ATTR_ACTIVITY,
ATTR_DELAY_SECS,
@ -19,11 +17,10 @@ from homeassistant.components.remote import (
RemoteEntity,
RemoteEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from . import AndroidTVRemoteConfigEntry
from .entity import AndroidTVRemoteBaseEntity
PARALLEL_UPDATES = 0
@ -31,11 +28,11 @@ PARALLEL_UPDATES = 0
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: AndroidTVRemoteConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Android TV remote entity based on a config entry."""
api: AndroidTVRemote = hass.data[DOMAIN][config_entry.entry_id]
api = config_entry.runtime_data
async_add_entities([AndroidTVRemoteEntity(api, config_entry)])

View File

@ -651,11 +651,8 @@ def websocket_delete_all_refresh_tokens(
continue
try:
hass.auth.async_remove_refresh_token(token)
except Exception as err: # pylint: disable=broad-except
getLogger(__name__).exception(
"During refresh token removal, the following error occurred: %s",
err,
)
except Exception: # pylint: disable=broad-except
getLogger(__name__).exception("Error during refresh token removal")
remove_failed = True
if remove_failed:

View File

@ -13,8 +13,10 @@ from .hub import AxisHub, get_axis_api
_LOGGER = logging.getLogger(__name__)
AxisConfigEntry = ConfigEntry[AxisHub]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, config_entry: AxisConfigEntry) -> bool:
"""Set up the Axis integration."""
hass.data.setdefault(AXIS_DOMAIN, {})
@ -25,8 +27,7 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
except AuthenticationRequired as err:
raise ConfigEntryAuthFailed from err
hub = AxisHub(hass, config_entry, api)
hass.data[AXIS_DOMAIN][config_entry.entry_id] = hub
hub = config_entry.runtime_data = AxisHub(hass, config_entry, api)
await hub.async_update_device_registry()
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
hub.setup()
@ -42,7 +43,6 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
"""Unload Axis device config entry."""
hass.data[AXIS_DOMAIN].pop(config_entry.entry_id)
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)

View File

@ -17,11 +17,11 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import async_call_later
from . import AxisConfigEntry
from .entity import AxisEventDescription, AxisEventEntity
from .hub import AxisHub
@ -177,11 +177,11 @@ ENTITY_DESCRIPTIONS = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: AxisConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up a Axis binary sensor."""
AxisHub.get_hub(hass, config_entry).entity_loader.register_platform(
config_entry.runtime_data.entity_loader.register_platform(
async_add_entities, AxisBinarySensor, ENTITY_DESCRIPTIONS
)

View File

@ -4,12 +4,12 @@ from urllib.parse import urlencode
from homeassistant.components.camera import CameraEntityFeature
from homeassistant.components.mjpeg import MjpegCamera, filter_urllib3_logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import HTTP_DIGEST_AUTHENTICATION
from homeassistant.core import HomeAssistant
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AxisConfigEntry
from .const import DEFAULT_STREAM_PROFILE, DEFAULT_VIDEO_SOURCE
from .entity import AxisEntity
from .hub import AxisHub
@ -17,13 +17,13 @@ from .hub import AxisHub
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: AxisConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Axis camera video stream."""
filter_urllib3_logging()
hub = AxisHub.get_hub(hass, config_entry)
hub = config_entry.runtime_data
if (
not (prop := hub.api.vapix.params.property_handler.get("0"))

View File

@ -32,6 +32,7 @@ from homeassistant.core import callback
from homeassistant.helpers.device_registry import format_mac
from homeassistant.util.network import is_link_local
from . import AxisConfigEntry
from .const import (
CONF_STREAM_PROFILE,
CONF_VIDEO_SOURCE,
@ -260,13 +261,14 @@ class AxisFlowHandler(ConfigFlow, domain=AXIS_DOMAIN):
class AxisOptionsFlowHandler(OptionsFlowWithConfigEntry):
"""Handle Axis device options."""
config_entry: AxisConfigEntry
hub: AxisHub
async def async_step_init(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Manage the Axis device options."""
self.hub = AxisHub.get_hub(self.hass, self.config_entry)
self.hub = self.config_entry.runtime_data
return await self.async_step_configure_stream()
async def async_step_configure_stream(

View File

@ -5,11 +5,10 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME
from homeassistant.core import HomeAssistant
from .hub import AxisHub
from . import AxisConfigEntry
REDACT_CONFIG = {CONF_MAC, CONF_PASSWORD, CONF_UNIQUE_ID, CONF_USERNAME}
REDACT_BASIC_DEVICE_INFO = {"SerialNumber", "SocSerialNumber"}
@ -17,10 +16,10 @@ REDACT_VAPIX_PARAMS = {"root.Network", "System.SerialNumber"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: AxisConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
hub = AxisHub.get_hub(hass, config_entry)
hub = config_entry.runtime_data
diag: dict[str, Any] = hub.additional_diagnostics.copy()
diag["config"] = async_redact_data(config_entry.as_dict(), REDACT_CONFIG)

View File

@ -2,11 +2,10 @@
from __future__ import annotations
from typing import Any
from typing import TYPE_CHECKING, Any
import axis
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, format_mac
@ -17,12 +16,15 @@ from .config import AxisConfig
from .entity_loader import AxisEntityLoader
from .event_source import AxisEventSource
if TYPE_CHECKING:
from .. import AxisConfigEntry
class AxisHub:
"""Manages a Axis device."""
def __init__(
self, hass: HomeAssistant, config_entry: ConfigEntry, api: axis.AxisDevice
self, hass: HomeAssistant, config_entry: AxisConfigEntry, api: axis.AxisDevice
) -> None:
"""Initialize the device."""
self.hass = hass
@ -37,13 +39,6 @@ class AxisHub:
self.additional_diagnostics: dict[str, Any] = {}
@callback
@staticmethod
def get_hub(hass: HomeAssistant, config_entry: ConfigEntry) -> AxisHub:
"""Get Axis hub from config entry."""
hub: AxisHub = hass.data[AXIS_DOMAIN][config_entry.entry_id]
return hub
@property
def available(self) -> bool:
"""Connection state to the device."""
@ -63,7 +58,7 @@ class AxisHub:
@staticmethod
async def async_new_address_callback(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: AxisConfigEntry
) -> None:
"""Handle signals of device getting new address.
@ -71,7 +66,7 @@ class AxisHub:
This is a static method because a class method (bound method),
cannot be used with weak references.
"""
hub = AxisHub.get_hub(hass, config_entry)
hub = config_entry.runtime_data
hub.config = AxisConfig.from_config_entry(config_entry)
hub.event_source.config_entry = config_entry
hub.api.config.host = hub.config.host

View File

@ -11,10 +11,10 @@ from homeassistant.components.light import (
LightEntity,
LightEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AxisConfigEntry
from .entity import TOPIC_TO_EVENT_TYPE, AxisEventDescription, AxisEventEntity
from .hub import AxisHub
@ -45,11 +45,11 @@ ENTITY_DESCRIPTIONS = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: AxisConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Axis light platform."""
AxisHub.get_hub(hass, config_entry).entity_loader.register_platform(
config_entry.runtime_data.entity_loader.register_platform(
async_add_entities, AxisLight, ENTITY_DESCRIPTIONS
)

View File

@ -10,11 +10,11 @@ from homeassistant.components.switch import (
SwitchEntity,
SwitchEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from . import AxisConfigEntry
from .entity import AxisEventDescription, AxisEventEntity
from .hub import AxisHub
@ -38,11 +38,11 @@ ENTITY_DESCRIPTIONS = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: AxisConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Axis switch platform."""
AxisHub.get_hub(hass, config_entry).entity_loader.register_platform(
config_entry.runtime_data.entity_loader.register_platform(
async_add_entities, AxisSwitch, ENTITY_DESCRIPTIONS
)

View File

@ -97,10 +97,9 @@ class HomeAssistantBluetoothManager(BluetoothManager):
matched_domains = self._integration_matcher.match_domains(service_info)
if self._debug:
_LOGGER.debug(
"%s: %s %s match: %s",
"%s: %s match: %s",
self._async_describe_source(service_info),
service_info.address,
service_info.advertisement,
service_info,
matched_domains,
)

View File

@ -16,10 +16,10 @@
"requirements": [
"bleak==0.21.1",
"bleak-retry-connector==3.5.0",
"bluetooth-adapters==0.19.1",
"bluetooth-adapters==0.19.2",
"bluetooth-auto-recovery==1.4.2",
"bluetooth-data-tools==1.19.0",
"dbus-fast==2.21.1",
"habluetooth==2.8.1"
"habluetooth==3.0.1"
]
}

View File

@ -2,12 +2,17 @@
from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any
from boschshcpy import SHCSession
from boschshcpy.device import SHCDevice
from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
@ -20,341 +25,207 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from .const import DATA_SESSION, DOMAIN
from .entity import SHCEntity
@dataclass(frozen=True, kw_only=True)
class SHCSensorEntityDescription(SensorEntityDescription):
"""Describes a SHC sensor."""
value_fn: Callable[[SHCDevice], StateType]
attributes_fn: Callable[[SHCDevice], dict[str, Any]] | None = None
TEMPERATURE_SENSOR = "temperature"
HUMIDITY_SENSOR = "humidity"
VALVE_TAPPET_SENSOR = "valvetappet"
PURITY_SENSOR = "purity"
AIR_QUALITY_SENSOR = "airquality"
TEMPERATURE_RATING_SENSOR = "temperature_rating"
HUMIDITY_RATING_SENSOR = "humidity_rating"
PURITY_RATING_SENSOR = "purity_rating"
POWER_SENSOR = "power"
ENERGY_SENSOR = "energy"
COMMUNICATION_QUALITY_SENSOR = "communication_quality"
SENSOR_DESCRIPTIONS: dict[str, SHCSensorEntityDescription] = {
TEMPERATURE_SENSOR: SHCSensorEntityDescription(
key=TEMPERATURE_SENSOR,
device_class=SensorDeviceClass.TEMPERATURE,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
value_fn=lambda device: device.temperature,
),
HUMIDITY_SENSOR: SHCSensorEntityDescription(
key=HUMIDITY_SENSOR,
device_class=SensorDeviceClass.HUMIDITY,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda device: device.humidity,
),
PURITY_SENSOR: SHCSensorEntityDescription(
key=PURITY_SENSOR,
translation_key=PURITY_SENSOR,
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
value_fn=lambda device: device.purity,
),
AIR_QUALITY_SENSOR: SHCSensorEntityDescription(
key=AIR_QUALITY_SENSOR,
translation_key="air_quality",
value_fn=lambda device: device.combined_rating.name,
attributes_fn=lambda device: {
"rating_description": device.description,
},
),
TEMPERATURE_RATING_SENSOR: SHCSensorEntityDescription(
key=TEMPERATURE_RATING_SENSOR,
translation_key=TEMPERATURE_RATING_SENSOR,
value_fn=lambda device: device.temperature_rating.name,
),
COMMUNICATION_QUALITY_SENSOR: SHCSensorEntityDescription(
key=COMMUNICATION_QUALITY_SENSOR,
translation_key=COMMUNICATION_QUALITY_SENSOR,
value_fn=lambda device: device.communicationquality.name,
),
HUMIDITY_RATING_SENSOR: SHCSensorEntityDescription(
key=HUMIDITY_RATING_SENSOR,
translation_key=HUMIDITY_RATING_SENSOR,
value_fn=lambda device: device.humidity_rating.name,
),
PURITY_RATING_SENSOR: SHCSensorEntityDescription(
key=PURITY_RATING_SENSOR,
translation_key=PURITY_RATING_SENSOR,
value_fn=lambda device: device.purity_rating.name,
),
POWER_SENSOR: SHCSensorEntityDescription(
key=POWER_SENSOR,
device_class=SensorDeviceClass.POWER,
native_unit_of_measurement=UnitOfPower.WATT,
value_fn=lambda device: device.powerconsumption,
),
ENERGY_SENSOR: SHCSensorEntityDescription(
key=ENERGY_SENSOR,
device_class=SensorDeviceClass.ENERGY,
state_class=SensorStateClass.TOTAL_INCREASING,
native_unit_of_measurement=UnitOfEnergy.KILO_WATT_HOUR,
value_fn=lambda device: device.energyconsumption / 1000.0,
),
VALVE_TAPPET_SENSOR: SHCSensorEntityDescription(
key=VALVE_TAPPET_SENSOR,
translation_key=VALVE_TAPPET_SENSOR,
state_class=SensorStateClass.MEASUREMENT,
native_unit_of_measurement=PERCENTAGE,
value_fn=lambda device: device.position,
attributes_fn=lambda device: {
"valve_tappet_state": device.valvestate.name,
},
),
}
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the SHC sensor platform."""
entities: list[SensorEntity] = []
session: SHCSession = hass.data[DOMAIN][config_entry.entry_id][DATA_SESSION]
for sensor in session.device_helper.thermostats:
entities.append(
TemperatureSensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
)
entities.append(
ValveTappetSensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
entities: list[SensorEntity] = [
SHCSensor(
device,
SENSOR_DESCRIPTIONS[sensor_type],
session.information.unique_id,
config_entry.entry_id,
)
for device in session.device_helper.thermostats
for sensor_type in (TEMPERATURE_SENSOR, VALVE_TAPPET_SENSOR)
]
for sensor in session.device_helper.wallthermostats:
entities.append(
TemperatureSensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
)
entities.append(
HumiditySensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
entities.extend(
SHCSensor(
device,
SENSOR_DESCRIPTIONS[sensor_type],
session.information.unique_id,
config_entry.entry_id,
)
for device in session.device_helper.wallthermostats
for sensor_type in (TEMPERATURE_SENSOR, HUMIDITY_SENSOR)
)
for sensor in session.device_helper.twinguards:
entities.append(
TemperatureSensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
entities.extend(
SHCSensor(
device,
SENSOR_DESCRIPTIONS[sensor_type],
session.information.unique_id,
config_entry.entry_id,
)
entities.append(
HumiditySensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
)
entities.append(
PuritySensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
)
entities.append(
AirQualitySensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
)
entities.append(
TemperatureRatingSensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
)
entities.append(
HumidityRatingSensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
)
entities.append(
PurityRatingSensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
for device in session.device_helper.twinguards
for sensor_type in (
TEMPERATURE_SENSOR,
HUMIDITY_SENSOR,
PURITY_SENSOR,
AIR_QUALITY_SENSOR,
TEMPERATURE_RATING_SENSOR,
HUMIDITY_RATING_SENSOR,
PURITY_RATING_SENSOR,
)
)
for sensor in (
session.device_helper.smart_plugs + session.device_helper.light_switches_bsm
):
entities.append(
PowerSensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
entities.extend(
SHCSensor(
device,
SENSOR_DESCRIPTIONS[sensor_type],
session.information.unique_id,
config_entry.entry_id,
)
entities.append(
EnergySensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
for device in (
session.device_helper.smart_plugs + session.device_helper.light_switches_bsm
)
for sensor_type in (POWER_SENSOR, ENERGY_SENSOR)
)
for sensor in session.device_helper.smart_plugs_compact:
entities.append(
PowerSensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
)
entities.append(
EnergySensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
)
entities.append(
CommunicationQualitySensor(
device=sensor,
parent_id=session.information.unique_id,
entry_id=config_entry.entry_id,
)
entities.extend(
SHCSensor(
device,
SENSOR_DESCRIPTIONS[sensor_type],
session.information.unique_id,
config_entry.entry_id,
)
for device in session.device_helper.smart_plugs_compact
for sensor_type in (POWER_SENSOR, ENERGY_SENSOR, COMMUNICATION_QUALITY_SENSOR)
)
async_add_entities(entities)
class TemperatureSensor(SHCEntity, SensorEntity):
"""Representation of an SHC temperature reporting sensor."""
class SHCSensor(SHCEntity, SensorEntity):
"""Representation of a SHC sensor."""
_attr_device_class = SensorDeviceClass.TEMPERATURE
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = UnitOfTemperature.CELSIUS
entity_description: SHCSensorEntityDescription
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC temperature reporting sensor."""
def __init__(
self,
device: SHCDevice,
entity_description: SHCSensorEntityDescription,
parent_id: str,
entry_id: str,
) -> None:
"""Initialize sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_temperature"
self.entity_description = entity_description
self._attr_unique_id = f"{device.serial}_{entity_description.key}"
@property
def native_value(self):
def native_value(self) -> StateType:
"""Return the state of the sensor."""
return self._device.temperature
class HumiditySensor(SHCEntity, SensorEntity):
"""Representation of an SHC humidity reporting sensor."""
_attr_device_class = SensorDeviceClass.HUMIDITY
_attr_native_unit_of_measurement = PERCENTAGE
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC humidity reporting sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_humidity"
return self.entity_description.value_fn(self._device)
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.humidity
class PuritySensor(SHCEntity, SensorEntity):
"""Representation of an SHC purity reporting sensor."""
_attr_translation_key = "purity"
_attr_native_unit_of_measurement = CONCENTRATION_PARTS_PER_MILLION
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC purity reporting sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_purity"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.purity
class AirQualitySensor(SHCEntity, SensorEntity):
"""Representation of an SHC airquality reporting sensor."""
_attr_translation_key = "air_quality"
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC airquality reporting sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_airquality"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.combined_rating.name
@property
def extra_state_attributes(self):
def extra_state_attributes(self) -> dict[str, Any] | None:
"""Return the state attributes."""
return {
"rating_description": self._device.description,
}
class TemperatureRatingSensor(SHCEntity, SensorEntity):
"""Representation of an SHC temperature rating sensor."""
_attr_translation_key = "temperature_rating"
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC temperature rating sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_temperature_rating"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.temperature_rating.name
class CommunicationQualitySensor(SHCEntity, SensorEntity):
"""Representation of an SHC communication quality reporting sensor."""
_attr_translation_key = "communication_quality"
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC communication quality reporting sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_communication_quality"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.communicationquality.name
class HumidityRatingSensor(SHCEntity, SensorEntity):
"""Representation of an SHC humidity rating sensor."""
_attr_translation_key = "humidity_rating"
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC humidity rating sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_humidity_rating"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.humidity_rating.name
class PurityRatingSensor(SHCEntity, SensorEntity):
"""Representation of an SHC purity rating sensor."""
_attr_translation_key = "purity_rating"
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC purity rating sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_purity_rating"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.purity_rating.name
class PowerSensor(SHCEntity, SensorEntity):
"""Representation of an SHC power reporting sensor."""
_attr_device_class = SensorDeviceClass.POWER
_attr_native_unit_of_measurement = UnitOfPower.WATT
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC power reporting sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_power"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.powerconsumption
class EnergySensor(SHCEntity, SensorEntity):
"""Representation of an SHC energy reporting sensor."""
_attr_device_class = SensorDeviceClass.ENERGY
_attr_state_class = SensorStateClass.TOTAL_INCREASING
_attr_native_unit_of_measurement = UnitOfEnergy.KILO_WATT_HOUR
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC energy reporting sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{self._device.serial}_energy"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.energyconsumption / 1000.0
class ValveTappetSensor(SHCEntity, SensorEntity):
"""Representation of an SHC valve tappet reporting sensor."""
_attr_translation_key = "valvetappet"
_attr_state_class = SensorStateClass.MEASUREMENT
_attr_native_unit_of_measurement = PERCENTAGE
def __init__(self, device: SHCDevice, parent_id: str, entry_id: str) -> None:
"""Initialize an SHC valve tappet reporting sensor."""
super().__init__(device, parent_id, entry_id)
self._attr_unique_id = f"{device.serial}_valvetappet"
@property
def native_value(self):
"""Return the state of the sensor."""
return self._device.position
@property
def extra_state_attributes(self):
"""Return the state attributes."""
return {
"valve_tappet_state": self._device.valvestate.name,
}
if self.entity_description.attributes_fn is not None:
return self.entity_description.attributes_fn(self._device)
return None

View File

@ -43,21 +43,21 @@ SWITCH_TYPES: dict[str, SHCSwitchEntityDescription] = {
"smartplug": SHCSwitchEntityDescription(
key="smartplug",
device_class=SwitchDeviceClass.OUTLET,
on_key="state",
on_key="switchstate",
on_value=SHCSmartPlug.PowerSwitchService.State.ON,
should_poll=False,
),
"smartplugcompact": SHCSwitchEntityDescription(
key="smartplugcompact",
device_class=SwitchDeviceClass.OUTLET,
on_key="state",
on_key="switchstate",
on_value=SHCSmartPlugCompact.PowerSwitchService.State.ON,
should_poll=False,
),
"lightswitch": SHCSwitchEntityDescription(
key="lightswitch",
device_class=SwitchDeviceClass.SWITCH,
on_key="state",
on_key="switchstate",
on_value=SHCLightSwitch.PowerSwitchService.State.ON,
should_poll=False,
),

View File

@ -12,9 +12,10 @@ from homeassistant.const import CONF_HOST, CONF_MAC, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.aiohttp_client import async_create_clientsession
from .const import DOMAIN
from .coordinator import BraviaTVCoordinator
BraviaTVConfigEntry = ConfigEntry[BraviaTVCoordinator]
PLATFORMS: Final[list[Platform]] = [
Platform.BUTTON,
Platform.MEDIA_PLAYER,
@ -22,7 +23,9 @@ PLATFORMS: Final[list[Platform]] = [
]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, config_entry: BraviaTVConfigEntry
) -> bool:
"""Set up a config entry."""
host = config_entry.data[CONF_HOST]
mac = config_entry.data[CONF_MAC]
@ -40,26 +43,22 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = coordinator
config_entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, config_entry: BraviaTVConfigEntry
) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
if unload_ok:
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
async def update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None:
async def update_listener(
hass: HomeAssistant, config_entry: BraviaTVConfigEntry
) -> None:
"""Handle options update."""
await hass.config_entries.async_reload(config_entry.entry_id)

View File

@ -10,12 +10,11 @@ from homeassistant.components.button import (
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from . import BraviaTVConfigEntry
from .coordinator import BraviaTVCoordinator
from .entity import BraviaTVEntity
@ -45,12 +44,12 @@ BUTTONS: tuple[BraviaTVButtonDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: BraviaTVConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Bravia TV Button entities."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
unique_id = config_entry.unique_id
assert unique_id is not None

View File

@ -3,21 +3,19 @@
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_MAC, CONF_PIN
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import BraviaTVCoordinator
from . import BraviaTVConfigEntry
TO_REDACT = {CONF_MAC, CONF_PIN, "macAddr"}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: BraviaTVConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: BraviaTVCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
device_info = await coordinator.client.get_system_info()

View File

@ -15,22 +15,22 @@ from homeassistant.components.media_player import (
MediaType,
)
from homeassistant.components.media_player.browse_media import BrowseMedia
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN, SourceType
from . import BraviaTVConfigEntry
from .const import SourceType
from .entity import BraviaTVEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: BraviaTVConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Bravia TV Media Player from a config_entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
unique_id = config_entry.unique_id
assert unique_id is not None

View File

@ -6,22 +6,21 @@ from collections.abc import Iterable
from typing import Any
from homeassistant.components.remote import ATTR_NUM_REPEATS, RemoteEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from . import BraviaTVConfigEntry
from .entity import BraviaTVEntity
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: BraviaTVConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up Bravia TV Remote from a config entry."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
unique_id = config_entry.unique_id
assert unique_id is not None

View File

@ -24,8 +24,10 @@ PLATFORMS: list[Platform] = [Platform.TODO]
_LOGGER = logging.getLogger(__name__)
BringConfigEntry = ConfigEntry[BringDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: BringConfigEntry) -> bool:
"""Set up Bring! from a config entry."""
email = entry.data[CONF_EMAIL]
@ -57,7 +59,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = BringDataUpdateCoordinator(hass, bring)
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)
@ -66,7 +68,4 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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

@ -15,7 +15,6 @@ from homeassistant.components.todo import (
TodoListEntity,
TodoListEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import config_validation as cv, entity_platform
@ -23,6 +22,7 @@ from homeassistant.helpers.config_validation import make_entity_service_schema
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import BringConfigEntry
from .const import (
ATTR_ITEM_NAME,
ATTR_NOTIFICATION_TYPE,
@ -34,11 +34,11 @@ from .coordinator import BringData, BringDataUpdateCoordinator
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: BringConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the sensor from a config entry created in the integrations UI."""
coordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
unique_id = config_entry.unique_id

View File

@ -373,8 +373,11 @@ class BroadlinkRemote(BroadlinkEntity, RemoteEntity, RestoreEntity):
start_time = dt_util.utcnow()
while (dt_util.utcnow() - start_time) < LEARNING_TIMEOUT:
await asyncio.sleep(1)
found = await device.async_request(device.api.check_frequency)[0]
if found:
is_found, frequency = await device.async_request(
device.api.check_frequency
)
if is_found:
_LOGGER.info("Radiofrequency detected: %s MHz", frequency)
break
else:
await device.async_request(device.api.cancel_sweep_frequency)

View File

@ -2,29 +2,23 @@
from __future__ import annotations
from asyncio import timeout
from datetime import timedelta
import logging
from brother import Brother, SnmpError
from brother import Brother, BrotherSensors, SnmpError, UnsupportedModelError
from homeassistant.config_entries import ConfigEntry
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
from homeassistant.const import CONF_HOST, CONF_TYPE, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DATA_CONFIG_ENTRY, DOMAIN, SNMP
from .const import DOMAIN, SNMP
from .coordinator import BrotherDataUpdateCoordinator
from .utils import get_snmp_engine
PLATFORMS = [Platform.SENSOR]
SCAN_INTERVAL = timedelta(seconds=30)
_LOGGER = logging.getLogger(__name__)
BrotherConfigEntry = ConfigEntry[BrotherDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: BrotherConfigEntry) -> bool:
"""Set up Brother from a config entry."""
host = entry.data[CONF_HOST]
printer_type = entry.data[CONF_TYPE]
@ -40,48 +34,25 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
coordinator = BrotherDataUpdateCoordinator(hass, brother)
await coordinator.async_config_entry_first_refresh()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN].setdefault(DATA_CONFIG_ENTRY, {})
hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id] = coordinator
hass.data[DOMAIN][SNMP] = snmp_engine
entry.runtime_data = coordinator
hass.data.setdefault(DOMAIN, {SNMP: snmp_engine})
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: BrotherConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
if unload_ok:
hass.data[DOMAIN][DATA_CONFIG_ENTRY].pop(entry.entry_id)
if not hass.data[DOMAIN][DATA_CONFIG_ENTRY]:
hass.data[DOMAIN].pop(SNMP)
hass.data[DOMAIN].pop(DATA_CONFIG_ENTRY)
loaded_entries = [
entry
for entry in hass.config_entries.async_entries(DOMAIN)
if entry.state == ConfigEntryState.LOADED
]
# We only want to remove the SNMP engine when unloading the last config entry
if unload_ok and len(loaded_entries) == 1:
hass.data[DOMAIN].pop(SNMP)
return unload_ok
class BrotherDataUpdateCoordinator(DataUpdateCoordinator[BrotherSensors]): # pylint: disable=hass-enforce-coordinator-module
"""Class to manage fetching Brother data from the printer."""
def __init__(self, hass: HomeAssistant, brother: Brother) -> None:
"""Initialize."""
self.brother = brother
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=SCAN_INTERVAL,
)
async def _async_update_data(self) -> BrotherSensors:
"""Update data via library."""
try:
async with timeout(20):
data = await self.brother.async_update()
except (ConnectionError, SnmpError, UnsupportedModelError) as error:
raise UpdateFailed(error) from error
return data

View File

@ -2,12 +2,13 @@
from __future__ import annotations
from datetime import timedelta
from typing import Final
DATA_CONFIG_ENTRY: Final = "config_entry"
DOMAIN: Final = "brother"
PRINTER_TYPES: Final = ["laser", "ink"]
SNMP: Final = "snmp"
UPDATE_INTERVAL = timedelta(seconds=30)

View File

@ -0,0 +1,37 @@
"""Coordinator for Brother integration."""
from asyncio import timeout
import logging
from brother import Brother, BrotherSensors, SnmpError, UnsupportedModelError
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN, UPDATE_INTERVAL
_LOGGER = logging.getLogger(__name__)
class BrotherDataUpdateCoordinator(DataUpdateCoordinator[BrotherSensors]):
"""Class to manage fetching Brother data from the printer."""
def __init__(self, hass: HomeAssistant, brother: Brother) -> None:
"""Initialize."""
self.brother = brother
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=UPDATE_INTERVAL,
)
async def _async_update_data(self) -> BrotherSensors:
"""Update data via library."""
try:
async with timeout(20):
data = await self.brother.async_update()
except (ConnectionError, SnmpError, UnsupportedModelError) as error:
raise UpdateFailed(error) from error
return data

View File

@ -5,20 +5,16 @@ from __future__ import annotations
from dataclasses import asdict
from typing import Any
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from . import BrotherDataUpdateCoordinator
from .const import DATA_CONFIG_ENTRY, DOMAIN
from . import BrotherConfigEntry
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: BrotherConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: BrotherDataUpdateCoordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][
config_entry.entry_id
]
coordinator = config_entry.runtime_data
return {
"info": dict(config_entry.data),

View File

@ -16,7 +16,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
@ -25,8 +24,8 @@ from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import BrotherDataUpdateCoordinator
from .const import DATA_CONFIG_ENTRY, DOMAIN
from . import BrotherConfigEntry, BrotherDataUpdateCoordinator
from .const import DOMAIN
ATTR_COUNTER = "counter"
ATTR_REMAINING_PAGES = "remaining_pages"
@ -318,11 +317,12 @@ SENSOR_TYPES: tuple[BrotherSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: BrotherConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add Brother entities from a config_entry."""
coordinator = hass.data[DOMAIN][DATA_CONFIG_ENTRY][entry.entry_id]
coordinator = entry.runtime_data
# Due to the change of the attribute name of one sensor, it is necessary to migrate
# the unique_id to the new one.
entity_registry = er.async_get(hass)

View File

@ -7,21 +7,21 @@ from homeassistant.const import CONF_HOST, CONF_PORT, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers.start import async_at_started
from .const import DOMAIN
from .coordinator import CertExpiryDataUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
CertExpiryConfigEntry = ConfigEntry[CertExpiryDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: CertExpiryConfigEntry) -> bool:
"""Load the saved entities."""
host: str = entry.data[CONF_HOST]
port: int = entry.data[CONF_PORT]
coordinator = CertExpiryDataUpdateCoordinator(hass, host, port)
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = coordinator
entry.runtime_data = coordinator
if entry.unique_id is None:
hass.config_entries.async_update_entry(entry, unique_id=f"{host}:{port}")

View File

@ -12,7 +12,7 @@ from homeassistant.components.sensor import (
SensorDeviceClass,
SensorEntity,
)
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_START
from homeassistant.core import Event, HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
@ -22,7 +22,7 @@ from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import CertExpiryDataUpdateCoordinator
from . import CertExpiryConfigEntry, CertExpiryDataUpdateCoordinator
from .const import DEFAULT_PORT, DOMAIN
SCAN_INTERVAL = timedelta(hours=12)
@ -62,15 +62,13 @@ async def async_setup_platform(
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: CertExpiryConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add cert-expiry entry."""
coordinator: CertExpiryDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
sensors = [
SSLCertificateTimestamp(coordinator),
]
sensors = [SSLCertificateTimestamp(coordinator)]
async_add_entities(sensors, True)

View File

@ -142,6 +142,9 @@ async def websocket_list_agents(
agent = manager.async_get_agent(agent_info.id)
assert agent is not None
if isinstance(agent, ConversationEntity):
continue
supported_languages = agent.supported_languages
if language and supported_languages != MATCH_ALL:
supported_languages = language_util.matches(

View File

@ -2,7 +2,7 @@
from __future__ import annotations
from homeassistant.components.notify import DOMAIN, NotifyEntity
from homeassistant.components.notify import DOMAIN, NotifyEntity, NotifyEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
@ -33,12 +33,15 @@ class DemoNotifyEntity(NotifyEntity):
) -> None:
"""Initialize the Demo button entity."""
self._attr_unique_id = unique_id
self._attr_supported_features = NotifyEntityFeature.TITLE
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=device_name,
)
async def async_send_message(self, message: str) -> None:
async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message to a user."""
event_notitifcation = {"message": message}
self.hass.bus.async_fire(EVENT_NOTIFY, event_notitifcation)
event_notification = {"message": message}
if title is not None:
event_notification["title"] = title
self.hass.bus.async_fire(EVENT_NOTIFY, event_notification)

View File

@ -18,19 +18,15 @@ from homeassistant.core import Event, HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.device_registry import DeviceEntry
from .const import (
CONF_MYDEVOLO,
DEFAULT_MYDEVOLO,
DOMAIN,
GATEWAY_SERIAL_PATTERN,
PLATFORMS,
)
from .const import CONF_MYDEVOLO, DEFAULT_MYDEVOLO, GATEWAY_SERIAL_PATTERN, PLATFORMS
DevoloHomeControlConfigEntry = ConfigEntry[list[HomeControl]]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, entry: DevoloHomeControlConfigEntry
) -> bool:
"""Set up the devolo account from a config entry."""
hass.data.setdefault(DOMAIN, {})
mydevolo = configure_mydevolo(entry.data)
credentials_valid = await hass.async_add_executor_job(mydevolo.credentials_valid)
@ -47,11 +43,22 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
uuid = await hass.async_add_executor_job(mydevolo.uuid)
hass.config_entries.async_update_entry(entry, unique_id=uuid)
def shutdown(event: Event) -> None:
for gateway in entry.runtime_data:
gateway.websocket_disconnect(
f"websocket disconnect requested by {EVENT_HOMEASSISTANT_STOP}"
)
# Listen when EVENT_HOMEASSISTANT_STOP is fired
entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, shutdown)
)
try:
zeroconf_instance = await zeroconf.async_get_instance(hass)
hass.data[DOMAIN][entry.entry_id] = {"gateways": [], "listener": None}
entry.runtime_data = []
for gateway_id in gateway_ids:
hass.data[DOMAIN][entry.entry_id]["gateways"].append(
entry.runtime_data.append(
await hass.async_add_executor_job(
partial(
HomeControl,
@ -66,31 +73,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
def shutdown(event: Event) -> None:
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]:
gateway.websocket_disconnect(
f"websocket disconnect requested by {EVENT_HOMEASSISTANT_STOP}"
)
# Listen when EVENT_HOMEASSISTANT_STOP is fired
hass.data[DOMAIN][entry.entry_id]["listener"] = hass.bus.async_listen_once(
EVENT_HOMEASSISTANT_STOP, shutdown
)
return True
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, entry: DevoloHomeControlConfigEntry
) -> bool:
"""Unload a config entry."""
unload = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
await asyncio.gather(
*(
hass.async_add_executor_job(gateway.websocket_disconnect)
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]
for gateway in entry.runtime_data
)
)
hass.data[DOMAIN][entry.entry_id]["listener"]()
hass.data[DOMAIN].pop(entry.entry_id)
return unload

View File

@ -9,12 +9,11 @@ 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 AddEntitiesCallback
from .const import DOMAIN
from . import DevoloHomeControlConfigEntry
from .devolo_device import DevoloDeviceEntity
DEVICE_CLASS_MAPPING = {
@ -28,12 +27,14 @@ DEVICE_CLASS_MAPPING = {
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: DevoloHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Get all binary sensor and multi level sensor devices and setup them via config entry."""
entities: list[BinarySensorEntity] = []
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]:
for gateway in entry.runtime_data:
entities.extend(
DevoloBinaryDeviceEntity(
homecontrol=gateway,

View File

@ -13,17 +13,18 @@ from homeassistant.components.climate import (
ClimateEntityFeature,
HVACMode,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PRECISION_HALVES, PRECISION_TENTHS, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from . import DevoloHomeControlConfigEntry
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: DevoloHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Get all cover devices and setup them via config entry."""
@ -33,7 +34,7 @@ async def async_setup_entry(
device_instance=device,
element_uid=multi_level_switch,
)
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]
for gateway in entry.runtime_data
for device in gateway.multi_level_switch_devices
for multi_level_switch in device.multi_level_switch_property
if device.device_model_uid

View File

@ -9,16 +9,17 @@ from homeassistant.components.cover import (
CoverEntity,
CoverEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from . import DevoloHomeControlConfigEntry
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: DevoloHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Get all cover devices and setup them via config entry."""
@ -28,7 +29,7 @@ async def async_setup_entry(
device_instance=device,
element_uid=multi_level_switch,
)
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]
for gateway in entry.runtime_data
for device in gateway.multi_level_switch_devices
for multi_level_switch in device.multi_level_switch_property
if multi_level_switch.startswith("devolo.Blinds")

View File

@ -4,24 +4,19 @@ from __future__ import annotations
from typing import Any
from devolo_home_control_api.homecontrol import HomeControl
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from . import DevoloHomeControlConfigEntry
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: DevoloHomeControlConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
gateways: list[HomeControl] = hass.data[DOMAIN][entry.entry_id]["gateways"]
device_info = [
{
"gateway": {
@ -38,7 +33,7 @@ async def async_get_config_entry_diagnostics(
for device_id, properties in gateway.devices.items()
],
}
for gateway in gateways
for gateway in entry.runtime_data
]
return {

View File

@ -8,16 +8,17 @@ from devolo_home_control_api.devices.zwave import Zwave
from devolo_home_control_api.homecontrol import HomeControl
from homeassistant.components.light import ATTR_BRIGHTNESS, ColorMode, LightEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from . import DevoloHomeControlConfigEntry
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: DevoloHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Get all light devices and setup them via config entry."""
@ -27,7 +28,7 @@ async def async_setup_entry(
device_instance=device,
element_uid=multi_level_switch.element_uid,
)
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]
for gateway in entry.runtime_data
for device in gateway.multi_level_switch_devices
for multi_level_switch in device.multi_level_switch_property.values()
if multi_level_switch.switch_type == "dimmer"

View File

@ -10,12 +10,11 @@ from homeassistant.components.sensor import (
SensorEntity,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from . import DevoloHomeControlConfigEntry
from .devolo_device import DevoloDeviceEntity
DEVICE_CLASS_MAPPING = {
@ -39,12 +38,14 @@ STATE_CLASS_MAPPING = {
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: DevoloHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Get all sensor devices and setup them via config entry."""
entities: list[SensorEntity] = []
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]:
for gateway in entry.runtime_data:
entities.extend(
DevoloGenericMultiLevelDeviceEntity(
homecontrol=gateway,

View File

@ -6,16 +6,17 @@ from devolo_home_control_api.devices.zwave import Zwave
from devolo_home_control_api.homecontrol import HomeControl
from homeassistant.components.siren import ATTR_TONE, SirenEntity, SirenEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from . import DevoloHomeControlConfigEntry
from .devolo_multi_level_switch import DevoloMultiLevelSwitchDeviceEntity
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: DevoloHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Get all binary sensor and multi level sensor devices and setup them via config entry."""
@ -25,7 +26,7 @@ async def async_setup_entry(
device_instance=device,
element_uid=multi_level_switch,
)
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]
for gateway in entry.runtime_data
for device in gateway.multi_level_switch_devices
for multi_level_switch in device.multi_level_switch_property
if multi_level_switch.startswith("devolo.SirenMultiLevelSwitch")

View File

@ -8,16 +8,17 @@ from devolo_home_control_api.devices.zwave import Zwave
from devolo_home_control_api.homecontrol import HomeControl
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from . import DevoloHomeControlConfigEntry
from .devolo_device import DevoloDeviceEntity
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: DevoloHomeControlConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Get all devices and setup the switch devices via config entry."""
@ -27,7 +28,7 @@ async def async_setup_entry(
device_instance=device,
element_uid=binary_switch,
)
for gateway in hass.data[DOMAIN][entry.entry_id]["gateways"]
for gateway in entry.runtime_data
for device in gateway.binary_switch_devices
for binary_switch in device.binary_switch_property
# Exclude the binary switch which also has multi_level_switches here,

View File

@ -12,16 +12,15 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.httpx_client import get_async_client
from .const import DOMAIN
from .coordinator import DiscovergyUpdateCoordinator
PLATFORMS = [Platform.SENSOR]
DiscovergyConfigEntry = ConfigEntry[list[DiscovergyUpdateCoordinator]]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: DiscovergyConfigEntry) -> bool:
"""Set up Discovergy from a config entry."""
hass.data.setdefault(DOMAIN, {})
client = Discovergy(
email=entry.data[CONF_EMAIL],
password=entry.data[CONF_PASSWORD],
@ -53,7 +52,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await coordinator.async_config_entry_first_refresh()
coordinators.append(coordinator)
hass.data[DOMAIN][entry.entry_id] = coordinators
entry.runtime_data = coordinators
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
entry.async_on_unload(entry.add_update_listener(async_reload_entry))
@ -63,11 +62,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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)
async def async_reload_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:

View File

@ -6,11 +6,9 @@ from dataclasses import asdict
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN
from .coordinator import DiscovergyUpdateCoordinator
from . import DiscovergyConfigEntry
TO_REDACT_METER = {
"serial_number",
@ -22,14 +20,13 @@ TO_REDACT_METER = {
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: DiscovergyConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
flattened_meter: list[dict] = []
last_readings: dict[str, dict] = {}
coordinators: list[DiscovergyUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id]
for coordinator in coordinators:
for coordinator in entry.runtime_data:
# make a dict of meter data and redact some data
flattened_meter.append(
async_redact_data(asdict(coordinator.meter), TO_REDACT_METER)

View File

@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
EntityCategory,
UnitOfElectricPotential,
@ -25,6 +24,7 @@ from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import DiscovergyConfigEntry
from .const import DOMAIN, MANUFACTURER
from .coordinator import DiscovergyUpdateCoordinator
@ -163,13 +163,13 @@ ADDITIONAL_SENSORS: tuple[DiscovergySensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: DiscovergyConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Discovergy sensors."""
coordinators: list[DiscovergyUpdateCoordinator] = hass.data[DOMAIN][entry.entry_id]
entities: list[DiscovergySensor] = []
for coordinator in coordinators:
for coordinator in entry.runtime_data:
sensors: tuple[DiscovergySensorEntityDescription, ...] = ()
# select sensor descriptions based on meter type and combine with additional sensors

View File

@ -2,27 +2,27 @@
from __future__ import annotations
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from .const import DOMAIN, PLATFORMS
from .coordinator import DwdWeatherWarningsCoordinator
from .const import PLATFORMS
from .coordinator import DwdWeatherWarningsConfigEntry, DwdWeatherWarningsCoordinator
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, entry: DwdWeatherWarningsConfigEntry
) -> bool:
"""Set up a config entry."""
coordinator = DwdWeatherWarningsCoordinator(hass)
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: DwdWeatherWarningsConfigEntry
) -> 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

@ -19,11 +19,13 @@ from .const import (
from .exceptions import EntityNotFoundError
from .util import get_position_data
DwdWeatherWarningsConfigEntry = ConfigEntry["DwdWeatherWarningsCoordinator"]
class DwdWeatherWarningsCoordinator(DataUpdateCoordinator[None]):
"""Custom coordinator for the dwd_weather_warnings integration."""
config_entry: ConfigEntry
config_entry: DwdWeatherWarningsConfigEntry
api: DwdWeatherWarningsAPI
def __init__(self, hass: HomeAssistant) -> None:

View File

@ -14,7 +14,6 @@ from __future__ import annotations
from typing import Any
from homeassistant.components.sensor import SensorEntity, SensorEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -40,7 +39,7 @@ from .const import (
DEFAULT_NAME,
DOMAIN,
)
from .coordinator import DwdWeatherWarningsCoordinator
from .coordinator import DwdWeatherWarningsConfigEntry, DwdWeatherWarningsCoordinator
SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
SensorEntityDescription(
@ -55,10 +54,12 @@ SENSOR_TYPES: tuple[SensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: DwdWeatherWarningsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up entities from config entry."""
coordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
[
@ -80,7 +81,7 @@ class DwdWeatherWarningsSensor(
def __init__(
self,
coordinator: DwdWeatherWarningsCoordinator,
entry: ConfigEntry,
entry: DwdWeatherWarningsConfigEntry,
description: SensorEntityDescription,
) -> None:
"""Initialize a DWD-Weather-Warnings sensor."""

View File

@ -85,6 +85,6 @@ class EcobeeNotifyEntity(EcobeeBaseEntity, NotifyEntity):
f"{self.thermostat["identifier"]}_notify_{thermostat_index}"
)
def send_message(self, message: str) -> None:
def send_message(self, message: str, title: str | None = None) -> None:
"""Send a message."""
self.data.ecobee.send_message(self.thermostat_index, message)

View File

@ -17,7 +17,7 @@ from homeassistant.components.light import (
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
@ -94,7 +94,7 @@ class GoveeLight(CoordinatorEntity[GoveeLocalApiCoordinator], LightEntity):
name=device.sku,
manufacturer=MANUFACTURER,
model=device.sku,
connections={(CONNECTION_NETWORK_MAC, device.fingerprint)},
serial_number=device.fingerprint,
)
@property

View File

@ -6,5 +6,5 @@
"dependencies": ["network"],
"documentation": "https://www.home-assistant.io/integrations/govee_light_local",
"iot_class": "local_push",
"requirements": ["govee-local-api==1.4.4"]
"requirements": ["govee-local-api==1.4.5"]
}

View File

@ -1,7 +1,9 @@
"""The habitica integration."""
from http import HTTPStatus
import logging
from aiohttp import ClientResponseError
from habitipy.aio import HabitipyAsync
import voluptuous as vol
@ -16,6 +18,7 @@ from homeassistant.const import (
Platform,
)
from homeassistant.core import HomeAssistant, ServiceCall
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import ConfigType
@ -30,9 +33,12 @@ from .const import (
EVENT_API_CALL_SUCCESS,
SERVICE_API_CALL,
)
from .coordinator import HabiticaDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
HabiticaConfigEntry = ConfigEntry[HabiticaDataUpdateCoordinator]
SENSORS_TYPES = ["name", "hp", "maxHealth", "mp", "maxMP", "exp", "toNextLevel", "lvl"]
INSTANCE_SCHEMA = vol.All(
@ -104,7 +110,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
return True
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: HabiticaConfigEntry) -> bool:
"""Set up habitica from a config entry."""
class HAHabitipyAsync(HabitipyAsync):
@ -120,7 +126,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
api = None
for entry in entries:
if entry.data[CONF_NAME] == name:
api = hass.data[DOMAIN].get(entry.entry_id)
api = entry.runtime_data.api
break
if api is None:
_LOGGER.error("API_CALL: User '%s' not configured", name)
@ -139,24 +145,40 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
EVENT_API_CALL_SUCCESS, {ATTR_NAME: name, ATTR_PATH: path, ATTR_DATA: data}
)
data = hass.data.setdefault(DOMAIN, {})
config = entry.data
websession = async_get_clientsession(hass)
url = config[CONF_URL]
username = config[CONF_API_USER]
password = config[CONF_API_KEY]
name = config.get(CONF_NAME)
config_dict = {"url": url, "login": username, "password": password}
api = HAHabitipyAsync(config_dict)
user = await api.user.get()
if name is None:
url = entry.data[CONF_URL]
username = entry.data[CONF_API_USER]
password = entry.data[CONF_API_KEY]
api = HAHabitipyAsync(
{
"url": url,
"login": username,
"password": password,
}
)
try:
user = await api.user.get(userFields="profile")
except ClientResponseError as e:
if e.status == HTTPStatus.TOO_MANY_REQUESTS:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception",
) from e
raise ConfigEntryNotReady(e) from e
if not entry.data.get(CONF_NAME):
name = user["profile"]["name"]
hass.config_entries.async_update_entry(
entry,
data={**entry.data, CONF_NAME: name},
)
data[entry.entry_id] = api
coordinator = HabiticaDataUpdateCoordinator(hass, api)
await coordinator.async_config_entry_first_refresh()
entry.runtime_data = coordinator
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
if not hass.services.has_service(DOMAIN, SERVICE_API_CALL):
@ -169,10 +191,6 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> 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)
if len(hass.config_entries.async_entries(DOMAIN)) == 1:
hass.services.async_remove(DOMAIN, SERVICE_API_CALL)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -0,0 +1,56 @@
"""DataUpdateCoordinator for the Habitica integration."""
from __future__ import annotations
from dataclasses import dataclass
from datetime import timedelta
import logging
from typing import Any
from aiohttp import ClientResponseError
from habitipy.aio import HabitipyAsync
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN
_LOGGER = logging.getLogger(__name__)
@dataclass
class HabiticaData:
"""Coordinator data class."""
user: dict[str, Any]
tasks: list[dict]
class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
"""Habitica Data Update Coordinator."""
config_entry: ConfigEntry
def __init__(self, hass: HomeAssistant, habitipy: HabitipyAsync) -> None:
"""Initialize the Habitica data coordinator."""
super().__init__(
hass,
_LOGGER,
name=DOMAIN,
update_interval=timedelta(seconds=30),
)
self.api = habitipy
async def _async_update_data(self) -> HabiticaData:
user_fields = set(self.async_contexts())
try:
user_response = await self.api.user.get(userFields=",".join(user_fields))
tasks_response = []
for task_type in ("todos", "dailys", "habits", "rewards"):
tasks_response.extend(await self.api.tasks.user.get(type=task_type))
except ClientResponseError as error:
raise UpdateFailed(f"Error communicating with API: {error}") from error
return HabiticaData(user=user_response, tasks=tasks_response)

View File

@ -4,13 +4,9 @@ from __future__ import annotations
from collections import namedtuple
from dataclasses import dataclass
from datetime import timedelta
from enum import StrEnum
from http import HTTPStatus
import logging
from typing import TYPE_CHECKING, Any
from aiohttp import ClientResponseError
from typing import TYPE_CHECKING, cast
from homeassistant.components.sensor import (
SensorDeviceClass,
@ -22,14 +18,15 @@ from homeassistant.const import CONF_NAME, CONF_URL
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import Throttle
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import HabiticaConfigEntry
from .const import DOMAIN, MANUFACTURER, NAME
from .coordinator import HabiticaDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
MIN_TIME_BETWEEN_UPDATES = timedelta(minutes=15)
@dataclass(kw_only=True, frozen=True)
class HabitipySensorEntityDescription(SensorEntityDescription):
@ -122,14 +119,14 @@ SENSOR_DESCRIPTIONS: dict[str, HabitipySensorEntityDescription] = {
SensorType = namedtuple("SensorType", ["name", "icon", "unit", "path"])
TASKS_TYPES = {
"habits": SensorType(
"Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habits"]
"Habits", "mdi:clipboard-list-outline", "n_of_tasks", ["habit"]
),
"dailys": SensorType(
"Dailys", "mdi:clipboard-list-outline", "n_of_tasks", ["dailys"]
"Dailys", "mdi:clipboard-list-outline", "n_of_tasks", ["daily"]
),
"todos": SensorType("TODOs", "mdi:clipboard-list-outline", "n_of_tasks", ["todos"]),
"todos": SensorType("TODOs", "mdi:clipboard-list-outline", "n_of_tasks", ["todo"]),
"rewards": SensorType(
"Rewards", "mdi:clipboard-list-outline", "n_of_tasks", ["rewards"]
"Rewards", "mdi:clipboard-list-outline", "n_of_tasks", ["reward"]
),
}
@ -163,79 +160,26 @@ TASKS_MAP = {
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: HabiticaConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the habitica sensors."""
name = config_entry.data[CONF_NAME]
sensor_data = HabitipyData(hass.data[DOMAIN][config_entry.entry_id])
await sensor_data.update()
coordinator = config_entry.runtime_data
entities: list[SensorEntity] = [
HabitipySensor(sensor_data, description, config_entry)
HabitipySensor(coordinator, description, config_entry)
for description in SENSOR_DESCRIPTIONS.values()
]
entities.extend(
HabitipyTaskSensor(name, task_type, sensor_data, config_entry)
HabitipyTaskSensor(name, task_type, coordinator, config_entry)
for task_type in TASKS_TYPES
)
async_add_entities(entities, True)
class HabitipyData:
"""Habitica API user data cache."""
tasks: dict[str, Any]
def __init__(self, api) -> None:
"""Habitica API user data cache."""
self.api = api
self.data = None
self.tasks = {}
@Throttle(MIN_TIME_BETWEEN_UPDATES)
async def update(self):
"""Get a new fix from Habitica servers."""
try:
self.data = await self.api.user.get()
except ClientResponseError as error:
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
_LOGGER.warning(
(
"Sensor data update for %s has too many API requests;"
" Skipping the update"
),
DOMAIN,
)
else:
_LOGGER.error(
"Count not update sensor data for %s (%s)",
DOMAIN,
error,
)
for task_type in TASKS_TYPES:
try:
self.tasks[task_type] = await self.api.tasks.user.get(type=task_type)
except ClientResponseError as error:
if error.status == HTTPStatus.TOO_MANY_REQUESTS:
_LOGGER.warning(
(
"Sensor data update for %s has too many API requests;"
" Skipping the update"
),
DOMAIN,
)
else:
_LOGGER.error(
"Count not update sensor data for %s (%s)",
DOMAIN,
error,
)
class HabitipySensor(SensorEntity):
class HabitipySensor(CoordinatorEntity[HabiticaDataUpdateCoordinator], SensorEntity):
"""A generic Habitica sensor."""
_attr_has_entity_name = True
@ -243,15 +187,14 @@ class HabitipySensor(SensorEntity):
def __init__(
self,
coordinator,
coordinator: HabiticaDataUpdateCoordinator,
entity_description: HabitipySensorEntityDescription,
entry: ConfigEntry,
) -> None:
"""Initialize a generic Habitica sensor."""
super().__init__()
super().__init__(coordinator, context=entity_description.value_path[0])
if TYPE_CHECKING:
assert entry.unique_id
self.coordinator = coordinator
self.entity_description = entity_description
self._attr_unique_id = f"{entry.unique_id}_{entity_description.key}"
self._attr_device_info = DeviceInfo(
@ -263,25 +206,27 @@ class HabitipySensor(SensorEntity):
identifiers={(DOMAIN, entry.unique_id)},
)
async def async_update(self) -> None:
"""Update Sensor state."""
await self.coordinator.update()
data = self.coordinator.data
@property
def native_value(self) -> StateType:
"""Return the state of the device."""
data = self.coordinator.data.user
for element in self.entity_description.value_path:
data = data[element]
self._attr_native_value = data
return cast(StateType, data)
class HabitipyTaskSensor(SensorEntity):
class HabitipyTaskSensor(
CoordinatorEntity[HabiticaDataUpdateCoordinator], SensorEntity
):
"""A Habitica task sensor."""
def __init__(self, name, task_name, updater, entry):
def __init__(self, name, task_name, coordinator, entry):
"""Initialize a generic Habitica task."""
super().__init__(coordinator)
self._name = name
self._task_name = task_name
self._task_type = TASKS_TYPES[task_name]
self._state = None
self._updater = updater
self._attr_unique_id = f"{entry.unique_id}_{task_name}"
self._attr_device_info = DeviceInfo(
entry_type=DeviceEntryType.SERVICE,
@ -292,14 +237,6 @@ class HabitipyTaskSensor(SensorEntity):
identifiers={(DOMAIN, entry.unique_id)},
)
async def async_update(self) -> None:
"""Update Condition and Forecast."""
await self._updater.update()
all_tasks = self._updater.tasks
for element in self._task_type.path:
tasks_length = len(all_tasks[element])
self._state = tasks_length
@property
def icon(self):
"""Return the icon to use in the frontend, if any."""
@ -313,26 +250,29 @@ class HabitipyTaskSensor(SensorEntity):
@property
def native_value(self):
"""Return the state of the device."""
return self._state
return len(
[
task
for task in self.coordinator.data.tasks
if task.get("type") in self._task_type.path
]
)
@property
def extra_state_attributes(self):
"""Return the state attributes of all user tasks."""
if self._updater.tasks is not None:
all_received_tasks = self._updater.tasks
for element in self._task_type.path:
received_tasks = all_received_tasks[element]
attrs = {}
attrs = {}
# Map tasks to TASKS_MAP
for received_task in received_tasks:
# Map tasks to TASKS_MAP
for received_task in self.coordinator.data.tasks:
if received_task.get("type") in self._task_type.path:
task_id = received_task[TASKS_MAP_ID]
task = {}
for map_key, map_value in TASKS_MAP.items():
if value := received_task.get(map_value):
task[map_key] = value
attrs[task_id] = task
return attrs
return attrs
@property
def native_unit_of_measurement(self):

View File

@ -59,6 +59,11 @@
}
}
},
"exceptions": {
"setup_rate_limit_exception": {
"message": "Currently rate limited, try again later"
}
},
"services": {
"api_call": {
"name": "API name",

View File

@ -35,7 +35,11 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import ATTR_TEMPERATURE, UnitOfTemperature
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import device_registry as dr, issue_registry as ir
from homeassistant.helpers import (
device_registry as dr,
entity_registry as er,
issue_registry as ir,
)
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
@ -99,7 +103,7 @@ async def async_setup_entry(
heat_away_temp = entry.options.get(CONF_HEAT_AWAY_TEMPERATURE)
data: HoneywellData = hass.data[DOMAIN][entry.entry_id]
_async_migrate_unique_id(hass, data.devices)
async_add_entities(
[
HoneywellUSThermostat(data, device, cool_away_temp, heat_away_temp)
@ -109,6 +113,21 @@ async def async_setup_entry(
remove_stale_devices(hass, entry, data.devices)
def _async_migrate_unique_id(
hass: HomeAssistant, devices: dict[str, SomeComfortDevice]
) -> None:
"""Migrate entities to string."""
entity_registry = er.async_get(hass)
for device in devices.values():
entity_id = entity_registry.async_get_entity_id(
"climate", DOMAIN, device.deviceid
)
if entity_id is not None:
entity_registry.async_update_entity(
entity_id, new_unique_id=str(device.deviceid)
)
def remove_stale_devices(
hass: HomeAssistant,
config_entry: ConfigEntry,
@ -161,7 +180,7 @@ class HoneywellUSThermostat(ClimateEntity):
self._away = False
self._retry = 0
self._attr_unique_id = device.deviceid
self._attr_unique_id = str(device.deviceid)
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, device.deviceid)},

View File

@ -75,7 +75,7 @@ CONFIG_SCHEMA = vol.Schema(
vol.Optional(CONF_FOLDER, default="INBOX"): str,
vol.Optional(CONF_SEARCH, default="UnSeen UnDeleted"): str,
# The default for new entries is to not include text and headers
vol.Optional(CONF_EVENT_MESSAGE_DATA, default=[]): cv.ensure_list,
vol.Optional(CONF_EVENT_MESSAGE_DATA, default=[]): EVENT_MESSAGE_DATA_SELECTOR,
}
)
CONFIG_SCHEMA_ADVANCED = {

View File

@ -5,5 +5,5 @@
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/imgw_pib",
"iot_class": "cloud_polling",
"requirements": ["imgw_pib==1.0.0"]
"requirements": ["imgw_pib==1.0.1"]
}

View File

@ -3,7 +3,7 @@
from __future__ import annotations
from homeassistant.components import persistent_notification
from homeassistant.components.notify import NotifyEntity
from homeassistant.components.notify import NotifyEntity, NotifyEntityFeature
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import DeviceInfo
@ -25,6 +25,12 @@ async def async_setup_entry(
device_name="MyBox",
entity_name="Personal notifier",
),
DemoNotify(
unique_id="just_notify_me_title",
device_name="MyBox",
entity_name="Personal notifier with title",
supported_features=NotifyEntityFeature.TITLE,
),
]
)
@ -40,15 +46,19 @@ class DemoNotify(NotifyEntity):
unique_id: str,
device_name: str,
entity_name: str | None,
supported_features: NotifyEntityFeature = NotifyEntityFeature(0),
) -> None:
"""Initialize the Demo button entity."""
self._attr_unique_id = unique_id
self._attr_supported_features = supported_features
self._attr_device_info = DeviceInfo(
identifiers={(DOMAIN, unique_id)},
name=device_name,
)
self._attr_name = entity_name
async def async_send_message(self, message: str) -> None:
async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send out a persistent notification."""
persistent_notification.async_create(self.hass, message, "Demo notification")
persistent_notification.async_create(
self.hass, message, title or "Demo notification"
)

View File

@ -108,6 +108,6 @@ class KNXNotify(KnxEntity, NotifyEntity):
self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY)
self._attr_unique_id = str(self._device.remote_value.group_address)
async def async_send_message(self, message: str) -> None:
async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a notification to knx bus."""
await self._device.set(message)

View File

@ -10,19 +10,18 @@ from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.util import slugify
from .const import CONF_STORAGE_KEY, CONF_TODO_LIST_NAME, DOMAIN
from .const import CONF_STORAGE_KEY, CONF_TODO_LIST_NAME
from .store import LocalTodoListStore
PLATFORMS: list[Platform] = [Platform.TODO]
STORAGE_PATH = ".storage/local_todo.{key}.ics"
LocalTodoConfigEntry = ConfigEntry[LocalTodoListStore]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: LocalTodoConfigEntry) -> bool:
"""Set up Local To-do from a config entry."""
hass.data.setdefault(DOMAIN, {})
path = Path(hass.config.path(STORAGE_PATH.format(key=entry.data[CONF_STORAGE_KEY])))
store = LocalTodoListStore(hass, path)
try:
@ -30,7 +29,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except OSError as err:
raise ConfigEntryNotReady("Failed to load file {path}: {err}") from err
hass.data[DOMAIN][entry.entry_id] = store
entry.runtime_data = store
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
@ -39,10 +38,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def async_remove_entry(hass: HomeAssistant, entry: ConfigEntry) -> None:

View File

@ -14,14 +14,14 @@ from homeassistant.components.todo import (
TodoListEntity,
TodoListEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.setup import SetupPhases, async_pause_setup
from homeassistant.util import dt as dt_util
from .const import CONF_TODO_LIST_NAME, DOMAIN
from . import LocalTodoConfigEntry
from .const import CONF_TODO_LIST_NAME
from .store import LocalTodoListStore
_LOGGER = logging.getLogger(__name__)
@ -63,12 +63,12 @@ def _migrate_calendar(calendar: Calendar) -> bool:
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: LocalTodoConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the local_todo todo platform."""
store: LocalTodoListStore = hass.data[DOMAIN][config_entry.entry_id]
store = config_entry.runtime_data
ics = await store.async_load()
with async_pause_setup(hass, SetupPhases.WAIT_IMPORT_PACKAGES):

View File

@ -106,4 +106,4 @@ class LutronEventEntity(LutronKeypad, EventEntity):
}
self.hass.bus.fire("lutron_event", data)
self._trigger_event(action)
self.async_write_ha_state()
self.schedule_update_ha_state()

View File

@ -398,6 +398,8 @@ class MatterLight(MatterEntity, LightEntity):
def _check_transition_blocklist(self) -> None:
"""Check if this device is reported to have non working transitions."""
device_info = self._endpoint.device_info
if isinstance(device_info, clusters.BridgedDeviceBasicInformation):
return
if (
device_info.vendorID,
device_info.productID,

View File

@ -21,8 +21,12 @@ PLATFORMS = [Platform.WEATHER]
_LOGGER = logging.getLogger(__name__)
MetWeatherConfigEntry = ConfigEntry[MetDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
async def async_setup_entry(
hass: HomeAssistant, config_entry: MetWeatherConfigEntry
) -> bool:
"""Set up Met as config entry."""
# Don't setup if tracking home location and latitude or longitude isn't set.
# Also, filters out our onboarding default location.
@ -44,10 +48,10 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
if config_entry.data.get(CONF_TRACK_HOME, False):
coordinator.track_home()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][config_entry.entry_id] = coordinator
config_entry.runtime_data = coordinator
config_entry.async_on_unload(config_entry.add_update_listener(async_update_entry))
config_entry.async_on_unload(coordinator.untrack_home)
await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
@ -56,19 +60,14 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b
return True
async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool:
async def async_unload_entry(
hass: HomeAssistant, config_entry: MetWeatherConfigEntry
) -> bool:
"""Unload a config entry."""
unload_ok = await hass.config_entries.async_unload_platforms(
config_entry, PLATFORMS
)
hass.data[DOMAIN][config_entry.entry_id].untrack_home()
hass.data[DOMAIN].pop(config_entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS)
async def async_update_entry(hass: HomeAssistant, config_entry: ConfigEntry):
async def async_update_entry(hass: HomeAssistant, config_entry: MetWeatherConfigEntry):
"""Reload Met component when options changed."""
await hass.config_entries.async_reload(config_entry.entry_id)

View File

@ -21,7 +21,6 @@ from homeassistant.components.weather import (
SingleCoordinatorWeatherEntity,
WeatherEntityFeature,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_LATITUDE,
CONF_LONGITUDE,
@ -37,6 +36,7 @@ from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.unit_system import METRIC_SYSTEM
from . import MetWeatherConfigEntry
from .const import (
ATTR_CONDITION_CLEAR_NIGHT,
ATTR_CONDITION_SUNNY,
@ -53,11 +53,11 @@ DEFAULT_NAME = "Met.no"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: MetWeatherConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add a weather entity from a config_entry."""
coordinator: MetDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
entity_registry = er.async_get(hass)
name: str | None
@ -120,7 +120,7 @@ class MetWeather(SingleCoordinatorWeatherEntity[MetDataUpdateCoordinator]):
def __init__(
self,
coordinator: MetDataUpdateCoordinator,
config_entry: ConfigEntry,
config_entry: MetWeatherConfigEntry,
name: str,
is_metric: bool,
) -> None:

View File

@ -91,8 +91,6 @@ class MetOfficeWeather(
CoordinatorWeatherEntity[
TimestampDataUpdateCoordinator[MetOfficeData],
TimestampDataUpdateCoordinator[MetOfficeData],
TimestampDataUpdateCoordinator[MetOfficeData],
TimestampDataUpdateCoordinator[MetOfficeData], # Can be removed in Python 3.12
]
):
"""Implementation of a Met Office weather condition."""

View File

@ -218,8 +218,7 @@ class FlowHandler(ConfigFlow, domain=DOMAIN):
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Handle re-authentication with Aladdin Connect."""
"""Handle re-authentication with MQTT broker."""
self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"])
return await self.async_step_reauth_confirm()

View File

@ -83,7 +83,7 @@ class MqttNotify(MqttEntity, NotifyEntity):
async def _subscribe_topics(self) -> None:
"""(Re)Subscribe to topics."""
async def async_send_message(self, message: str) -> None:
async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message."""
payload = self._command_template(message)
await self.async_publish(

View File

@ -2,17 +2,14 @@
from __future__ import annotations
import asyncio
import logging
from typing import cast
from typing import TYPE_CHECKING
from aiohttp.client_exceptions import ClientConnectorError, ClientError
from nettigo_air_monitor import (
ApiError,
AuthFailedError,
ConnectionOptions,
InvalidSensorDataError,
NAMSensors,
NettigoAirMonitor,
)
@ -21,25 +18,20 @@ from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_HOST, CONF_PASSWORD, CONF_USERNAME, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr, entity_registry as er
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
ATTR_SDS011,
ATTR_SPS30,
DEFAULT_UPDATE_INTERVAL,
DOMAIN,
MANUFACTURER,
)
from .const import ATTR_SDS011, ATTR_SPS30, DOMAIN
from .coordinator import NAMDataUpdateCoordinator
_LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.BUTTON, Platform.SENSOR]
NAMConfigEntry = ConfigEntry[NAMDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: NAMConfigEntry) -> bool:
"""Set up Nettigo as config entry."""
host: str = entry.data[CONF_HOST]
username: str | None = entry.data.get(CONF_USERNAME)
@ -60,11 +52,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
except AuthFailedError as err:
raise ConfigEntryAuthFailed from err
if TYPE_CHECKING:
assert entry.unique_id
coordinator = NAMDataUpdateCoordinator(hass, nam, entry.unique_id)
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)
@ -81,57 +75,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: NAMConfigEntry) -> 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
class NAMDataUpdateCoordinator(DataUpdateCoordinator[NAMSensors]): # pylint: disable=hass-enforce-coordinator-module
"""Class to manage fetching Nettigo Air Monitor data."""
def __init__(
self,
hass: HomeAssistant,
nam: NettigoAirMonitor,
unique_id: str | None,
) -> None:
"""Initialize."""
self._unique_id = unique_id
self.nam = nam
super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL
)
async def _async_update_data(self) -> NAMSensors:
"""Update data via library."""
try:
async with asyncio.timeout(10):
data = await self.nam.async_update()
# We do not need to catch AuthFailed exception here because sensor data is
# always available without authorization.
except (ApiError, ClientConnectorError, InvalidSensorDataError) as error:
raise UpdateFailed(error) from error
return data
@property
def unique_id(self) -> str | None:
"""Return a unique_id."""
return self._unique_id
@property
def device_info(self) -> DeviceInfo:
"""Return the device info."""
return DeviceInfo(
connections={(dr.CONNECTION_NETWORK_MAC, cast(str, self._unique_id))},
name="Nettigo Air Monitor",
sw_version=self.nam.software_version,
manufacturer=MANUFACTURER,
configuration_url=f"http://{self.nam.host}/",
)
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -9,14 +9,12 @@ from homeassistant.components.button import (
ButtonEntity,
ButtonEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import NAMDataUpdateCoordinator
from .const import DOMAIN
from . import NAMConfigEntry, NAMDataUpdateCoordinator
PARALLEL_UPDATES = 1
@ -30,10 +28,10 @@ RESTART_BUTTON: ButtonEntityDescription = ButtonEntityDescription(
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant, entry: NAMConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Add a Nettigo Air Monitor entities from a config_entry."""
coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
buttons: list[NAMButton] = []
buttons.append(NAMButton(coordinator, RESTART_BUTTON))

View File

@ -0,0 +1,57 @@
"""The Nettigo Air Monitor coordinator."""
import asyncio
import logging
from aiohttp.client_exceptions import ClientConnectorError
from nettigo_air_monitor import (
ApiError,
InvalidSensorDataError,
NAMSensors,
NettigoAirMonitor,
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC, DeviceInfo
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DEFAULT_UPDATE_INTERVAL, DOMAIN, MANUFACTURER
_LOGGER = logging.getLogger(__name__)
class NAMDataUpdateCoordinator(DataUpdateCoordinator[NAMSensors]):
"""Class to manage fetching Nettigo Air Monitor data."""
def __init__(
self,
hass: HomeAssistant,
nam: NettigoAirMonitor,
unique_id: str,
) -> None:
"""Initialize."""
self.unique_id = unique_id
self.device_info = DeviceInfo(
connections={(CONNECTION_NETWORK_MAC, unique_id)},
name="Nettigo Air Monitor",
sw_version=nam.software_version,
manufacturer=MANUFACTURER,
configuration_url=f"http://{nam.host}/",
)
self.nam = nam
super().__init__(
hass, _LOGGER, name=DOMAIN, update_interval=DEFAULT_UPDATE_INTERVAL
)
async def _async_update_data(self) -> NAMSensors:
"""Update data via library."""
try:
async with asyncio.timeout(10):
data = await self.nam.async_update()
# We do not need to catch AuthFailed exception here because sensor data is
# always available without authorization.
except (ApiError, ClientConnectorError, InvalidSensorDataError) as error:
raise UpdateFailed(error) from error
return data

View File

@ -6,21 +6,19 @@ from dataclasses import asdict
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from . import NAMDataUpdateCoordinator
from .const import DOMAIN
from . import NAMConfigEntry
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: NAMConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][config_entry.entry_id]
coordinator = config_entry.runtime_data
return {
"info": async_redact_data(config_entry.data, TO_REDACT),

View File

@ -8,7 +8,7 @@
"iot_class": "local_polling",
"loggers": ["nettigo_air_monitor"],
"quality_scale": "platinum",
"requirements": ["nettigo-air-monitor==3.0.0"],
"requirements": ["nettigo-air-monitor==3.0.1"],
"zeroconf": [
{
"type": "_http._tcp.local.",

View File

@ -16,7 +16,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
CONCENTRATION_PARTS_PER_MILLION,
@ -33,7 +32,7 @@ from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.util.dt import utcnow
from . import NAMDataUpdateCoordinator
from . import NAMConfigEntry, NAMDataUpdateCoordinator
from .const import (
ATTR_BME280_HUMIDITY,
ATTR_BME280_PRESSURE,
@ -347,10 +346,10 @@ SENSORS: tuple[NAMSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant, entry: NAMConfigEntry, async_add_entities: AddEntitiesCallback
) -> None:
"""Add a Nettigo Air Monitor entities from a config_entry."""
coordinator: NAMDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
# Due to the change of the attribute name of two sensors, it is necessary to migrate
# the unique_ids to the new names.

View File

@ -30,8 +30,10 @@ CONFIG_SCHEMA = cv.removed(DOMAIN, raise_if_present=False)
_LOGGER = logging.getLogger(__name__)
NextcloudConfigEntry = ConfigEntry[NextcloudDataUpdateCoordinator]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: NextcloudConfigEntry) -> bool:
"""Set up the Nextcloud integration."""
# migrate old entity unique ids
@ -71,17 +73,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: NextcloudConfigEntry) -> bool:
"""Unload Nextcloud integration."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
if not hass.data[DOMAIN]:
hass.data.pop(DOMAIN)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)

View File

@ -8,13 +8,11 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import NextcloudDataUpdateCoordinator
from . import NextcloudConfigEntry
from .entity import NextcloudEntity
BINARY_SENSORS: Final[list[BinarySensorEntityDescription]] = [
@ -54,10 +52,12 @@ BINARY_SENSORS: Final[list[BinarySensorEntityDescription]] = [
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: NextcloudConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Nextcloud binary sensors."""
coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
NextcloudBinarySensor(coordinator, entry, sensor)
for sensor in BINARY_SENSORS

View File

@ -2,11 +2,11 @@
from urllib.parse import urlparse
from homeassistant.config_entries import ConfigEntry
from homeassistant.helpers.device_registry import DeviceInfo
from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import NextcloudConfigEntry
from .const import DOMAIN
from .coordinator import NextcloudDataUpdateCoordinator
@ -19,7 +19,7 @@ class NextcloudEntity(CoordinatorEntity[NextcloudDataUpdateCoordinator]):
def __init__(
self,
coordinator: NextcloudDataUpdateCoordinator,
entry: ConfigEntry,
entry: NextcloudConfigEntry,
description: EntityDescription,
) -> None:
"""Initialize the Nextcloud sensor."""

View File

@ -13,7 +13,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
PERCENTAGE,
EntityCategory,
@ -24,8 +23,7 @@ from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utc_from_timestamp
from .const import DOMAIN
from .coordinator import NextcloudDataUpdateCoordinator
from . import NextcloudConfigEntry
from .entity import NextcloudEntity
UNIT_OF_LOAD: Final[str] = "load"
@ -602,10 +600,12 @@ SENSORS: Final[list[NextcloudSensorEntityDescription]] = [
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: NextcloudConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Nextcloud sensors."""
coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
async_add_entities(
NextcloudSensor(coordinator, entry, sensor)
for sensor in SENSORS

View File

@ -3,20 +3,20 @@
from __future__ import annotations
from homeassistant.components.update import UpdateEntity, UpdateEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from .const import DOMAIN
from .coordinator import NextcloudDataUpdateCoordinator
from . import NextcloudConfigEntry
from .entity import NextcloudEntity
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: NextcloudConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the Nextcloud update entity."""
coordinator: NextcloudDataUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
coordinator = entry.runtime_data
if coordinator.data.get("update_available") is None:
return
async_add_entities(

View File

@ -3,10 +3,21 @@
from __future__ import annotations
import asyncio
from dataclasses import dataclass
from datetime import timedelta
from aiohttp.client_exceptions import ClientConnectorError
from nextdns import ApiError, NextDns
from nextdns import (
AnalyticsDnssec,
AnalyticsEncryption,
AnalyticsIpVersions,
AnalyticsProtocols,
AnalyticsStatus,
ApiError,
ConnectionStatus,
NextDns,
Settings,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform
@ -23,7 +34,6 @@ from .const import (
ATTR_SETTINGS,
ATTR_STATUS,
CONF_PROFILE_ID,
DOMAIN,
UPDATE_INTERVAL_ANALYTICS,
UPDATE_INTERVAL_CONNECTION,
UPDATE_INTERVAL_SETTINGS,
@ -39,6 +49,22 @@ from .coordinator import (
NextDnsUpdateCoordinator,
)
NextDnsConfigEntry = ConfigEntry["NextDnsData"]
@dataclass
class NextDnsData:
"""Data for the NextDNS integration."""
connection: NextDnsUpdateCoordinator[ConnectionStatus]
dnssec: NextDnsUpdateCoordinator[AnalyticsDnssec]
encryption: NextDnsUpdateCoordinator[AnalyticsEncryption]
ip_versions: NextDnsUpdateCoordinator[AnalyticsIpVersions]
protocols: NextDnsUpdateCoordinator[AnalyticsProtocols]
settings: NextDnsUpdateCoordinator[Settings]
status: NextDnsUpdateCoordinator[AnalyticsStatus]
PLATFORMS = [Platform.BINARY_SENSOR, Platform.BUTTON, Platform.SENSOR, Platform.SWITCH]
COORDINATORS: list[tuple[str, type[NextDnsUpdateCoordinator], timedelta]] = [
(ATTR_CONNECTION, NextDnsConnectionUpdateCoordinator, UPDATE_INTERVAL_CONNECTION),
@ -51,7 +77,7 @@ COORDINATORS: list[tuple[str, type[NextDnsUpdateCoordinator], timedelta]] = [
]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_setup_entry(hass: HomeAssistant, entry: NextDnsConfigEntry) -> bool:
"""Set up NextDNS as config entry."""
api_key = entry.data[CONF_API_KEY]
profile_id = entry.data[CONF_PROFILE_ID]
@ -75,18 +101,13 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
await asyncio.gather(*tasks)
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinators
entry.runtime_data = NextDnsData(**coordinators)
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: NextDnsConfigEntry) -> bool:
"""Unload a config entry."""
unload_ok: bool = 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

@ -4,7 +4,6 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Generic
from nextdns import ConnectionStatus
@ -13,36 +12,33 @@ from homeassistant.components.binary_sensor import (
BinarySensorEntity,
BinarySensorEntityDescription,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTR_CONNECTION, DOMAIN
from .coordinator import CoordinatorDataT, NextDnsConnectionUpdateCoordinator
from . import NextDnsConfigEntry
from .coordinator import NextDnsUpdateCoordinator
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class NextDnsBinarySensorEntityDescription(
BinarySensorEntityDescription, Generic[CoordinatorDataT]
):
class NextDnsBinarySensorEntityDescription(BinarySensorEntityDescription):
"""NextDNS binary sensor entity description."""
state: Callable[[CoordinatorDataT, str], bool]
state: Callable[[ConnectionStatus, str], bool]
SENSORS = (
NextDnsBinarySensorEntityDescription[ConnectionStatus](
NextDnsBinarySensorEntityDescription(
key="this_device_nextdns_connection_status",
entity_category=EntityCategory.DIAGNOSTIC,
translation_key="device_connection_status",
device_class=BinarySensorDeviceClass.CONNECTIVITY,
state=lambda data, _: data.connected,
),
NextDnsBinarySensorEntityDescription[ConnectionStatus](
NextDnsBinarySensorEntityDescription(
key="this_device_profile_connection_status",
entity_category=EntityCategory.DIAGNOSTIC,
translation_key="device_profile_connection_status",
@ -54,13 +50,11 @@ SENSORS = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: NextDnsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add NextDNS entities from a config_entry."""
coordinator: NextDnsConnectionUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
ATTR_CONNECTION
]
coordinator = entry.runtime_data.connection
async_add_entities(
NextDnsBinarySensor(coordinator, description) for description in SENSORS
@ -68,7 +62,7 @@ async def async_setup_entry(
class NextDnsBinarySensor(
CoordinatorEntity[NextDnsConnectionUpdateCoordinator], BinarySensorEntity
CoordinatorEntity[NextDnsUpdateCoordinator[ConnectionStatus]], BinarySensorEntity
):
"""Define an NextDNS binary sensor."""
@ -77,7 +71,7 @@ class NextDnsBinarySensor(
def __init__(
self,
coordinator: NextDnsConnectionUpdateCoordinator,
coordinator: NextDnsUpdateCoordinator[ConnectionStatus],
description: NextDnsBinarySensorEntityDescription,
) -> None:
"""Initialize."""

View File

@ -2,15 +2,16 @@
from __future__ import annotations
from nextdns import AnalyticsStatus
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTR_STATUS, DOMAIN
from .coordinator import NextDnsStatusUpdateCoordinator
from . import NextDnsConfigEntry
from .coordinator import NextDnsUpdateCoordinator
PARALLEL_UPDATES = 1
@ -22,27 +23,26 @@ CLEAR_LOGS_BUTTON = ButtonEntityDescription(
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: NextDnsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add aNextDNS entities from a config_entry."""
coordinator: NextDnsStatusUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
ATTR_STATUS
]
coordinator = entry.runtime_data.status
buttons: list[NextDnsButton] = []
buttons.append(NextDnsButton(coordinator, CLEAR_LOGS_BUTTON))
async_add_entities(buttons)
async_add_entities([NextDnsButton(coordinator, CLEAR_LOGS_BUTTON)])
class NextDnsButton(CoordinatorEntity[NextDnsStatusUpdateCoordinator], ButtonEntity):
class NextDnsButton(
CoordinatorEntity[NextDnsUpdateCoordinator[AnalyticsStatus]], ButtonEntity
):
"""Define an NextDNS button."""
_attr_has_entity_name = True
def __init__(
self,
coordinator: NextDnsStatusUpdateCoordinator,
coordinator: NextDnsUpdateCoordinator[AnalyticsStatus],
description: ButtonEntityDescription,
) -> None:
"""Initialize."""

View File

@ -6,36 +6,25 @@ from dataclasses import asdict
from typing import Any
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_UNIQUE_ID
from homeassistant.core import HomeAssistant
from .const import (
ATTR_DNSSEC,
ATTR_ENCRYPTION,
ATTR_IP_VERSIONS,
ATTR_PROTOCOLS,
ATTR_SETTINGS,
ATTR_STATUS,
CONF_PROFILE_ID,
DOMAIN,
)
from . import NextDnsConfigEntry
from .const import CONF_PROFILE_ID
TO_REDACT = {CONF_API_KEY, CONF_PROFILE_ID, CONF_UNIQUE_ID}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, config_entry: ConfigEntry
hass: HomeAssistant, config_entry: NextDnsConfigEntry
) -> dict[str, Any]:
"""Return diagnostics for a config entry."""
coordinators = hass.data[DOMAIN][config_entry.entry_id]
dnssec_coordinator = coordinators[ATTR_DNSSEC]
encryption_coordinator = coordinators[ATTR_ENCRYPTION]
ip_versions_coordinator = coordinators[ATTR_IP_VERSIONS]
protocols_coordinator = coordinators[ATTR_PROTOCOLS]
settings_coordinator = coordinators[ATTR_SETTINGS]
status_coordinator = coordinators[ATTR_STATUS]
dnssec_coordinator = config_entry.runtime_data.dnssec
encryption_coordinator = config_entry.runtime_data.encryption
ip_versions_coordinator = config_entry.runtime_data.ip_versions
protocols_coordinator = config_entry.runtime_data.protocols
settings_coordinator = config_entry.runtime_data.settings
status_coordinator = config_entry.runtime_data.status
return {
"config_entry": async_redact_data(config_entry.as_dict(), TO_REDACT),

View File

@ -19,20 +19,19 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import PERCENTAGE, EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.typing import StateType
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from . import NextDnsConfigEntry
from .const import (
ATTR_DNSSEC,
ATTR_ENCRYPTION,
ATTR_IP_VERSIONS,
ATTR_PROTOCOLS,
ATTR_STATUS,
DOMAIN,
)
from .coordinator import CoordinatorDataT, NextDnsUpdateCoordinator
@ -301,14 +300,14 @@ SENSORS: tuple[NextDnsSensorEntityDescription, ...] = (
async def async_setup_entry(
hass: HomeAssistant,
entry: ConfigEntry,
entry: NextDnsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add a NextDNS entities from a config_entry."""
coordinators = hass.data[DOMAIN][entry.entry_id]
async_add_entities(
NextDnsSensor(coordinators[description.coordinator_type], description)
NextDnsSensor(
getattr(entry.runtime_data, description.coordinator_type), description
)
for description in SENSORS
)

View File

@ -4,518 +4,515 @@ from __future__ import annotations
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Generic
from typing import Any
from aiohttp import ClientError
from aiohttp.client_exceptions import ClientConnectorError
from nextdns import ApiError, Settings
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import EntityCategory
from homeassistant.core import HomeAssistant, callback
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from .const import ATTR_SETTINGS, DOMAIN
from .coordinator import CoordinatorDataT, NextDnsSettingsUpdateCoordinator
from . import NextDnsConfigEntry
from .coordinator import NextDnsUpdateCoordinator
PARALLEL_UPDATES = 1
@dataclass(frozen=True, kw_only=True)
class NextDnsSwitchEntityDescription(
SwitchEntityDescription, Generic[CoordinatorDataT]
):
class NextDnsSwitchEntityDescription(SwitchEntityDescription):
"""NextDNS switch entity description."""
state: Callable[[CoordinatorDataT], bool]
state: Callable[[Settings], bool]
SWITCHES = (
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_page",
translation_key="block_page",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.block_page,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="cache_boost",
translation_key="cache_boost",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.cache_boost,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="cname_flattening",
translation_key="cname_flattening",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.cname_flattening,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="anonymized_ecs",
translation_key="anonymized_ecs",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.anonymized_ecs,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="logs",
translation_key="logs",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.logs,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="web3",
translation_key="web3",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.web3,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="allow_affiliate",
translation_key="allow_affiliate",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.allow_affiliate,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_disguised_trackers",
translation_key="block_disguised_trackers",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.block_disguised_trackers,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="ai_threat_detection",
translation_key="ai_threat_detection",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.ai_threat_detection,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_csam",
translation_key="block_csam",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.block_csam,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_ddns",
translation_key="block_ddns",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.block_ddns,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_nrd",
translation_key="block_nrd",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.block_nrd,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_parked_domains",
translation_key="block_parked_domains",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.block_parked_domains,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="cryptojacking_protection",
translation_key="cryptojacking_protection",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.cryptojacking_protection,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="dga_protection",
translation_key="dga_protection",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.dga_protection,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="dns_rebinding_protection",
translation_key="dns_rebinding_protection",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.dns_rebinding_protection,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="google_safe_browsing",
translation_key="google_safe_browsing",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.google_safe_browsing,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="idn_homograph_attacks_protection",
translation_key="idn_homograph_attacks_protection",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.idn_homograph_attacks_protection,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="threat_intelligence_feeds",
translation_key="threat_intelligence_feeds",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.threat_intelligence_feeds,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="typosquatting_protection",
translation_key="typosquatting_protection",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.typosquatting_protection,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_bypass_methods",
translation_key="block_bypass_methods",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.block_bypass_methods,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="safesearch",
translation_key="safesearch",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.safesearch,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="youtube_restricted_mode",
translation_key="youtube_restricted_mode",
entity_category=EntityCategory.CONFIG,
state=lambda data: data.youtube_restricted_mode,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_9gag",
translation_key="block_9gag",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_9gag,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_amazon",
translation_key="block_amazon",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_amazon,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_bereal",
translation_key="block_bereal",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_bereal,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_blizzard",
translation_key="block_blizzard",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_blizzard,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_chatgpt",
translation_key="block_chatgpt",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_chatgpt,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_dailymotion",
translation_key="block_dailymotion",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_dailymotion,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_discord",
translation_key="block_discord",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_discord,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_disneyplus",
translation_key="block_disneyplus",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_disneyplus,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_ebay",
translation_key="block_ebay",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_ebay,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_facebook",
translation_key="block_facebook",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_facebook,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_fortnite",
translation_key="block_fortnite",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_fortnite,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_google_chat",
translation_key="block_google_chat",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_google_chat,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_hbomax",
translation_key="block_hbomax",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_hbomax,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_hulu",
name="Block Hulu",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_hulu,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_imgur",
translation_key="block_imgur",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_imgur,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_instagram",
translation_key="block_instagram",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_instagram,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_leagueoflegends",
translation_key="block_leagueoflegends",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_leagueoflegends,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_mastodon",
translation_key="block_mastodon",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_mastodon,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_messenger",
translation_key="block_messenger",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_messenger,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_minecraft",
translation_key="block_minecraft",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_minecraft,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_netflix",
translation_key="block_netflix",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_netflix,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_pinterest",
translation_key="block_pinterest",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_pinterest,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_playstation_network",
translation_key="block_playstation_network",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_playstation_network,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_primevideo",
translation_key="block_primevideo",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_primevideo,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_reddit",
translation_key="block_reddit",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_reddit,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_roblox",
translation_key="block_roblox",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_roblox,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_signal",
translation_key="block_signal",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_signal,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_skype",
translation_key="block_skype",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_skype,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_snapchat",
translation_key="block_snapchat",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_snapchat,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_spotify",
translation_key="block_spotify",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_spotify,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_steam",
translation_key="block_steam",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_steam,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_telegram",
translation_key="block_telegram",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_telegram,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_tiktok",
translation_key="block_tiktok",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_tiktok,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_tinder",
translation_key="block_tinder",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_tinder,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_tumblr",
translation_key="block_tumblr",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_tumblr,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_twitch",
translation_key="block_twitch",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_twitch,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_twitter",
translation_key="block_twitter",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_twitter,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_vimeo",
translation_key="block_vimeo",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_vimeo,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_vk",
translation_key="block_vk",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_vk,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_whatsapp",
translation_key="block_whatsapp",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_whatsapp,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_xboxlive",
translation_key="block_xboxlive",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_xboxlive,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_youtube",
translation_key="block_youtube",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_youtube,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_zoom",
translation_key="block_zoom",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_zoom,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_dating",
translation_key="block_dating",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_dating,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_gambling",
translation_key="block_gambling",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_gambling,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_online_gaming",
translation_key="block_online_gaming",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_online_gaming,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_piracy",
translation_key="block_piracy",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_piracy,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_porn",
translation_key="block_porn",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_porn,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_social_networks",
translation_key="block_social_networks",
entity_category=EntityCategory.CONFIG,
entity_registry_enabled_default=False,
state=lambda data: data.block_social_networks,
),
NextDnsSwitchEntityDescription[Settings](
NextDnsSwitchEntityDescription(
key="block_video_streaming",
translation_key="block_video_streaming",
entity_category=EntityCategory.CONFIG,
@ -526,19 +523,21 @@ SWITCHES = (
async def async_setup_entry(
hass: HomeAssistant, entry: ConfigEntry, async_add_entities: AddEntitiesCallback
hass: HomeAssistant,
entry: NextDnsConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Add NextDNS entities from a config_entry."""
coordinator: NextDnsSettingsUpdateCoordinator = hass.data[DOMAIN][entry.entry_id][
ATTR_SETTINGS
]
coordinator = entry.runtime_data.settings
async_add_entities(
NextDnsSwitch(coordinator, description) for description in SWITCHES
)
class NextDnsSwitch(CoordinatorEntity[NextDnsSettingsUpdateCoordinator], SwitchEntity):
class NextDnsSwitch(
CoordinatorEntity[NextDnsUpdateCoordinator[Settings]], SwitchEntity
):
"""Define an NextDNS switch."""
_attr_has_entity_name = True
@ -546,7 +545,7 @@ class NextDnsSwitch(CoordinatorEntity[NextDnsSettingsUpdateCoordinator], SwitchE
def __init__(
self,
coordinator: NextDnsSettingsUpdateCoordinator,
coordinator: NextDnsUpdateCoordinator[Settings],
description: NextDnsSwitchEntityDescription,
) -> None:
"""Initialize."""

View File

@ -3,6 +3,7 @@
from __future__ import annotations
from datetime import timedelta
from enum import IntFlag
from functools import cached_property, partial
import logging
from typing import Any, final, override
@ -58,6 +59,12 @@ PLATFORM_SCHEMA = vol.Schema(
)
class NotifyEntityFeature(IntFlag):
"""Supported features of a notify entity."""
TITLE = 1
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the notify services."""
@ -73,7 +80,10 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
component = hass.data[DOMAIN] = EntityComponent[NotifyEntity](_LOGGER, DOMAIN, hass)
component.async_register_entity_service(
SERVICE_SEND_MESSAGE,
{vol.Required(ATTR_MESSAGE): cv.string},
{
vol.Required(ATTR_MESSAGE): cv.string,
vol.Optional(ATTR_TITLE): cv.string,
},
"_async_send_message",
)
@ -128,6 +138,7 @@ class NotifyEntity(RestoreEntity):
"""Representation of a notify entity."""
entity_description: NotifyEntityDescription
_attr_supported_features: NotifyEntityFeature = NotifyEntityFeature(0)
_attr_should_poll = False
_attr_device_class: None
_attr_state: None = None
@ -162,10 +173,19 @@ class NotifyEntity(RestoreEntity):
self.async_write_ha_state()
await self.async_send_message(**kwargs)
def send_message(self, message: str) -> None:
def send_message(self, message: str, title: str | None = None) -> None:
"""Send a message."""
raise NotImplementedError
async def async_send_message(self, message: str) -> None:
async def async_send_message(self, message: str, title: str | None = None) -> None:
"""Send a message."""
await self.hass.async_add_executor_job(partial(self.send_message, message))
kwargs: dict[str, Any] = {}
if (
title is not None
and self.supported_features
and self.supported_features & NotifyEntityFeature.TITLE
):
kwargs[ATTR_TITLE] = title
await self.hass.async_add_executor_job(
partial(self.send_message, message, **kwargs)
)

View File

@ -29,6 +29,13 @@ send_message:
required: true
selector:
text:
title:
required: false
selector:
text:
filter:
supported_features:
- notify.NotifyEntityFeature.TITLE
persistent_notification:
fields:

View File

@ -35,6 +35,10 @@
"message": {
"name": "Message",
"description": "Your notification message."
},
"title": {
"name": "Title",
"description": "Title for your notification message."
}
}
},

View File

@ -26,22 +26,30 @@ from homeassistant.helpers import device_registry as dr
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import (
COORDINATOR,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
INTEGRATION_SUPPORTED_COMMANDS,
PLATFORMS,
PYNUT_DATA,
PYNUT_UNIQUE_ID,
USER_AVAILABLE_COMMANDS,
)
NUT_FAKE_SERIAL = ["unknown", "blank"]
_LOGGER = logging.getLogger(__name__)
NutConfigEntry = ConfigEntry["NutRuntimeData"]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@dataclass
class NutRuntimeData:
"""Runtime data definition."""
coordinator: DataUpdateCoordinator
data: PyNUTData
unique_id: str
user_available_commands: set[str]
async def async_setup_entry(hass: HomeAssistant, entry: NutConfigEntry) -> bool:
"""Set up Network UPS Tools (NUT) from a config entry."""
# strip out the stale options CONF_RESOURCES,
@ -110,13 +118,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
else:
user_available_commands = set()
hass.data.setdefault(DOMAIN, {})
hass.data[DOMAIN][entry.entry_id] = {
COORDINATOR: coordinator,
PYNUT_DATA: data,
PYNUT_UNIQUE_ID: unique_id,
USER_AVAILABLE_COMMANDS: user_available_commands,
}
entry.runtime_data = NutRuntimeData(
coordinator, data, unique_id, user_available_commands
)
device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
@ -135,9 +139,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Unload a config entry."""
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
hass.data[DOMAIN].pop(entry.entry_id)
return unload_ok
return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
async def _async_update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None:

View File

@ -15,15 +15,8 @@ DEFAULT_PORT = 3493
KEY_STATUS = "ups.status"
KEY_STATUS_DISPLAY = "ups.status.display"
COORDINATOR = "coordinator"
DEFAULT_SCAN_INTERVAL = 60
PYNUT_DATA = "data"
PYNUT_UNIQUE_ID = "unique_id"
USER_AVAILABLE_COMMANDS = "user_available_commands"
STATE_TYPES = {
"OL": "Online",
"OB": "On Battery",

View File

@ -4,19 +4,15 @@ from __future__ import annotations
import voluptuous as vol
from homeassistant.components.device_automation import InvalidDeviceAutomationConfig
from homeassistant.const import CONF_DEVICE_ID, CONF_DOMAIN, CONF_TYPE
from homeassistant.core import Context, HomeAssistant
from homeassistant.helpers import device_registry as dr
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.typing import ConfigType, TemplateVarsType
from . import PyNUTData
from .const import (
DOMAIN,
INTEGRATION_SUPPORTED_COMMANDS,
PYNUT_DATA,
USER_AVAILABLE_COMMANDS,
)
from . import NutRuntimeData
from .const import DOMAIN, INTEGRATION_SUPPORTED_COMMANDS
ACTION_TYPES = {cmd.replace(".", "_") for cmd in INTEGRATION_SUPPORTED_COMMANDS}
@ -31,18 +27,15 @@ async def async_get_actions(
hass: HomeAssistant, device_id: str
) -> list[dict[str, str]]:
"""List device actions for Network UPS Tools (NUT) devices."""
if (entry_id := _get_entry_id_from_device_id(hass, device_id)) is None:
if (runtime_data := _get_runtime_data_from_device_id(hass, device_id)) is None:
return []
base_action = {
CONF_DEVICE_ID: device_id,
CONF_DOMAIN: DOMAIN,
}
user_available_commands: set[str] = hass.data[DOMAIN][entry_id][
USER_AVAILABLE_COMMANDS
]
return [
{CONF_TYPE: _get_device_action_name(command_name)} | base_action
for command_name in user_available_commands
for command_name in runtime_data.user_available_commands
]
@ -56,9 +49,12 @@ async def async_call_action_from_config(
device_action_name: str = config[CONF_TYPE]
command_name = _get_command_name(device_action_name)
device_id: str = config[CONF_DEVICE_ID]
entry_id = _get_entry_id_from_device_id(hass, device_id)
data: PyNUTData = hass.data[DOMAIN][entry_id][PYNUT_DATA]
await data.async_run_command(command_name)
runtime_data = _get_runtime_data_from_device_id(hass, device_id)
if not runtime_data:
raise InvalidDeviceAutomationConfig(
f"Unable to find a NUT device with id {device_id}"
)
await runtime_data.data.async_run_command(command_name)
def _get_device_action_name(command_name: str) -> str:
@ -69,8 +65,14 @@ def _get_command_name(device_action_name: str) -> str:
return device_action_name.replace("_", ".")
def _get_entry_id_from_device_id(hass: HomeAssistant, device_id: str) -> str | None:
def _get_runtime_data_from_device_id(
hass: HomeAssistant, device_id: str
) -> NutRuntimeData | None:
device_registry = dr.async_get(hass)
if (device := device_registry.async_get(device_id)) is None:
return None
return next(entry for entry in device.config_entries)
entry = hass.config_entries.async_get_entry(
next(entry_id for entry_id in device.config_entries)
)
assert entry and isinstance(entry.runtime_data, NutRuntimeData)
return entry.runtime_data

View File

@ -7,27 +7,26 @@ from typing import Any
import attr
from homeassistant.components.diagnostics import async_redact_data
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr, entity_registry as er
from . import PyNUTData
from .const import DOMAIN, PYNUT_DATA, PYNUT_UNIQUE_ID, USER_AVAILABLE_COMMANDS
from . import NutConfigEntry
from .const import DOMAIN
TO_REDACT = {CONF_PASSWORD, CONF_USERNAME}
async def async_get_config_entry_diagnostics(
hass: HomeAssistant, entry: ConfigEntry
hass: HomeAssistant, entry: NutConfigEntry
) -> dict[str, dict[str, Any]]:
"""Return diagnostics for a config entry."""
data = {"entry": async_redact_data(entry.as_dict(), TO_REDACT)}
hass_data = hass.data[DOMAIN][entry.entry_id]
hass_data = entry.runtime_data
# Get information from Nut library
nut_data: PyNUTData = hass_data[PYNUT_DATA]
nut_cmd: set[str] = hass_data[USER_AVAILABLE_COMMANDS]
nut_data = hass_data.data
nut_cmd = hass_data.user_available_commands
data["nut_data"] = {
"ups_list": nut_data.ups_list,
"status": nut_data.status,
@ -38,7 +37,7 @@ async def async_get_config_entry_diagnostics(
device_registry = dr.async_get(hass)
entity_registry = er.async_get(hass)
hass_device = device_registry.async_get_device(
identifiers={(DOMAIN, hass_data[PYNUT_UNIQUE_ID])}
identifiers={(DOMAIN, hass_data.unique_id)}
)
if not hass_device:
return data

View File

@ -12,7 +12,6 @@ from homeassistant.components.sensor import (
SensorEntityDescription,
SensorStateClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_MANUFACTURER,
ATTR_MODEL,
@ -36,16 +35,8 @@ from homeassistant.helpers.update_coordinator import (
DataUpdateCoordinator,
)
from . import PyNUTData
from .const import (
COORDINATOR,
DOMAIN,
KEY_STATUS,
KEY_STATUS_DISPLAY,
PYNUT_DATA,
PYNUT_UNIQUE_ID,
STATE_TYPES,
)
from . import NutConfigEntry, PyNUTData
from .const import DOMAIN, KEY_STATUS, KEY_STATUS_DISPLAY, STATE_TYPES
NUT_DEV_INFO_TO_DEV_INFO: dict[str, str] = {
"manufacturer": ATTR_MANUFACTURER,
@ -968,15 +959,15 @@ def _get_nut_device_info(data: PyNUTData) -> DeviceInfo:
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
config_entry: NutConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the NUT sensors."""
pynut_data = hass.data[DOMAIN][config_entry.entry_id]
coordinator = pynut_data[COORDINATOR]
data = pynut_data[PYNUT_DATA]
unique_id = pynut_data[PYNUT_UNIQUE_ID]
pynut_data = config_entry.runtime_data
coordinator = pynut_data.coordinator
data = pynut_data.data
unique_id = pynut_data.unique_id
status = coordinator.data
resources = [sensor_id for sensor_id in SENSOR_TYPES if sensor_id in status]

View File

@ -2,21 +2,18 @@
from __future__ import annotations
from collections.abc import Awaitable, Callable
from dataclasses import dataclass
import datetime
import logging
from typing import TYPE_CHECKING
from pynws import SimpleNWS
from pynws import SimpleNWS, call_with_retry
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, CONF_LATITUDE, CONF_LONGITUDE, Platform
from homeassistant.core import HomeAssistant, callback
from homeassistant.core import HomeAssistant
from homeassistant.helpers import debounce
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
from homeassistant.helpers.event import async_track_point_in_utc_time
from homeassistant.helpers.update_coordinator import TimestampDataUpdateCoordinator
from homeassistant.util.dt import utcnow
@ -27,8 +24,10 @@ _LOGGER = logging.getLogger(__name__)
PLATFORMS = [Platform.SENSOR, Platform.WEATHER]
DEFAULT_SCAN_INTERVAL = datetime.timedelta(minutes=10)
FAILED_SCAN_INTERVAL = datetime.timedelta(minutes=1)
DEBOUNCE_TIME = 60 # in seconds
RETRY_INTERVAL = datetime.timedelta(minutes=1)
RETRY_STOP = datetime.timedelta(minutes=10)
DEBOUNCE_TIME = 10 * 60 # in seconds
def base_unique_id(latitude: float, longitude: float) -> str:
@ -41,62 +40,9 @@ class NWSData:
"""Data for the National Weather Service integration."""
api: SimpleNWS
coordinator_observation: NwsDataUpdateCoordinator
coordinator_forecast: NwsDataUpdateCoordinator
coordinator_forecast_hourly: NwsDataUpdateCoordinator
class NwsDataUpdateCoordinator(TimestampDataUpdateCoordinator[None]): # pylint: disable=hass-enforce-coordinator-module
"""NWS data update coordinator.
Implements faster data update intervals for failed updates and exposes a last successful update time.
"""
def __init__(
self,
hass: HomeAssistant,
logger: logging.Logger,
*,
name: str,
update_interval: datetime.timedelta,
failed_update_interval: datetime.timedelta,
update_method: Callable[[], Awaitable[None]] | None = None,
request_refresh_debouncer: debounce.Debouncer | None = None,
) -> None:
"""Initialize NWS coordinator."""
super().__init__(
hass,
logger,
name=name,
update_interval=update_interval,
update_method=update_method,
request_refresh_debouncer=request_refresh_debouncer,
)
self.failed_update_interval = failed_update_interval
@callback
def _schedule_refresh(self) -> None:
"""Schedule a refresh."""
if self._unsub_refresh:
self._unsub_refresh()
self._unsub_refresh = None
# We _floor_ utcnow to create a schedule on a rounded second,
# minimizing the time between the point and the real activation.
# That way we obtain a constant update frequency,
# as long as the update process takes less than a second
if self.last_update_success:
if TYPE_CHECKING:
# the base class allows None, but this one doesn't
assert self.update_interval is not None
update_interval = self.update_interval
else:
update_interval = self.failed_update_interval
self._unsub_refresh = async_track_point_in_utc_time(
self.hass,
self._handle_refresh_interval,
utcnow().replace(microsecond=0) + update_interval,
)
coordinator_observation: TimestampDataUpdateCoordinator[None]
coordinator_forecast: TimestampDataUpdateCoordinator[None]
coordinator_forecast_hourly: TimestampDataUpdateCoordinator[None]
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
@ -114,39 +60,57 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
async def update_observation() -> None:
"""Retrieve recent observations."""
await nws_data.update_observation(start_time=utcnow() - UPDATE_TIME_PERIOD)
await call_with_retry(
nws_data.update_observation,
RETRY_INTERVAL,
RETRY_STOP,
start_time=utcnow() - UPDATE_TIME_PERIOD,
)
coordinator_observation = NwsDataUpdateCoordinator(
async def update_forecast() -> None:
"""Retrieve twice-daily forecsat."""
await call_with_retry(
nws_data.update_forecast,
RETRY_INTERVAL,
RETRY_STOP,
)
async def update_forecast_hourly() -> None:
"""Retrieve hourly forecast."""
await call_with_retry(
nws_data.update_forecast_hourly,
RETRY_INTERVAL,
RETRY_STOP,
)
coordinator_observation = TimestampDataUpdateCoordinator(
hass,
_LOGGER,
name=f"NWS observation station {station}",
update_method=update_observation,
update_interval=DEFAULT_SCAN_INTERVAL,
failed_update_interval=FAILED_SCAN_INTERVAL,
request_refresh_debouncer=debounce.Debouncer(
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
),
)
coordinator_forecast = NwsDataUpdateCoordinator(
coordinator_forecast = TimestampDataUpdateCoordinator(
hass,
_LOGGER,
name=f"NWS forecast station {station}",
update_method=nws_data.update_forecast,
update_method=update_forecast,
update_interval=DEFAULT_SCAN_INTERVAL,
failed_update_interval=FAILED_SCAN_INTERVAL,
request_refresh_debouncer=debounce.Debouncer(
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
),
)
coordinator_forecast_hourly = NwsDataUpdateCoordinator(
coordinator_forecast_hourly = TimestampDataUpdateCoordinator(
hass,
_LOGGER,
name=f"NWS forecast hourly station {station}",
update_method=nws_data.update_forecast_hourly,
update_method=update_forecast_hourly,
update_interval=DEFAULT_SCAN_INTERVAL,
failed_update_interval=FAILED_SCAN_INTERVAL,
request_refresh_debouncer=debounce.Debouncer(
hass, _LOGGER, cooldown=DEBOUNCE_TIME, immediate=True
),

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