mirror of https://github.com/home-assistant/core
Merge branch 'dev' of github.com:home-assistant/core into idasen_none_bledevice_fix
This commit is contained in:
commit
df103ef16d
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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"))
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -49,6 +49,7 @@ PLATFORMS = [
|
|||
Platform.NOTIFY,
|
||||
Platform.NUMBER,
|
||||
Platform.SENSOR,
|
||||
Platform.SWITCH,
|
||||
Platform.WEATHER,
|
||||
]
|
||||
|
||||
|
|
|
@ -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."
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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
|
|
@ -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
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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}/",
|
||||
)
|
|
@ -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),
|
||||
|
|
|
@ -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.",
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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
|
||||
)
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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)
|
||||
)
|
||||
|
|
|
@ -29,6 +29,13 @@ send_message:
|
|||
required: true
|
||||
selector:
|
||||
text:
|
||||
title:
|
||||
required: false
|
||||
selector:
|
||||
text:
|
||||
filter:
|
||||
supported_features:
|
||||
- notify.NotifyEntityFeature.TITLE
|
||||
|
||||
persistent_notification:
|
||||
fields:
|
||||
|
|
|
@ -35,6 +35,10 @@
|
|||
"message": {
|
||||
"name": "Message",
|
||||
"description": "Your notification message."
|
||||
},
|
||||
"title": {
|
||||
"name": "Title",
|
||||
"description": "Title for your notification message."
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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
|
||||
),
|
||||
|
|
|
@ -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"]
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -31,6 +31,7 @@ def fake_service_info(name, service_uuid, manufacturer_data):
|
|||
tx_power=-127,
|
||||
platform_data=(),
|
||||
),
|
||||
tx_power=-127,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -155,6 +155,7 @@ def inject_advertisement_with_time_and_source_connectable(
|
|||
advertisement=adv,
|
||||
connectable=connectable,
|
||||
time=time,
|
||||
tx_power=adv.tx_power,
|
||||
)
|
||||
)
|
||||
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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]
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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": [
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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)
|
|
@ -38,4 +38,5 @@ def fake_service_info():
|
|||
tx_power=-127,
|
||||
platform_data=(),
|
||||
),
|
||||
tx_power=-127,
|
||||
)
|
||||
|
|
|
@ -16,4 +16,5 @@ COOKER_SERVICE_INFO = BluetoothServiceInfoBleak(
|
|||
advertisement=generate_advertisement_data(),
|
||||
time=0,
|
||||
connectable=True,
|
||||
tx_power=-127,
|
||||
)
|
||||
|
|
|
@ -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
|
||||
|
||||
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
|
|
@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
@ -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
Loading…
Reference in New Issue