Merge branch 'dev' of github.com:home-assistant/core into idasen_none_bledevice_fix

This commit is contained in:
abmantis 2024-05-04 01:32:25 +01:00
commit df103ef16d
118 changed files with 1711 additions and 1375 deletions

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

@ -51,8 +51,9 @@ from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.issue_registry import async_delete_issue
from homeassistant.loader import async_get_bluetooth
from . import models, passive_update_processor
from . import passive_update_processor
from .api import (
_get_manager,
async_address_present,
async_ble_device_from_address,
async_discovered_service_info,
@ -76,7 +77,6 @@ from .const import (
CONF_ADAPTER,
CONF_DETAILS,
CONF_PASSIVE,
DATA_MANAGER,
DOMAIN,
FALLBACK_MAXIMUM_STALE_ADVERTISEMENT_SECONDS,
LINUX_FIRMWARE_LOAD_FALLBACK_SECONDS,
@ -230,10 +230,8 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
hass, integration_matcher, bluetooth_adapters, bluetooth_storage, slot_manager
)
set_manager(manager)
await storage_setup_task
await manager.async_setup()
hass.data[DATA_MANAGER] = models.MANAGER = manager
hass.async_create_background_task(
_async_start_adapter_discovery(hass, manager, bluetooth_adapters),
@ -314,7 +312,7 @@ async def async_update_device(
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up a config entry for a bluetooth scanner."""
manager: HomeAssistantBluetoothManager = hass.data[DATA_MANAGER]
manager = _get_manager(hass)
address = entry.unique_id
assert address is not None
adapter = await manager.async_get_adapter_from_address_or_recover(address)

View File

@ -15,10 +15,12 @@ from habluetooth import (
BluetoothScannerDevice,
BluetoothScanningMode,
HaBleakScannerWrapper,
get_manager,
)
from home_assistant_bluetooth import BluetoothServiceInfoBleak
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback as hass_callback
from homeassistant.helpers.singleton import singleton
from .const import DATA_MANAGER
from .manager import HomeAssistantBluetoothManager
@ -29,9 +31,10 @@ if TYPE_CHECKING:
from bleak.backends.device import BLEDevice
@singleton(DATA_MANAGER)
def _get_manager(hass: HomeAssistant) -> HomeAssistantBluetoothManager:
"""Get the bluetooth manager."""
return cast(HomeAssistantBluetoothManager, hass.data[DATA_MANAGER])
return cast(HomeAssistantBluetoothManager, get_manager())
@hass_callback
@ -68,8 +71,6 @@ def async_discovered_service_info(
hass: HomeAssistant, connectable: bool = True
) -> Iterable[BluetoothServiceInfoBleak]:
"""Return the discovered devices list."""
if DATA_MANAGER not in hass.data:
return []
return _get_manager(hass).async_discovered_service_info(connectable)
@ -78,8 +79,6 @@ def async_last_service_info(
hass: HomeAssistant, address: str, connectable: bool = True
) -> BluetoothServiceInfoBleak | None:
"""Return the last service info for an address."""
if DATA_MANAGER not in hass.data:
return None
return _get_manager(hass).async_last_service_info(address, connectable)
@ -88,8 +87,6 @@ def async_ble_device_from_address(
hass: HomeAssistant, address: str, connectable: bool = True
) -> BLEDevice | None:
"""Return BLEDevice for an address if its present."""
if DATA_MANAGER not in hass.data:
return None
return _get_manager(hass).async_ble_device_from_address(address, connectable)
@ -106,8 +103,6 @@ def async_address_present(
hass: HomeAssistant, address: str, connectable: bool = True
) -> bool:
"""Check if an address is present in the bluetooth device list."""
if DATA_MANAGER not in hass.data:
return False
return _get_manager(hass).async_address_present(address, connectable)

View File

@ -14,6 +14,7 @@ from bluetooth_adapters import (
adapter_model,
get_adapters,
)
from habluetooth import get_manager
import voluptuous as vol
from homeassistant.components import onboarding
@ -25,7 +26,6 @@ from homeassistant.helpers.schema_config_entry_flow import (
)
from homeassistant.helpers.typing import DiscoveryInfoType
from . import models
from .const import CONF_ADAPTER, CONF_DETAILS, CONF_PASSIVE, DOMAIN
from .util import adapter_title
@ -185,4 +185,4 @@ class BluetoothConfigFlow(ConfigFlow, domain=DOMAIN):
@callback
def async_supports_options_flow(cls, config_entry: ConfigEntry) -> bool:
"""Return options flow support for this handler."""
return bool(models.MANAGER and models.MANAGER.supports_passive_scan)
return bool((manager := get_manager()) and manager.supports_passive_scan)

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

@ -20,6 +20,6 @@
"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

@ -4,17 +4,9 @@ from __future__ import annotations
from collections.abc import Callable
from enum import Enum
from typing import TYPE_CHECKING
from home_assistant_bluetooth import BluetoothServiceInfoBleak
if TYPE_CHECKING:
from .manager import HomeAssistantBluetoothManager
MANAGER: HomeAssistantBluetoothManager | None = None
BluetoothChange = Enum("BluetoothChange", "ADVERTISEMENT")
BluetoothCallback = Callable[[BluetoothServiceInfoBleak, BluetoothChange], None]
ProcessAdvertisementCallback = Callable[[BluetoothServiceInfoBleak], bool]

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

@ -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,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

@ -49,6 +49,7 @@ PLATFORMS = [
Platform.NOTIFY,
Platform.NUMBER,
Platform.SENSOR,
Platform.SWITCH,
Platform.WEATHER,
]

View File

@ -10,7 +10,7 @@
},
"iot_class": "cloud_polling",
"loggers": ["pyecobee"],
"requirements": ["python-ecobee-api==0.2.17"],
"requirements": ["python-ecobee-api==0.2.18"],
"zeroconf": [
{
"type": "_ecobee._tcp.local."

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

@ -0,0 +1,90 @@
"""Support for using switch with ecobee thermostats."""
from __future__ import annotations
import logging
from typing import Any
from homeassistant.components.switch import SwitchEntity
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util
from . import EcobeeData
from .const import DOMAIN
from .entity import EcobeeBaseEntity
_LOGGER = logging.getLogger(__name__)
DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
async def async_setup_entry(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up the ecobee thermostat switch entity."""
data: EcobeeData = hass.data[DOMAIN]
async_add_entities(
(
EcobeeVentilator20MinSwitch(data, index)
for index, thermostat in enumerate(data.ecobee.thermostats)
if thermostat["settings"]["ventilatorType"] != "none"
),
True,
)
class EcobeeVentilator20MinSwitch(EcobeeBaseEntity, SwitchEntity):
"""A Switch class, representing 20 min timer for an ecobee thermostat with ventilator attached."""
_attr_has_entity_name = True
_attr_name = "Ventilator 20m Timer"
def __init__(
self,
data: EcobeeData,
thermostat_index: int,
) -> None:
"""Initialize ecobee ventilator platform."""
super().__init__(data, thermostat_index)
self._attr_unique_id = f"{self.base_unique_id}_ventilator_20m_timer"
self._attr_is_on = False
self.update_without_throttle = False
self._operating_timezone = dt_util.get_time_zone(
self.thermostat["location"]["timeZone"]
)
async def async_update(self) -> None:
"""Get the latest state from the thermostat."""
if self.update_without_throttle:
await self.data.update(no_throttle=True)
self.update_without_throttle = False
else:
await self.data.update()
ventilator_off_date_time = self.thermostat["settings"]["ventilatorOffDateTime"]
self._attr_is_on = ventilator_off_date_time and dt_util.parse_datetime(
ventilator_off_date_time, raise_on_error=True
).replace(tzinfo=self._operating_timezone) >= dt_util.now(
self._operating_timezone
)
async def async_turn_on(self, **kwargs: Any) -> None:
"""Set ventilator 20 min timer on."""
await self.hass.async_add_executor_job(
self.data.ecobee.set_ventilator_timer, self.thermostat_index, True
)
self.update_without_throttle = True
async def async_turn_off(self, **kwargs: Any) -> None:
"""Set ventilator 20 min timer off."""
await self.hass.async_add_executor_job(
self.data.ecobee.set_ventilator_timer, self.thermostat_index, False
)
self.update_without_throttle = True

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

@ -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

@ -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

@ -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,13 @@
from __future__ import annotations
import asyncio
import logging
from typing import cast
from aiohttp.client_exceptions import ClientConnectorError, ClientError
from nettigo_air_monitor import (
ApiError,
AuthFailedError,
ConnectionOptions,
InvalidSensorDataError,
NAMSensors,
NettigoAirMonitor,
)
@ -21,25 +17,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)
@ -63,8 +54,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
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 +71,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,67 @@
"""The Nettigo Air Monitor coordinator."""
import asyncio
import logging
from typing import cast
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,
) -> 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={(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}/",
)

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

@ -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

@ -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
),

View File

@ -7,5 +7,5 @@
"iot_class": "cloud_polling",
"loggers": ["metar", "pynws"],
"quality_scale": "platinum",
"requirements": ["pynws==1.6.0"]
"requirements": ["pynws[retry]==1.7.0"]
}

View File

@ -25,7 +25,10 @@ from homeassistant.const import (
)
from homeassistant.core import HomeAssistant
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
TimestampDataUpdateCoordinator,
)
from homeassistant.util.dt import utcnow
from homeassistant.util.unit_conversion import (
DistanceConverter,
@ -34,7 +37,7 @@ from homeassistant.util.unit_conversion import (
)
from homeassistant.util.unit_system import US_CUSTOMARY_SYSTEM
from . import NWSData, NwsDataUpdateCoordinator, base_unique_id, device_info
from . import NWSData, base_unique_id, device_info
from .const import ATTRIBUTION, CONF_STATION, DOMAIN, OBSERVATION_VALID_TIME
PARALLEL_UPDATES = 0
@ -158,7 +161,7 @@ async def async_setup_entry(
)
class NWSSensor(CoordinatorEntity[NwsDataUpdateCoordinator], SensorEntity):
class NWSSensor(CoordinatorEntity[TimestampDataUpdateCoordinator[None]], SensorEntity):
"""An NWS Sensor Entity."""
entity_description: NWSSensorEntityDescription

View File

@ -2,6 +2,7 @@
from __future__ import annotations
from functools import partial
from types import MappingProxyType
from typing import TYPE_CHECKING, Any, cast
@ -34,7 +35,6 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant, callback
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util.dt import utcnow
from homeassistant.util.unit_conversion import SpeedConverter, TemperatureConverter
from . import NWSData, base_unique_id, device_info
@ -46,7 +46,6 @@ from .const import (
DOMAIN,
FORECAST_VALID_TIME,
HOURLY,
OBSERVATION_VALID_TIME,
)
PARALLEL_UPDATES = 0
@ -140,96 +139,69 @@ class NWSWeather(CoordinatorWeatherEntity):
self.nws = nws_data.api
latitude = entry_data[CONF_LATITUDE]
longitude = entry_data[CONF_LONGITUDE]
self.coordinator_forecast_legacy = nws_data.coordinator_forecast
self.station = self.nws.station
self.observation: dict[str, Any] | None = None
self._forecast_hourly: list[dict[str, Any]] | None = None
self._forecast_legacy: list[dict[str, Any]] | None = None
self._forecast_twice_daily: list[dict[str, Any]] | None = None
self.station = self.nws.station
self._attr_unique_id = _calculate_unique_id(entry_data, DAYNIGHT)
self._attr_device_info = device_info(latitude, longitude)
self._attr_name = self.station
async def async_added_to_hass(self) -> None:
"""Set up a listener and load data."""
"""When entity is added to hass."""
await super().async_added_to_hass()
self.async_on_remove(
self.coordinator_forecast_legacy.async_add_listener(
self._handle_legacy_forecast_coordinator_update
self.async_on_remove(partial(self._remove_forecast_listener, "daily"))
self.async_on_remove(partial(self._remove_forecast_listener, "hourly"))
self.async_on_remove(partial(self._remove_forecast_listener, "twice_daily"))
for forecast_type in ("twice_daily", "hourly"):
if (coordinator := self.forecast_coordinators[forecast_type]) is None:
continue
self.unsub_forecast[forecast_type] = coordinator.async_add_listener(
partial(self._handle_forecast_update, forecast_type)
)
)
# Load initial data from coordinators
self._handle_coordinator_update()
self._handle_hourly_forecast_coordinator_update()
self._handle_twice_daily_forecast_coordinator_update()
self._handle_legacy_forecast_coordinator_update()
@callback
def _handle_coordinator_update(self) -> None:
"""Load data from integration."""
self.observation = self.nws.observation
self.async_write_ha_state()
@callback
def _handle_hourly_forecast_coordinator_update(self) -> None:
"""Handle updated data from the hourly forecast coordinator."""
self._forecast_hourly = self.nws.forecast_hourly
@callback
def _handle_twice_daily_forecast_coordinator_update(self) -> None:
"""Handle updated data from the twice daily forecast coordinator."""
self._forecast_twice_daily = self.nws.forecast
@callback
def _handle_legacy_forecast_coordinator_update(self) -> None:
"""Handle updated data from the legacy forecast coordinator."""
self._forecast_legacy = self.nws.forecast
self.async_write_ha_state()
@property
def native_temperature(self) -> float | None:
"""Return the current temperature."""
if self.observation:
return self.observation.get("temperature")
if observation := self.nws.observation:
return observation.get("temperature")
return None
@property
def native_pressure(self) -> int | None:
"""Return the current pressure."""
if self.observation:
return self.observation.get("seaLevelPressure")
if observation := self.nws.observation:
return observation.get("seaLevelPressure")
return None
@property
def humidity(self) -> float | None:
"""Return the name of the sensor."""
if self.observation:
return self.observation.get("relativeHumidity")
if observation := self.nws.observation:
return observation.get("relativeHumidity")
return None
@property
def native_wind_speed(self) -> float | None:
"""Return the current windspeed."""
if self.observation:
return self.observation.get("windSpeed")
if observation := self.nws.observation:
return observation.get("windSpeed")
return None
@property
def wind_bearing(self) -> int | None:
"""Return the current wind bearing (degrees)."""
if self.observation:
return self.observation.get("windDirection")
if observation := self.nws.observation:
return observation.get("windDirection")
return None
@property
def condition(self) -> str | None:
"""Return current condition."""
weather = None
if self.observation:
weather = self.observation.get("iconWeather")
time = cast(str, self.observation.get("iconTime"))
if observation := self.nws.observation:
weather = observation.get("iconWeather")
time = cast(str, observation.get("iconTime"))
if weather:
return convert_condition(time, weather)
@ -238,8 +210,8 @@ class NWSWeather(CoordinatorWeatherEntity):
@property
def native_visibility(self) -> int | None:
"""Return visibility."""
if self.observation:
return self.observation.get("visibility")
if observation := self.nws.observation:
return observation.get("visibility")
return None
def _forecast(
@ -302,33 +274,12 @@ class NWSWeather(CoordinatorWeatherEntity):
@callback
def _async_forecast_hourly(self) -> list[Forecast] | None:
"""Return the hourly forecast in native units."""
return self._forecast(self._forecast_hourly, HOURLY)
return self._forecast(self.nws.forecast_hourly, HOURLY)
@callback
def _async_forecast_twice_daily(self) -> list[Forecast] | None:
"""Return the twice daily forecast in native units."""
return self._forecast(self._forecast_twice_daily, DAYNIGHT)
@property
def available(self) -> bool:
"""Return if state is available."""
last_success = (
self.coordinator.last_update_success
and self.coordinator_forecast_legacy.last_update_success
)
if (
self.coordinator.last_update_success_time
and self.coordinator_forecast_legacy.last_update_success_time
):
last_success_time = (
utcnow() - self.coordinator.last_update_success_time
< OBSERVATION_VALID_TIME
and utcnow() - self.coordinator_forecast_legacy.last_update_success_time
< FORECAST_VALID_TIME
)
else:
last_success_time = False
return last_success or last_success_time
return self._forecast(self.nws.forecast, DAYNIGHT)
async def async_update(self) -> None:
"""Update the entity.
@ -336,4 +287,7 @@ class NWSWeather(CoordinatorWeatherEntity):
Only used by the generic entity update service.
"""
await self.coordinator.async_request_refresh()
await self.coordinator_forecast_legacy.async_request_refresh()
for forecast_type in ("twice_daily", "hourly"):
if (coordinator := self.forecast_coordinators[forecast_type]) is not None:
await coordinator.async_request_refresh()

View File

@ -45,7 +45,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up conversation entities."""
agent = OllamaConversationEntity(hass, config_entry)
agent = OllamaConversationEntity(config_entry)
async_add_entities([agent])
@ -56,9 +56,8 @@ class OllamaConversationEntity(
_attr_has_entity_name = True
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
def __init__(self, entry: ConfigEntry) -> None:
"""Initialize the agent."""
self.hass = hass
self.entry = entry
# conversation id -> message history
@ -223,21 +222,21 @@ class OllamaConversationEntity(
]
for state in exposed_states:
entity = entity_registry.async_get(state.entity_id)
entity_entry = entity_registry.async_get(state.entity_id)
names = [state.name]
area_names = []
if entity is not None:
if entity_entry is not None:
# Add aliases
names.extend(entity.aliases)
if entity.area_id and (
area := area_registry.async_get_area(entity.area_id)
names.extend(entity_entry.aliases)
if entity_entry.area_id and (
area := area_registry.async_get_area(entity_entry.area_id)
):
# Entity is in area
area_names.append(area.name)
area_names.extend(area.aliases)
elif entity.device_id and (
device := device_registry.async_get(entity.device_id)
elif entity_entry.device_id and (
device := device_registry.async_get(entity_entry.device_id)
):
# Check device area
if device.area_id and (

View File

@ -5,7 +5,6 @@ from __future__ import annotations
import openai
import voluptuous as vol
from homeassistant.components import conversation
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_API_KEY, Platform
from homeassistant.core import (
@ -115,5 +114,4 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
return False
hass.data[DOMAIN].pop(entry.entry_id)
conversation.async_unset_agent(hass, entry)
return True

View File

@ -3,7 +3,7 @@
import logging
DOMAIN = "openai_conversation"
LOGGER = logging.getLogger(__name__)
LOGGER = logging.getLogger(__package__)
CONF_PROMPT = "prompt"
DEFAULT_PROMPT = """This smart home is controlled by Home Assistant.

View File

@ -35,7 +35,7 @@ async def async_setup_entry(
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up conversation entities."""
agent = OpenAIConversationEntity(hass, config_entry)
agent = OpenAIConversationEntity(config_entry)
async_add_entities([agent])
@ -44,9 +44,10 @@ class OpenAIConversationEntity(
):
"""OpenAI conversation agent."""
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
_attr_has_entity_name = True
def __init__(self, entry: ConfigEntry) -> None:
"""Initialize the agent."""
self.hass = hass
self.entry = entry
self.history: dict[str, list[dict]] = {}
self._attr_name = entry.title

View File

@ -21,9 +21,8 @@
"universal_silabs_flasher"
],
"requirements": [
"bellows==0.38.3",
"bellows==0.38.4",
"pyserial==3.5",
"pyserial-asyncio==0.6",
"zha-quirks==0.0.115",
"zigpy-deconz==0.23.1",
"zigpy==0.64.0",

View File

@ -98,6 +98,7 @@ def _entity_features() -> dict[str, type[IntFlag]]:
from homeassistant.components.light import LightEntityFeature
from homeassistant.components.lock import LockEntityFeature
from homeassistant.components.media_player import MediaPlayerEntityFeature
from homeassistant.components.notify import NotifyEntityFeature
from homeassistant.components.remote import RemoteEntityFeature
from homeassistant.components.siren import SirenEntityFeature
from homeassistant.components.todo import TodoListEntityFeature
@ -119,6 +120,7 @@ def _entity_features() -> dict[str, type[IntFlag]]:
"LightEntityFeature": LightEntityFeature,
"LockEntityFeature": LockEntityFeature,
"MediaPlayerEntityFeature": MediaPlayerEntityFeature,
"NotifyEntityFeature": NotifyEntityFeature,
"RemoteEntityFeature": RemoteEntityFeature,
"SirenEntityFeature": SirenEntityFeature,
"TodoListEntityFeature": TodoListEntityFeature,

View File

@ -90,7 +90,12 @@ class BlockedIntegration:
BLOCKED_CUSTOM_INTEGRATIONS: dict[str, BlockedIntegration] = {
# Added in 2024.3.0 because of https://github.com/home-assistant/core/issues/112464
"start_time": BlockedIntegration(AwesomeVersion("1.1.7"), "breaks Home Assistant")
"start_time": BlockedIntegration(AwesomeVersion("1.1.7"), "breaks Home Assistant"),
# Added in 2024.5.1 because of
# https://community.home-assistant.io/t/psa-2024-5-upgrade-failure-and-dreame-vacuum-custom-integration/724612
"dreame_vacuum": BlockedIntegration(
AwesomeVersion("1.0.4"), "crashes Home Assistant"
),
}
DATA_COMPONENTS = "components"

View File

@ -28,7 +28,7 @@ dbus-fast==2.21.1
fnv-hash-fast==0.5.0
ha-av==10.1.1
ha-ffmpeg==3.2.0
habluetooth==2.8.1
habluetooth==3.0.1
hass-nabucasa==0.78.0
hassil==1.6.1
home-assistant-bluetooth==1.12.0

View File

@ -541,7 +541,7 @@ beautifulsoup4==4.12.3
# beewi-smartclim==0.0.10
# homeassistant.components.zha
bellows==0.38.3
bellows==0.38.4
# homeassistant.components.bmw_connected_drive
bimmer-connected[china]==0.15.2
@ -983,7 +983,7 @@ gotailwind==0.2.2
govee-ble==0.31.2
# homeassistant.components.govee_light_local
govee-local-api==1.4.4
govee-local-api==1.4.5
# homeassistant.components.remote_rpi_gpio
gpiozero==1.6.2
@ -1035,7 +1035,7 @@ ha-philipsjs==3.1.1
habitipy==0.2.0
# homeassistant.components.bluetooth
habluetooth==2.8.1
habluetooth==3.0.1
# homeassistant.components.cloud
hass-nabucasa==0.78.0
@ -1137,7 +1137,7 @@ iglo==1.2.7
ihcsdk==2.8.5
# homeassistant.components.imgw_pib
imgw_pib==1.0.0
imgw_pib==1.0.1
# homeassistant.components.incomfort
incomfort-client==0.5.0
@ -1371,7 +1371,7 @@ netdata==1.1.0
netmap==0.7.0.2
# homeassistant.components.nam
nettigo-air-monitor==3.0.0
nettigo-air-monitor==3.0.1
# homeassistant.components.neurio_energy
neurio==0.3.1
@ -2004,7 +2004,7 @@ pynobo==1.8.1
pynuki==1.6.3
# homeassistant.components.nws
pynws==1.6.0
pynws[retry]==1.7.0
# homeassistant.components.nx584
pynx584==0.5
@ -2123,9 +2123,6 @@ pysensibo==1.0.36
# homeassistant.components.zha
pyserial-asyncio-fast==0.11
# homeassistant.components.zha
pyserial-asyncio==0.6
# homeassistant.components.acer_projector
# homeassistant.components.crownstone
# homeassistant.components.usb
@ -2215,7 +2212,7 @@ python-clementine-remote==1.0.1
python-digitalocean==1.13.2
# homeassistant.components.ecobee
python-ecobee-api==0.2.17
python-ecobee-api==0.2.18
# homeassistant.components.etherscan
python-etherscan-api==0.0.3

View File

@ -466,7 +466,7 @@ base36==0.1.1
beautifulsoup4==4.12.3
# homeassistant.components.zha
bellows==0.38.3
bellows==0.38.4
# homeassistant.components.bmw_connected_drive
bimmer-connected[china]==0.15.2
@ -806,7 +806,7 @@ gotailwind==0.2.2
govee-ble==0.31.2
# homeassistant.components.govee_light_local
govee-local-api==1.4.4
govee-local-api==1.4.5
# homeassistant.components.gpsd
gps3==0.33.3
@ -849,7 +849,7 @@ ha-philipsjs==3.1.1
habitipy==0.2.0
# homeassistant.components.bluetooth
habluetooth==2.8.1
habluetooth==3.0.1
# homeassistant.components.cloud
hass-nabucasa==0.78.0
@ -924,7 +924,7 @@ idasen-ha==2.5.1
ifaddr==0.2.0
# homeassistant.components.imgw_pib
imgw_pib==1.0.0
imgw_pib==1.0.1
# homeassistant.components.influxdb
influxdb-client==1.24.0
@ -1107,7 +1107,7 @@ nessclient==1.0.0
netmap==0.7.0.2
# homeassistant.components.nam
nettigo-air-monitor==3.0.0
nettigo-air-monitor==3.0.1
# homeassistant.components.nexia
nexia==2.0.8
@ -1567,7 +1567,7 @@ pynobo==1.8.1
pynuki==1.6.3
# homeassistant.components.nws
pynws==1.6.0
pynws[retry]==1.7.0
# homeassistant.components.nx584
pynx584==0.5
@ -1662,9 +1662,6 @@ pysensibo==1.0.36
# homeassistant.components.zha
pyserial-asyncio-fast==0.11
# homeassistant.components.zha
pyserial-asyncio==0.6
# homeassistant.components.acer_projector
# homeassistant.components.crownstone
# homeassistant.components.usb
@ -1730,7 +1727,7 @@ python-awair==0.2.4
python-bsblan==0.5.18
# homeassistant.components.ecobee
python-ecobee-api==0.2.17
python-ecobee-api==0.2.18
# homeassistant.components.fully_kiosk
python-fullykiosk==0.0.12

View File

@ -97,6 +97,7 @@ WAVE_SERVICE_INFO = BluetoothServiceInfoBleak(
),
connectable=True,
time=0,
tx_power=0,
)
VIEW_PLUS_SERVICE_INFO = BluetoothServiceInfoBleak(
@ -141,6 +142,7 @@ VIEW_PLUS_SERVICE_INFO = BluetoothServiceInfoBleak(
),
connectable=True,
time=0,
tx_power=0,
)
UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak(
@ -161,6 +163,7 @@ UNKNOWN_SERVICE_INFO = BluetoothServiceInfoBleak(
),
connectable=True,
time=0,
tx_power=0,
)
WAVE_DEVICE_INFO = AirthingsDevice(

View File

@ -31,6 +31,7 @@ def fake_service_info(name, service_uuid, manufacturer_data):
tx_power=-127,
platform_data=(),
),
tx_power=-127,
)

View File

@ -9,7 +9,6 @@ import pytest
from homeassistant.components import axis, zeroconf
from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN
from homeassistant.components.axis.hub import AxisHub
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.config_entries import SOURCE_ZEROCONF
from homeassistant.const import (
@ -52,7 +51,7 @@ async def test_device_setup(
device_registry: dr.DeviceRegistry,
) -> None:
"""Successful setup."""
hub = AxisHub.get_hub(hass, setup_config_entry)
hub = setup_config_entry.runtime_data
assert hub.api.vapix.firmware_version == "9.10.1"
assert hub.api.vapix.product_number == "M1065-LW"
@ -78,7 +77,7 @@ async def test_device_setup(
@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_BASIC_DEVICE_INFO])
async def test_device_info(hass: HomeAssistant, setup_config_entry) -> None:
"""Verify other path of device information works."""
hub = AxisHub.get_hub(hass, setup_config_entry)
hub = setup_config_entry.runtime_data
assert hub.api.vapix.firmware_version == "9.80.1"
assert hub.api.vapix.product_number == "M1065-LW"
@ -124,30 +123,26 @@ async def test_update_address(
hass: HomeAssistant, setup_config_entry, mock_vapix_requests
) -> None:
"""Test update address works."""
hub = AxisHub.get_hub(hass, setup_config_entry)
hub = setup_config_entry.runtime_data
assert hub.api.config.host == "1.2.3.4"
with patch(
"homeassistant.components.axis.async_setup_entry", return_value=True
) as mock_setup_entry:
mock_vapix_requests("2.3.4.5")
await hass.config_entries.flow.async_init(
AXIS_DOMAIN,
data=zeroconf.ZeroconfServiceInfo(
ip_address=ip_address("2.3.4.5"),
ip_addresses=[ip_address("2.3.4.5")],
hostname="mock_hostname",
name="name",
port=80,
properties={"macaddress": MAC},
type="mock_type",
),
context={"source": SOURCE_ZEROCONF},
)
await hass.async_block_till_done()
mock_vapix_requests("2.3.4.5")
await hass.config_entries.flow.async_init(
AXIS_DOMAIN,
data=zeroconf.ZeroconfServiceInfo(
ip_address=ip_address("2.3.4.5"),
ip_addresses=[ip_address("2.3.4.5")],
hostname="mock_hostname",
name="name",
port=80,
properties={"macaddress": MAC},
type="mock_type",
),
context={"source": SOURCE_ZEROCONF},
)
await hass.async_block_till_done()
assert hub.api.config.host == "2.3.4.5"
assert len(mock_setup_entry.mock_calls) == 1
async def test_device_unavailable(

View File

@ -155,6 +155,7 @@ def inject_advertisement_with_time_and_source_connectable(
advertisement=adv,
connectable=connectable,
time=time,
tx_power=adv.tx_power,
)
)

View File

@ -335,6 +335,7 @@ async def test_diagnostics_macos(
"service_uuids": [],
"source": "local",
"time": ANY,
"tx_power": -127,
}
],
"connectable_history": [
@ -363,6 +364,7 @@ async def test_diagnostics_macos(
"service_uuids": [],
"source": "local",
"time": ANY,
"tx_power": -127,
}
],
"scanners": [
@ -526,6 +528,7 @@ async def test_diagnostics_remote_adapter(
"service_uuids": [],
"source": "esp32",
"time": ANY,
"tx_power": -127,
}
],
"connectable_history": [
@ -554,6 +557,7 @@ async def test_diagnostics_remote_adapter(
"service_uuids": [],
"source": "esp32",
"time": ANY,
"tx_power": -127,
}
],
"scanners": [

View File

@ -8,7 +8,7 @@ from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch
from bleak import BleakError
from bleak.backends.scanner import AdvertisementData, BLEDevice
from bluetooth_adapters import DEFAULT_ADDRESS
from habluetooth import scanner
from habluetooth import scanner, set_manager
from habluetooth.wrappers import HaBleakScannerWrapper
import pytest
@ -1154,6 +1154,7 @@ async def test_async_discovered_device_api(
) -> None:
"""Test the async_discovered_device API."""
mock_bt = []
set_manager(None)
with (
patch(
"homeassistant.components.bluetooth.async_get_bluetooth",
@ -1169,8 +1170,10 @@ async def test_async_discovered_device_api(
},
),
):
assert not bluetooth.async_discovered_service_info(hass)
assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22")
with pytest.raises(RuntimeError, match="BluetoothManager has not been set"):
assert not bluetooth.async_discovered_service_info(hass)
with pytest.raises(RuntimeError, match="BluetoothManager has not been set"):
assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22")
await async_setup_with_default_adapter(hass)
with patch.object(hass.config_entries.flow, "async_init"):
@ -2744,6 +2747,7 @@ async def test_async_ble_device_from_address(
hass: HomeAssistant, mock_bleak_scanner_start: MagicMock, macos_adapter: None
) -> None:
"""Test the async_ble_device_from_address api."""
set_manager(None)
mock_bt = []
with (
patch(
@ -2760,11 +2764,15 @@ async def test_async_ble_device_from_address(
},
),
):
assert not bluetooth.async_discovered_service_info(hass)
assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22")
assert (
bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45") is None
)
with pytest.raises(RuntimeError, match="BluetoothManager has not been set"):
assert not bluetooth.async_discovered_service_info(hass)
with pytest.raises(RuntimeError, match="BluetoothManager has not been set"):
assert not bluetooth.async_address_present(hass, "44:44:22:22:11:22")
with pytest.raises(RuntimeError, match="BluetoothManager has not been set"):
assert (
bluetooth.async_ble_device_from_address(hass, "44:44:33:11:23:45")
is None
)
await async_setup_with_default_adapter(hass)

View File

@ -465,6 +465,7 @@ async def test_unavailable_after_no_data(
device=MagicMock(),
advertisement=MagicMock(),
connectable=True,
tx_power=0,
)
inject_bluetooth_service_info_bleak(hass, service_info_at_time)

View File

@ -92,6 +92,7 @@ async def test_do_not_see_device_if_time_not_updated(
advertisement=generate_advertisement_data(local_name="empty"),
time=0,
connectable=False,
tx_power=-127,
)
# Return with name with time = 0 for all the updates
mock_async_discovered_service_info.return_value = [device]
@ -157,6 +158,7 @@ async def test_see_device_if_time_updated(
advertisement=generate_advertisement_data(local_name="empty"),
time=0,
connectable=False,
tx_power=-127,
)
# Return with name with time = 0 initially
mock_async_discovered_service_info.return_value = [device]
@ -191,6 +193,7 @@ async def test_see_device_if_time_updated(
advertisement=generate_advertisement_data(local_name="empty"),
time=1,
connectable=False,
tx_power=-127,
)
# Return with name with time = 0 initially
mock_async_discovered_service_info.return_value = [device]
@ -237,6 +240,7 @@ async def test_preserve_new_tracked_device_name(
advertisement=generate_advertisement_data(local_name="empty"),
time=0,
connectable=False,
tx_power=-127,
)
# Return with name when seen first time
mock_async_discovered_service_info.return_value = [device]
@ -262,6 +266,7 @@ async def test_preserve_new_tracked_device_name(
advertisement=generate_advertisement_data(local_name="empty"),
time=0,
connectable=False,
tx_power=-127,
)
# Return with name when seen first time
mock_async_discovered_service_info.return_value = [device]
@ -305,6 +310,7 @@ async def test_tracking_battery_times_out(
advertisement=generate_advertisement_data(local_name="empty"),
time=0,
connectable=False,
tx_power=-127,
)
# Return with name when seen first time
mock_async_discovered_service_info.return_value = [device]
@ -373,6 +379,7 @@ async def test_tracking_battery_fails(
advertisement=generate_advertisement_data(local_name="empty"),
time=0,
connectable=False,
tx_power=-127,
)
# Return with name when seen first time
mock_async_discovered_service_info.return_value = [device]
@ -440,6 +447,7 @@ async def test_tracking_battery_successful(
advertisement=generate_advertisement_data(local_name="empty"),
time=0,
connectable=True,
tx_power=-127,
)
# Return with name when seen first time
mock_async_discovered_service_info.return_value = [device]

View File

@ -18,6 +18,7 @@ TEMP_HUMI_SERVICE_INFO = BluetoothServiceInfoBleak(
advertisement=generate_advertisement_data(local_name="Not it"),
time=0,
connectable=False,
tx_power=-127,
)
TEMP_HUMI_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak(
@ -36,6 +37,7 @@ TEMP_HUMI_ENCRYPTED_SERVICE_INFO = BluetoothServiceInfoBleak(
advertisement=generate_advertisement_data(local_name="Not it"),
time=0,
connectable=False,
tx_power=-127,
)
PRST_SERVICE_INFO = BluetoothServiceInfoBleak(
@ -54,6 +56,7 @@ PRST_SERVICE_INFO = BluetoothServiceInfoBleak(
advertisement=generate_advertisement_data(local_name="prst"),
time=0,
connectable=False,
tx_power=-127,
)
INVALID_PAYLOAD = BluetoothServiceInfoBleak(
@ -70,6 +73,7 @@ INVALID_PAYLOAD = BluetoothServiceInfoBleak(
advertisement=generate_advertisement_data(local_name="Not it"),
time=0,
connectable=False,
tx_power=-127,
)
NOT_BTHOME_SERVICE_INFO = BluetoothServiceInfoBleak(
@ -84,6 +88,7 @@ NOT_BTHOME_SERVICE_INFO = BluetoothServiceInfoBleak(
advertisement=generate_advertisement_data(local_name="Not it"),
time=0,
connectable=False,
tx_power=-127,
)
@ -103,6 +108,7 @@ def make_bthome_v1_adv(address: str, payload: bytes) -> BluetoothServiceInfoBlea
advertisement=generate_advertisement_data(local_name="Test Device"),
time=0,
connectable=False,
tx_power=-127,
)
@ -124,6 +130,7 @@ def make_encrypted_bthome_v1_adv(
advertisement=generate_advertisement_data(local_name="ATC 8F80A5"),
time=0,
connectable=False,
tx_power=-127,
)
@ -143,4 +150,5 @@ def make_bthome_v2_adv(address: str, payload: bytes) -> BluetoothServiceInfoBlea
advertisement=generate_advertisement_data(local_name="Test Device"),
time=0,
connectable=False,
tx_power=-127,
)

View File

@ -69,7 +69,17 @@ async def test_sending_message(hass: HomeAssistant, events: list[Event]) -> None
await hass.services.async_call(notify.DOMAIN, notify.SERVICE_SEND_MESSAGE, data)
await hass.async_block_till_done()
last_event = events[-1]
assert last_event.data[notify.ATTR_MESSAGE] == "Test message"
assert last_event.data == {notify.ATTR_MESSAGE: "Test message"}
data[notify.ATTR_TITLE] = "My title"
# Test with Title
await hass.services.async_call(notify.DOMAIN, notify.SERVICE_SEND_MESSAGE, data)
await hass.async_block_till_done()
last_event = events[-1]
assert last_event.data == {
notify.ATTR_MESSAGE: "Test message",
notify.ATTR_TITLE: "My title",
}
async def test_calling_notify_from_script_loaded_from_yaml(

View File

@ -257,9 +257,7 @@ class HomeControlMock(HomeControl):
self.gateway = MagicMock()
self.gateway.local_connection = True
self.gateway.firmware_version = "8.94.0"
def websocket_disconnect(self, event: str = "") -> None:
"""Mock disconnect of the websocket."""
self.websocket_disconnect = MagicMock()
class HomeControlMockBinarySensor(HomeControlMock):

View File

@ -6,8 +6,9 @@ from devolo_home_control_api.exceptions.gateway import GatewayOfflineError
import pytest
from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN
from homeassistant.components.devolo_home_control import DOMAIN
from homeassistant.components.devolo_home_control.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import EVENT_HOMEASSISTANT_STOP
from homeassistant.core import HomeAssistant
from homeassistant.helpers import device_registry as dr
from homeassistant.setup import async_setup_component
@ -63,6 +64,23 @@ async def test_unload_entry(hass: HomeAssistant) -> None:
assert entry.state is ConfigEntryState.NOT_LOADED
async def test_home_assistant_stop(hass: HomeAssistant) -> None:
"""Test home assistant stop."""
entry = configure_integration(hass)
test_gateway = HomeControlMock()
test_gateway2 = HomeControlMock()
with patch(
"homeassistant.components.devolo_home_control.HomeControl",
side_effect=[test_gateway, test_gateway2],
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_STOP)
await hass.async_block_till_done()
assert test_gateway.websocket_disconnect.called
assert test_gateway2.websocket_disconnect.called
async def test_remove_device(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,

View File

@ -6,7 +6,7 @@ from unittest.mock import AsyncMock, patch
from pydiscovergy.models import Reading
import pytest
from homeassistant.components.discovergy import DOMAIN
from homeassistant.components.discovergy.const import DOMAIN
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component

View File

@ -18,6 +18,7 @@ DKEY_DISCOVERY_INFO = BluetoothServiceInfoBleak(
),
time=0,
connectable=True,
tx_power=-127,
)
@ -36,4 +37,5 @@ NOT_DKEY_DISCOVERY_INFO = BluetoothServiceInfoBleak(
advertisement=generate_advertisement_data(),
time=0,
connectable=True,
tx_power=-127,
)

View File

@ -65,6 +65,9 @@ GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP = {
"identifier": 8675309,
"name": "ecobee",
"modelNumber": "athenaSmart",
"utcTime": "2022-01-01 10:00:00",
"thermostatTime": "2022-01-01 6:00:00",
"location": {"timeZone": "America/Toronto"},
"program": {
"climates": [
{"name": "Climate1", "climateRef": "c1"},
@ -92,7 +95,8 @@ GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP = {
"humidifierMode": "manual",
"humidity": "30",
"hasHeatPump": True,
"ventilatorType": "none",
"ventilatorType": "hrv",
"ventilatorOffDateTime": "2022-01-01 6:00:00",
},
"equipmentStatus": "fan",
"events": [

View File

@ -4,6 +4,11 @@
"identifier": 8675309,
"name": "ecobee",
"modelNumber": "athenaSmart",
"utcTime": "2022-01-01 10:00:00",
"thermostatTime": "2022-01-01 6:00:00",
"location": {
"timeZone": "America/Toronto"
},
"program": {
"climates": [
{ "name": "Climate1", "climateRef": "c1" },
@ -30,6 +35,7 @@
"ventilatorType": "hrv",
"ventilatorMinOnTimeHome": 20,
"ventilatorMinOnTimeAway": 10,
"ventilatorOffDateTime": "2022-01-01 6:00:00",
"isVentilatorTimerOn": false,
"hasHumidifier": true,
"humidifierMode": "manual",

View File

@ -0,0 +1,115 @@
"""The test for the ecobee thermostat switch module."""
import copy
from datetime import datetime, timedelta
from unittest import mock
from unittest.mock import patch
import pytest
from homeassistant.components.ecobee.switch import DATE_FORMAT
from homeassistant.components.switch import DOMAIN, SERVICE_TURN_OFF, SERVICE_TURN_ON
from homeassistant.const import ATTR_ENTITY_ID
from homeassistant.core import HomeAssistant
from .common import setup_platform
from tests.components.ecobee import GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP
VENTILATOR_20MIN_ID = "switch.ecobee_ventilator_20m_timer"
THERMOSTAT_ID = 0
@pytest.fixture(name="data")
def data_fixture():
"""Set up data mock."""
data = mock.Mock()
data.return_value = copy.deepcopy(GENERIC_THERMOSTAT_INFO_WITH_HEATPUMP)
return data
async def test_ventilator_20min_attributes(hass: HomeAssistant) -> None:
"""Test the ventilator switch on home attributes are correct."""
await setup_platform(hass, DOMAIN)
state = hass.states.get(VENTILATOR_20MIN_ID)
assert state.state == "off"
async def test_ventilator_20min_when_on(hass: HomeAssistant, data) -> None:
"""Test the ventilator switch goes on."""
data.return_value["settings"]["ventilatorOffDateTime"] = (
datetime.now() + timedelta(days=1)
).strftime(DATE_FORMAT)
with mock.patch("pyecobee.Ecobee.get_thermostat", data):
await setup_platform(hass, DOMAIN)
state = hass.states.get(VENTILATOR_20MIN_ID)
assert state.state == "on"
data.reset_mock()
async def test_ventilator_20min_when_off(hass: HomeAssistant, data) -> None:
"""Test the ventilator switch goes on."""
data.return_value["settings"]["ventilatorOffDateTime"] = (
datetime.now() - timedelta(days=1)
).strftime(DATE_FORMAT)
with mock.patch("pyecobee.Ecobee.get_thermostat", data):
await setup_platform(hass, DOMAIN)
state = hass.states.get(VENTILATOR_20MIN_ID)
assert state.state == "off"
data.reset_mock()
async def test_ventilator_20min_when_empty(hass: HomeAssistant, data) -> None:
"""Test the ventilator switch goes on."""
data.return_value["settings"]["ventilatorOffDateTime"] = ""
with mock.patch("pyecobee.Ecobee.get_thermostat", data):
await setup_platform(hass, DOMAIN)
state = hass.states.get(VENTILATOR_20MIN_ID)
assert state.state == "off"
data.reset_mock()
async def test_turn_on_20min_ventilator(hass: HomeAssistant) -> None:
"""Test the switch 20 min timer (On)."""
with patch(
"homeassistant.components.ecobee.Ecobee.set_ventilator_timer"
) as mock_set_20min_ventilator:
await setup_platform(hass, DOMAIN)
await hass.services.async_call(
DOMAIN,
SERVICE_TURN_ON,
{ATTR_ENTITY_ID: VENTILATOR_20MIN_ID},
blocking=True,
)
await hass.async_block_till_done()
mock_set_20min_ventilator.assert_called_once_with(THERMOSTAT_ID, True)
async def test_turn_off_20min_ventilator(hass: HomeAssistant) -> None:
"""Test the switch 20 min timer (off)."""
with patch(
"homeassistant.components.ecobee.Ecobee.set_ventilator_timer"
) as mock_set_20min_ventilator:
await setup_platform(hass, DOMAIN)
await hass.services.async_call(
DOMAIN,
SERVICE_TURN_OFF,
{ATTR_ENTITY_ID: VENTILATOR_20MIN_ID},
blocking=True,
)
await hass.async_block_till_done()
mock_set_20min_ventilator.assert_called_once_with(THERMOSTAT_ID, False)

View File

@ -38,4 +38,5 @@ def fake_service_info():
tx_power=-127,
platform_data=(),
),
tx_power=-127,
)

View File

@ -16,4 +16,5 @@ COOKER_SERVICE_INFO = BluetoothServiceInfoBleak(
advertisement=generate_advertisement_data(),
time=0,
connectable=True,
tx_power=-127,
)

View File

@ -1,6 +1,7 @@
"""Test helpers."""
from collections.abc import Generator
from datetime import UTC, datetime, timedelta
from unittest.mock import AsyncMock, patch
import pytest
@ -32,14 +33,17 @@ def mock_fyta_init():
"""Build a fixture for the Fyta API that connects successfully and returns one device."""
mock_fyta_api = AsyncMock()
with patch(
"homeassistant.components.fyta.FytaConnector",
return_value=mock_fyta_api,
) as mock_fyta_api:
mock_fyta_api.return_value.login.return_value = {
mock_fyta_api.expiration = datetime.now(tz=UTC) + timedelta(days=1)
mock_fyta_api.login = AsyncMock(
return_value={
CONF_ACCESS_TOKEN: ACCESS_TOKEN,
CONF_EXPIRATION: EXPIRATION,
}
)
with patch(
"homeassistant.components.fyta.FytaConnector.__new__",
return_value=mock_fyta_api,
):
yield mock_fyta_api

View File

@ -21,7 +21,7 @@ from tests.common import MockConfigEntry
USERNAME = "fyta_user"
PASSWORD = "fyta_pass"
ACCESS_TOKEN = "123xyz"
EXPIRATION = datetime.fromisoformat("2024-12-31T10:00:00").astimezone(UTC)
EXPIRATION = datetime.fromisoformat("2024-12-31T10:00:00").replace(tzinfo=UTC)
async def test_user_flow(

View File

@ -3,15 +3,24 @@
import pytest
from homeassistant.components import history
from homeassistant.components.recorder import Recorder
from homeassistant.const import CONF_DOMAINS, CONF_ENTITIES, CONF_EXCLUDE, CONF_INCLUDE
from homeassistant.setup import setup_component
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from tests.typing import RecorderInstanceGenerator
@pytest.fixture
def hass_history(hass_recorder):
"""Home Assistant fixture with history."""
hass = hass_recorder()
async def mock_recorder_before_hass(
async_setup_recorder_instance: RecorderInstanceGenerator,
) -> None:
"""Set up recorder."""
@pytest.fixture
async def hass_history(hass: HomeAssistant, recorder_mock: Recorder) -> None:
"""Home Assistant fixture with history."""
config = history.CONFIG_SCHEMA(
{
history.DOMAIN: {
@ -26,6 +35,4 @@ def hass_history(hass_recorder):
}
}
)
assert setup_component(hass, history.DOMAIN, config)
return hass
assert await async_setup_component(hass, history.DOMAIN, config)

View File

@ -24,7 +24,6 @@ from tests.components.recorder.common import (
assert_multiple_states_equal_without_context_and_last_changed,
assert_states_equal_without_context,
async_wait_recording_done,
wait_recording_done,
)
from tests.typing import ClientSessionGenerator
@ -39,25 +38,26 @@ def listeners_without_writes(listeners: dict[str, int]) -> dict[str, int]:
@pytest.mark.usefixtures("hass_history")
def test_setup() -> None:
async def test_setup() -> None:
"""Test setup method of history."""
# Verification occurs in the fixture
def test_get_significant_states(hass_history) -> None:
async def test_get_significant_states(hass: HomeAssistant, hass_history) -> None:
"""Test that only significant states are returned.
We should get back every thermostat change that
includes an attribute change, but only the state updates for
media player (attribute changes are not significant and not returned).
"""
hass = hass_history
zero, four, states = record_states(hass)
zero, four, states = await async_record_states(hass)
hist = get_significant_states(hass, zero, four, entity_ids=list(states))
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
def test_get_significant_states_minimal_response(hass_history) -> None:
async def test_get_significant_states_minimal_response(
hass: HomeAssistant, hass_history
) -> None:
"""Test that only significant states are returned.
When minimal responses is set only the first and
@ -67,8 +67,7 @@ def test_get_significant_states_minimal_response(hass_history) -> None:
includes an attribute change, but only the state updates for
media player (attribute changes are not significant and not returned).
"""
hass = hass_history
zero, four, states = record_states(hass)
zero, four, states = await async_record_states(hass)
hist = get_significant_states(
hass, zero, four, minimal_response=True, entity_ids=list(states)
)
@ -122,15 +121,16 @@ def test_get_significant_states_minimal_response(hass_history) -> None:
)
def test_get_significant_states_with_initial(hass_history) -> None:
async def test_get_significant_states_with_initial(
hass: HomeAssistant, hass_history
) -> None:
"""Test that only significant states are returned.
We should get back every thermostat change that
includes an attribute change, but only the state updates for
media player (attribute changes are not significant and not returned).
"""
hass = hass_history
zero, four, states = record_states(hass)
zero, four, states = await async_record_states(hass)
one_and_half = zero + timedelta(seconds=1.5)
for entity_id in states:
if entity_id == "media_player.test":
@ -149,15 +149,16 @@ def test_get_significant_states_with_initial(hass_history) -> None:
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
def test_get_significant_states_without_initial(hass_history) -> None:
async def test_get_significant_states_without_initial(
hass: HomeAssistant, hass_history
) -> None:
"""Test that only significant states are returned.
We should get back every thermostat change that
includes an attribute change, but only the state updates for
media player (attribute changes are not significant and not returned).
"""
hass = hass_history
zero, four, states = record_states(hass)
zero, four, states = await async_record_states(hass)
one = zero + timedelta(seconds=1)
one_with_microsecond = zero + timedelta(seconds=1, microseconds=1)
one_and_half = zero + timedelta(seconds=1.5)
@ -179,10 +180,11 @@ def test_get_significant_states_without_initial(hass_history) -> None:
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
def test_get_significant_states_entity_id(hass_history) -> None:
async def test_get_significant_states_entity_id(
hass: HomeAssistant, hass_history
) -> None:
"""Test that only significant states are returned for one entity."""
hass = hass_history
zero, four, states = record_states(hass)
zero, four, states = await async_record_states(hass)
del states["media_player.test2"]
del states["media_player.test3"]
del states["thermostat.test"]
@ -193,10 +195,11 @@ def test_get_significant_states_entity_id(hass_history) -> None:
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
def test_get_significant_states_multiple_entity_ids(hass_history) -> None:
async def test_get_significant_states_multiple_entity_ids(
hass: HomeAssistant, hass_history
) -> None:
"""Test that only significant states are returned for one entity."""
hass = hass_history
zero, four, states = record_states(hass)
zero, four, states = await async_record_states(hass)
del states["media_player.test2"]
del states["media_player.test3"]
del states["thermostat.test2"]
@ -211,14 +214,15 @@ def test_get_significant_states_multiple_entity_ids(hass_history) -> None:
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
def test_get_significant_states_are_ordered(hass_history) -> None:
async def test_get_significant_states_are_ordered(
hass: HomeAssistant, hass_history
) -> None:
"""Test order of results from get_significant_states.
When entity ids are given, the results should be returned with the data
in the same order.
"""
hass = hass_history
zero, four, _states = record_states(hass)
zero, four, _states = await async_record_states(hass)
entity_ids = ["media_player.test", "media_player.test2"]
hist = get_significant_states(hass, zero, four, entity_ids)
assert list(hist.keys()) == entity_ids
@ -227,15 +231,14 @@ def test_get_significant_states_are_ordered(hass_history) -> None:
assert list(hist.keys()) == entity_ids
def test_get_significant_states_only(hass_history) -> None:
async def test_get_significant_states_only(hass: HomeAssistant, hass_history) -> None:
"""Test significant states when significant_states_only is set."""
hass = hass_history
entity_id = "sensor.test"
def set_state(state, **kwargs):
async def set_state(state, **kwargs):
"""Set the state."""
hass.states.set(entity_id, state, **kwargs)
wait_recording_done(hass)
hass.states.async_set(entity_id, state, **kwargs)
await async_wait_recording_done(hass)
return hass.states.get(entity_id)
start = dt_util.utcnow() - timedelta(minutes=4)
@ -243,19 +246,19 @@ def test_get_significant_states_only(hass_history) -> None:
states = []
with freeze_time(start) as freezer:
set_state("123", attributes={"attribute": 10.64})
await set_state("123", attributes={"attribute": 10.64})
freezer.move_to(points[0])
# Attributes are different, state not
states.append(set_state("123", attributes={"attribute": 21.42}))
states.append(await set_state("123", attributes={"attribute": 21.42}))
freezer.move_to(points[1])
# state is different, attributes not
states.append(set_state("32", attributes={"attribute": 21.42}))
states.append(await set_state("32", attributes={"attribute": 21.42}))
freezer.move_to(points[2])
# everything is different
states.append(set_state("412", attributes={"attribute": 54.23}))
states.append(await set_state("412", attributes={"attribute": 54.23}))
hist = get_significant_states(
hass,
@ -288,13 +291,13 @@ def test_get_significant_states_only(hass_history) -> None:
)
def check_significant_states(hass, zero, four, states, config):
async def check_significant_states(hass, zero, four, states, config):
"""Check if significant states are retrieved."""
hist = get_significant_states(hass, zero, four)
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
def record_states(hass):
async def async_record_states(hass):
"""Record some test states.
We inject a bunch of state updates from media player, zone and
@ -308,10 +311,10 @@ def record_states(hass):
zone = "zone.home"
script_c = "script.can_cancel_this_one"
def set_state(entity_id, state, **kwargs):
async def set_state(entity_id, state, **kwargs):
"""Set the state."""
hass.states.set(entity_id, state, **kwargs)
wait_recording_done(hass)
hass.states.async_set(entity_id, state, **kwargs)
await async_wait_recording_done(hass)
return hass.states.get(entity_id)
zero = dt_util.utcnow()
@ -323,55 +326,63 @@ def record_states(hass):
states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []}
with freeze_time(one) as freezer:
states[mp].append(
set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)})
await set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)})
)
states[mp2].append(
set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)})
await set_state(
mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}
)
)
states[mp3].append(
set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)})
await set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)})
)
states[therm].append(
set_state(therm, 20, attributes={"current_temperature": 19.5})
await set_state(therm, 20, attributes={"current_temperature": 19.5})
)
freezer.move_to(one + timedelta(microseconds=1))
states[mp].append(
set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)})
await set_state(
mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}
)
)
freezer.move_to(two)
# This state will be skipped only different in time
set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)})
await set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)})
# This state will be skipped because domain is excluded
set_state(zone, "zoning")
await set_state(zone, "zoning")
states[script_c].append(
set_state(script_c, "off", attributes={"can_cancel": True})
await set_state(script_c, "off", attributes={"can_cancel": True})
)
states[therm].append(
set_state(therm, 21, attributes={"current_temperature": 19.8})
await set_state(therm, 21, attributes={"current_temperature": 19.8})
)
states[therm2].append(
set_state(therm2, 20, attributes={"current_temperature": 19})
await set_state(therm2, 20, attributes={"current_temperature": 19})
)
freezer.move_to(three)
states[mp].append(
set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)})
await set_state(
mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}
)
)
states[mp3].append(
set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)})
await set_state(
mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}
)
)
# Attributes changed even though state is the same
states[therm].append(
set_state(therm, 21, attributes={"current_temperature": 20})
await set_state(therm, 21, attributes={"current_temperature": 20})
)
return zero, four, states
async def test_fetch_period_api(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test the fetch period view for history."""
await async_setup_component(hass, "history", {})
@ -383,8 +394,8 @@ async def test_fetch_period_api(
async def test_fetch_period_api_with_use_include_order(
recorder_mock: Recorder,
hass: HomeAssistant,
recorder_mock: Recorder,
hass_client: ClientSessionGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
@ -402,7 +413,7 @@ async def test_fetch_period_api_with_use_include_order(
async def test_fetch_period_api_with_minimal_response(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test the fetch period view for history with minimal_response."""
now = dt_util.utcnow()
@ -444,7 +455,7 @@ async def test_fetch_period_api_with_minimal_response(
async def test_fetch_period_api_with_no_timestamp(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test the fetch period view for history with no timestamp."""
await async_setup_component(hass, "history", {})
@ -454,8 +465,8 @@ async def test_fetch_period_api_with_no_timestamp(
async def test_fetch_period_api_with_include_order(
recorder_mock: Recorder,
hass: HomeAssistant,
recorder_mock: Recorder,
hass_client: ClientSessionGenerator,
caplog: pytest.LogCaptureFixture,
) -> None:
@ -482,7 +493,7 @@ async def test_fetch_period_api_with_include_order(
async def test_entity_ids_limit_via_api(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test limiting history to entity_ids."""
await async_setup_component(
@ -508,7 +519,7 @@ async def test_entity_ids_limit_via_api(
async def test_entity_ids_limit_via_api_with_skip_initial_state(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test limiting history to entity_ids with skip_initial_state."""
await async_setup_component(
@ -542,7 +553,7 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state(
async def test_fetch_period_api_before_history_started(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test the fetch period view for history for the far past."""
await async_setup_component(
@ -563,7 +574,7 @@ async def test_fetch_period_api_before_history_started(
async def test_fetch_period_api_far_future(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test the fetch period view for history for the far future."""
await async_setup_component(
@ -584,7 +595,7 @@ async def test_fetch_period_api_far_future(
async def test_fetch_period_api_with_invalid_datetime(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test the fetch period view for history with an invalid date time."""
await async_setup_component(
@ -603,7 +614,7 @@ async def test_fetch_period_api_with_invalid_datetime(
async def test_fetch_period_api_invalid_end_time(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test the fetch period view for history with an invalid end time."""
await async_setup_component(
@ -625,7 +636,7 @@ async def test_fetch_period_api_invalid_end_time(
async def test_entity_ids_limit_via_api_with_end_time(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test limiting history to entity_ids with end_time."""
await async_setup_component(
@ -671,7 +682,7 @@ async def test_entity_ids_limit_via_api_with_end_time(
async def test_fetch_period_api_with_no_entity_ids(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test the fetch period view for history with minimal_response."""
await async_setup_component(hass, "history", {})
@ -724,13 +735,13 @@ async def test_fetch_period_api_with_no_entity_ids(
],
)
async def test_history_with_invalid_entity_ids(
hass: HomeAssistant,
recorder_mock: Recorder,
hass_client: ClientSessionGenerator,
filter_entity_id,
status_code,
response_contains1,
response_contains2,
recorder_mock: Recorder,
hass: HomeAssistant,
hass_client: ClientSessionGenerator,
) -> None:
"""Test sending valid and invalid entity_ids to the API."""
await async_setup_component(

View File

@ -27,7 +27,6 @@ from tests.components.recorder.common import (
async_recorder_block_till_done,
async_wait_recording_done,
old_db_schema,
wait_recording_done,
)
from tests.typing import ClientSessionGenerator, WebSocketGenerator
@ -40,33 +39,34 @@ def db_schema_30():
@pytest.fixture
def legacy_hass_history(hass_history):
def legacy_hass_history(hass: HomeAssistant, hass_history):
"""Home Assistant fixture to use legacy history recording."""
instance = recorder.get_instance(hass_history)
instance = recorder.get_instance(hass)
with patch.object(instance.states_meta_manager, "active", False):
yield hass_history
yield
@pytest.mark.usefixtures("legacy_hass_history")
def test_setup() -> None:
async def test_setup() -> None:
"""Test setup method of history."""
# Verification occurs in the fixture
def test_get_significant_states(legacy_hass_history) -> None:
async def test_get_significant_states(hass: HomeAssistant, legacy_hass_history) -> None:
"""Test that only significant states are returned.
We should get back every thermostat change that
includes an attribute change, but only the state updates for
media player (attribute changes are not significant and not returned).
"""
hass = legacy_hass_history
zero, four, states = record_states(hass)
zero, four, states = await async_record_states(hass)
hist = get_significant_states(hass, zero, four, entity_ids=list(states))
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
def test_get_significant_states_minimal_response(legacy_hass_history) -> None:
async def test_get_significant_states_minimal_response(
hass: HomeAssistant, legacy_hass_history
) -> None:
"""Test that only significant states are returned.
When minimal responses is set only the first and
@ -76,8 +76,7 @@ def test_get_significant_states_minimal_response(legacy_hass_history) -> None:
includes an attribute change, but only the state updates for
media player (attribute changes are not significant and not returned).
"""
hass = legacy_hass_history
zero, four, states = record_states(hass)
zero, four, states = await async_record_states(hass)
hist = get_significant_states(
hass, zero, four, minimal_response=True, entity_ids=list(states)
)
@ -132,15 +131,16 @@ def test_get_significant_states_minimal_response(legacy_hass_history) -> None:
)
def test_get_significant_states_with_initial(legacy_hass_history) -> None:
async def test_get_significant_states_with_initial(
hass: HomeAssistant, legacy_hass_history
) -> None:
"""Test that only significant states are returned.
We should get back every thermostat change that
includes an attribute change, but only the state updates for
media player (attribute changes are not significant and not returned).
"""
hass = legacy_hass_history
zero, four, states = record_states(hass)
zero, four, states = await async_record_states(hass)
one = zero + timedelta(seconds=1)
one_with_microsecond = zero + timedelta(seconds=1, microseconds=1)
one_and_half = zero + timedelta(seconds=1.5)
@ -162,15 +162,16 @@ def test_get_significant_states_with_initial(legacy_hass_history) -> None:
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
def test_get_significant_states_without_initial(legacy_hass_history) -> None:
async def test_get_significant_states_without_initial(
hass: HomeAssistant, legacy_hass_history
) -> None:
"""Test that only significant states are returned.
We should get back every thermostat change that
includes an attribute change, but only the state updates for
media player (attribute changes are not significant and not returned).
"""
hass = legacy_hass_history
zero, four, states = record_states(hass)
zero, four, states = await async_record_states(hass)
one = zero + timedelta(seconds=1)
one_with_microsecond = zero + timedelta(seconds=1, microseconds=1)
one_and_half = zero + timedelta(seconds=1.5)
@ -193,13 +194,13 @@ def test_get_significant_states_without_initial(legacy_hass_history) -> None:
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
def test_get_significant_states_entity_id(hass_history) -> None:
async def test_get_significant_states_entity_id(
hass: HomeAssistant, hass_history
) -> None:
"""Test that only significant states are returned for one entity."""
hass = hass_history
instance = recorder.get_instance(hass)
with patch.object(instance.states_meta_manager, "active", False):
zero, four, states = record_states(hass)
zero, four, states = await async_record_states(hass)
del states["media_player.test2"]
del states["media_player.test3"]
del states["thermostat.test"]
@ -210,10 +211,11 @@ def test_get_significant_states_entity_id(hass_history) -> None:
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
def test_get_significant_states_multiple_entity_ids(legacy_hass_history) -> None:
async def test_get_significant_states_multiple_entity_ids(
hass: HomeAssistant, legacy_hass_history
) -> None:
"""Test that only significant states are returned for one entity."""
hass = legacy_hass_history
zero, four, states = record_states(hass)
zero, four, states = await async_record_states(hass)
del states["media_player.test2"]
del states["media_player.test3"]
del states["thermostat.test2"]
@ -228,14 +230,15 @@ def test_get_significant_states_multiple_entity_ids(legacy_hass_history) -> None
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
def test_get_significant_states_are_ordered(legacy_hass_history) -> None:
async def test_get_significant_states_are_ordered(
hass: HomeAssistant, legacy_hass_history
) -> None:
"""Test order of results from get_significant_states.
When entity ids are given, the results should be returned with the data
in the same order.
"""
hass = legacy_hass_history
zero, four, _states = record_states(hass)
zero, four, _states = await async_record_states(hass)
entity_ids = ["media_player.test", "media_player.test2"]
hist = get_significant_states(hass, zero, four, entity_ids)
assert list(hist.keys()) == entity_ids
@ -244,15 +247,16 @@ def test_get_significant_states_are_ordered(legacy_hass_history) -> None:
assert list(hist.keys()) == entity_ids
def test_get_significant_states_only(legacy_hass_history) -> None:
async def test_get_significant_states_only(
hass: HomeAssistant, legacy_hass_history
) -> None:
"""Test significant states when significant_states_only is set."""
hass = legacy_hass_history
entity_id = "sensor.test"
def set_state(state, **kwargs):
async def set_state(state, **kwargs):
"""Set the state."""
hass.states.set(entity_id, state, **kwargs)
wait_recording_done(hass)
hass.states.async_set(entity_id, state, **kwargs)
await async_wait_recording_done(hass)
return hass.states.get(entity_id)
start = dt_util.utcnow() - timedelta(minutes=4)
@ -260,19 +264,19 @@ def test_get_significant_states_only(legacy_hass_history) -> None:
states = []
with freeze_time(start) as freezer:
set_state("123", attributes={"attribute": 10.64})
await set_state("123", attributes={"attribute": 10.64})
freezer.move_to(points[0])
# Attributes are different, state not
states.append(set_state("123", attributes={"attribute": 21.42}))
states.append(await set_state("123", attributes={"attribute": 21.42}))
freezer.move_to(points[1])
# state is different, attributes not
states.append(set_state("32", attributes={"attribute": 21.42}))
states.append(await set_state("32", attributes={"attribute": 21.42}))
freezer.move_to(points[2])
# everything is different
states.append(set_state("412", attributes={"attribute": 54.23}))
states.append(await set_state("412", attributes={"attribute": 54.23}))
hist = get_significant_states(
hass,
@ -311,7 +315,7 @@ def check_significant_states(hass, zero, four, states, config):
assert_dict_of_states_equal_without_context_and_last_changed(states, hist)
def record_states(hass):
async def async_record_states(hass):
"""Record some test states.
We inject a bunch of state updates from media player, zone and
@ -325,10 +329,10 @@ def record_states(hass):
zone = "zone.home"
script_c = "script.can_cancel_this_one"
def set_state(entity_id, state, **kwargs):
async def async_set_state(entity_id, state, **kwargs):
"""Set the state."""
hass.states.set(entity_id, state, **kwargs)
wait_recording_done(hass)
hass.states.async_set(entity_id, state, **kwargs)
await async_wait_recording_done(hass)
return hass.states.get(entity_id)
zero = dt_util.utcnow()
@ -340,55 +344,69 @@ def record_states(hass):
states = {therm: [], therm2: [], mp: [], mp2: [], mp3: [], script_c: []}
with freeze_time(one) as freezer:
states[mp].append(
set_state(mp, "idle", attributes={"media_title": str(sentinel.mt1)})
await async_set_state(
mp, "idle", attributes={"media_title": str(sentinel.mt1)}
)
)
states[mp2].append(
set_state(mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)})
await async_set_state(
mp2, "YouTube", attributes={"media_title": str(sentinel.mt2)}
)
)
states[mp3].append(
set_state(mp3, "idle", attributes={"media_title": str(sentinel.mt1)})
await async_set_state(
mp3, "idle", attributes={"media_title": str(sentinel.mt1)}
)
)
states[therm].append(
set_state(therm, 20, attributes={"current_temperature": 19.5})
await async_set_state(therm, 20, attributes={"current_temperature": 19.5})
)
freezer.move_to(one + timedelta(microseconds=1))
states[mp].append(
set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt2)})
await async_set_state(
mp, "YouTube", attributes={"media_title": str(sentinel.mt2)}
)
)
freezer.move_to(two)
# This state will be skipped only different in time
set_state(mp, "YouTube", attributes={"media_title": str(sentinel.mt3)})
await async_set_state(
mp, "YouTube", attributes={"media_title": str(sentinel.mt3)}
)
# This state will be skipped because domain is excluded
set_state(zone, "zoning")
await async_set_state(zone, "zoning")
states[script_c].append(
set_state(script_c, "off", attributes={"can_cancel": True})
await async_set_state(script_c, "off", attributes={"can_cancel": True})
)
states[therm].append(
set_state(therm, 21, attributes={"current_temperature": 19.8})
await async_set_state(therm, 21, attributes={"current_temperature": 19.8})
)
states[therm2].append(
set_state(therm2, 20, attributes={"current_temperature": 19})
await async_set_state(therm2, 20, attributes={"current_temperature": 19})
)
freezer.move_to(three)
states[mp].append(
set_state(mp, "Netflix", attributes={"media_title": str(sentinel.mt4)})
await async_set_state(
mp, "Netflix", attributes={"media_title": str(sentinel.mt4)}
)
)
states[mp3].append(
set_state(mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)})
await async_set_state(
mp3, "Netflix", attributes={"media_title": str(sentinel.mt3)}
)
)
# Attributes changed even though state is the same
states[therm].append(
set_state(therm, 21, attributes={"current_temperature": 20})
await async_set_state(therm, 21, attributes={"current_temperature": 20})
)
return zero, four, states
async def test_fetch_period_api(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test the fetch period view for history."""
await async_setup_component(hass, "history", {})
@ -402,7 +420,7 @@ async def test_fetch_period_api(
async def test_fetch_period_api_with_minimal_response(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test the fetch period view for history with minimal_response."""
now = dt_util.utcnow()
@ -445,7 +463,7 @@ async def test_fetch_period_api_with_minimal_response(
async def test_fetch_period_api_with_no_timestamp(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test the fetch period view for history with no timestamp."""
await async_setup_component(hass, "history", {})
@ -457,7 +475,7 @@ async def test_fetch_period_api_with_no_timestamp(
async def test_fetch_period_api_with_include_order(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test the fetch period view for history."""
await async_setup_component(
@ -481,7 +499,7 @@ async def test_fetch_period_api_with_include_order(
async def test_entity_ids_limit_via_api(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test limiting history to entity_ids."""
await async_setup_component(
@ -509,7 +527,7 @@ async def test_entity_ids_limit_via_api(
async def test_entity_ids_limit_via_api_with_skip_initial_state(
recorder_mock: Recorder, hass: HomeAssistant, hass_client: ClientSessionGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_client: ClientSessionGenerator
) -> None:
"""Test limiting history to entity_ids with skip_initial_state."""
await async_setup_component(
@ -545,7 +563,7 @@ async def test_entity_ids_limit_via_api_with_skip_initial_state(
async def test_history_during_period(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history_during_period."""
now = dt_util.utcnow()
@ -693,7 +711,7 @@ async def test_history_during_period(
async def test_history_during_period_impossible_conditions(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history_during_period returns when condition cannot be true."""
await async_setup_component(hass, "history", {})
@ -757,10 +775,10 @@ async def test_history_during_period_impossible_conditions(
"time_zone", ["UTC", "Europe/Berlin", "America/Chicago", "US/Hawaii"]
)
async def test_history_during_period_significant_domain(
time_zone,
recorder_mock: Recorder,
hass: HomeAssistant,
recorder_mock: Recorder,
hass_ws_client: WebSocketGenerator,
time_zone,
) -> None:
"""Test history_during_period with climate domain."""
hass.config.set_time_zone(time_zone)
@ -941,7 +959,7 @@ async def test_history_during_period_significant_domain(
async def test_history_during_period_bad_start_time(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history_during_period bad state time."""
await async_setup_component(
@ -966,7 +984,7 @@ async def test_history_during_period_bad_start_time(
async def test_history_during_period_bad_end_time(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history_during_period bad end time."""
now = dt_util.utcnow()

View File

@ -39,7 +39,7 @@ def test_setup() -> None:
async def test_history_during_period(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history_during_period."""
now = dt_util.utcnow()
@ -173,7 +173,7 @@ async def test_history_during_period(
async def test_history_during_period_impossible_conditions(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history_during_period returns when condition cannot be true."""
await async_setup_component(hass, "history", {})
@ -235,10 +235,10 @@ async def test_history_during_period_impossible_conditions(
"time_zone", ["UTC", "Europe/Berlin", "America/Chicago", "US/Hawaii"]
)
async def test_history_during_period_significant_domain(
time_zone,
recorder_mock: Recorder,
hass: HomeAssistant,
recorder_mock: Recorder,
hass_ws_client: WebSocketGenerator,
time_zone,
) -> None:
"""Test history_during_period with climate domain."""
hass.config.set_time_zone(time_zone)
@ -403,7 +403,7 @@ async def test_history_during_period_significant_domain(
async def test_history_during_period_bad_start_time(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history_during_period bad state time."""
await async_setup_component(
@ -427,7 +427,7 @@ async def test_history_during_period_bad_start_time(
async def test_history_during_period_bad_end_time(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history_during_period bad end time."""
now = dt_util.utcnow()
@ -454,7 +454,7 @@ async def test_history_during_period_bad_end_time(
async def test_history_stream_historical_only(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history stream."""
now = dt_util.utcnow()
@ -525,7 +525,7 @@ async def test_history_stream_historical_only(
async def test_history_stream_significant_domain_historical_only(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test the stream with climate domain with historical states only."""
now = dt_util.utcnow()
@ -726,7 +726,7 @@ async def test_history_stream_significant_domain_historical_only(
async def test_history_stream_bad_start_time(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history stream bad state time."""
await async_setup_component(
@ -750,7 +750,7 @@ async def test_history_stream_bad_start_time(
async def test_history_stream_end_time_before_start_time(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history stream with an end_time before the start_time."""
end_time = dt_util.utcnow() - timedelta(seconds=2)
@ -778,7 +778,7 @@ async def test_history_stream_end_time_before_start_time(
async def test_history_stream_bad_end_time(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history stream bad end time."""
now = dt_util.utcnow()
@ -805,7 +805,7 @@ async def test_history_stream_bad_end_time(
async def test_history_stream_live_no_attributes_minimal_response(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history stream with history and live data and no_attributes and minimal_response."""
now = dt_util.utcnow()
@ -882,7 +882,7 @@ async def test_history_stream_live_no_attributes_minimal_response(
async def test_history_stream_live(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history stream with history and live data."""
now = dt_util.utcnow()
@ -985,7 +985,7 @@ async def test_history_stream_live(
async def test_history_stream_live_minimal_response(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history stream with history and live data and minimal_response."""
now = dt_util.utcnow()
@ -1082,7 +1082,7 @@ async def test_history_stream_live_minimal_response(
async def test_history_stream_live_no_attributes(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history stream with history and live data and no_attributes."""
now = dt_util.utcnow()
@ -1163,7 +1163,7 @@ async def test_history_stream_live_no_attributes(
async def test_history_stream_live_no_attributes_minimal_response_specific_entities(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history stream with history and live data and no_attributes and minimal_response with specific entities."""
now = dt_util.utcnow()
@ -1241,7 +1241,7 @@ async def test_history_stream_live_no_attributes_minimal_response_specific_entit
async def test_history_stream_live_with_future_end_time(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history stream with history and live data with future end time."""
now = dt_util.utcnow()
@ -1334,8 +1334,8 @@ async def test_history_stream_live_with_future_end_time(
@pytest.mark.parametrize("include_start_time_state", [True, False])
async def test_history_stream_before_history_starts(
recorder_mock: Recorder,
hass: HomeAssistant,
recorder_mock: Recorder,
hass_ws_client: WebSocketGenerator,
include_start_time_state,
) -> None:
@ -1385,7 +1385,7 @@ async def test_history_stream_before_history_starts(
async def test_history_stream_for_entity_with_no_possible_changes(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history stream for future with no possible changes where end time is less than or equal to now."""
await async_setup_component(
@ -1436,7 +1436,7 @@ async def test_history_stream_for_entity_with_no_possible_changes(
async def test_overflow_queue(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test overflowing the history stream queue."""
now = dt_util.utcnow()
@ -1513,7 +1513,7 @@ async def test_overflow_queue(
async def test_history_during_period_for_invalid_entity_ids(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history_during_period for valid and invalid entity ids."""
now = dt_util.utcnow()
@ -1656,7 +1656,7 @@ async def test_history_during_period_for_invalid_entity_ids(
async def test_history_stream_for_invalid_entity_ids(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history stream for invalid and valid entity ids."""
@ -1824,7 +1824,7 @@ async def test_history_stream_for_invalid_entity_ids(
async def test_history_stream_historical_only_with_start_time_state_past(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history stream."""
await async_setup_component(

View File

@ -24,7 +24,7 @@ def db_schema_32():
async def test_history_during_period(
recorder_mock: Recorder, hass: HomeAssistant, hass_ws_client: WebSocketGenerator
hass: HomeAssistant, recorder_mock: Recorder, hass_ws_client: WebSocketGenerator
) -> None:
"""Test history_during_period."""
now = dt_util.utcnow()

View File

@ -235,6 +235,7 @@ async def test_device_tracker_random_address_infrequent_changes(
connectable=False,
device=device,
advertisement=previous_service_info.advertisement,
tx_power=-127,
),
)
device = async_ble_device_from_address(hass, "AA:BB:CC:DD:EE:14", False)

View File

@ -20,6 +20,7 @@ IDASEN_DISCOVERY_INFO = BluetoothServiceInfoBleak(
advertisement=generate_advertisement_data(),
time=0,
connectable=True,
tx_power=-127,
)
NOT_IDASEN_DISCOVERY_INFO = BluetoothServiceInfoBleak(
@ -34,6 +35,7 @@ NOT_IDASEN_DISCOVERY_INFO = BluetoothServiceInfoBleak(
advertisement=generate_advertisement_data(),
time=0,
connectable=True,
tx_power=-127,
)

View File

@ -21,6 +21,7 @@ IMPROV_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
),
time=0,
connectable=True,
tx_power=-127,
)
@ -39,6 +40,7 @@ PROVISIONED_IMPROV_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
),
time=0,
connectable=True,
tx_power=-127,
)
@ -57,4 +59,5 @@ NOT_IMPROV_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
advertisement=generate_advertisement_data(),
time=0,
connectable=True,
tx_power=-127,
)

View File

@ -46,6 +46,7 @@ SERVICE_INFO = BluetoothServiceInfoBleak(
device=generate_ble_device("aa:bb:cc:dd:ee:ff", "mibp"),
time=0,
connectable=True,
tx_power=-127,
)

View File

@ -16,6 +16,7 @@ LD2410_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
advertisement=generate_advertisement_data(),
time=0,
connectable=True,
tx_power=-127,
)
NOT_LD2410_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
@ -33,4 +34,5 @@ NOT_LD2410_BLE_DISCOVERY_INFO = BluetoothServiceInfoBleak(
advertisement=generate_advertisement_data(),
time=0,
connectable=True,
tx_power=-127,
)

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